@morscherlab/mint-sdk 1.4.0-beta.8 → 1.4.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/dist/{ActionMenu-CScINahU.js → AlertBox-B2Bgr3ei.js} +40 -209
  2. package/dist/AlertBox-B2Bgr3ei.js.map +1 -0
  3. package/dist/{AppSidebar-BmiiPfz9.js → AppSidebar-Dq9W1jE2.js} +160 -109
  4. package/dist/AppSidebar-Dq9W1jE2.js.map +1 -0
  5. package/dist/{ConfirmDialog-CJ7hqYUP.js → ConfirmDialog-nbJj51is.js} +2 -2
  6. package/dist/{ConfirmDialog-CJ7hqYUP.js.map → ConfirmDialog-nbJj51is.js.map} +1 -1
  7. package/dist/ExperimentPopover-5T1fAq2L.js +337 -0
  8. package/dist/ExperimentPopover-5T1fAq2L.js.map +1 -0
  9. package/dist/{ExperimentSelectorModal-D-9z7cNx.js → ExperimentSelectorModal-65peG4Up.js} +198 -181
  10. package/dist/ExperimentSelectorModal-65peG4Up.js.map +1 -0
  11. package/dist/{SegmentedControl-DERBN-Xd.js → SegmentedControl-BtnnjWzs.js} +2 -2
  12. package/dist/{SegmentedControl-DERBN-Xd.js.map → SegmentedControl-BtnnjWzs.js.map} +1 -1
  13. package/dist/{SettingsModal-CSLE31yg.js → SettingsModal-483nyI6b.js} +9 -9
  14. package/dist/SettingsModal-483nyI6b.js.map +1 -0
  15. package/dist/components/AppSidebar.vue.d.ts +1 -1
  16. package/dist/components/ArtifactSaveDialog.vue.d.ts +9 -1
  17. package/dist/components/AuditTrail.vue.d.ts +1 -1
  18. package/dist/components/BaseSelect.vue.d.ts +2 -2
  19. package/dist/components/ChartContainer.vue.d.ts +2 -0
  20. package/dist/components/DatePicker.vue.d.ts +1 -1
  21. package/dist/components/ExperimentPopover.vue.d.ts +18 -0
  22. package/dist/components/ExperimentSelectorModal.vue.d.ts +0 -4
  23. package/dist/components/FilePicker.vue.d.ts +1 -1
  24. package/dist/components/FileUploader.vue.d.ts +2 -2
  25. package/dist/components/PluginIcon.vue.d.ts +2 -2
  26. package/dist/components/SampleSelector.vue.d.ts +6 -6
  27. package/dist/components/SequenceProgressBar.vue.d.ts +3 -0
  28. package/dist/components/StepWizard.vue.d.ts +3 -0
  29. package/dist/components/internal/AutoGroupModalInternal.vue.d.ts +1 -1
  30. package/dist/components/internal/DatePickerTimeInternal.vue.d.ts +1 -1
  31. package/dist/components/internal/ResourcePickerListInternal.vue.d.ts +2 -7
  32. package/dist/components/internal/SelectMultipleInternal.vue.d.ts +4 -0
  33. package/dist/composables/createClient.d.ts +2 -0
  34. package/dist/composables/index.d.ts +1 -0
  35. package/dist/composables/pluginIdentity.d.ts +24 -0
  36. package/dist/composables/useAppExperiment.d.ts +8 -0
  37. package/dist/composables/useCalendarGrid.d.ts +1 -1
  38. package/dist/composables/useExperimentSelector.d.ts +6 -2
  39. package/dist/composables/usePlatform.d.ts +1 -0
  40. package/dist/{controlSchemaModel-CP5-_nAj.js → controlSchemaModel-B8CTXagP.js} +2 -2
  41. package/dist/{controlSchemaModel-CP5-_nAj.js.map → controlSchemaModel-B8CTXagP.js.map} +1 -1
  42. package/dist/{data-C2PwI_dn.js → data-CgILxDLW.js} +178 -124
  43. package/dist/data-CgILxDLW.js.map +1 -0
  44. package/dist/data.js +8 -8
  45. package/dist/{experimentStatus-BZmU4czV.js → experimentStatus-Cqp6qNrR.js} +3 -3
  46. package/dist/{experimentStatus-BZmU4czV.js.map → experimentStatus-Cqp6qNrR.js.map} +1 -1
  47. package/dist/index.js +19 -19
  48. package/dist/{install-DAID6c2-.js → install-BtLWTk6H.js} +15 -14
  49. package/dist/{install-DAID6c2-.js.map → install-BtLWTk6H.js.map} +1 -1
  50. package/dist/install.js +1 -1
  51. package/dist/{lab-DLwRl_Ql.js → lab-MHzp-_J8.js} +128 -147
  52. package/dist/lab-MHzp-_J8.js.map +1 -0
  53. package/dist/lab.js +4 -3
  54. package/dist/{records-BzTcxekG.js → records-B6gpM_3e.js} +472 -274
  55. package/dist/records-B6gpM_3e.js.map +1 -0
  56. package/dist/{runtime-D6gVat6r.js → runtime-7vfl_bI3.js} +27 -31
  57. package/dist/runtime-7vfl_bI3.js.map +1 -0
  58. package/dist/runtime.js +6 -6
  59. package/dist/{shell-5ju97nOS.js → shell-Cs9OJMnm.js} +5 -5
  60. package/dist/{shell-5ju97nOS.js.map → shell-Cs9OJMnm.js.map} +1 -1
  61. package/dist/shell.js +5 -5
  62. package/dist/styles.css +1606 -1666
  63. package/dist/types/componentWorkflowTypes.d.ts +2 -0
  64. package/dist/types/generated/platformContext.d.ts +1 -0
  65. package/dist/types/generated-ui.d.ts +4 -0
  66. package/dist/{BaseModal-DfRIuVoj.js → useDropdownState-8yWMu-7_.js} +60 -2
  67. package/dist/useDropdownState-8yWMu-7_.js.map +1 -0
  68. package/dist/{useExperimentSamples-CxcnxZ8h.js → useExperimentSamples-DHnxKFAF.js} +3 -3
  69. package/dist/{useExperimentSamples-CxcnxZ8h.js.map → useExperimentSamples-DHnxKFAF.js.map} +1 -1
  70. package/dist/{useListReorder-CcbEYxX-.js → useListReorder-BitdUhxn.js} +109 -277
  71. package/dist/useListReorder-BitdUhxn.js.map +1 -0
  72. package/dist/{useRackEditor-BD5NIo5s.js → useRackEditor-DiwCt77M.js} +71 -57
  73. package/dist/useRackEditor-DiwCt77M.js.map +1 -0
  74. package/dist/{useRealtime-PalKue84.js → useRealtime-C3iy9QkU.js} +7 -4
  75. package/dist/useRealtime-C3iy9QkU.js.map +1 -0
  76. package/dist/useRequest-D506eHi8.js +244 -0
  77. package/dist/useRequest-D506eHi8.js.map +1 -0
  78. package/dist/utils/time.d.ts +7 -0
  79. package/package.json +1 -1
  80. package/src/__tests__/components/AppTopBar.test.ts +51 -2
  81. package/src/__tests__/components/AppTopBarPillNav.test.ts +35 -0
  82. package/src/__tests__/components/ArtifactSaveDialog.test.ts +78 -0
  83. package/src/__tests__/components/BaseSelect.multiple.test.ts +87 -32
  84. package/src/__tests__/components/BaseSelect.test.ts +2 -1
  85. package/src/__tests__/components/BatchProgressList.test.ts +16 -0
  86. package/src/__tests__/components/CalendarGridPanel.test.ts +2 -2
  87. package/src/__tests__/components/ChartContainer.test.ts +11 -0
  88. package/src/__tests__/components/ChemicalFormula.test.ts +6 -3
  89. package/src/__tests__/components/DatePicker.datetime.test.ts +6 -0
  90. package/src/__tests__/components/DatePicker.test.ts +21 -0
  91. package/src/__tests__/components/DatePicker.time.test.ts +2 -2
  92. package/src/__tests__/components/EmptyState.test.ts +1 -1
  93. package/src/__tests__/components/ExperimentPopover.test.ts +125 -16
  94. package/src/__tests__/components/ExperimentSelectorModal.test.ts +119 -13
  95. package/src/__tests__/components/GeneratedPluginView.test.ts +22 -1
  96. package/src/__tests__/components/InstrumentStateBadge.test.ts +4 -4
  97. package/src/__tests__/components/ResourcePickerListInternal.test.ts +0 -20
  98. package/src/__tests__/components/SampleSelector.test.ts +13 -8
  99. package/src/__tests__/components/SequenceProgressBar.test.ts +12 -0
  100. package/src/__tests__/components/StepWizard.test.ts +11 -0
  101. package/src/__tests__/components/WellPlate.decoration.test.ts +11 -4
  102. package/src/__tests__/components/deprecationTags.test.ts +1 -1
  103. package/src/__tests__/composables/useAppExperiment.test.ts +27 -1
  104. package/src/__tests__/composables/useCalendarGrid.test.ts +4 -2
  105. package/src/__tests__/composables/useExperimentSelector.test.ts +111 -0
  106. package/src/__tests__/styles/baseControlsRules.test.ts +6 -2
  107. package/src/__tests__/styles/controlRefreshRules.test.ts +0 -6
  108. package/src/__tests__/styles/cssTokens.test.ts +2 -2
  109. package/src/__tests__/styles/menusShellRefresh.test.ts +14 -10
  110. package/src/__tests__/utils/plotlyTemplate.test.ts +4 -4
  111. package/src/__tests__/utils/time.test.ts +18 -1
  112. package/src/components/AdductText.story.vue +7 -0
  113. package/src/components/AdductText.vue +1 -1
  114. package/src/components/AlertBox.story.vue +2 -2
  115. package/src/components/AlertBox.vue +18 -16
  116. package/src/components/AppSidebar.story.vue +0 -28
  117. package/src/components/AppTopBar.story.vue +47 -12
  118. package/src/components/AppTopBar.vue +118 -114
  119. package/src/components/ArtifactSaveDialog.story.vue +23 -0
  120. package/src/components/ArtifactSaveDialog.vue +102 -12
  121. package/src/components/AuditTrail.story.vue +2 -1
  122. package/src/components/AuditTrail.vue +2 -21
  123. package/src/components/Avatar.vue +10 -14
  124. package/src/components/BaseSelect.multiple.story.vue +13 -2
  125. package/src/components/BaseSelect.vue +4 -3
  126. package/src/components/BatchProgressList.vue +35 -25
  127. package/src/components/ChartContainer.story.vue +2 -1
  128. package/src/components/ChartContainer.vue +8 -6
  129. package/src/components/ConfirmDialog.story.vue +1 -1
  130. package/src/components/EmptyState.vue +1 -1
  131. package/src/components/ExperimentPopover.story.vue +165 -53
  132. package/src/components/ExperimentPopover.vue +169 -187
  133. package/src/components/ExperimentSelectorModal.story.vue +40 -36
  134. package/src/components/ExperimentSelectorModal.vue +124 -110
  135. package/src/components/FormField.story.vue +8 -48
  136. package/src/components/GeneratedPluginView.vue +7 -1
  137. package/src/components/InstrumentStateBadge.vue +1 -1
  138. package/src/components/JobsStatusTray.vue +1 -1
  139. package/src/components/LoadingSpinner.vue +1 -1
  140. package/src/components/NumberInput.vue +3 -6
  141. package/src/components/PluginIcon.story.vue +2 -10
  142. package/src/components/PluginIcon.vue +2 -2
  143. package/src/components/PluginWorkspaceView.story.vue +1 -1
  144. package/src/components/RuntimeUpdateNotice.vue +1 -1
  145. package/src/components/SampleSelector.story.vue +1 -1
  146. package/src/components/SampleSelector.vue +27 -37
  147. package/src/components/SampleSelectorGroupNode.vue +1 -1
  148. package/src/components/SectionCard.story.vue +2 -2
  149. package/src/components/SequenceProgressBar.story.vue +9 -0
  150. package/src/components/SequenceProgressBar.vue +6 -1
  151. package/src/components/SettingsModal.schema.ts +1 -1
  152. package/src/components/SettingsModal.vue +2 -2
  153. package/src/components/SmartGroupFieldRecipe.story.vue +1 -1
  154. package/src/components/SmartGroupManual.story.vue +1 -1
  155. package/src/components/StepWizard.story.vue +33 -0
  156. package/src/components/StepWizard.vue +8 -1
  157. package/src/components/WellPlate.story.vue +3 -1
  158. package/src/components/WellPlate.vue +7 -0
  159. package/src/components/internal/AppTopBarPillNavInternal.vue +33 -2
  160. package/src/components/internal/DatePickerDateInternal.vue +16 -40
  161. package/src/components/internal/DatePickerDateTimeInternal.vue +3 -13
  162. package/src/components/internal/DatePickerTimeInternal.vue +5 -8
  163. package/src/components/internal/ResourcePickerListInternal.vue +45 -85
  164. package/src/components/internal/SelectMultipleInternal.vue +182 -65
  165. package/src/components/internal/SelectSearchableInternal.vue +2 -3
  166. package/src/composables/createClient.ts +2 -0
  167. package/src/composables/index.ts +1 -0
  168. package/src/composables/pluginIdentity.ts +31 -0
  169. package/src/composables/useAppExperiment.ts +20 -0
  170. package/src/composables/useCalendarGrid.ts +4 -3
  171. package/src/composables/useExperimentSelector.ts +77 -31
  172. package/src/styles/components/adduct-text.css +13 -5
  173. package/src/styles/components/alert-box.css +39 -58
  174. package/src/styles/components/app-layout.css +6 -5
  175. package/src/styles/components/app-page-selector.css +3 -1
  176. package/src/styles/components/app-pill-nav.css +11 -12
  177. package/src/styles/components/app-sidebar.css +9 -18
  178. package/src/styles/components/app-top-bar.css +57 -55
  179. package/src/styles/components/artifact-save-dialog.css +153 -2
  180. package/src/styles/components/audit-trail.css +16 -19
  181. package/src/styles/components/avatar.css +6 -4
  182. package/src/styles/components/batch-progress-list.css +38 -36
  183. package/src/styles/components/button.css +28 -59
  184. package/src/styles/components/calendar.css +12 -12
  185. package/src/styles/components/chart-container.css +34 -23
  186. package/src/styles/components/checkbox.css +2 -12
  187. package/src/styles/components/collapsible-card.css +27 -29
  188. package/src/styles/components/confirm-dialog.css +13 -14
  189. package/src/styles/components/dataframe.css +12 -4
  190. package/src/styles/components/date-picker.css +80 -92
  191. package/src/styles/components/datetime-picker.css +31 -43
  192. package/src/styles/components/divider.css +20 -20
  193. package/src/styles/components/dose-calculator.css +3 -3
  194. package/src/styles/components/empty-state.css +20 -17
  195. package/src/styles/components/experiment-code-badge.css +2 -0
  196. package/src/styles/components/experiment-popover.css +174 -329
  197. package/src/styles/components/experiment-selector-modal.css +75 -56
  198. package/src/styles/components/file-picker.css +3 -3
  199. package/src/styles/components/file-uploader.css +38 -49
  200. package/src/styles/components/form-builder.css +3 -3
  201. package/src/styles/components/formula-input.css +1 -1
  202. package/src/styles/components/icon-button.css +18 -24
  203. package/src/styles/components/input.css +5 -4
  204. package/src/styles/components/instrument-monitor.css +13 -15
  205. package/src/styles/components/jobs-status-tray.css +31 -25
  206. package/src/styles/components/modal.css +2 -4
  207. package/src/styles/components/molecule-input.css +2 -2
  208. package/src/styles/components/multi-select.css +65 -117
  209. package/src/styles/components/number-input.css +26 -19
  210. package/src/styles/components/pill.css +3 -2
  211. package/src/styles/components/plate-editor.css +0 -1
  212. package/src/styles/components/plugin-icon.css +26 -23
  213. package/src/styles/components/popover.css +2 -2
  214. package/src/styles/components/presence-avatars.css +1 -1
  215. package/src/styles/components/protocol-step-editor.css +1 -3
  216. package/src/styles/components/resource-card.css +2 -2
  217. package/src/styles/components/resource-picker-list.css +6 -57
  218. package/src/styles/components/sample-hierarchy-tree.css +4 -4
  219. package/src/styles/components/sample-selector.css +95 -79
  220. package/src/styles/components/schedule-calendar.css +8 -8
  221. package/src/styles/components/searchable-select.css +12 -12
  222. package/src/styles/components/section-card.css +28 -47
  223. package/src/styles/components/segmented-control.css +18 -15
  224. package/src/styles/components/select.css +3 -3
  225. package/src/styles/components/sequence-input.css +2 -2
  226. package/src/styles/components/sequence-progress-bar.css +17 -8
  227. package/src/styles/components/smart-group.css +18 -18
  228. package/src/styles/components/step-wizard.css +52 -16
  229. package/src/styles/components/tabs.css +2 -2
  230. package/src/styles/components/textarea.css +3 -3
  231. package/src/styles/components/time-picker.css +36 -21
  232. package/src/styles/components/toast.css +12 -10
  233. package/src/styles/components/toggle.css +1 -11
  234. package/src/styles/components/tooltip.css +10 -9
  235. package/src/styles/components/unit-input.css +8 -7
  236. package/src/styles/components/well-plate.css +9 -2
  237. package/src/styles/variables.css +106 -75
  238. package/src/types/componentWorkflowTypes.ts +2 -0
  239. package/src/types/generated/platformContext.ts +1 -0
  240. package/src/types/generated-ui.ts +4 -0
  241. package/src/utils/time.ts +21 -0
  242. package/dist/ActionMenu-CScINahU.js.map +0 -1
  243. package/dist/AppSidebar-BmiiPfz9.js.map +0 -1
  244. package/dist/BaseModal-DfRIuVoj.js.map +0 -1
  245. package/dist/ExperimentPopover-C4sGw-is.js +0 -359
  246. package/dist/ExperimentPopover-C4sGw-is.js.map +0 -1
  247. package/dist/ExperimentSelectorModal-D-9z7cNx.js.map +0 -1
  248. package/dist/SettingsModal-CSLE31yg.js.map +0 -1
  249. package/dist/data-C2PwI_dn.js.map +0 -1
  250. package/dist/lab-DLwRl_Ql.js.map +0 -1
  251. package/dist/records-BzTcxekG.js.map +0 -1
  252. package/dist/runtime-D6gVat6r.js.map +0 -1
  253. package/dist/useListReorder-CcbEYxX-.js.map +0 -1
  254. package/dist/useRackEditor-BD5NIo5s.js.map +0 -1
  255. package/dist/useRealtime-PalKue84.js.map +0 -1
  256. package/dist/useRequest-D5JDfsgZ.js +0 -76
  257. package/dist/useRequest-D5JDfsgZ.js.map +0 -1
@@ -1,11 +1,11 @@
1
1
  <script setup lang="ts" generic="T">
2
2
  /**
3
- * Searchable, optionally grouped single-select list shared by the experiment and
3
+ * Searchable single-select list shared by the experiment and
4
4
  * artifact selector modals: two-line rows (title, then code + meta) with status and date
5
5
  * in a right-hand column, a filled selected row, skeleton,
6
6
  * error and empty states, and the combobox keyboard model (↑ ↓ Enter).
7
7
  */
8
- import { computed, nextTick, reactive, ref, useId, watch } from 'vue'
8
+ import { computed, nextTick, ref, useId, watch } from 'vue'
9
9
  import BaseInput from '../BaseInput.vue'
10
10
  import Skeleton from '../Skeleton.vue'
11
11
  import EmptyState from '../EmptyState.vue'
@@ -26,8 +26,6 @@ export interface ResourcePickerRow {
26
26
  const props = withDefaults(
27
27
  defineProps<{
28
28
  items: T[]
29
- /** When set, rows render under collapsible group headers in this order. */
30
- groups?: { label: string; items: T[] }[] | null
31
29
  itemKey: (item: T) => string | number
32
30
  describe: (item: T) => ResourcePickerRow
33
31
  selectedKey?: string | number | null
@@ -39,9 +37,10 @@ const props = withDefaults(
39
37
  error?: string | null
40
38
  emptyTitle?: string
41
39
  emptyDescription?: string
40
+ /** Focus the search field when the list mounts (the modal's focus trap honours `autofocus`). */
41
+ autofocusSearch?: boolean
42
42
  }>(),
43
43
  {
44
- groups: null,
45
44
  selectedKey: null,
46
45
  placeholder: 'Search…',
47
46
  loading: false,
@@ -57,50 +56,33 @@ const emit = defineEmits<{
57
56
  defineSlots<{
58
57
  /** Extra filters on the search row. */
59
58
  filters?: () => unknown
60
- /** A second filter row under the search. */
61
- advanced?: () => unknown
62
59
  /** Row controls before the check; keys pressed inside them never select the row. */
63
60
  actions?: (props: { item: T }) => unknown
64
61
  }>()
65
62
 
66
63
  const listId = `mint-resource-picker-${useId().replace(/:/g, '')}`
67
- const collapsed = reactive(new Set<string>())
68
64
  const activeIndex = ref(-1)
69
65
  const listRef = ref<HTMLElement | null>(null)
70
66
 
71
- const visibleGroups = computed(() =>
72
- props.groups?.map((group) => ({
73
- ...group,
74
- rows: collapsed.has(group.label) ? [] : group.items,
75
- })),
76
- )
77
- /** Keyboard order: the rows on screen, top to bottom. */
78
- const flat = computed(() =>
79
- visibleGroups.value ? visibleGroups.value.flatMap((group) => group.rows) : props.items,
80
- )
81
- const indexByKey = computed(() => new Map(flat.value.map((item, i) => [props.itemKey(item), i])))
67
+ const indexByKey = computed(() => new Map(props.items.map((item, i) => [props.itemKey(item), i])))
82
68
  const showList = computed(() => !props.loading && !props.error && props.items.length > 0)
83
69
 
84
70
  function optionId(item: T): string {
85
71
  return `${listId}-${props.itemKey(item)}`
86
72
  }
87
73
  const activeDescendant = computed(() => {
88
- const item = flat.value[activeIndex.value]
74
+ const item = props.items[activeIndex.value]
89
75
  return item === undefined ? undefined : optionId(item)
90
76
  })
91
77
 
92
78
  function indexOf(item: T): number {
93
79
  return indexByKey.value.get(props.itemKey(item)) ?? -1
94
80
  }
95
- function toggleGroup(label: string) {
96
- if (collapsed.has(label)) collapsed.delete(label)
97
- else collapsed.add(label)
98
- }
99
81
  function keydown(event: KeyboardEvent) {
100
- // Keys belong to the search combobox; a focused header, filter or row action keeps its own Enter.
82
+ // Keys belong to the search combobox; a focused filter or row action keeps its own Enter.
101
83
  const target = event.target as HTMLElement | null
102
84
  if (target !== event.currentTarget && !target?.closest('.mint-resource-picker__search')) return
103
- const rows = flat.value
85
+ const rows = props.items
104
86
  if (!rows.length) return
105
87
  if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
106
88
  event.preventDefault()
@@ -120,7 +102,7 @@ function keydown(event: KeyboardEvent) {
120
102
  }
121
103
 
122
104
  watch(
123
- () => [props.items, props.groups],
105
+ () => props.items,
124
106
  () => {
125
107
  activeIndex.value = -1
126
108
  },
@@ -136,6 +118,7 @@ watch(
136
118
  :placeholder="placeholder"
137
119
  size="sm"
138
120
  type="search"
121
+ :autofocus="autofocusSearch"
139
122
  role="combobox"
140
123
  :aria-label="`Search ${label.toLowerCase()}`"
141
124
  :aria-expanded="showList"
@@ -151,7 +134,6 @@ watch(
151
134
  </BaseInput>
152
135
  <slot name="filters" />
153
136
  </div>
154
- <slot name="advanced" />
155
137
 
156
138
  <div v-if="loading" class="mint-resource-picker__frame" aria-busy="true">
157
139
  <div v-for="n in 4" :key="n" class="mint-resource-picker__skeleton-row">
@@ -174,64 +156,42 @@ watch(
174
156
  role="listbox"
175
157
  :aria-label="label"
176
158
  >
177
- <template v-for="group in visibleGroups ?? [{ label: '', items, rows: items }]" :key="group.label">
178
- <button
179
- v-if="visibleGroups"
180
- type="button"
181
- class="mint-resource-picker__group"
182
- :aria-expanded="!collapsed.has(group.label)"
183
- @click="toggleGroup(group.label)"
184
- >
185
- <svg
186
- class="mint-resource-picker__chevron"
187
- viewBox="0 0 16 16"
188
- fill="none"
189
- stroke="currentColor"
190
- stroke-width="1.5"
191
- aria-hidden="true"
159
+ <div
160
+ v-for="item in items"
161
+ :id="optionId(item)"
162
+ :key="itemKey(item)"
163
+ role="option"
164
+ :aria-selected="itemKey(item) === selectedKey"
165
+ class="mint-resource-picker__row"
166
+ :class="{
167
+ 'mint-resource-picker__row--selected': itemKey(item) === selectedKey,
168
+ 'mint-resource-picker__row--active': indexOf(item) === activeIndex,
169
+ }"
170
+ @click="emit('select', item)"
171
+ @mouseenter="activeIndex = indexOf(item)"
172
+ >
173
+ <span class="mint-resource-picker__text">
174
+ <span class="mint-resource-picker__title">{{ describe(item).title }}</span>
175
+ <span v-if="describe(item).code || describe(item).meta?.length" class="mint-resource-picker__meta"
176
+ ><span v-if="describe(item).code" class="mint-resource-picker__code mint-exp-code mint-exp-code--sm">{{
177
+ describe(item).code
178
+ }}</span
179
+ ><span v-if="describe(item).meta?.length">{{ describe(item).meta!.join(' · ') }}</span></span
192
180
  >
193
- <path d="M4 6l4 4 4-4" />
194
- </svg>
195
- <span class="mint-resource-picker__group-label">{{ group.label }}</span>
196
- <span class="mint-resource-picker__group-count">{{ group.items.length }}</span>
197
- </button>
198
- <div
199
- v-for="item in group.rows"
200
- :id="optionId(item)"
201
- :key="itemKey(item)"
202
- role="option"
203
- :aria-selected="itemKey(item) === selectedKey"
204
- class="mint-resource-picker__row"
205
- :class="{
206
- 'mint-resource-picker__row--selected': itemKey(item) === selectedKey,
207
- 'mint-resource-picker__row--active': indexOf(item) === activeIndex,
208
- }"
209
- @click="emit('select', item)"
210
- @mouseenter="activeIndex = indexOf(item)"
211
- >
212
- <span class="mint-resource-picker__text">
213
- <span class="mint-resource-picker__title">{{ describe(item).title }}</span>
214
- <span v-if="describe(item).code || describe(item).meta?.length" class="mint-resource-picker__meta"
215
- ><span v-if="describe(item).code" class="mint-resource-picker__code mint-exp-code mint-exp-code--sm">{{
216
- describe(item).code
217
- }}</span
218
- ><span v-if="describe(item).meta?.length">{{ describe(item).meta!.join(' · ') }}</span></span
219
- >
220
- </span>
221
- <span v-if="describe(item).status || describe(item).date" class="mint-resource-picker__aside">
222
- <span
223
- v-if="describe(item).status"
224
- class="mint-resource-picker__status"
225
- :class="`mint-resource-picker__status--${describe(item).status!.tone}`"
226
- >{{ describe(item).status!.label }}</span
227
- >
228
- <span v-if="describe(item).date" class="mint-resource-picker__date">{{ describe(item).date }}</span>
229
- </span>
230
- <span v-if="$slots.actions" class="mint-resource-picker__actions" @click.stop>
231
- <slot name="actions" :item="item" />
232
- </span>
233
- </div>
234
- </template>
181
+ </span>
182
+ <span v-if="describe(item).status || describe(item).date" class="mint-resource-picker__aside">
183
+ <span
184
+ v-if="describe(item).status"
185
+ class="mint-resource-picker__status"
186
+ :class="`mint-resource-picker__status--${describe(item).status!.tone}`"
187
+ >{{ describe(item).status!.label }}</span
188
+ >
189
+ <span v-if="describe(item).date" class="mint-resource-picker__date">{{ describe(item).date }}</span>
190
+ </span>
191
+ <span v-if="$slots.actions" class="mint-resource-picker__actions" @click.stop>
192
+ <slot name="actions" :item="item" />
193
+ </span>
194
+ </div>
235
195
  </div>
236
196
  </div>
237
197
  </template>
@@ -1,7 +1,9 @@
1
1
  <script setup lang="ts">
2
- /** BaseSelect with `multiple`: a checkbox chip list with an optional selection cap (`max`). */
3
- import { computed } from 'vue'
2
+ /** BaseSelect with `multiple`: a field of removable chips that opens a filterable checkbox listbox, capped by `max`. */
3
+ import { computed, nextTick, ref, useAttrs, useId, watch } from 'vue'
4
+ import { Check, ChevronDown, X } from 'lucide-vue-next'
4
5
  import type { OptionPrimitive, SelectOption, SelectOptionInput } from '../../types'
6
+ import { useEventListener } from '../../composables/useEventListener'
5
7
  import { useListSelection } from '../../composables/useListSelection'
6
8
  import { normalizeOptionInput } from '../../utils/options'
7
9
 
@@ -15,6 +17,8 @@ interface Props {
15
17
  invalid?: boolean
16
18
  max?: number
17
19
  size?: 'sm' | 'md' | 'lg'
20
+ ariaLabelledby?: string
21
+ ariaDescribedby?: string
18
22
  }
19
23
 
20
24
  const props = withDefaults(defineProps<Props>(), {
@@ -23,103 +27,216 @@ const props = withDefaults(defineProps<Props>(), {
23
27
  invalid: false,
24
28
  max: undefined,
25
29
  size: 'md',
30
+ ariaLabelledby: undefined,
31
+ ariaDescribedby: undefined,
32
+ })
33
+
34
+ defineOptions({ inheritAttrs: false })
35
+
36
+ // The wrapper takes class and style; every other attribute (aria-label, data-*) names the focusable field.
37
+ const attrs = useAttrs()
38
+ const fieldAttrs = computed(() => {
39
+ const { class: _class, style: _style, ...rest } = attrs
40
+ return rest
26
41
  })
27
42
 
28
43
  const emit = defineEmits<{
29
44
  'update:modelValue': [value: OptionPrimitive[]]
30
45
  }>()
31
46
 
47
+ const id = useId().replace(/:/g, '')
48
+ const listboxId = `mint-multi-select-${id}`
49
+ const root = ref<HTMLElement | null>(null)
50
+ const field = ref<HTMLElement | null>(null)
51
+ const search = ref<HTMLInputElement | null>(null)
52
+ const open = ref(false)
53
+ const query = ref('')
54
+ const activeIndex = ref(-1)
55
+
32
56
  const normalizedOptions = computed<MultiOption[]>(() => props.options.map(normalizeOptionInput<OptionPrimitive, MultiOption>))
33
- const optionSelection = useListSelection({
57
+ const selection = useListSelection({
34
58
  selected: () => props.modelValue,
35
59
  items: normalizedOptions,
36
60
  getValue: option => option.value,
37
61
  disabled: () => props.disabled,
38
62
  max: () => props.max,
39
63
  })
40
- const canSelectMore = optionSelection.canAddMore
41
64
 
42
- function isSelected(value: OptionPrimitive): boolean {
43
- return optionSelection.isSelected(value)
65
+ const chips = computed(() => props.modelValue.map(value => ({
66
+ value,
67
+ label: normalizedOptions.value.find(option => option.value === value)?.label ?? String(value),
68
+ })))
69
+ const visibleOptions = computed(() => {
70
+ const needle = query.value.trim().toLocaleLowerCase()
71
+ if (!needle) return normalizedOptions.value
72
+ return normalizedOptions.value.filter(option => option.label.toLocaleLowerCase().includes(needle))
73
+ })
74
+ const activeDescendant = computed(() =>
75
+ activeIndex.value >= 0 ? `${listboxId}-option-${activeIndex.value}` : undefined,
76
+ )
77
+
78
+ function isUnavailable(option: MultiOption): boolean {
79
+ return !!option.disabled || (!selection.isSelected(option.value) && !selection.canAddMore.value)
44
80
  }
45
81
 
46
- function toggleOption(option: MultiOption) {
47
- if (props.disabled || option.disabled) return
82
+ async function openPanel(): Promise<void> {
83
+ if (props.disabled || open.value) return
84
+ query.value = ''
85
+ open.value = true
86
+ activeIndex.value = visibleOptions.value.length > 0 ? 0 : -1
87
+ await nextTick()
88
+ search.value?.focus()
89
+ }
48
90
 
49
- if (isSelected(option.value) || canSelectMore.value) {
50
- emit('update:modelValue', optionSelection.toggleValue(option.value))
91
+ async function closePanel(restoreFocus = false): Promise<void> {
92
+ open.value = false
93
+ query.value = ''
94
+ activeIndex.value = -1
95
+ if (restoreFocus) {
96
+ await nextTick()
97
+ field.value?.focus()
51
98
  }
52
99
  }
53
100
 
54
- function handleKeydown(event: KeyboardEvent, option: MultiOption) {
55
- if (event.key === 'Enter' || event.key === ' ') {
56
- event.preventDefault()
57
- toggleOption(option)
58
- }
101
+ function moveActive(direction: 1 | -1): void {
102
+ const count = visibleOptions.value.length
103
+ if (count === 0) return
104
+ const start = activeIndex.value < 0 ? (direction === 1 ? -1 : count) : activeIndex.value
105
+ activeIndex.value = (start + direction + count) % count
106
+ void nextTick(() => document.getElementById(activeDescendant.value ?? '')?.scrollIntoView({ block: 'nearest' }))
107
+ }
108
+
109
+ function toggle(option: MultiOption): void {
110
+ if (props.disabled || isUnavailable(option)) return
111
+ emit('update:modelValue', selection.toggleValue(option.value))
112
+ }
113
+
114
+ function remove(value: OptionPrimitive): void {
115
+ if (props.disabled) return
116
+ emit('update:modelValue', props.modelValue.filter(selected => selected !== value))
117
+ }
118
+
119
+ function handleFieldKeydown(event: KeyboardEvent): void {
120
+ if (event.key !== 'Enter' && event.key !== ' ' && event.key !== 'ArrowDown') return
121
+ event.preventDefault()
122
+ void openPanel()
59
123
  }
60
124
 
61
- const selectedLabels = computed(() => {
62
- return props.modelValue
63
- .map(v => normalizedOptions.value.find(o => o.value === v)?.label)
64
- .filter(Boolean)
125
+ function handleSearchKeydown(event: KeyboardEvent): void {
126
+ if (event.key === 'ArrowDown') moveActive(1)
127
+ else if (event.key === 'ArrowUp') moveActive(-1)
128
+ else if (event.key === 'Enter') {
129
+ const option = visibleOptions.value[activeIndex.value]
130
+ if (option) toggle(option)
131
+ } else if (event.key === 'Escape') {
132
+ void closePanel(true)
133
+ } else if (event.key === 'Backspace' && query.value === '' && props.modelValue.length > 0) {
134
+ remove(props.modelValue[props.modelValue.length - 1]!)
135
+ } else return
136
+ event.preventDefault()
137
+ }
138
+
139
+ watch(visibleOptions, options => { activeIndex.value = options.length > 0 ? 0 : -1 })
140
+ watch(() => props.disabled, disabled => { if (disabled) void closePanel() })
141
+ useEventListener(() => document, 'pointerdown', event => {
142
+ if (open.value && !root.value?.contains(event.target as Node)) void closePanel()
65
143
  })
66
144
  </script>
67
145
 
68
146
  <template>
69
147
  <div
70
- :class="[
71
- 'mint-multi-select',
72
- `mint-multi-select--${size}`,
73
- disabled ? 'mint-multi-select--disabled' : '',
74
- invalid ? 'mint-multi-select--error' : '',
75
- ]"
148
+ ref="root"
149
+ :class="['mint-multi-select', `mint-multi-select--${size}`, $attrs.class]"
150
+ :style="$attrs.style"
151
+ @focusout="!root?.contains($event.relatedTarget as Node | null) && closePanel()"
76
152
  >
77
153
  <div
78
- v-if="modelValue.length === 0"
79
- class="mint-multi-select__placeholder"
154
+ v-bind="fieldAttrs"
155
+ ref="field"
156
+ role="combobox"
157
+ :tabindex="disabled ? undefined : 0"
158
+ :class="[
159
+ 'mint-multi-select__field',
160
+ {
161
+ 'mint-multi-select__field--open': open,
162
+ 'mint-multi-select__field--error': invalid,
163
+ 'mint-multi-select__field--disabled': disabled,
164
+ },
165
+ ]"
166
+ aria-haspopup="listbox"
167
+ :aria-expanded="open"
168
+ :aria-controls="listboxId"
169
+ :aria-disabled="disabled || undefined"
170
+ :aria-invalid="invalid || undefined"
171
+ :aria-labelledby="ariaLabelledby || undefined"
172
+ :aria-describedby="ariaDescribedby || undefined"
173
+ @click="open ? closePanel() : openPanel()"
174
+ @keydown.self="handleFieldKeydown"
80
175
  >
81
- {{ placeholder }}
176
+ <div class="mint-multi-select__chips">
177
+ <span v-for="chip in chips" :key="String(chip.value)" class="mint-multi-select__chip">
178
+ {{ chip.label }}
179
+ <button
180
+ v-if="!disabled"
181
+ type="button"
182
+ tabindex="-1"
183
+ class="mint-multi-select__chip-remove"
184
+ :aria-label="`Remove ${chip.label}`"
185
+ @click.stop="remove(chip.value)"
186
+ >
187
+ <X :size="11" :stroke-width="2.25" aria-hidden="true" />
188
+ </button>
189
+ </span>
190
+ <span v-if="chips.length === 0" class="mint-multi-select__placeholder">{{ placeholder }}</span>
191
+ </div>
192
+ <ChevronDown class="mint-multi-select__icon" :size="14" aria-hidden="true" />
82
193
  </div>
83
194
 
84
- <div class="mint-multi-select__options" role="group" :aria-invalid="invalid || undefined">
85
- <button
86
- v-for="option in normalizedOptions"
87
- :key="String(option.value)"
88
- type="button"
89
- role="checkbox"
90
- :aria-checked="isSelected(option.value)"
91
- :disabled="disabled || option.disabled || (!isSelected(option.value) && !canSelectMore)"
92
- :class="[
93
- 'mint-multi-select__chip',
94
- `mint-multi-select__chip--${size}`,
95
- isSelected(option.value) ? 'mint-multi-select__chip--active' : '',
96
- option.disabled ? 'mint-multi-select__chip--disabled' : '',
97
- !isSelected(option.value) && !canSelectMore ? 'mint-multi-select__chip--disabled' : '',
98
- ]"
99
- @click="toggleOption(option)"
100
- @keydown="handleKeydown($event, option)"
101
- >
102
- <svg
103
- class="mint-multi-select__chip-check"
104
- viewBox="0 0 24 24"
105
- fill="none"
106
- stroke="currentColor"
107
- stroke-width="2.5"
108
- stroke-linecap="round"
109
- stroke-linejoin="round"
110
- aria-hidden="true"
111
- >
112
- <path d="M20 6 9 17l-5-5" />
195
+ <div v-if="open" class="mint-popover mint-searchable-select__panel">
196
+ <div class="mint-searchable-select__search-wrap">
197
+ <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" aria-hidden="true">
198
+ <circle cx="8.5" cy="8.5" r="5.5" /><path d="m13 13 4 4" />
113
199
  </svg>
114
- <span class="mint-multi-select__chip-label">{{ option.label }}</span>
115
- </button>
116
- </div>
200
+ <input
201
+ ref="search"
202
+ v-model="query"
203
+ role="combobox"
204
+ type="search"
205
+ class="mint-searchable-select__search"
206
+ placeholder="Filter…"
207
+ aria-label="Filter options"
208
+ :aria-expanded="open"
209
+ :aria-controls="listboxId"
210
+ :aria-activedescendant="activeDescendant"
211
+ aria-autocomplete="list"
212
+ @keydown="handleSearchKeydown"
213
+ >
214
+ </div>
117
215
 
118
- <div
119
- v-if="modelValue.length > 0"
120
- class="mint-multi-select__summary"
121
- >
122
- {{ selectedLabels.join(', ') }}
216
+ <ul :id="listboxId" class="mint-searchable-select__list" role="listbox" aria-multiselectable="true">
217
+ <li
218
+ v-for="(option, index) in visibleOptions"
219
+ :id="`${listboxId}-option-${index}`"
220
+ :key="String(option.value)"
221
+ role="option"
222
+ class="mint-menu-item mint-multi-select__option"
223
+ :class="{ 'mint-menu-item--active': activeIndex === index }"
224
+ :aria-selected="selection.isSelected(option.value)"
225
+ :aria-disabled="isUnavailable(option) || undefined"
226
+ @mousedown.prevent
227
+ @mouseenter="!isUnavailable(option) && (activeIndex = index)"
228
+ @click="toggle(option)"
229
+ >
230
+ <span
231
+ :class="['mint-multi-select__check', { 'mint-multi-select__check--on': selection.isSelected(option.value) }]"
232
+ aria-hidden="true"
233
+ >
234
+ <Check v-if="selection.isSelected(option.value)" :size="10" :stroke-width="3.5" />
235
+ </span>
236
+ <span class="mint-menu-item__label">{{ option.label }}</span>
237
+ </li>
238
+ <li v-if="visibleOptions.length === 0" class="mint-searchable-select__empty">No matching options</li>
239
+ </ul>
123
240
  </div>
124
241
  </div>
125
242
  </template>
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  /** BaseSelect with `searchable`: a single-select listbox with a search field, descriptions, disabled reasons, and custom slots. */
3
3
  import { computed, nextTick, ref, useAttrs, useId, watch } from 'vue'
4
+ import { ChevronsUpDown } from 'lucide-vue-next'
4
5
  import type { SelectOption, SelectOptionInput } from '../../types'
5
6
  import { useEventListener } from '../../composables/useEventListener'
6
7
  import { normalizeOptionInput } from '../../utils/options'
@@ -176,9 +177,7 @@ useEventListener(() => document, 'pointerdown', event => {
176
177
  <span :class="['mint-searchable-select__value', { 'mint-searchable-select__value--placeholder': !selectedOption }]">
177
178
  {{ selectedOption?.label ?? placeholder }}
178
179
  </span>
179
- <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" aria-hidden="true">
180
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.75" d="m6 8 4 4 4-4" />
181
- </svg>
180
+ <ChevronsUpDown :size="14" aria-hidden="true" />
182
181
  </button>
183
182
 
184
183
  <div v-if="open" class="mint-popover mint-searchable-select__panel">
@@ -86,6 +86,8 @@ export interface PluginContract {
86
86
  schemaVersion: 2
87
87
  plugin: {
88
88
  name?: string
89
+ displayName?: string
90
+ subtitle?: string
89
91
  packageName?: string
90
92
  version?: string
91
93
  description?: string
@@ -6,6 +6,7 @@ export { useToast } from './useToast'
6
6
  export { usePlatform } from './usePlatform'
7
7
  export { useExperiment, type ExperimentLoadStatus, type UseExperimentReturn } from './useExperiment'
8
8
  export { resolvePluginFrontendBase } from './pluginFrontendBase'
9
+ export { providePluginIdentity, type PluginIdentityContract } from './pluginIdentity'
9
10
  export {
10
11
  useRequest,
11
12
  type RequestKind,
@@ -0,0 +1,31 @@
1
+ import { reactive, readonly } from 'vue'
2
+ import type { PluginContract } from './createClient'
3
+
4
+ /** The `plugin` block of the generated contract (`./generated/mint-plugin`) that `providePluginIdentity` reads. */
5
+ export interface PluginIdentityContract {
6
+ plugin: Pick<PluginContract['plugin'], 'name' | 'displayName' | 'subtitle' | 'description' | 'icon'>
7
+ }
8
+
9
+ const identity = reactive<{ name?: string; subtitle?: string; icon?: string }>({})
10
+
11
+ /** The seeded identity AppTopBar shows in standalone mode. Internal: not exported from the package. */
12
+ export const pluginIdentity = readonly(identity)
13
+
14
+ /**
15
+ * Seed the standalone AppTopBar with the plugin name, subtitle and icon. In integrated
16
+ * mode the platform supplies them. Explicit `title`/`subtitle` props and the `#icon` slot still win.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * import { providePluginIdentity } from '@morscherlab/mint-sdk'
21
+ * import { contract } from './generated/mint-plugin'
22
+ *
23
+ * providePluginIdentity(contract)
24
+ * ```
25
+ */
26
+ export function providePluginIdentity(contract: PluginIdentityContract): void {
27
+ const { plugin } = contract
28
+ identity.name = plugin.displayName || plugin.name || undefined
29
+ identity.subtitle = plugin.subtitle || plugin.description || undefined
30
+ identity.icon = plugin.icon || undefined
31
+ }
@@ -23,12 +23,19 @@ export interface UseAppExperimentOptions {
23
23
  onDetach?: () => void
24
24
  saveDisabled?: Ref<boolean> | ComputedRef<boolean>
25
25
  saveDisabledMessage?: string | Ref<string | undefined> | ComputedRef<string | undefined>
26
+ /** Unsaved changes; marks the Save button with a dot. */
27
+ dirty?: Ref<boolean> | ComputedRef<boolean>
26
28
  }
27
29
 
28
30
  export interface AppExperimentPopoverBinding {
29
31
  experimentName?: string
30
32
  experimentCode?: string
31
33
  experimentStatus?: ExperimentStatus
34
+ experimentId?: number | null
35
+ experimentProject?: string
36
+ experimentOwner?: string
37
+ /** ISO timestamp of the experiment's last update. */
38
+ experimentUpdatedAt?: string
32
39
  /** An experiment is bound but its record has not arrived yet. */
33
40
  isResolving: boolean
34
41
  /** Resolution failed; the binding is kept so the user can retry or detach. */
@@ -39,6 +46,7 @@ export interface AppExperimentPopoverBinding {
39
46
  saveDisabledMessage?: string
40
47
  saveLoading: boolean
41
48
  saveSuccessMessage?: string
49
+ dirty?: boolean
42
50
  }
43
51
 
44
52
  export interface AppExperimentSelectorModalBinding {
@@ -125,10 +133,19 @@ export function useAppExperiment(options: UseAppExperimentOptions = {}): UseAppE
125
133
  const showDetach = computed(() => experimentId.value !== null)
126
134
  const saveDisabled = computed(() => toValue(options.saveDisabled) ?? false)
127
135
  const saveDisabledMessage = computed(() => toValue(options.saveDisabledMessage))
136
+ const dirty = computed(() => toValue(options.dirty) ?? false)
137
+ const experimentOwner = computed(() => {
138
+ const owner = store.current?.created_by_user
139
+ return owner ? [owner.first_name, owner.last_name].filter(Boolean).join(' ') || owner.username : undefined
140
+ })
128
141
  const popover = computed<AppExperimentPopoverBinding>(() => ({
129
142
  experimentName: experimentName.value,
130
143
  experimentCode: experimentCode.value,
131
144
  experimentStatus: experimentStatus.value,
145
+ experimentId: experimentId.value,
146
+ experimentProject: store.current?.project_name || store.current?.project || undefined,
147
+ experimentOwner: experimentOwner.value,
148
+ experimentUpdatedAt: store.current?.updated_at,
132
149
  isResolving: isResolving.value,
133
150
  error: error.value ?? undefined,
134
151
  showSave: showSave.value,
@@ -137,6 +154,7 @@ export function useAppExperiment(options: UseAppExperimentOptions = {}): UseAppE
137
154
  saveDisabledMessage: saveDisabledMessage.value,
138
155
  saveLoading: saveLoading.value,
139
156
  saveSuccessMessage: saveSuccessMessage.value,
157
+ dirty: dirty.value,
140
158
  }))
141
159
  const selectorModal = computed<AppExperimentSelectorModalBinding>(() => ({
142
160
  open: showModal.value,
@@ -166,6 +184,8 @@ export function useAppExperiment(options: UseAppExperimentOptions = {}): UseAppE
166
184
  saveLoading.value = true
167
185
  try {
168
186
  const message = await options.onSave()
187
+ // The save changed the record on the platform; read it back so "Updated" is current.
188
+ await store.refresh()
169
189
  if (message) {
170
190
  saveSuccessMessage.value = message
171
191
  if (successTimer) clearTimeout(successTimer)