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

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 (198) hide show
  1. package/README.md +14 -3
  2. package/dist/{AlertBox-B2Bgr3ei.js → AlertBox-DwgNTEqf.js} +3 -3
  3. package/dist/{AlertBox-B2Bgr3ei.js.map → AlertBox-DwgNTEqf.js.map} +1 -1
  4. package/dist/{AppSidebar-Dq9W1jE2.js → AppSidebar-CZZaxni0.js} +1103 -144
  5. package/dist/AppSidebar-CZZaxni0.js.map +1 -0
  6. package/dist/{ConfirmDialog-nbJj51is.js → ConfirmDialog-CjWoxAD1.js} +2 -2
  7. package/dist/{ConfirmDialog-nbJj51is.js.map → ConfirmDialog-CjWoxAD1.js.map} +1 -1
  8. package/dist/{ExperimentPopover-5T1fAq2L.js → ExperimentPopover-DujiuQIO.js} +5 -5
  9. package/dist/{ExperimentPopover-5T1fAq2L.js.map → ExperimentPopover-DujiuQIO.js.map} +1 -1
  10. package/dist/{ExperimentSelectorModal-65peG4Up.js → ExperimentSelectorModal-B4j4eUOJ.js} +114 -98
  11. package/dist/ExperimentSelectorModal-B4j4eUOJ.js.map +1 -0
  12. package/dist/{SegmentedControl-BtnnjWzs.js → SegmentedControl-DyUPH3I5.js} +2 -2
  13. package/dist/{SegmentedControl-BtnnjWzs.js.map → SegmentedControl-DyUPH3I5.js.map} +1 -1
  14. package/dist/{SettingsModal-483nyI6b.js → SettingsModal-D2MOOtuO.js} +26 -25
  15. package/dist/SettingsModal-D2MOOtuO.js.map +1 -0
  16. package/dist/__stories__/experiment-helpers.d.ts +10 -0
  17. package/dist/__tests__/components/AppTopBarOnboarding.test.d.ts +1 -0
  18. package/dist/__tests__/components/FloatingPanelsInModal.test.d.ts +1 -0
  19. package/dist/__tests__/composables/useFloatingPanel.test.d.ts +1 -0
  20. package/dist/__tests__/composables/useOnboarding.test.d.ts +1 -0
  21. package/dist/__tests__/styles/paletteTokens.test.d.ts +1 -0
  22. package/dist/__tests__/styles/v2AuditRules.test.d.ts +1 -0
  23. package/dist/components/AppTopBar.vue.d.ts +20 -5
  24. package/dist/components/BaseButton.vue.d.ts +4 -2
  25. package/dist/components/ExperimentSelectorModal.vue.d.ts +15 -1
  26. package/dist/components/JobsStatusTray.vue.d.ts +15 -4
  27. package/dist/components/PluginWorkspaceView.props.d.ts +3 -0
  28. package/dist/components/PluginWorkspaceView.vue.d.ts +3 -0
  29. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +3 -3
  30. package/dist/components/internal/AppTopBarOnboardingInternal.vue.d.ts +7 -0
  31. package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -1
  32. package/dist/components/internal/AutoGroupModalInternal.vue.d.ts +2 -2
  33. package/dist/components/internal/OnboardingLayerInternal.vue.d.ts +7 -0
  34. package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +8 -0
  35. package/dist/composables/pluginWorkspaceModel.d.ts +2 -2
  36. package/dist/composables/useFileImport.d.ts +7 -0
  37. package/dist/composables/useFloatingPanel.d.ts +29 -0
  38. package/dist/composables/useJobCenter.d.ts +41 -3
  39. package/dist/composables/useOnboarding.d.ts +119 -0
  40. package/dist/{controlSchemaModel-B8CTXagP.js → controlSchemaModel-Bsg0t5Ab.js} +2 -2
  41. package/dist/{controlSchemaModel-B8CTXagP.js.map → controlSchemaModel-Bsg0t5Ab.js.map} +1 -1
  42. package/dist/{data-CgILxDLW.js → data-Boeh4uPI.js} +13 -12
  43. package/dist/{data-CgILxDLW.js.map → data-Boeh4uPI.js.map} +1 -1
  44. package/dist/data.d.ts +1 -1
  45. package/dist/data.js +9 -9
  46. package/dist/{experiment-43OJtH7R.js → experiment-DyO54FQB.js} +25 -61
  47. package/dist/experiment-DyO54FQB.js.map +1 -0
  48. package/dist/{experimentStatus-Cqp6qNrR.js → experimentStatus-DCn2QKG7.js} +2 -2
  49. package/dist/{experimentStatus-Cqp6qNrR.js.map → experimentStatus-DCn2QKG7.js.map} +1 -1
  50. package/dist/index.js +20 -20
  51. package/dist/{install-BtLWTk6H.js → install-BEHp7Mo-.js} +15 -15
  52. package/dist/{install-BtLWTk6H.js.map → install-BEHp7Mo-.js.map} +1 -1
  53. package/dist/install.js +1 -1
  54. package/dist/{lab-MHzp-_J8.js → lab-D61eaX6f.js} +68 -61
  55. package/dist/{lab-MHzp-_J8.js.map → lab-D61eaX6f.js.map} +1 -1
  56. package/dist/lab.js +4 -4
  57. package/dist/{records-B6gpM_3e.js → records-D4aQXyZu.js} +193 -133
  58. package/dist/records-D4aQXyZu.js.map +1 -0
  59. package/dist/{runtime-7vfl_bI3.js → runtime-q-R0CNLh.js} +400 -33
  60. package/dist/runtime-q-R0CNLh.js.map +1 -0
  61. package/dist/runtime.d.ts +1 -1
  62. package/dist/runtime.js +7 -7
  63. package/dist/{shell-Cs9OJMnm.js → shell-6CGFvUUw.js} +10 -5
  64. package/dist/{shell-Cs9OJMnm.js.map → shell-6CGFvUUw.js.map} +1 -1
  65. package/dist/shell.d.ts +1 -0
  66. package/dist/shell.js +6 -6
  67. package/dist/stores/index.d.ts +1 -1
  68. package/dist/stores/index.js +2 -2
  69. package/dist/stores/settings.d.ts +4 -12
  70. package/dist/styles.css +1191 -479
  71. package/dist/types/components.d.ts +2 -0
  72. package/dist/types/index.d.ts +1 -1
  73. package/dist/types/jobs.d.ts +9 -0
  74. package/dist/types/platform.d.ts +1 -1
  75. package/dist/{useDropdownState-8yWMu-7_.js → useDropdownState-uFIz6Yv8.js} +16 -4
  76. package/dist/useDropdownState-uFIz6Yv8.js.map +1 -0
  77. package/dist/{useExperimentSamples-DHnxKFAF.js → useExperimentSamples-QEFoq4tf.js} +48 -6
  78. package/dist/useExperimentSamples-QEFoq4tf.js.map +1 -0
  79. package/dist/{useListReorder-BitdUhxn.js → useListReorder-DsuzwWGS.js} +122 -172
  80. package/dist/useListReorder-DsuzwWGS.js.map +1 -0
  81. package/dist/{useRackEditor-DiwCt77M.js → useRackEditor-KpZ4iJ6o.js} +22 -32
  82. package/dist/useRackEditor-KpZ4iJ6o.js.map +1 -0
  83. package/dist/useRealtime-B58Lo-Tm.js +508 -0
  84. package/dist/useRealtime-B58Lo-Tm.js.map +1 -0
  85. package/dist/{useRequest-D506eHi8.js → useRequest-BuDmW8Jc.js} +8 -37
  86. package/dist/useRequest-BuDmW8Jc.js.map +1 -0
  87. package/package.json +3 -2
  88. package/src/__stories__/experiment-helpers.ts +3 -3
  89. package/src/__tests__/components/ActionMenu.test.ts +2 -2
  90. package/src/__tests__/components/AppTopBar.test.ts +206 -1
  91. package/src/__tests__/components/AppTopBarOnboarding.test.ts +160 -0
  92. package/src/__tests__/components/AppTopBarPageSelector.test.ts +125 -9
  93. package/src/__tests__/components/BaseButton.test.ts +19 -0
  94. package/src/__tests__/components/BaseModal.test.ts +13 -0
  95. package/src/__tests__/components/ExperimentSelectorModal.test.ts +34 -1
  96. package/src/__tests__/components/FloatingPanelsInModal.test.ts +94 -0
  97. package/src/__tests__/components/GeneratedPluginView.test.ts +1 -1
  98. package/src/__tests__/components/JobsStatusTray.test.ts +373 -71
  99. package/src/__tests__/components/PluginWorkspaceView.test.ts +63 -1
  100. package/src/__tests__/components/SampleSelector.test.ts +20 -0
  101. package/src/__tests__/components/SettingsModal.test.ts +9 -2
  102. package/src/__tests__/composables/useFileImport.test.ts +36 -0
  103. package/src/__tests__/composables/useFloatingPanel.test.ts +68 -0
  104. package/src/__tests__/composables/useJobCenter.test.ts +262 -3
  105. package/src/__tests__/composables/useOnboarding.test.ts +317 -0
  106. package/src/__tests__/docs/frontendDocsCatalog.test.ts +4 -3
  107. package/src/__tests__/fixtures/table-import/cells.xlsx +0 -0
  108. package/src/__tests__/fixtures/table-import/samples.xlsx +0 -0
  109. package/src/__tests__/stores/settings.test.ts +21 -21
  110. package/src/__tests__/styles/cssTokens.test.ts +4 -1
  111. package/src/__tests__/styles/menusShellRefresh.test.ts +8 -2
  112. package/src/__tests__/styles/paletteTokens.test.ts +44 -0
  113. package/src/__tests__/styles/v2AuditRules.test.ts +104 -0
  114. package/src/components/ActionMenu.vue +6 -37
  115. package/src/components/AppTopBar.onboarding.story.vue +69 -0
  116. package/src/components/AppTopBar.story.vue +59 -4
  117. package/src/components/AppTopBar.vue +132 -10
  118. package/src/components/BaseButton.vue +16 -2
  119. package/src/components/BaseModal.vue +2 -1
  120. package/src/components/ExperimentSelectorModal.story.vue +38 -0
  121. package/src/components/ExperimentSelectorModal.vue +20 -1
  122. package/src/components/GeneratedPluginView.vue +4 -9
  123. package/src/components/GeneratedResultRenderer.vue +3 -4
  124. package/src/components/JobsStatusTray.story.vue +97 -43
  125. package/src/components/JobsStatusTray.vue +287 -290
  126. package/src/components/LayoutResizeHandle.story.vue +65 -4
  127. package/src/components/PluginWorkspaceView.props.ts +3 -0
  128. package/src/components/PluginWorkspaceView.vue +17 -0
  129. package/src/components/SampleSelector.story.vue +21 -6
  130. package/src/components/SampleSelector.vue +10 -3
  131. package/src/components/SectionCard.story.vue +33 -0
  132. package/src/components/SettingsModal.vue +7 -12
  133. package/src/components/Tooltip.vue +9 -32
  134. package/src/components/internal/AppTopBarOnboardingInternal.vue +172 -0
  135. package/src/components/internal/AppTopBarPageSelectorInternal.vue +140 -72
  136. package/src/components/internal/DatePickerDateInternal.vue +7 -45
  137. package/src/components/internal/DatePickerDateTimeInternal.vue +7 -1
  138. package/src/components/internal/DatePickerTimeInternal.vue +8 -1
  139. package/src/components/internal/NumberUnitInputInternal.vue +4 -18
  140. package/src/components/internal/OnboardingLayerInternal.vue +173 -0
  141. package/src/components/internal/SelectMultipleInternal.vue +6 -1
  142. package/src/components/internal/SelectSearchableInternal.vue +6 -1
  143. package/src/components/internal/WellEditPopupInternal.vue +11 -22
  144. package/src/components/internal/WorkspaceShellInternal.vue +12 -0
  145. package/src/composables/pluginWorkspaceModel.ts +2 -2
  146. package/src/composables/useFileImport.ts +51 -1
  147. package/src/composables/useFloatingPanel.ts +107 -0
  148. package/src/composables/useJobCenter.ts +188 -6
  149. package/src/composables/useOnboarding.ts +465 -0
  150. package/src/composables/usePluginWorkspace.ts +1 -1
  151. package/src/data.ts +1 -0
  152. package/src/runtime.ts +2 -1
  153. package/src/shell.ts +17 -0
  154. package/src/stores/index.ts +0 -2
  155. package/src/stores/settings.ts +20 -50
  156. package/src/styles/components/action-menu.css +1 -1
  157. package/src/styles/components/app-layout.css +6 -3
  158. package/src/styles/components/app-page-selector.css +120 -45
  159. package/src/styles/components/app-top-bar.css +165 -3
  160. package/src/styles/components/button.css +8 -0
  161. package/src/styles/components/dataframe.css +1 -1
  162. package/src/styles/components/date-picker.css +4 -2
  163. package/src/styles/components/datetime-picker.css +4 -4
  164. package/src/styles/components/experiment-selector-modal.css +13 -0
  165. package/src/styles/components/jobs-status-tray.css +134 -296
  166. package/src/styles/components/layout-resize-handle.css +59 -16
  167. package/src/styles/components/modal.css +3 -3
  168. package/src/styles/components/number-input.css +1 -1
  169. package/src/styles/components/onboarding.css +368 -0
  170. package/src/styles/components/progress-bar.css +3 -3
  171. package/src/styles/components/sample-selector.css +43 -0
  172. package/src/styles/components/searchable-select.css +3 -7
  173. package/src/styles/components/section-card.css +30 -6
  174. package/src/styles/components/settings-modal.css +7 -2
  175. package/src/styles/components/time-picker.css +3 -6
  176. package/src/styles/components/toast.css +1 -1
  177. package/src/styles/components/tooltip.css +2 -2
  178. package/src/styles/components/unit-input.css +1 -1
  179. package/src/styles/components/well-edit-popup.css +2 -2
  180. package/src/styles/index.css +1 -0
  181. package/src/styles/variables.css +153 -15
  182. package/src/types/components.ts +2 -0
  183. package/src/types/index.ts +1 -0
  184. package/src/types/jobs.ts +10 -0
  185. package/src/types/platform.ts +1 -1
  186. package/dist/AppSidebar-Dq9W1jE2.js.map +0 -1
  187. package/dist/ExperimentSelectorModal-65peG4Up.js.map +0 -1
  188. package/dist/SettingsModal-483nyI6b.js.map +0 -1
  189. package/dist/experiment-43OJtH7R.js.map +0 -1
  190. package/dist/records-B6gpM_3e.js.map +0 -1
  191. package/dist/runtime-7vfl_bI3.js.map +0 -1
  192. package/dist/useDropdownState-8yWMu-7_.js.map +0 -1
  193. package/dist/useExperimentSamples-DHnxKFAF.js.map +0 -1
  194. package/dist/useListReorder-BitdUhxn.js.map +0 -1
  195. package/dist/useRackEditor-DiwCt77M.js.map +0 -1
  196. package/dist/useRealtime-C3iy9QkU.js +0 -699
  197. package/dist/useRealtime-C3iy9QkU.js.map +0 -1
  198. package/dist/useRequest-D506eHi8.js.map +0 -1
@@ -3,15 +3,20 @@ import { ref } from 'vue'
3
3
  import LayoutResizeHandle from './LayoutResizeHandle.vue'
4
4
 
5
5
  const width = ref(320)
6
+ const gapWidth = ref(320)
7
+ const edgeWidth = ref(280)
8
+ const height = ref(50)
9
+
10
+ const panel = 'background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-lg); padding: 0.75rem;'
11
+ const caption = 'margin: 0 0 0.5rem; font: 0.75rem var(--font-mono); color: var(--text-secondary);'
6
12
  </script>
7
13
 
8
14
  <template>
9
15
  <Story title="Layout/LayoutResizeHandle">
10
16
  <Variant title="Keyboard splitter">
17
+ <!-- Directly between two panels, the handle is the gutter. -->
11
18
  <div style="display: flex; height: 16rem; padding: 2rem;">
12
- <div :style="{ width: `${width}px`, background: 'var(--bg-secondary)', border: '1px solid var(--border-color)' }">
13
- Panel width: {{ width }}px
14
- </div>
19
+ <div :style="`${panel} width: ${width}px;`">Panel width: {{ width }}px</div>
15
20
  <LayoutResizeHandle
16
21
  v-model="width"
17
22
  orientation="vertical"
@@ -19,7 +24,63 @@ const width = ref(320)
19
24
  :min="240"
20
25
  :max="480"
21
26
  />
22
- <div style="flex: 1; background: var(--bg-tertiary);" />
27
+ <div :style="`${panel} flex: 1;`" />
28
+ </div>
29
+ </Variant>
30
+
31
+ <Variant title="Gaps and edge">
32
+ <div style="display: grid; gap: 1.5rem; padding: 2rem;">
33
+ <div data-gap="flex-8">
34
+ <p :style="caption">flex, 8px gutter (compact), handle between the panels</p>
35
+ <div style="display: flex; height: 8rem; --mint-gutter: 0.5rem;">
36
+ <div :style="`${panel} width: ${gapWidth}px;`">Left panel</div>
37
+ <LayoutResizeHandle v-model="gapWidth" orientation="vertical" label="Resize left panel (8px)" :min="200" :max="480" />
38
+ <div :style="`${panel} flex: 1;`">Right panel</div>
39
+ </div>
40
+ </div>
41
+ <div data-gap="wrapper-12">
42
+ <p :style="caption">flex, 12px gutter, handle in a gutter-wide wrapper</p>
43
+ <div style="display: flex; height: 8rem; --mint-gutter: 0.75rem;">
44
+ <div :style="`${panel} width: ${gapWidth}px;`">Left panel</div>
45
+ <div style="display: flex; justify-content: center; flex-shrink: 0; width: var(--mint-gutter);">
46
+ <LayoutResizeHandle v-model="gapWidth" orientation="vertical" label="Resize left panel (12px)" :min="200" :max="480" />
47
+ </div>
48
+ <div :style="`${panel} flex: 1;`">Right panel</div>
49
+ </div>
50
+ </div>
51
+ <div data-gap="grid-8">
52
+ <p :style="caption">grid, 8px track</p>
53
+ <div :style="`display: grid; grid-template-columns: ${gapWidth}px 0.5rem 1fr; height: 8rem;`">
54
+ <div :style="panel">Left panel</div>
55
+ <LayoutResizeHandle
56
+ v-model="gapWidth"
57
+ orientation="vertical"
58
+ label="Resize left panel (grid)"
59
+ :min="200"
60
+ :max="480"
61
+ style="--mint-layout-resize-gap: 0.5rem;"
62
+ />
63
+ <div :style="panel">Right panel</div>
64
+ </div>
65
+ </div>
66
+ <div data-gap="edge">
67
+ <p :style="caption">edge="right", 12px gutter</p>
68
+ <div style="display: flex; gap: var(--mint-gutter); height: 8rem; --mint-gutter: 0.75rem;">
69
+ <aside :style="`${panel} position: relative; width: ${edgeWidth}px;`">
70
+ Sidebar
71
+ <LayoutResizeHandle v-model="edgeWidth" orientation="vertical" edge="right" label="Resize sidebar" :min="200" :max="480" />
72
+ </aside>
73
+ <div :style="`${panel} flex: 1;`">Main</div>
74
+ </div>
75
+ </div>
76
+ <div data-gap="horizontal">
77
+ <p :style="caption">horizontal, 8px gutter</p>
78
+ <div style="display: flex; flex-direction: column; height: 14rem; --mint-gutter: 0.5rem;">
79
+ <div :style="`${panel} flex: 0 0 ${height}%;`">Top panel</div>
80
+ <LayoutResizeHandle v-model="height" orientation="horizontal" label="Resize top panel" :min="20" :max="80" :step="1" />
81
+ <div :style="`${panel} flex: 1;`">Bottom panel</div>
82
+ </div>
83
+ </div>
23
84
  </div>
24
85
  </Variant>
25
86
  </Story>
@@ -4,6 +4,7 @@ import type {
4
4
  PillNavOption,
5
5
  PluginWorkspaceViewSpec,
6
6
  } from '../types'
7
+ import type { JobState } from '../types/jobs'
7
8
  import type {
8
9
  ControlComponentBinding,
9
10
  ControlComponentBindingsById,
@@ -46,6 +47,8 @@ export interface PluginWorkspaceViewEmits {
46
47
  'section-action': [sectionId: string, actionId: string]
47
48
  'form-submit': [sectionId: string, values: Record<string, unknown>]
48
49
  'form-cancel': [sectionId: string]
50
+ /** A job row in the jobs tray, or the just-completed job on the top-bar chip, was chosen. */
51
+ 'job-select': [job: JobState]
49
52
  }
50
53
 
51
54
  /** Typed `emit` function shape produced by `defineEmits<PluginWorkspaceViewEmits>()`. */
@@ -20,9 +20,12 @@ import type {
20
20
  PillNavItem,
21
21
  PillNavOption,
22
22
  } from '../types'
23
+ import type { JobState } from '../types/jobs'
23
24
  import { useAppExperiment } from '../composables/useAppExperiment'
25
+ import { useJobCenter } from '../composables/useJobCenter'
24
26
  import { usePluginWorkspace } from '../composables/usePluginWorkspace'
25
27
  import AppToastContainer from './AppToastContainer.vue'
28
+ import JobsStatusTray from './JobsStatusTray.vue'
26
29
  import WorkspaceControlsInternal from './internal/WorkspaceControlsInternal.vue'
27
30
  import WorkspaceShellInternal from './internal/WorkspaceShellInternal.vue'
28
31
  import WorkspaceSidebarInternal from './internal/WorkspaceSidebarInternal.vue'
@@ -76,6 +79,14 @@ if (props.spec.experiment) {
76
79
  })
77
80
  }
78
81
 
82
+ // The job center is setup-time structure too: no `jobs`, no jobs control.
83
+ const jobs = props.spec.jobs
84
+ const jobCenter = jobs ? useJobCenter({ source: jobs }) : null
85
+
86
+ function handleJobSelect(job: JobState) {
87
+ emit('job-select', job)
88
+ }
89
+
79
90
  const {
80
91
  shell,
81
92
  sidebar,
@@ -105,6 +116,11 @@ function handleFormCancel(sectionId: string) {
105
116
  <template>
106
117
  <WorkspaceShellInternal
107
118
  :shell="shell"
119
+ :job="jobCenter ? jobCenter.topBarJob.value : undefined"
120
+ :jobs-done="jobCenter?.completedCount.value"
121
+ :jobs-open="jobCenter?.isOpen.value"
122
+ :jobs-panel-id="jobCenter?.panelId"
123
+ @job-click="jobCenter?.handleTopBarClick(handleJobSelect)"
108
124
  @pill-select="handlePillSelect"
109
125
  @pill-option-select="(option: PillNavOption, item: PillNavItem) => emit('pill-option-select', option, item)"
110
126
  @settings-values-change="emit('settings-values-change', $event)"
@@ -158,6 +174,7 @@ function handleFormCancel(sectionId: string) {
158
174
  </template>
159
175
  </WorkspaceControlsInternal>
160
176
  <AppToastContainer v-if="spec.toasts !== false" />
177
+ <JobsStatusTray v-if="jobCenter" :center="jobCenter" @select="handleJobSelect" />
161
178
  </template>
162
179
  </WorkspaceShellInternal>
163
180
  </template>
@@ -21,6 +21,13 @@ const mockGroups: SampleGroup[] = [
21
21
  { name: 'Vehicle', color: '#F59E0B', samples: ['Vehicle_Rep1', 'Vehicle_Rep2', 'Vehicle_Rep3'] },
22
22
  ]
23
23
 
24
+ // 1024 selected runs in two groups: the long count must not push the actions out.
25
+ const manyRuns = Array.from({ length: 1024 }, (_, i) => `Run_${String(i + 1).padStart(4, '0')}`)
26
+ const manyGroups: SampleGroup[] = [
27
+ { name: 'Batch A', color: '#3B82F6', samples: manyRuns.slice(0, 512) },
28
+ { name: 'Batch B', color: '#10B981', samples: manyRuns.slice(512) },
29
+ ]
30
+
24
31
  const manualSamples = [
25
32
  'Pt001_TumorA_d7_rep1',
26
33
  'Pt001_TumorA_d7_rep1_reseq2024',
@@ -136,12 +143,20 @@ function handleSmartGroup(result: AutoGroupResult, state: { groups: SampleGroup[
136
143
  </Variant>
137
144
 
138
145
  <Variant title="Narrow Header">
139
- <div style="padding: 2rem; width: 320px;">
140
- <SampleSelector
141
- :model-value="['Control_Rep1']"
142
- :samples="mockSamples"
143
- :groups="mockGroups"
144
- />
146
+ <!-- At 17.625rem of header content width and below, the grouping button shows only its icon and the count shortens. -->
147
+ <div style="display: flex; align-items: flex-start; gap: 2rem; padding: 2rem;">
148
+ <div v-for="width in [220, 300, 340]" :key="width" :data-width="width" :style="{ width: `${width}px` }">
149
+ <p style="margin: 0 0 0.5rem; font: 0.75rem var(--font-mono); color: var(--text-secondary);">{{ width }}px</p>
150
+ <SampleSelector
151
+ :model-value="['Control_Rep1']"
152
+ :samples="mockSamples"
153
+ :groups="mockGroups"
154
+ />
155
+ </div>
156
+ <div data-width="320-long" style="width: 320px; height: 22rem;">
157
+ <p style="margin: 0 0 0.5rem; font: 0.75rem var(--font-mono); color: var(--text-secondary);">320px, 1024 selected</p>
158
+ <SampleSelector :model-value="manyRuns" :samples="manyRuns" :groups="manyGroups" />
159
+ </div>
145
160
  </div>
146
161
  </Variant>
147
162
 
@@ -256,6 +256,8 @@ function addNewGroup() {
256
256
  newGroupName.value = ''
257
257
  }
258
258
 
259
+ const groupingLabel = computed(() => (props.enableSmartGroup ? 'Smart group' : 'Group'))
260
+
259
261
  /** Selected samples among the resolved list, for the select-all header. */
260
262
  const selectedCount = computed(() => resolvedSamples.value.filter(s => props.modelValue.includes(s)).length)
261
263
 
@@ -271,16 +273,21 @@ defineExpose({ handleSmartGroupApply })
271
273
  <div class="mint-sample-selector">
272
274
  <div class="mint-sample-selector__header">
273
275
  <span class="mint-sample-selector__title">Samples</span>
274
- <span class="mint-sample-selector__selected">{{ selectedCount }} selected</span>
276
+ <!-- A narrow selector shows "N sel."; screen readers keep "N selected". -->
277
+ <span class="mint-sample-selector__selected">
278
+ <span class="mint-sample-selector__selected-full">{{ selectedCount }} selected</span>
279
+ <span class="mint-sample-selector__selected-short" aria-hidden="true">{{ selectedCount }} sel.</span>
280
+ </span>
275
281
  <div v-if="!isPlaceholderState && enableGrouping" class="mint-sample-selector__header-actions">
276
282
  <BaseButton
277
283
  variant="ghost"
278
284
  size="sm"
279
285
  :icon="enableSmartGroup ? WandSparkles : Layers"
280
- class="mint-sample-selector__action-btn"
286
+ :title="groupingLabel"
287
+ class="mint-sample-selector__action-btn mint-sample-selector__group-btn"
281
288
  @click="openGroupingModal(groupingActionMode)"
282
289
  >
283
- {{ enableSmartGroup ? 'Smart group' : 'Group' }}
290
+ {{ groupingLabel }}
284
291
  </BaseButton>
285
292
  <BaseButton
286
293
  v-if="internalGroups.length > 0"
@@ -107,6 +107,39 @@ function initPlayground() {
107
107
  </div>
108
108
  </Variant>
109
109
 
110
+ <Variant title="Rows with lead">
111
+ <div style="padding: 2rem; max-width: 600px; display: grid; gap: 1rem;">
112
+ <SectionCard title="Experiments" count="3 total" :icon="flaskIcon" accent="experiment" uppercase>
113
+ <div class="mint-section-card__rows">
114
+ <a v-for="e in experiments" :key="e.code" href="#" class="mint-section-card__row">
115
+ <span class="mint-section-card__row-lead">
116
+ <span style="width: 0.375rem; height: 0.375rem; border-radius: 50%; background: var(--mint-success)" />
117
+ </span>
118
+ <span style="flex: 1; min-width: 0">
119
+ <span style="display: block; font-size: 0.8125rem; font-weight: 500; color: var(--text-primary)">{{ e.name }}</span>
120
+ <span style="display: block; font-family: var(--font-mono); font-size: 0.65625rem; color: var(--text-muted)">{{ e.code }}</span>
121
+ </span>
122
+ <span style="flex-shrink: 0; font-size: 0.71875rem; color: var(--text-muted)">{{ e.updated }}</span>
123
+ </a>
124
+ </div>
125
+ </SectionCard>
126
+ <SectionCard title="Projects" count="2 active" :icon="folderIcon" accent="project" uppercase>
127
+ <div class="mint-section-card__rows">
128
+ <a href="#" class="mint-section-card__row">
129
+ <span class="mint-section-card__row-lead" aria-hidden="true" />
130
+ <span style="flex: 1; font-size: 0.8125rem; font-weight: 500; color: var(--text-primary)">Hypoxia metabolic rewiring</span>
131
+ <span style="font-size: 0.71875rem; color: var(--text-muted)">12 experiments</span>
132
+ </a>
133
+ <a href="#" class="mint-section-card__row">
134
+ <span class="mint-section-card__row-lead" aria-hidden="true" />
135
+ <span style="flex: 1; font-size: 0.8125rem; font-weight: 500; color: var(--text-primary)">Serine one-carbon flux</span>
136
+ <span style="font-size: 0.71875rem; color: var(--text-muted)">7 experiments</span>
137
+ </a>
138
+ </div>
139
+ </SectionCard>
140
+ </div>
141
+ </Variant>
142
+
110
143
  <Variant title="Detail card with footer">
111
144
  <div style="padding: 2rem; max-width: 600px;">
112
145
  <SectionCard title="Analysis artifacts" :count="4" divider="light">
@@ -358,13 +358,8 @@ const themeOptions: { value: ThemeMode; label: string; halves: ('light' | 'dark'
358
358
  { value: 'system', label: 'System', halves: ['light', 'dark'] },
359
359
  ]
360
360
 
361
- /** Five stops along the palette's primary → accent hue arc (shortest way round), as in the board. */
362
- function paletteSwatches(key: ColorPalette): string[] {
363
- const [from, to] = colorPalettes[key].hues
364
- const span = ((to - from + 540) % 360) - 180
365
- const tone = key === 'pastel' ? '60% 72%' : '65% 48%'
366
- return Array.from({ length: 5 }, (_, i) => `hsl(${Math.round(from + (span * i) / 4)} ${tone})`)
367
- }
361
+ /** Accent tokens a palette swatch shows; `data-palette` on the strip resolves them per palette. */
362
+ const paletteSwatchTokens = ['--color-primary', '--color-primary-hover', '--color-primary-soft', '--color-primary-border']
368
363
 
369
364
  const densityPreviewRows = [
370
365
  ['S-0012', '2.41', '12.5'],
@@ -562,7 +557,7 @@ function canShowForCurrentUser(item: AccessControlled): boolean {
562
557
  <div class="mint-settings-modal__setting">
563
558
  <div class="mint-settings-modal__setting-text">
564
559
  <span class="mint-settings-modal__setting-label">Color palette</span>
565
- <span class="mint-settings-modal__setting-hint">Primary and accent hues across the interface</span>
560
+ <span class="mint-settings-modal__setting-hint">Accent colour across the interface</span>
566
561
  </div>
567
562
  <div class="mint-settings-modal__palettes">
568
563
  <button
@@ -573,12 +568,12 @@ function canShowForCurrentUser(item: AccessControlled): boolean {
573
568
  :class="['mint-settings-modal__palette', { 'mint-settings-modal__palette--active': settings.colorPalette === key }]"
574
569
  @click="settings.colorPalette = key as ColorPalette"
575
570
  >
576
- <span class="mint-settings-modal__swatches" aria-hidden="true">
571
+ <span class="mint-settings-modal__swatches" :data-palette="key" aria-hidden="true">
577
572
  <span
578
- v-for="color in paletteSwatches(key as ColorPalette)"
579
- :key="color"
573
+ v-for="token in paletteSwatchTokens"
574
+ :key="token"
580
575
  class="mint-settings-modal__swatch"
581
- :style="{ backgroundColor: color }"
576
+ :style="{ backgroundColor: `var(${token})` }"
582
577
  />
583
578
  </span>
584
579
  <span class="mint-settings-modal__palette-name">{{ palette.name }}</span>
@@ -1,8 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  /** Floating tooltip using @floating-ui with auto-flip, delay, optional keyboard shortcut badge, and body Teleport. */
3
- import { ref, onBeforeUnmount, nextTick } from 'vue'
4
- import { computePosition, flip, offset, shift, type Placement } from '@floating-ui/dom'
5
- import { useEventListener } from '../composables/useEventListener'
3
+ import { ref, onBeforeUnmount } from 'vue'
4
+ import { useFloatingPanel } from '../composables/useFloatingPanel'
6
5
 
7
6
  interface Props {
8
7
  text: string
@@ -21,40 +20,19 @@ const props = withDefaults(defineProps<Props>(), {
21
20
 
22
21
  const tooltipId = `mint-tooltip-${Math.random().toString(36).slice(2, 9)}`
23
22
  const visible = ref(false)
24
- const actualPlacement = ref<Placement>(props.position)
25
23
  const triggerRef = ref<HTMLElement | null>(null)
26
24
  const contentRef = ref<HTMLElement | null>(null)
27
- const coords = ref({ top: '0px', left: '0px' })
28
25
  let timeoutId: ReturnType<typeof setTimeout> | undefined
29
26
 
30
- async function updatePosition() {
31
- if (!triggerRef.value || !contentRef.value) return
32
-
33
- const { x, y, placement } = await computePosition(triggerRef.value, contentRef.value, {
34
- placement: props.position,
35
- middleware: [
36
- offset(6),
37
- flip(),
38
- shift({ padding: 8 }),
39
- ],
40
- })
41
-
42
- coords.value = { top: `${y}px`, left: `${x}px` }
43
- actualPlacement.value = placement
44
- }
45
-
46
- useEventListener(() => window, 'scroll', updatePosition, {
47
- passive: true,
48
- capture: true,
49
- enabled: visible,
27
+ // Follows the trigger while visible; the placement after flip picks the arrow side.
28
+ const { style: coords, placement: actualPlacement } = useFloatingPanel(triggerRef, contentRef, visible, {
29
+ placement: () => props.position,
30
+ offset: 6,
50
31
  })
51
- useEventListener(() => window, 'resize', updatePosition, { enabled: visible })
52
32
 
53
- async function show() {
54
- timeoutId = setTimeout(async () => {
33
+ function show() {
34
+ timeoutId = setTimeout(() => {
55
35
  visible.value = true
56
- await nextTick()
57
- await updatePosition()
58
36
  }, props.delay)
59
37
  }
60
38
 
@@ -96,8 +74,7 @@ function resolvedMaxWidth(value: string | number | undefined): string | undefine
96
74
  maxWidth ? 'mint-tooltip__content--multiline' : '',
97
75
  ]"
98
76
  :style="{
99
- top: coords.top,
100
- left: coords.left,
77
+ ...coords,
101
78
  maxWidth: resolvedMaxWidth(maxWidth),
102
79
  }"
103
80
  role="tooltip"
@@ -0,0 +1,172 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * The top-bar onboarding controls: the "Getting started n/m" chip while the
4
+ * checklist is open, and the Help button with the checklist, tours and news.
5
+ */
6
+ import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
7
+ import { autoUpdate, computePosition, offset, shift } from '@floating-ui/dom'
8
+ import { Check, CircleHelp, Play } from 'lucide-vue-next'
9
+ import { useEventListener } from '../../composables/useEventListener'
10
+ import type { OnboardingState } from '../../composables/useOnboarding'
11
+ import OnboardingLayerInternal from './OnboardingLayerInternal.vue'
12
+
13
+ const props = defineProps<{ state: OnboardingState }>()
14
+
15
+ const root = ref<HTMLElement | null>(null)
16
+ const helpButton = ref<HTMLElement | null>(null)
17
+ const panel = ref<HTMLElement | null>(null)
18
+ const panelPosition = ref({ x: 0, y: 0 })
19
+ const open = ref(false)
20
+ const hasContent = computed(() =>
21
+ props.state.tasks.value.length > 0 || props.state.tours.value.length > 0 || props.state.news.value.length > 0)
22
+ const progress = computed(() => {
23
+ const total = props.state.tasks.value.length
24
+ return total ? Math.round((props.state.tasksDone.value / total) * 100) : 0
25
+ })
26
+
27
+ function toggle() {
28
+ open.value = !open.value
29
+ if (open.value) props.state.refresh()
30
+ }
31
+
32
+ // The panel goes to the body, as the top bar clips its overflow; it follows the button.
33
+ let stopAutoUpdate: (() => void) | undefined
34
+ watch(open, async (isOpen) => {
35
+ stopAutoUpdate?.()
36
+ stopAutoUpdate = undefined
37
+ if (!isOpen) return
38
+ await nextTick()
39
+ const button = helpButton.value
40
+ const floating = panel.value
41
+ if (!button || !floating || !open.value) return
42
+ stopAutoUpdate = autoUpdate(button, floating, async () => {
43
+ const { x, y } = await computePosition(button, floating, {
44
+ strategy: 'fixed',
45
+ placement: 'bottom-end',
46
+ middleware: [offset(8), shift({ padding: 8 })],
47
+ })
48
+ panelPosition.value = { x, y }
49
+ })
50
+ })
51
+ onBeforeUnmount(() => stopAutoUpdate?.())
52
+
53
+ function run(action: () => void) {
54
+ open.value = false
55
+ action()
56
+ }
57
+
58
+ useEventListener(document, 'pointerdown', (event) => {
59
+ const target = event.target as Node
60
+ if (open.value && !root.value?.contains(target) && !panel.value?.contains(target)) open.value = false
61
+ })
62
+ useEventListener(document, 'keydown', (event) => {
63
+ if (open.value && event.key === 'Escape') open.value = false
64
+ })
65
+ </script>
66
+
67
+ <template>
68
+ <div v-if="hasContent" ref="root" class="mint-onboarding-help">
69
+ <button
70
+ v-if="state.checklistVisible.value"
71
+ type="button"
72
+ class="mint-onboarding-help__chip"
73
+ :class="{ 'mint-onboarding-help__chip--open': open }"
74
+ data-onboarding="topbar-checklist"
75
+ :aria-expanded="open"
76
+ @click="toggle"
77
+ >
78
+ Getting started
79
+ <span class="mint-onboarding-help__count">{{ state.tasksDone.value }}/{{ state.tasks.value.length }}</span>
80
+ <span class="mint-onboarding-help__bar" aria-hidden="true"><span :style="{ width: `${progress}%` }" /></span>
81
+ </button>
82
+ <button
83
+ ref="helpButton"
84
+ type="button"
85
+ class="mint-topbar__settings-btn"
86
+ :class="{ 'mint-topbar__settings-btn--open': open }"
87
+ data-onboarding="topbar-help"
88
+ :aria-label="state.unseenNews.value ? `Help, ${state.unseenNews.value} new` : 'Help'"
89
+ :aria-expanded="open"
90
+ @click="toggle"
91
+ >
92
+ <CircleHelp class="mint-topbar__settings-icon" aria-hidden="true" />
93
+ <span v-if="state.unseenNews.value" class="mint-onboarding-help__badge" aria-hidden="true">{{ state.unseenNews.value }}</span>
94
+ </button>
95
+
96
+ <Teleport to="body">
97
+ <div
98
+ v-if="open"
99
+ ref="panel"
100
+ class="mint-popover mint-onboarding-help__panel"
101
+ role="dialog"
102
+ aria-label="Help"
103
+ :style="{ left: `${panelPosition.x}px`, top: `${panelPosition.y}px` }"
104
+ >
105
+ <template v-if="state.tasks.value.length">
106
+ <div class="mint-onboarding-help__header">
107
+ <span class="mint-onboarding-help__heading">Getting started</span>
108
+ <span class="mint-onboarding-help__count">{{ state.tasksDone.value }}/{{ state.tasks.value.length }}</span>
109
+ </div>
110
+ <div class="mint-onboarding-help__progress" aria-hidden="true"><span :style="{ width: `${progress}%` }" /></div>
111
+ <ul class="mint-onboarding-help__todos">
112
+ <li
113
+ v-for="task in state.tasks.value"
114
+ :key="task.id"
115
+ :class="['mint-onboarding-help__todo', { 'mint-onboarding-help__todo--done': task.done }]"
116
+ >
117
+ <span class="mint-onboarding-help__check" role="img" :aria-label="task.done ? 'Done' : 'Not done'">
118
+ <Check v-if="task.done" :size="10" :stroke-width="3" aria-hidden="true" />
119
+ </span>
120
+ <span class="mint-onboarding-help__todo-title">{{ task.title }}</span>
121
+ <button
122
+ v-if="task.action && !task.done"
123
+ type="button"
124
+ class="mint-onboarding-help__link"
125
+ @click="run(task.action.run)"
126
+ >
127
+ {{ task.action.label }}
128
+ </button>
129
+ </li>
130
+ </ul>
131
+ <button
132
+ v-if="state.tasksDone.value < state.tasks.value.length"
133
+ type="button"
134
+ class="mint-onboarding-help__link mint-onboarding-help__hide"
135
+ @click="state.setChecklistHidden(!state.checklistHidden.value)"
136
+ >
137
+ {{ state.checklistHidden.value ? 'Show checklist in the top bar' : 'Hide checklist from the top bar' }}
138
+ </button>
139
+ </template>
140
+
141
+ <template v-if="state.tours.value.length">
142
+ <div class="mint-onboarding-help__section">Tours</div>
143
+ <button
144
+ v-for="tour in state.tours.value"
145
+ :key="tour.id"
146
+ type="button"
147
+ class="mint-menu-item"
148
+ @click="run(() => state.startTour(tour.id))"
149
+ >
150
+ <Play class="mint-onboarding-help__icon" aria-hidden="true" />
151
+ {{ tour.title }}
152
+ </button>
153
+ </template>
154
+
155
+ <template v-if="state.news.value.length">
156
+ <div class="mint-onboarding-help__section">What's new</div>
157
+ <button
158
+ v-for="item in state.news.value"
159
+ :key="item.id"
160
+ type="button"
161
+ class="mint-menu-item"
162
+ @click="run(() => state.showNews(item.id))"
163
+ >
164
+ {{ item.title }}
165
+ <span v-if="item.unseen" class="mint-onboarding-help__new">New</span>
166
+ </button>
167
+ </template>
168
+ </div>
169
+ </Teleport>
170
+ </div>
171
+ <OnboardingLayerInternal :state="state" />
172
+ </template>