@morscherlab/mint-sdk 1.5.0-beta.1 → 1.5.0-beta.10

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 (174) hide show
  1. package/dist/{AlertBox-CIhqsWbN.js → AlertBox-Cai0w7gF.js} +3 -3
  2. package/dist/{AlertBox-CIhqsWbN.js.map → AlertBox-Cai0w7gF.js.map} +1 -1
  3. package/dist/{AppSidebar-BDrd7Kwn.js → AppSidebar-C-Vq5x4I.js} +410 -127
  4. package/dist/AppSidebar-C-Vq5x4I.js.map +1 -0
  5. package/dist/{ConfirmDialog-CjWoxAD1.js → ConfirmDialog-BjFsOpBe.js} +3 -3
  6. package/dist/ConfirmDialog-BjFsOpBe.js.map +1 -0
  7. package/dist/{ExperimentPopover-BM92LlrQ.js → ExperimentPopover-DYy_s0kI.js} +25 -37
  8. package/dist/ExperimentPopover-DYy_s0kI.js.map +1 -0
  9. package/dist/{ExperimentSelectorModal-LsB1ABmh.js → ExperimentSelectorModal-Bfh5bdkv.js} +37 -8
  10. package/dist/ExperimentSelectorModal-Bfh5bdkv.js.map +1 -0
  11. package/dist/{SegmentedControl-Dz41vAFM.js → SegmentedControl-B8KgyOjd.js} +2 -2
  12. package/dist/{SegmentedControl-Dz41vAFM.js.map → SegmentedControl-B8KgyOjd.js.map} +1 -1
  13. package/dist/{SettingsModal-C-MSR-bk.js → SettingsModal-DwfrD1ER.js} +7 -7
  14. package/dist/{SettingsModal-C-MSR-bk.js.map → SettingsModal-DwfrD1ER.js.map} +1 -1
  15. package/dist/__tests__/components/AppTopBarCompact.test.d.ts +1 -0
  16. package/dist/__tests__/components/AppTopBarCompactReset.test.d.ts +1 -0
  17. package/dist/__tests__/components/AutoGroupModal.experimentLoad.test.d.ts +1 -0
  18. package/dist/components/AppTopBar.compact.d.ts +5 -0
  19. package/dist/components/AppTopBar.vue.d.ts +17 -17
  20. package/dist/components/FormBuilder.vue.d.ts +1 -1
  21. package/dist/components/PluginIcon.vue.d.ts +2 -2
  22. package/dist/components/PluginWorkspaceView.vue.d.ts +4 -0
  23. package/dist/components/SectionCard.vue.d.ts +13 -1
  24. package/dist/components/SettingsModal.vue.d.ts +1 -1
  25. package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -2
  26. package/dist/components/internal/FormSchemaInternal.vue.d.ts +9 -9
  27. package/dist/components/internal/FormSectionRenderer.vue.d.ts +4 -4
  28. package/dist/components/internal/LucideIconInternal.vue.d.ts +17 -0
  29. package/dist/composables/appLayoutSidebar.d.ts +9 -0
  30. package/dist/composables/useExperimentSelector.d.ts +2 -0
  31. package/dist/composables/useOnboarding.d.ts +1 -1
  32. package/dist/composables/usePluginWorkspace.d.ts +2 -0
  33. package/dist/composables/useVersionedSave.d.ts +5 -2
  34. package/dist/{controlSchemaModel-BbXbMgc6.js → controlSchemaModel-BtFNZj8t.js} +2 -2
  35. package/dist/{controlSchemaModel-BbXbMgc6.js.map → controlSchemaModel-BtFNZj8t.js.map} +1 -1
  36. package/dist/{data-LzdkEZJ-.js → data-CJztFns8.js} +31 -15
  37. package/dist/{data-LzdkEZJ-.js.map → data-CJztFns8.js.map} +1 -1
  38. package/dist/data.js +8 -8
  39. package/dist/{experimentStatus-DCn2QKG7.js → experimentStatus-Deokk7Br.js} +2 -2
  40. package/dist/{experimentStatus-DCn2QKG7.js.map → experimentStatus-Deokk7Br.js.map} +1 -1
  41. package/dist/index.d.ts +0 -1
  42. package/dist/index.js +15 -20
  43. package/dist/index.js.map +1 -1
  44. package/dist/{install-UTqrAgHe.js → install-Bk02tJ5M.js} +15 -40
  45. package/dist/install-Bk02tJ5M.js.map +1 -0
  46. package/dist/install.js +1 -1
  47. package/dist/{lab-CbdDLAVr.js → lab-BYhb_cJI.js} +47 -117
  48. package/dist/lab-BYhb_cJI.js.map +1 -0
  49. package/dist/lab.js +4 -4
  50. package/dist/{records-CWRztE5M.js → records-GtiVMlTQ.js} +207 -171
  51. package/dist/records-GtiVMlTQ.js.map +1 -0
  52. package/dist/{runtime-DrL6lKAb.js → runtime-aZPUPxoB.js} +112 -54
  53. package/dist/runtime-aZPUPxoB.js.map +1 -0
  54. package/dist/runtime.js +6 -6
  55. package/dist/{shell-Ch7cJnjo.js → shell-Bk9mD4Lc.js} +5 -5
  56. package/dist/{shell-Ch7cJnjo.js.map → shell-Bk9mD4Lc.js.map} +1 -1
  57. package/dist/shell.js +5 -5
  58. package/dist/styles.css +315 -134
  59. package/dist/styles.d.ts +1 -0
  60. package/dist/types/components.d.ts +22 -2
  61. package/dist/types/generated-ui.d.ts +2 -0
  62. package/dist/types/index.d.ts +2 -2
  63. package/dist/types/instrument.d.ts +5 -17
  64. package/dist/{useDropdownState-uFIz6Yv8.js → useDropdownState-CcBzUYZ6.js} +14 -5
  65. package/dist/useDropdownState-CcBzUYZ6.js.map +1 -0
  66. package/dist/{useExperimentSamples-Cz2v3V_q.js → useExperimentSamples-CpNXvpUn.js} +3 -3
  67. package/dist/{useExperimentSamples-Cz2v3V_q.js.map → useExperimentSamples-CpNXvpUn.js.map} +1 -1
  68. package/dist/{useListReorder-QqJc1dpX.js → useListReorder-Bp4EAI2w.js} +16 -4
  69. package/dist/useListReorder-Bp4EAI2w.js.map +1 -0
  70. package/dist/{useRackEditor-BHGM1Ngz.js → useRackEditor-i2bdPUFf.js} +156 -27
  71. package/dist/useRackEditor-i2bdPUFf.js.map +1 -0
  72. package/dist/{useRealtime-CNQbrS01.js → useRealtime-BLQEjbuq.js} +3 -3
  73. package/dist/{useRealtime-CNQbrS01.js.map → useRealtime-BLQEjbuq.js.map} +1 -1
  74. package/dist/useRequest-CXyPw3eh.js +76 -0
  75. package/dist/useRequest-CXyPw3eh.js.map +1 -0
  76. package/dist/utils/deprecation.d.ts +2 -0
  77. package/dist/utils/lucideIcon.d.ts +4 -0
  78. package/package.json +7 -4
  79. package/src/__tests__/components/AppTopBar.test.ts +102 -20
  80. package/src/__tests__/components/AppTopBarCompact.test.ts +65 -0
  81. package/src/__tests__/components/AppTopBarCompactReset.test.ts +24 -0
  82. package/src/__tests__/components/AppTopBarOnboarding.test.ts +10 -0
  83. package/src/__tests__/components/AutoGroupModal.experimentLoad.test.ts +58 -0
  84. package/src/__tests__/components/BaseModal.test.ts +33 -2
  85. package/src/__tests__/components/ConfirmDialog.test.ts +2 -2
  86. package/src/__tests__/components/ExperimentPopover.test.ts +32 -0
  87. package/src/__tests__/components/ExperimentSelectorModal.test.ts +16 -0
  88. package/src/__tests__/components/PluginWorkspaceView.test.ts +20 -1
  89. package/src/__tests__/components/SectionCard.test.ts +33 -0
  90. package/src/__tests__/components/deprecationTags.test.ts +4 -18
  91. package/src/__tests__/composables/useJobCenter.test.ts +35 -0
  92. package/src/__tests__/composables/useJobsEndpoint.test.ts +50 -0
  93. package/src/__tests__/composables/useOnboarding.test.ts +2 -2
  94. package/src/__tests__/composables/useVersionedSave.test.ts +71 -6
  95. package/src/__tests__/docs/frontendDocsCatalog.test.ts +3 -3
  96. package/src/__tests__/styles/menusShellRefresh.test.ts +10 -0
  97. package/src/components/AppLayout.story.vue +5 -5
  98. package/src/components/AppLayout.vue +13 -1
  99. package/src/components/AppSidebar.vue +2 -2
  100. package/src/components/AppTopBar.compact.ts +60 -0
  101. package/src/components/AppTopBar.story.vue +39 -14
  102. package/src/components/AppTopBar.vue +222 -89
  103. package/src/components/BaseModal.vue +16 -5
  104. package/src/components/CollapsibleCard.vue +1 -1
  105. package/src/components/ConfirmDialog.vue +1 -1
  106. package/src/components/ExperimentPopover.story.vue +2 -2
  107. package/src/components/ExperimentPopover.vue +6 -22
  108. package/src/components/ExperimentSelectorModal.vue +35 -0
  109. package/src/components/FileUploader.vue +3 -35
  110. package/src/components/GeneratedPluginView.vue +34 -81
  111. package/src/components/GeneratedResultRenderer.vue +2 -1
  112. package/src/components/InstrumentStatusPanels.vue +3 -3
  113. package/src/components/PlotlyChart.vue +1 -1
  114. package/src/components/PluginIcon.story.vue +2 -2
  115. package/src/components/PluginIcon.vue +2 -2
  116. package/src/components/PluginWorkspaceView.vue +10 -1
  117. package/src/components/SectionCard.vue +42 -6
  118. package/src/components/internal/AppTopBarOnboardingInternal.vue +30 -14
  119. package/src/components/internal/AppTopBarPageSelectorInternal.vue +12 -0
  120. package/src/components/internal/AutoGroupModalInternal.vue +14 -3
  121. package/src/components/internal/LucideIconInternal.vue +23 -0
  122. package/src/composables/appLayoutSidebar.ts +11 -0
  123. package/src/composables/index.ts +1 -2
  124. package/src/composables/useExperimentSelector.ts +4 -0
  125. package/src/composables/useJobCenter.ts +21 -1
  126. package/src/composables/useJobsEndpoint.ts +15 -4
  127. package/src/composables/useOnboarding.ts +3 -3
  128. package/src/composables/usePluginWorkspace.ts +15 -4
  129. package/src/composables/useVersionedSave.ts +32 -7
  130. package/src/generated-ui/main.ts +3 -1
  131. package/src/index.ts +0 -3
  132. package/src/styles/components/alert-box.css +1 -1
  133. package/src/styles/components/app-layout.css +5 -0
  134. package/src/styles/components/app-sidebar.css +5 -5
  135. package/src/styles/components/app-top-bar.css +130 -9
  136. package/src/styles/components/button.css +3 -3
  137. package/src/styles/components/checkbox.css +1 -1
  138. package/src/styles/components/collapsible-card.css +1 -1
  139. package/src/styles/components/confirm-dialog.css +2 -2
  140. package/src/styles/components/file-uploader.css +3 -3
  141. package/src/styles/components/icon-button.css +3 -3
  142. package/src/styles/components/loading-spinner.css +1 -1
  143. package/src/styles/components/onboarding.css +39 -0
  144. package/src/styles/components/pill.css +1 -1
  145. package/src/styles/components/plugin-icon.css +2 -2
  146. package/src/styles/components/presence-avatars.css +4 -4
  147. package/src/styles/components/progress-bar.css +1 -1
  148. package/src/styles/components/section-card.css +65 -6
  149. package/src/styles/components/sequence-input.css +1 -1
  150. package/src/styles/components/slider.css +1 -1
  151. package/src/styles/components/toggle.css +1 -1
  152. package/src/styles/variables.css +10 -4
  153. package/src/types/components.ts +23 -2
  154. package/src/types/generated-ui.ts +2 -0
  155. package/src/types/index.ts +1 -1
  156. package/src/types/instrument.ts +5 -18
  157. package/src/utils/deprecation.ts +8 -0
  158. package/src/utils/lucideIcon.ts +25 -0
  159. package/src/utils/lucideIconIndex.d.ts +2 -0
  160. package/dist/AppSidebar-BDrd7Kwn.js.map +0 -1
  161. package/dist/ConfirmDialog-CjWoxAD1.js.map +0 -1
  162. package/dist/ExperimentPopover-BM92LlrQ.js.map +0 -1
  163. package/dist/ExperimentSelectorModal-LsB1ABmh.js.map +0 -1
  164. package/dist/deprecatedRootExports.d.ts +0 -191
  165. package/dist/install-UTqrAgHe.js.map +0 -1
  166. package/dist/lab-CbdDLAVr.js.map +0 -1
  167. package/dist/records-CWRztE5M.js.map +0 -1
  168. package/dist/runtime-DrL6lKAb.js.map +0 -1
  169. package/dist/useDropdownState-uFIz6Yv8.js.map +0 -1
  170. package/dist/useListReorder-QqJc1dpX.js.map +0 -1
  171. package/dist/useRackEditor-BHGM1Ngz.js.map +0 -1
  172. package/dist/useRequest-Bfsh7gIm.js +0 -215
  173. package/dist/useRequest-Bfsh7gIm.js.map +0 -1
  174. package/src/deprecatedRootExports.ts +0 -226
@@ -298,6 +298,12 @@ export function useJobCenter(
298
298
  watch([source, ownActiveJobs], ([currentSource, now], [previousSource, before]) => {
299
299
  if (currentSource !== previousSource) {
300
300
  justFinished.value = null
301
+ // The failures a replacement source already lists are its baseline.
302
+ if (previousSource) {
303
+ hasBaseline = false
304
+ awaitingLoad = false
305
+ if (jobs.value.length > 0) takeBaseline()
306
+ }
301
307
  return
302
308
  }
303
309
  const nowIds = new Set(now.map(job => job.job_id))
@@ -405,6 +411,16 @@ export function useJobCenter(
405
411
  }
406
412
  }
407
413
 
414
+ // A cancelled job stays pending until the source reports another status.
415
+ const cancelling = new Map<string, JobStatus>()
416
+ watch(jobs, (list) => {
417
+ for (const [jobId, status] of cancelling) {
418
+ if (list.find(job => job.job_id === jobId)?.status === status) continue
419
+ cancelling.delete(jobId)
420
+ setPending(jobId, false)
421
+ }
422
+ })
423
+
408
424
  async function cancelJob(job: JobState): Promise<boolean> {
409
425
  const sourceCancel = source.value?.cancel
410
426
  if (!job.can_cancel || !sourceCancel || pendingJobIds.value.has(job.job_id)) {
@@ -412,14 +428,18 @@ export function useJobCenter(
412
428
  }
413
429
  setPending(job.job_id, true)
414
430
  error.value = null
431
+ let cancelled = false
415
432
  try {
416
433
  await sourceCancel(job.job_id)
434
+ cancelled = true
417
435
  return active
418
436
  } catch (cause) {
419
437
  if (active) error.value = errorMessage(cause, 'Failed to cancel job')
420
438
  return false
421
439
  } finally {
422
- if (active) setPending(job.job_id, false)
440
+ const status = jobs.value.find(listed => listed.job_id === job.job_id)?.status
441
+ if (cancelled && status === job.status) cancelling.set(job.job_id, status)
442
+ else if (active) setPending(job.job_id, false)
423
443
  }
424
444
  }
425
445
 
@@ -677,16 +677,27 @@ export function useJobsEndpoint(
677
677
  ]))
678
678
  jobs.value = nextJobs
679
679
  for (const job of nextJobs) settleJobWaiters(job)
680
- for (const jobId of previousJobIds) {
681
- if (!nextJobIds.has(jobId)) {
682
- rejectJobWaiters(jobId, new Error(`Job ${jobId} is no longer available`))
683
- }
680
+ for (const jobId of [...jobWaiters.keys()]) {
681
+ if (!nextJobIds.has(jobId)) void confirmMissingJob(jobId)
684
682
  }
685
683
  pruneResultState()
686
684
  pruneJobRevisions()
687
685
  return nextJobs
688
686
  }
689
687
 
688
+ /**
689
+ * A stream snapshot built before a submit can arrive after the submit reply,
690
+ * so a waited job missing from it is gone only once the server says so.
691
+ */
692
+ async function confirmMissingJob(jobId: string): Promise<void> {
693
+ try {
694
+ await getJob(jobId)
695
+ } catch (cause) {
696
+ if (isRetryableJobLookupError(cause)) return
697
+ rejectJobWaiters(jobId, new Error(`Job ${jobId} is no longer available`))
698
+ }
699
+ }
700
+
690
701
  function upsertJob(
691
702
  input: GeneratedAnalysisJobInput,
692
703
  fallbackDefinitionId?: string,
@@ -108,7 +108,7 @@ export interface OnboardingState {
108
108
  showNews: (id: string) => void
109
109
  next: () => void
110
110
  back: () => void
111
- /** End the run (× or Esc). Every step of the tour counts as seen. */
111
+ /** End the run (× or Esc). Every step of every tour of the app counts as seen; Help can still play them. */
112
112
  dismiss: () => void
113
113
  complete: (taskId: string) => void
114
114
  setChecklistHidden: (hidden: boolean) => void
@@ -381,8 +381,8 @@ export function provideOnboarding(definition: OnboardingDefinition, options: Pro
381
381
 
382
382
  function dismiss() {
383
383
  autoStartAbort?.abort()
384
- const tour = run?.tour
385
- if (tour) markSeen(...tour.steps.map((step) => stepKey(tour, step)))
384
+ // × on a tour ends all tours of the app, so the user closes tours at most once per app.
385
+ if (run?.tour) markSeen(...(definition.tours ?? []).flatMap((tour) => tour.steps.map((step) => stepKey(tour, step))))
386
386
  stop()
387
387
  }
388
388
 
@@ -1,4 +1,4 @@
1
- import { computed } from 'vue'
1
+ import { computed, ref } from 'vue'
2
2
  import type { ComputedRef } from 'vue'
3
3
  import type { PillNavItem, PluginWorkspaceSidebarSpec } from '../types'
4
4
  import type {
@@ -48,6 +48,8 @@ export interface UsePluginWorkspaceReturn {
48
48
  handleControlValuesUpdate: (values: Record<string, unknown>) => void
49
49
  /** Resolves an AppTopBar `pill-select` payload into the active view and emits `update:pill` and `pill-select`. */
50
50
  handlePillSelect: (item: PillNavItem) => void
51
+ /** Applies an AppSidebar `update:collapsed` payload and emits `update:sidebarCollapsed`. */
52
+ handleSidebarCollapsedUpdate: (value: boolean) => void
51
53
  }
52
54
 
53
55
  /**
@@ -69,7 +71,7 @@ export function usePluginWorkspace(options: UsePluginWorkspaceOptions): UsePlugi
69
71
  const sidebarSpec = computed<false | PluginWorkspaceSidebarSpec>(() => props.spec?.sidebar ?? {})
70
72
  const topBarSpec = computed(() => props.spec?.topBar === false
71
73
  ? false
72
- : applySpecDefaults({ themeToggle: true, settings: {}, standaloneLabel: false }, props.spec?.topBar))
74
+ : applySpecDefaults({ settings: {}, standaloneLabel: false }, props.spec?.topBar))
73
75
 
74
76
  const controlsState = usePluginWorkspaceControls({
75
77
  model: () => sidebarSpec.value === false ? undefined : sidebarSpec.value.controls,
@@ -94,6 +96,14 @@ export function usePluginWorkspace(options: UsePluginWorkspaceOptions): UsePlugi
94
96
  const topBarSlotNames = computed<string[]>(() => getPluginWorkspaceTopBarSlotNames(slots))
95
97
  const sidebarSlotNames = computed<string[]>(() => getPluginWorkspaceSidebarSlotNames(slots))
96
98
 
99
+ // The workspace owns the collapsed state (unless bound) so AppLayout reserves the collapsed width.
100
+ const internalCollapsed = ref(false)
101
+ const sidebarCollapsed = computed(() => props.sidebarCollapsed ?? internalCollapsed.value)
102
+ function handleSidebarCollapsedUpdate(value: boolean) {
103
+ internalCollapsed.value = value
104
+ emit('update:sidebarCollapsed', value)
105
+ }
106
+
97
107
  const hasSidebarSurface = computed(() => hasPluginWorkspaceSidebarSurface({
98
108
  sidebar: sidebarSpec.value,
99
109
  hasSidebarSlot: Boolean(slots.sidebar),
@@ -114,7 +124,7 @@ export function usePluginWorkspace(options: UsePluginWorkspaceOptions): UsePlugi
114
124
  const sidebar = sidebarSpec.value === false ? {} : sidebarSpec.value
115
125
  return {
116
126
  sidebarPosition: sidebar.position ?? 'left',
117
- sidebarWidth: sidebar.width,
127
+ sidebarWidth: sidebarCollapsed.value ? (sidebar.collapsedWidth ?? '3rem') : sidebar.width,
118
128
  floating: props.spec?.floating ?? true,
119
129
  responsiveSidebar: sidebar.responsive ?? true,
120
130
  sidebarToggleLabel: sidebar.toggleLabel ?? 'Open workspace sidebar',
@@ -142,7 +152,7 @@ export function usePluginWorkspace(options: UsePluginWorkspaceOptions): UsePlugi
142
152
  },
143
153
  activeView: navigationState.resolvedActiveView.value,
144
154
  values: controlsState.internalControlValues,
145
- collapsed: props.sidebarCollapsed,
155
+ collapsed: sidebarCollapsed.value,
146
156
  formLoading: props.formLoading ?? false,
147
157
  formDisabled: props.formDisabled ?? false,
148
158
  formReadonly: props.formReadonly ?? false,
@@ -157,5 +167,6 @@ export function usePluginWorkspace(options: UsePluginWorkspaceOptions): UsePlugi
157
167
  hasSidebarSurface,
158
168
  handleControlValuesUpdate: controlsState.handleControlValuesUpdate,
159
169
  handlePillSelect: navigationState.handlePillSelect,
170
+ handleSidebarCollapsedUpdate,
160
171
  }
161
172
  }
@@ -26,8 +26,11 @@ export interface UseVersionedSaveOptions<T> {
26
26
  /**
27
27
  * Refetch the entity and reset the form to it. The user's edits are discarded.
28
28
  * Reject when no fresh record arrives: the form then keeps the edits and the conflict.
29
+ * `auto` is true for the refetch after another user's write on a clean form: when
30
+ * the form holds edits by the time the record arrives, skip the reset (the stale
31
+ * banner then shows the write).
29
32
  */
30
- reload: () => unknown
33
+ reload: (auto?: boolean) => unknown
31
34
  /**
32
35
  * The realtime topic of the open entity (`experiment:42`). `null` turns the
33
36
  * stale banner off: pass it while the form is closed or the platform's
@@ -43,7 +46,7 @@ export interface UseVersionedSaveOptions<T> {
43
46
  }
44
47
 
45
48
  export interface UseVersionedSaveReturn<T> {
46
- /** Save with `If-Match`. Resolves `undefined` on a 412: `conflict` is then set. Other errors reject. */
49
+ /** Save with `If-Match`. Resolves `undefined` on a 412 (`conflict` set, `stale` cleared) and, unsent, while a save runs. Other errors reject. */
47
50
  save: () => Promise<T | undefined>
48
51
  /** Save again on top of the version the 412 reported. Resolves `undefined` on another 412. */
49
52
  overwrite: () => Promise<T | undefined>
@@ -99,8 +102,11 @@ export function useVersionedSave<T>(options: UseVersionedSaveOptions<T>): UseVer
99
102
  const conflict = ref<VersionConflict | null>(null)
100
103
  const stale = ref<StaleChange | null>(null)
101
104
  const fieldLabel = options.fieldLabel ?? fieldNameLabel
105
+ // A newer reload or another topic makes the result of an automatic reload obsolete.
106
+ let reloadToken = 0
102
107
 
103
108
  async function send(version: number | null | undefined): Promise<T | undefined> {
109
+ if (saving.value) return undefined
104
110
  saving.value = true
105
111
  try {
106
112
  const headers: Record<string, string> = version == null ? {} : { 'If-Match': `"v${version}"` }
@@ -111,7 +117,9 @@ export function useVersionedSave<T>(options: UseVersionedSaveOptions<T>): UseVer
111
117
  } catch (cause) {
112
118
  const refused = conflictVersion(cause)
113
119
  if (!refused) throw cause
120
+ // One banner: the conflict dialog replaces the stale banner.
114
121
  conflict.value = refused
122
+ stale.value = null
115
123
  return undefined
116
124
  } finally {
117
125
  saving.value = false
@@ -119,19 +127,35 @@ export function useVersionedSave<T>(options: UseVersionedSaveOptions<T>): UseVer
119
127
  }
120
128
 
121
129
  async function reload(): Promise<void> {
130
+ reloadToken += 1
122
131
  await options.reload()
123
132
  conflict.value = null
124
133
  stale.value = null
125
134
  }
126
135
 
127
- function onEvent(event: RealtimeEvent): void {
128
- // A resync names no writer, and the user's own save echoes back while it runs.
129
- if (event.type === 'resync' || saving.value || event.topic !== toValue(options.topic)) return
130
- if (!toValue(options.dirty)) {
136
+ async function autoReload(event: RealtimeEvent): Promise<void> {
137
+ const token = ++reloadToken
138
+ try {
139
+ await options.reload(true)
140
+ } catch {
131
141
  // A failed background refetch leaves a clean form as it is; the host reports its own faults.
132
- reload().catch(() => {})
133
142
  return
134
143
  }
144
+ if (token !== reloadToken || conflict.value) return
145
+ // The user started editing during the refetch: the host kept the edits, so show the write.
146
+ if (toValue(options.dirty)) markStale(event)
147
+ else stale.value = null
148
+ }
149
+
150
+ function onEvent(event: RealtimeEvent): void {
151
+ // A resync names no writer, the user's own save echoes back while it runs,
152
+ // and an open conflict dialog already says that the record changed.
153
+ if (event.type === 'resync' || saving.value || conflict.value || event.topic !== toValue(options.topic)) return
154
+ if (!toValue(options.dirty)) void autoReload(event)
155
+ else markStale(event)
156
+ }
157
+
158
+ function markStale(event: RealtimeEvent): void {
135
159
  const actor = (event.actor_id && options.userName?.(event.actor_id)) || 'Someone'
136
160
  const previous = stale.value
137
161
  stale.value = {
@@ -147,6 +171,7 @@ export function useVersionedSave<T>(options: UseVersionedSaveOptions<T>): UseVer
147
171
  }, onEvent)
148
172
  // Another entity, or a closed form: the old writes and the old 412 no longer apply.
149
173
  watch(() => toValue(options.topic), () => {
174
+ reloadToken += 1
150
175
  conflict.value = null
151
176
  stale.value = null
152
177
  })
@@ -2,6 +2,7 @@ import { createPinia } from 'pinia'
2
2
  import { createApp, h } from 'vue'
3
3
 
4
4
  import GeneratedPluginView from '../components/GeneratedPluginView.vue'
5
+ import { useSettingsStore } from '../stores/settings'
5
6
  import '../styles/index.css'
6
7
 
7
8
  function generatedApiBase(location: Location): string {
@@ -13,9 +14,10 @@ const app = createApp({
13
14
  name: 'MintGeneratedUiApp',
14
15
  render: () => h(GeneratedPluginView, {
15
16
  baseUrl: generatedApiBase(window.location),
16
- showStandaloneLabel: true,
17
17
  }),
18
18
  })
19
19
 
20
20
  app.use(createPinia())
21
+ // Theme, palette and density apply before the first paint, as in the platform.
22
+ useSettingsStore().initialize()
21
23
  app.mount('#app')
package/src/index.ts CHANGED
@@ -16,8 +16,5 @@ export * from './composables'
16
16
  // Stores
17
17
  export * from './stores'
18
18
 
19
- // Root names outside L1 and L2, deprecated until MINT 1.5
20
- export * from './deprecatedRootExports'
21
-
22
19
  // Types
23
20
  export type * from './types'
@@ -31,7 +31,7 @@
31
31
  .mint-alert__icon--info { background-color: var(--mint-info-bg); color: var(--mint-info); }
32
32
  .mint-alert__icon--neutral { background-color: var(--bg-tertiary); color: var(--text-secondary); }
33
33
  .mint-alert__icon--primary { background-color: var(--color-primary-soft); color: var(--color-primary); }
34
- .mint-alert__icon--cta { background-color: var(--color-primary-soft); color: var(--color-cta-hover); }
34
+ .mint-alert__icon--cta { background-color: var(--color-primary-soft); color: var(--_ink-primary); }
35
35
 
36
36
  .mint-alert__content {
37
37
  flex: 1;
@@ -182,3 +182,8 @@
182
182
  transition: none;
183
183
  }
184
184
  }
185
+
186
+ /* An AppTopBar in the layout renders the drawer button itself. */
187
+ .mint-layout--responsive-sidebar:has(.mint-topbar__sidebar-toggle) .mint-layout__sidebar-toggle {
188
+ display: none;
189
+ }
@@ -97,8 +97,8 @@
97
97
  min-width: 1.375rem;
98
98
  padding: 0.125rem 0.375rem;
99
99
  border-radius: var(--radius);
100
- background: color-mix(in srgb, var(--color-cta) 14%, transparent);
101
- color: var(--color-cta);
100
+ background: color-mix(in srgb, var(--color-primary) 14%, transparent);
101
+ color: var(--color-primary);
102
102
  font-size: 0.6875rem;
103
103
  font-weight: 700;
104
104
  line-height: 1.1;
@@ -369,7 +369,7 @@
369
369
  width: 11px;
370
370
  height: 11px;
371
371
  border-radius: 9999px;
372
- background: white;
372
+ background: var(--control-knob);
373
373
  box-shadow: var(--shadow-sm);
374
374
  transition: left 180ms var(--mint-ease-out-quart);
375
375
  }
@@ -469,7 +469,7 @@
469
469
  padding: 0 0.25rem;
470
470
  border-radius: var(--radius);
471
471
  background: var(--mint-sidebar-tone);
472
- color: white;
472
+ color: var(--text-on-accent);
473
473
  font-size: 0.6875rem;
474
474
  font-weight: 700;
475
475
  line-height: 1rem;
@@ -492,7 +492,7 @@
492
492
  .mint-sidebar__rail-badge--cta,
493
493
  .mint-sidebar__tag--cta,
494
494
  .mint-sidebar__group-action--cta {
495
- --mint-sidebar-tone: var(--color-cta);
495
+ --mint-sidebar-tone: var(--color-primary);
496
496
  }
497
497
 
498
498
  .mint-sidebar__rail-badge--success,
@@ -12,6 +12,8 @@
12
12
  padding: 0.75rem 1rem;
13
13
  background-color: var(--bg-secondary);
14
14
  box-sizing: border-box;
15
+ /* The two-row layout follows the bar's own width, not the window's. */
16
+ container: mint-topbar / inline-size;
15
17
  }
16
18
 
17
19
  /* Main container layout — equal side columns keep the centre nav on the bar
@@ -72,11 +74,62 @@
72
74
  grid-column: 3;
73
75
  display: flex;
74
76
  align-items: center;
75
- gap: 0.125rem;
77
+ gap: 0.375rem;
76
78
  min-width: 0;
77
79
  justify-content: flex-end;
78
80
  }
79
81
 
82
+ /* Context group (standalone badge, run slot, onboarding, plugin buttons), then
83
+ the fixed tail: theme, settings and the host's account controls, always last. */
84
+ .mint-topbar__context,
85
+ .mint-topbar__tail {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 0.125rem;
89
+ min-width: 0;
90
+ }
91
+
92
+ .mint-topbar__tail {
93
+ flex-shrink: 0;
94
+ }
95
+
96
+ /* The deprecated #actions slot keeps a fixed width, so its content cannot push the bar. */
97
+ .mint-topbar__legacy-actions {
98
+ display: inline-flex;
99
+ align-items: center;
100
+ max-width: 16rem;
101
+ overflow: hidden;
102
+ }
103
+
104
+ /* Below 1024px of bar width: row 1 is identity + tail, row 2 the pills + context.
105
+ Row 1 never holds more than the identity, so the tail never moves. */
106
+ @container mint-topbar (max-width: 1023.98px) {
107
+ .mint-topbar__container--stackable {
108
+ /* The pills keep their width while it fits; the context group takes the rest and its experiment
109
+ name truncates. On a bar narrower than the pills they overflow their cell, never the tail. */
110
+ grid-template-columns: minmax(0, max-content) minmax(0, 1fr) auto;
111
+ grid-template-areas:
112
+ "start start tail"
113
+ "center context context";
114
+ row-gap: 0.375rem;
115
+ }
116
+
117
+ .mint-topbar__container--stackable .mint-topbar__start { grid-area: start; }
118
+ .mint-topbar__container--stackable .mint-topbar__right { display: contents; }
119
+ .mint-topbar__container--stackable .mint-topbar__tail { grid-area: tail; }
120
+
121
+ .mint-topbar__container--stackable .mint-topbar__center {
122
+ grid-area: center;
123
+ justify-content: flex-start;
124
+ }
125
+
126
+ .mint-topbar__container--stackable .mint-topbar__context {
127
+ /* Stretch, not justify-self: end, which would size the group to its nowrap min-content. */
128
+ grid-area: context;
129
+ justify-content: flex-end;
130
+ }
131
+ }
132
+
80
133
  /* Logo styles */
81
134
  .mint-topbar__logo {
82
135
  display: flex;
@@ -170,17 +223,20 @@
170
223
  .mint-topbar__plugin-name,
171
224
  .mint-topbar__plugin-subtitle,
172
225
  .mint-topbar-title,
173
- .mint-topbar-subtitle {
226
+ .mint-topbar-subtitle,
227
+ .mint-topbar__title-only {
174
228
  overflow: hidden;
175
229
  text-overflow: ellipsis;
176
230
  white-space: nowrap;
177
231
  }
178
232
 
179
- /* The settings button has the IconButton ghost look: 32px, --radius-md,
180
- 16px icon, neutral ink, offset focus ring. */
181
- .mint-topbar__settings-btn {
233
+ /* The settings button, plugin icon buttons and the drawer button have the
234
+ IconButton ghost look: 32px, --radius-md, 16px icon, neutral ink, offset focus ring. */
235
+ .mint-topbar__settings-btn,
236
+ .mint-topbar__icon-btn {
182
237
  position: relative;
183
238
  display: inline-flex;
239
+ flex-shrink: 0;
184
240
  align-items: center;
185
241
  justify-content: center;
186
242
  width: 2rem;
@@ -195,23 +251,62 @@
195
251
  transition: background-color var(--mint-transition), color var(--mint-transition);
196
252
  }
197
253
 
198
- .mint-topbar__settings-btn:hover {
254
+ .mint-topbar__settings-btn:hover,
255
+ .mint-topbar__icon-btn:hover:not(:disabled) {
199
256
  background: var(--bg-hover);
200
257
  color: var(--text-primary);
201
258
  }
202
259
 
203
- .mint-topbar__settings-btn:focus-visible {
260
+ .mint-topbar__settings-btn:focus-visible,
261
+ .mint-topbar__icon-btn:focus-visible {
204
262
  outline: none;
205
263
  box-shadow: var(--focus-ring-offset);
206
264
  }
207
265
 
208
- .mint-topbar__settings-icon {
266
+ .mint-topbar__icon-btn:disabled {
267
+ cursor: not-allowed;
268
+ opacity: 0.5;
269
+ }
270
+
271
+ .mint-topbar__settings-icon,
272
+ .mint-topbar__icon-btn-icon {
209
273
  width: 1rem;
210
274
  height: 1rem;
211
275
  }
212
276
 
277
+ /* Status dot of a plugin icon button (TopBarAction.dot). */
278
+ .mint-topbar__icon-btn-dot {
279
+ position: absolute;
280
+ top: 0.3125rem;
281
+ right: 0.3125rem;
282
+ width: 0.4375rem;
283
+ height: 0.4375rem;
284
+ border-radius: 50%;
285
+ box-shadow: 0 0 0 2px var(--bg-secondary);
286
+ background: var(--text-muted);
287
+ }
288
+
289
+ .mint-topbar__icon-btn-dot--primary,
290
+ .mint-topbar__icon-btn-dot--cta { background: var(--color-primary); }
291
+ .mint-topbar__icon-btn-dot--success { background: var(--mint-success); }
292
+ .mint-topbar__icon-btn-dot--warning { background: var(--mint-warning); }
293
+ .mint-topbar__icon-btn-dot--error { background: var(--mint-error); }
294
+ .mint-topbar__icon-btn-dot--info { background: var(--mint-info); }
295
+
296
+ /* The drawer button shows only while the AppLayout sidebar is a drawer. */
297
+ .mint-topbar__sidebar-toggle {
298
+ display: none;
299
+ }
300
+
301
+ @media (max-width: 1023.98px) {
302
+ .mint-topbar__sidebar-toggle {
303
+ display: inline-flex;
304
+ }
305
+ }
306
+
213
307
  @media (prefers-reduced-motion: reduce) {
214
- .mint-topbar__settings-btn {
308
+ .mint-topbar__settings-btn,
309
+ .mint-topbar__icon-btn {
215
310
  transition: none;
216
311
  }
217
312
  }
@@ -393,3 +488,29 @@
393
488
  clip-path: inset(50%);
394
489
  white-space: nowrap;
395
490
  }
491
+
492
+ /* ─── Labels or icons, by fit ───
493
+ Save and Jobs show their words while the content fits; AppTopBar.compact.ts
494
+ sets --compact when it does not (a name is cut, the experiment chip is
495
+ squeezed or a group overflows), and they keep only their icons. Plugin
496
+ buttons with an icon are always icon-only. Accessible names and tooltips
497
+ keep the words. */
498
+ .mint-topbar--compact .mint-experiment-popover__save {
499
+ width: 32px;
500
+ padding: 0;
501
+ justify-content: center;
502
+ }
503
+
504
+ .mint-topbar--compact .mint-experiment-popover__save-label,
505
+ .mint-topbar--compact .mint-topbar__job-label {
506
+ display: none;
507
+ }
508
+
509
+ .mint-topbar--compact .mint-topbar__job--idle {
510
+ padding-inline: 0.5rem;
511
+ }
512
+
513
+ /* One total width cap for the experiment chip, code and chevron included. */
514
+ .mint-topbar .mint-experiment-popover__trigger {
515
+ max-width: 16rem;
516
+ }
@@ -68,17 +68,17 @@
68
68
  }
69
69
 
70
70
  .mint-button--cta {
71
- background-color: var(--color-cta);
71
+ background-color: var(--color-primary);
72
72
  color: var(--text-on-accent);
73
73
  }
74
74
 
75
75
  .mint-button--cta:hover:not(.mint-button--disabled) {
76
- background-color: var(--color-cta-hover);
76
+ background-color: var(--color-primary-hover);
77
77
  }
78
78
 
79
79
  .mint-button--danger {
80
80
  background-color: var(--mint-error);
81
- color: white;
81
+ color: var(--text-on-danger);
82
82
  }
83
83
 
84
84
  .mint-button--danger:hover:not(.mint-button--disabled) {
@@ -133,7 +133,7 @@
133
133
  .mint-checkbox__native:disabled + .mint-checkbox__box.mint-checkbox__box--indeterminate {
134
134
  background-color: var(--text-muted);
135
135
  border-color: var(--text-muted);
136
- color: #FFFFFF;
136
+ color: var(--control-knob);
137
137
  }
138
138
 
139
139
  .mint-checkbox__box--sm {
@@ -159,7 +159,7 @@
159
159
 
160
160
  .mint-collapsible-card__badge--cta,
161
161
  .mint-collapsible-card__action--cta {
162
- --mint-sidebar-badge-color: var(--color-cta);
162
+ --mint-sidebar-badge-color: var(--color-primary);
163
163
  }
164
164
 
165
165
  .mint-collapsible-card__badge--success,
@@ -48,8 +48,8 @@
48
48
  }
49
49
 
50
50
  .mint-confirm__icon--cta {
51
- background-color: color-mix(in srgb, var(--color-cta) 12%, transparent);
52
- color: var(--color-cta);
51
+ background-color: color-mix(in srgb, var(--color-primary) 12%, transparent);
52
+ color: var(--color-primary);
53
53
  }
54
54
 
55
55
  .mint-confirm__icon--neutral {
@@ -160,9 +160,9 @@
160
160
  font-size: 0.6875rem;
161
161
  font-weight: 700;
162
162
  font-family: var(--font-mono);
163
- color: var(--badge-color);
164
- background-color: color-mix(in srgb, var(--badge-color) 10%, transparent);
165
- border: 1px solid color-mix(in srgb, var(--badge-color) 20%, transparent);
163
+ color: var(--text-secondary);
164
+ background-color: var(--bg-tertiary);
165
+ border: 1px solid var(--border-color);
166
166
  }
167
167
 
168
168
  .mint-file-uploader__file-meta {
@@ -57,17 +57,17 @@
57
57
  }
58
58
 
59
59
  .mint-icon-button--cta {
60
- background-color: var(--color-cta);
60
+ background-color: var(--color-primary);
61
61
  color: var(--text-on-accent);
62
62
  }
63
63
 
64
64
  .mint-icon-button--cta:hover:not(.mint-icon-button--disabled) {
65
- background-color: var(--color-cta-hover);
65
+ background-color: var(--color-primary-hover);
66
66
  }
67
67
 
68
68
  .mint-icon-button--danger {
69
69
  background-color: var(--mint-error);
70
- color: white;
70
+ color: var(--text-on-danger);
71
71
  }
72
72
 
73
73
  .mint-icon-button--danger:hover:not(.mint-icon-button--disabled) {
@@ -33,7 +33,7 @@
33
33
 
34
34
  /* Tones */
35
35
  .mint-spinner--primary { color: var(--color-primary); }
36
- .mint-spinner--cta { color: var(--color-cta); }
36
+ .mint-spinner--cta { color: var(--color-primary); }
37
37
  .mint-spinner--success { color: var(--mint-success); }
38
38
  .mint-spinner--warning { color: var(--mint-warning); }
39
39
  .mint-spinner--error { color: var(--mint-error); }