@morscherlab/mint-sdk 1.5.0-beta.6 → 1.5.0-beta.8
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.
- package/dist/{AlertBox-CexONPhA.js → AlertBox-Cai0w7gF.js} +3 -3
- package/dist/{AlertBox-CexONPhA.js.map → AlertBox-Cai0w7gF.js.map} +1 -1
- package/dist/{AppSidebar-FxlF5_dI.js → AppSidebar-C1DT4nsZ.js} +351 -90
- package/dist/AppSidebar-C1DT4nsZ.js.map +1 -0
- package/dist/{ConfirmDialog-D9QXFpsy.js → ConfirmDialog-BjFsOpBe.js} +2 -2
- package/dist/{ConfirmDialog-D9QXFpsy.js.map → ConfirmDialog-BjFsOpBe.js.map} +1 -1
- package/dist/{ExperimentPopover-Bbd6RaoH.js → ExperimentPopover-DYy_s0kI.js} +25 -37
- package/dist/ExperimentPopover-DYy_s0kI.js.map +1 -0
- package/dist/{ExperimentSelectorModal-CUkKmgfr.js → ExperimentSelectorModal-Bfh5bdkv.js} +5 -5
- package/dist/{ExperimentSelectorModal-CUkKmgfr.js.map → ExperimentSelectorModal-Bfh5bdkv.js.map} +1 -1
- package/dist/{SegmentedControl-C-sckoJZ.js → SegmentedControl-B8KgyOjd.js} +2 -2
- package/dist/{SegmentedControl-C-sckoJZ.js.map → SegmentedControl-B8KgyOjd.js.map} +1 -1
- package/dist/{SettingsModal-BIC0dAYc.js → SettingsModal-DPNblTe9.js} +7 -7
- package/dist/{SettingsModal-BIC0dAYc.js.map → SettingsModal-DPNblTe9.js.map} +1 -1
- package/dist/__tests__/components/AppTopBarCompact.test.d.ts +1 -0
- package/dist/__tests__/components/AppTopBarCompactReset.test.d.ts +1 -0
- package/dist/__tests__/components/AutoGroupModal.experimentLoad.test.d.ts +1 -0
- package/dist/components/AppTopBar.compact.d.ts +5 -0
- package/dist/components/AppTopBar.vue.d.ts +17 -17
- package/dist/components/PluginWorkspaceView.vue.d.ts +4 -0
- package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -2
- package/dist/components/internal/LucideIconInternal.vue.d.ts +17 -0
- package/dist/composables/appLayoutSidebar.d.ts +9 -0
- package/dist/composables/useVersionedSave.d.ts +5 -2
- package/dist/{controlSchemaModel-C_XqpkgE.js → controlSchemaModel-BtFNZj8t.js} +2 -2
- package/dist/{controlSchemaModel-C_XqpkgE.js.map → controlSchemaModel-BtFNZj8t.js.map} +1 -1
- package/dist/{data-CfY7o8QC.js → data-CJztFns8.js} +31 -15
- package/dist/{data-CfY7o8QC.js.map → data-CJztFns8.js.map} +1 -1
- package/dist/data.js +8 -8
- package/dist/{experimentStatus-DCn2QKG7.js → experimentStatus-Deokk7Br.js} +2 -2
- package/dist/{experimentStatus-DCn2QKG7.js.map → experimentStatus-Deokk7Br.js.map} +1 -1
- package/dist/index.js +14 -14
- package/dist/{install-QF3Sh82O.js → install-9FkKs0iW.js} +14 -15
- package/dist/{install-QF3Sh82O.js.map → install-9FkKs0iW.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{lab-BcX5JBSY.js → lab-BYhb_cJI.js} +22 -14
- package/dist/lab-BYhb_cJI.js.map +1 -0
- package/dist/lab.js +4 -4
- package/dist/{records-BAymyHAn.js → records-GtiVMlTQ.js} +198 -62
- package/dist/records-GtiVMlTQ.js.map +1 -0
- package/dist/{runtime-DngeJtdq.js → runtime-DcMsvlaS.js} +29 -12
- package/dist/runtime-DcMsvlaS.js.map +1 -0
- package/dist/runtime.js +6 -6
- package/dist/{shell-CWZi97nW.js → shell-XZYkAZS_.js} +5 -5
- package/dist/{shell-CWZi97nW.js.map → shell-XZYkAZS_.js.map} +1 -1
- package/dist/shell.js +5 -5
- package/dist/styles.css +136 -12
- package/dist/types/components.d.ts +22 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/{useDropdownState-uFIz6Yv8.js → useDropdownState-CcBzUYZ6.js} +14 -5
- package/dist/useDropdownState-CcBzUYZ6.js.map +1 -0
- package/dist/{useExperimentSamples-DEDAKa2E.js → useExperimentSamples-CpNXvpUn.js} +3 -3
- package/dist/{useExperimentSamples-DEDAKa2E.js.map → useExperimentSamples-CpNXvpUn.js.map} +1 -1
- package/dist/{useListReorder-W1bzK_bo.js → useListReorder-Bp4EAI2w.js} +3 -3
- package/dist/{useListReorder-W1bzK_bo.js.map → useListReorder-Bp4EAI2w.js.map} +1 -1
- package/dist/{useRackEditor-Bmtuo0aj.js → useRackEditor-i2bdPUFf.js} +8 -8
- package/dist/{useRackEditor-Bmtuo0aj.js.map → useRackEditor-i2bdPUFf.js.map} +1 -1
- package/dist/{useRealtime-jOetF2mM.js → useRealtime-BLQEjbuq.js} +3 -3
- package/dist/{useRealtime-jOetF2mM.js.map → useRealtime-BLQEjbuq.js.map} +1 -1
- package/dist/useRequest-CXyPw3eh.js +76 -0
- package/dist/useRequest-CXyPw3eh.js.map +1 -0
- package/dist/utils/deprecation.d.ts +2 -0
- package/dist/utils/lucideIcon.d.ts +4 -0
- package/package.json +1 -1
- package/src/__tests__/components/AppTopBar.test.ts +102 -20
- package/src/__tests__/components/AppTopBarCompact.test.ts +65 -0
- package/src/__tests__/components/AppTopBarCompactReset.test.ts +24 -0
- package/src/__tests__/components/AutoGroupModal.experimentLoad.test.ts +58 -0
- package/src/__tests__/components/BaseModal.test.ts +33 -2
- package/src/__tests__/components/ExperimentPopover.test.ts +32 -0
- package/src/__tests__/components/PluginWorkspaceView.test.ts +1 -1
- package/src/__tests__/composables/useJobCenter.test.ts +35 -0
- package/src/__tests__/composables/useVersionedSave.test.ts +71 -6
- package/src/__tests__/styles/menusShellRefresh.test.ts +5 -0
- package/src/components/AppLayout.story.vue +5 -5
- package/src/components/AppLayout.vue +13 -1
- package/src/components/AppTopBar.compact.ts +60 -0
- package/src/components/AppTopBar.story.vue +39 -14
- package/src/components/AppTopBar.vue +222 -89
- package/src/components/BaseModal.vue +16 -5
- package/src/components/ExperimentPopover.story.vue +2 -2
- package/src/components/ExperimentPopover.vue +6 -22
- package/src/components/GeneratedPluginView.vue +0 -1
- package/src/components/PluginWorkspaceView.vue +8 -0
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +12 -0
- package/src/components/internal/AutoGroupModalInternal.vue +14 -3
- package/src/components/internal/LucideIconInternal.vue +23 -0
- package/src/composables/appLayoutSidebar.ts +11 -0
- package/src/composables/useJobCenter.ts +21 -1
- package/src/composables/usePluginWorkspace.ts +1 -1
- package/src/composables/useVersionedSave.ts +32 -7
- package/src/styles/components/app-layout.css +5 -0
- package/src/styles/components/app-top-bar.css +128 -8
- package/src/types/components.ts +23 -2
- package/src/types/index.ts +1 -0
- package/src/utils/deprecation.ts +8 -0
- package/src/utils/lucideIcon.ts +25 -0
- package/src/utils/lucideIconIndex.d.ts +2 -0
- package/dist/AppSidebar-FxlF5_dI.js.map +0 -1
- package/dist/ExperimentPopover-Bbd6RaoH.js.map +0 -1
- package/dist/lab-BcX5JBSY.js.map +0 -1
- package/dist/records-BAymyHAn.js.map +0 -1
- package/dist/runtime-DngeJtdq.js.map +0 -1
- package/dist/useDropdownState-uFIz6Yv8.js.map +0 -1
- package/dist/useRequest-D9-3BS-3.js +0 -215
- package/dist/useRequest-D9-3BS-3.js.map +0 -1
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
PillNavItem,
|
|
11
11
|
PageSelectorItem,
|
|
12
12
|
PluginInfo,
|
|
13
|
+
TopBarAction,
|
|
13
14
|
TopBarJob,
|
|
14
15
|
} from '../types'
|
|
15
16
|
|
|
@@ -87,6 +88,17 @@ const samplePluginWithSubtitle: PluginInfo = {
|
|
|
87
88
|
api_prefix: '/api/ms-planner',
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
// Page icons are Lucide names; plugin buttons come from spec.actions (two show, the rest in More).
|
|
92
|
+
const sampleIconPages: PageSelectorItem[] = [
|
|
93
|
+
{ id: 'monitor', label: 'Monitor', icon: 'activity' },
|
|
94
|
+
{ id: 'planner', label: 'Planner', icon: 'calendar-days' },
|
|
95
|
+
{ id: 'queue', label: 'Queue', icon: 'list-ordered' },
|
|
96
|
+
]
|
|
97
|
+
const sampleActions: TopBarAction[] = [
|
|
98
|
+
{ id: 'status', label: 'Database ready', icon: 'database', dot: 'success', onSelect: () => {} },
|
|
99
|
+
{ id: 'export', label: 'Export', icon: 'download', onSelect: () => {} },
|
|
100
|
+
{ id: 'reindex', label: 'Reindex', onSelect: () => {} },
|
|
101
|
+
]
|
|
90
102
|
// The seed is module state: clear it when the variant unmounts.
|
|
91
103
|
function seedIdentity({ app }: { app: App }) {
|
|
92
104
|
providePluginIdentity({
|
|
@@ -109,7 +121,6 @@ function seedIdentity({ app }: { app: App }) {
|
|
|
109
121
|
<AppTopBar
|
|
110
122
|
:spec="{
|
|
111
123
|
pills: samplePillNav.slice(1, 3),
|
|
112
|
-
themeToggle: true,
|
|
113
124
|
homePath: '',
|
|
114
125
|
standaloneLabel: false,
|
|
115
126
|
}"
|
|
@@ -127,7 +138,7 @@ function seedIdentity({ app }: { app: App }) {
|
|
|
127
138
|
<Variant title="Refresh · Workspace">
|
|
128
139
|
<div style="padding: 2rem; background: var(--bg-primary); min-height: 22rem;">
|
|
129
140
|
<AppTopBar
|
|
130
|
-
:spec="{ pills: samplePillNav,
|
|
141
|
+
:spec="{ pills: samplePillNav, homePath: '' }"
|
|
131
142
|
:page-selector="samplePageSelector"
|
|
132
143
|
current-page-selector-id="workspace"
|
|
133
144
|
pill="workspace"
|
|
@@ -139,7 +150,7 @@ function seedIdentity({ app }: { app: App }) {
|
|
|
139
150
|
<div style="padding: 2rem; background: var(--bg-primary); min-height: 12rem;">
|
|
140
151
|
<ExperimentProvider>
|
|
141
152
|
<AppTopBar
|
|
142
|
-
:spec="{ pills: samplePillNav,
|
|
153
|
+
:spec="{ pills: samplePillNav, homePath: '' }"
|
|
143
154
|
:page-selector="samplePageSelector"
|
|
144
155
|
current-page-selector-id="experiments"
|
|
145
156
|
pill="experiments"
|
|
@@ -152,7 +163,7 @@ function seedIdentity({ app }: { app: App }) {
|
|
|
152
163
|
<div style="padding: 2rem; background: var(--bg-primary); min-height: 12rem;">
|
|
153
164
|
<ExperimentProvider :plugin="samplePlugin">
|
|
154
165
|
<AppTopBar
|
|
155
|
-
:spec="{ pills: samplePillNav,
|
|
166
|
+
:spec="{ pills: samplePillNav, homePath: '' }"
|
|
156
167
|
:page-selector="samplePluginPages"
|
|
157
168
|
current-page-selector-id="peaks"
|
|
158
169
|
pill="experiments"
|
|
@@ -168,7 +179,7 @@ function seedIdentity({ app }: { app: App }) {
|
|
|
168
179
|
<AppTopBar
|
|
169
180
|
v-for="row in runSlotJobs"
|
|
170
181
|
:key="row.label"
|
|
171
|
-
:spec="{
|
|
182
|
+
:spec="{ homePath: '' }"
|
|
172
183
|
:page-selector="samplePluginPages"
|
|
173
184
|
current-page-selector-id="peaks"
|
|
174
185
|
:job="row.job"
|
|
@@ -193,7 +204,7 @@ function seedIdentity({ app }: { app: App }) {
|
|
|
193
204
|
</div>
|
|
194
205
|
<ExperimentProvider :plugin="samplePlugin" :save="false">
|
|
195
206
|
<AppTopBar
|
|
196
|
-
:spec="{
|
|
207
|
+
:spec="{ homePath: '' }"
|
|
197
208
|
:page-selector="samplePluginPages"
|
|
198
209
|
current-page-selector-id="peaks"
|
|
199
210
|
:job="runSlotJobs[liveRow].job"
|
|
@@ -205,7 +216,7 @@ function seedIdentity({ app }: { app: App }) {
|
|
|
205
216
|
<Variant title="Refresh · Admin">
|
|
206
217
|
<div style="padding: 2rem; background: var(--bg-primary); min-height: 12rem;">
|
|
207
218
|
<AppTopBar
|
|
208
|
-
:spec="{ pills: samplePillNav,
|
|
219
|
+
:spec="{ pills: samplePillNav, homePath: '' }"
|
|
209
220
|
:page-selector="samplePageSelector"
|
|
210
221
|
current-page-selector-id="admin"
|
|
211
222
|
pill="admin"
|
|
@@ -217,7 +228,7 @@ function seedIdentity({ app }: { app: App }) {
|
|
|
217
228
|
<div style="padding: 2rem; background: var(--bg-primary); min-height: 12rem;">
|
|
218
229
|
<ExperimentProvider :preselect="false">
|
|
219
230
|
<AppTopBar
|
|
220
|
-
:spec="{ pills: samplePillNav,
|
|
231
|
+
:spec="{ pills: samplePillNav, homePath: '' }"
|
|
221
232
|
:page-selector="samplePageSelector"
|
|
222
233
|
current-page-selector-id="workspace"
|
|
223
234
|
pill="workspace"
|
|
@@ -232,7 +243,6 @@ function seedIdentity({ app }: { app: App }) {
|
|
|
232
243
|
<AppTopBar
|
|
233
244
|
:spec="{
|
|
234
245
|
pills: samplePillNav.slice(1, 3),
|
|
235
|
-
themeToggle: true,
|
|
236
246
|
homePath: '',
|
|
237
247
|
standaloneLabel: false,
|
|
238
248
|
}"
|
|
@@ -256,7 +266,6 @@ function seedIdentity({ app }: { app: App }) {
|
|
|
256
266
|
:spec="{
|
|
257
267
|
variant: state.variant,
|
|
258
268
|
logo: state.showLogo,
|
|
259
|
-
themeToggle: state.showThemeToggle,
|
|
260
269
|
homePath: state.homePath,
|
|
261
270
|
}"
|
|
262
271
|
:title="state.title"
|
|
@@ -274,7 +283,6 @@ function seedIdentity({ app }: { app: App }) {
|
|
|
274
283
|
:options="variants.map(v => ({ label: v, value: v }))"
|
|
275
284
|
/>
|
|
276
285
|
<HstCheckbox v-model="state.showLogo" title="Show Logo" />
|
|
277
|
-
<HstCheckbox v-model="state.showThemeToggle" title="Show Theme Toggle" />
|
|
278
286
|
<HstText v-model="state.homePath" title="Home Path" />
|
|
279
287
|
</template>
|
|
280
288
|
</Variant>
|
|
@@ -282,7 +290,7 @@ function seedIdentity({ app }: { app: App }) {
|
|
|
282
290
|
<Variant title="Base · Title + Subtitle">
|
|
283
291
|
<div style="padding: 2rem;">
|
|
284
292
|
<AppTopBar
|
|
285
|
-
:spec="{
|
|
293
|
+
:spec="{}"
|
|
286
294
|
title="Dose-Response Analysis"
|
|
287
295
|
subtitle="Experiment EXP-2024-001"
|
|
288
296
|
/>
|
|
@@ -306,7 +314,7 @@ function seedIdentity({ app }: { app: App }) {
|
|
|
306
314
|
<Variant title="Navigation · Page selector">
|
|
307
315
|
<div style="padding: 2rem;">
|
|
308
316
|
<AppTopBar
|
|
309
|
-
:spec="{
|
|
317
|
+
:spec="{ homePath: '' }"
|
|
310
318
|
title="Results"
|
|
311
319
|
:page-selector="samplePageSelector"
|
|
312
320
|
current-page-selector-id="plugins"
|
|
@@ -354,11 +362,28 @@ function seedIdentity({ app }: { app: App }) {
|
|
|
354
362
|
<div style="padding: 2rem;">
|
|
355
363
|
<ExperimentProvider>
|
|
356
364
|
<AppTopBar
|
|
357
|
-
:spec="{
|
|
365
|
+
:spec="{ homePath: '' }"
|
|
358
366
|
title="Analysis"
|
|
359
367
|
/>
|
|
360
368
|
</ExperimentProvider>
|
|
361
369
|
</div>
|
|
362
370
|
</Variant>
|
|
371
|
+
|
|
372
|
+
<Variant title="Layout · Fixed tail and two rows">
|
|
373
|
+
<!-- The theme and settings buttons always close the bar. Below 1024px of bar width the
|
|
374
|
+
pills and the context group (experiment, plugin buttons) move to a second row. -->
|
|
375
|
+
<div style="padding: 2rem; background: var(--bg-primary); display: grid; gap: 1rem;">
|
|
376
|
+
<ExperimentProvider v-for="width in ['1360px', '1180px', '900px', '768px']" :key="width" :plugin="samplePlugin">
|
|
377
|
+
<div :style="{ width, maxWidth: '100%' }">
|
|
378
|
+
<AppTopBar
|
|
379
|
+
:spec="{ pills: samplePillNav.slice(0, 3), homePath: '', actions: sampleActions }"
|
|
380
|
+
:page-selector="sampleIconPages"
|
|
381
|
+
current-page-selector-id="monitor"
|
|
382
|
+
pill="workspace"
|
|
383
|
+
/>
|
|
384
|
+
</div>
|
|
385
|
+
</ExperimentProvider>
|
|
386
|
+
</div>
|
|
387
|
+
</Variant>
|
|
363
388
|
</Story>
|
|
364
389
|
</template>
|
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
* Full application top bar with brand logo, static plugin identity, page selector, centered pill nav, experiment popover, and an account slot.
|
|
4
4
|
*
|
|
5
5
|
* The `#icon` slot applies in standalone mode only; integrated branding comes from `Metadata.icon`.
|
|
6
|
+
*
|
|
7
|
+
* The order is fixed: identity or page selector, pills, the context group (standalone badge, run slot,
|
|
8
|
+
* onboarding, `spec.actions`), then the tail: theme toggle, settings, `#account`. Below 1024px of bar
|
|
9
|
+
* width the pills and the context group move to a second row. Inside a responsive AppLayout the bar
|
|
10
|
+
* holds the sidebar drawer button.
|
|
6
11
|
*/
|
|
7
12
|
import { ref, computed, inject, defineAsyncComponent, onMounted, onBeforeUnmount, toValue, useSlots, watch, type Component } from 'vue'
|
|
8
13
|
import type {
|
|
14
|
+
ActionMenuItem,
|
|
9
15
|
PillNavOption,
|
|
10
16
|
PillNavItem,
|
|
11
17
|
PillNavItemInput,
|
|
@@ -13,7 +19,13 @@ import type {
|
|
|
13
19
|
PageSelectorItemInput,
|
|
14
20
|
PluginWorkspaceTopBarSpec,
|
|
15
21
|
SettingsTab,
|
|
22
|
+
TopBarSettingsConfig,
|
|
16
23
|
} from '../types/components'
|
|
24
|
+
import { APP_LAYOUT_SIDEBAR_KEY } from '../composables/appLayoutSidebar'
|
|
25
|
+
import { warnDeprecatedOnce } from '../utils/deprecation'
|
|
26
|
+
import ActionMenu from './ActionMenu.vue'
|
|
27
|
+
import BaseButton from './BaseButton.vue'
|
|
28
|
+
import LucideIconInternal from './internal/LucideIconInternal.vue'
|
|
17
29
|
import { normalizeItemInput } from '../utils/items'
|
|
18
30
|
import { applySpecDefaults } from '../composables/componentSpecs'
|
|
19
31
|
import ThemeToggle from './ThemeToggle.vue'
|
|
@@ -27,7 +39,8 @@ import { useOnboarding } from '../composables/useOnboarding'
|
|
|
27
39
|
import { pluginIdentity } from '../composables/pluginIdentity'
|
|
28
40
|
import { integrationHost } from '../utils/integrationHost'
|
|
29
41
|
import { trackTopBarForToasts } from './AppTopBar.toastOffset'
|
|
30
|
-
import {
|
|
42
|
+
import { trackTopBarCompact } from './AppTopBar.compact'
|
|
43
|
+
import { CircleAlert, CircleCheck, CircleX, ListChecks, Ellipsis, LoaderCircle, Menu, X } from 'lucide-vue-next'
|
|
31
44
|
import type { JobStatus, TopBarJob } from '../types/jobs'
|
|
32
45
|
import type { Tone } from '../types/components'
|
|
33
46
|
|
|
@@ -67,12 +80,23 @@ const config = computed(() => applySpecDefaults({
|
|
|
67
80
|
variant: 'card',
|
|
68
81
|
homePath: '/',
|
|
69
82
|
logo: true,
|
|
70
|
-
themeToggle: false,
|
|
71
|
-
settings: false,
|
|
72
83
|
standaloneLabel: 'Standalone',
|
|
73
84
|
}, props.spec))
|
|
74
|
-
|
|
75
|
-
const
|
|
85
|
+
// The theme and settings buttons always close the bar; without settings the modal has only Appearance.
|
|
86
|
+
const settingsConfig = computed<TopBarSettingsConfig>(() => toValue(config.value.settings) || {})
|
|
87
|
+
|
|
88
|
+
/** Plugin buttons: two show, the rest fold into the More menu. */
|
|
89
|
+
const MAX_VISIBLE_ACTIONS = 2
|
|
90
|
+
const visibleActions = computed(() => (config.value.actions ?? []).slice(0, MAX_VISIBLE_ACTIONS))
|
|
91
|
+
const overflowActions = computed(() => (config.value.actions ?? []).slice(MAX_VISIBLE_ACTIONS))
|
|
92
|
+
const overflowMenuItems = computed<ActionMenuItem[]>(() =>
|
|
93
|
+
overflowActions.value.map(({ id, label, disabled, loading }) => ({ id, label, disabled: disabled || loading })),
|
|
94
|
+
)
|
|
95
|
+
function selectOverflowAction(item: ActionMenuItem) {
|
|
96
|
+
overflowActions.value.find(action => action.id === item.id)?.onSelect()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const drawer = inject(APP_LAYOUT_SIDEBAR_KEY, null)
|
|
76
100
|
|
|
77
101
|
const SettingsModal = defineAsyncComponent(() => import('./SettingsModal.vue'))
|
|
78
102
|
const ExperimentPopover = defineAsyncComponent(() => import('./ExperimentPopover.vue'))
|
|
@@ -90,10 +114,18 @@ const emit = defineEmits<{
|
|
|
90
114
|
const settingsOpen = ref(false)
|
|
91
115
|
const headerEl = ref<HTMLElement | null>(null)
|
|
92
116
|
let untrackToastOffset: (() => void) | undefined
|
|
117
|
+
// Labels give way to icons when the content does not fit (AppTopBar.compact.ts).
|
|
118
|
+
const compact = ref(false)
|
|
119
|
+
let compactTracker: ReturnType<typeof trackTopBarCompact> | undefined
|
|
93
120
|
onMounted(() => {
|
|
94
|
-
if (headerEl.value)
|
|
121
|
+
if (!headerEl.value) return
|
|
122
|
+
untrackToastOffset = trackTopBarForToasts(headerEl.value)
|
|
123
|
+
compactTracker = trackTopBarCompact(headerEl.value, compact)
|
|
124
|
+
})
|
|
125
|
+
onBeforeUnmount(() => {
|
|
126
|
+
untrackToastOffset?.()
|
|
127
|
+
compactTracker?.stop()
|
|
95
128
|
})
|
|
96
|
-
onBeforeUnmount(() => untrackToastOffset?.())
|
|
97
129
|
const { integrated: isIntegrated, plugin } = usePlatform()
|
|
98
130
|
const isStandalone = computed(() => !isIntegrated.value)
|
|
99
131
|
const appExperiment = inject(APP_EXPERIMENT_KEY, null)
|
|
@@ -139,6 +171,19 @@ const JOB_ICONS: Record<JobStatus, Component> = {
|
|
|
139
171
|
}
|
|
140
172
|
|
|
141
173
|
const slots = useSlots()
|
|
174
|
+
if (props.spec.themeToggle !== undefined) {
|
|
175
|
+
warnDeprecatedOnce('topbar.themeToggle', 'AppTopBar spec.themeToggle is ignored: the theme toggle always shows. Remove it; it is removed in 2.0.')
|
|
176
|
+
}
|
|
177
|
+
for (const name of ['nav', 'center', 'actions', 'page-selector-icon', 'page-selector-item-icon'] as const) {
|
|
178
|
+
if (slots[name]) {
|
|
179
|
+
warnDeprecatedOnce(
|
|
180
|
+
`topbar.slot.${name}`,
|
|
181
|
+
name.startsWith('page-selector')
|
|
182
|
+
? `AppTopBar #${name} is deprecated: give the page an icon (a Lucide name). It is removed in 2.0.`
|
|
183
|
+
: `AppTopBar #${name} is deprecated${name === 'actions' ? ': use spec.actions' : ''}. It is removed in 2.0.`,
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
142
187
|
// Standalone: the seeded identity (providePluginIdentity) stands in for a missing icon and title.
|
|
143
188
|
const pluginIcon = computed(() => plugin.value?.icon || (slots.logo ? undefined : pluginIdentity.icon))
|
|
144
189
|
const seeded = computed(() => isStandalone.value && !props.title && !props.subtitle)
|
|
@@ -168,6 +213,28 @@ const pageSelectorPluginName = computed(() =>
|
|
|
168
213
|
// Platforms before 1.4 send no subtitle; the description fills line 2.
|
|
169
214
|
const identitySubtitle = computed(() => plugin.value?.subtitle || plugin.value?.description)
|
|
170
215
|
const hasPillNav = computed(() => !!config.value.pills?.length)
|
|
216
|
+
const showStandaloneBadge = computed(() => config.value.standaloneLabel !== false && isStandalone.value)
|
|
217
|
+
const showRunSlot = computed(() => showExperiment.value || props.job !== undefined || runSlotJoined.value)
|
|
218
|
+
/** The context group: standalone badge, run slot, onboarding and plugin buttons. Below 1024px it is row 2. */
|
|
219
|
+
// New content can need (or free) the label room again. One source per fact, so a job's
|
|
220
|
+
// changing progress label (a new job object each update) does not re-measure.
|
|
221
|
+
watch(
|
|
222
|
+
[
|
|
223
|
+
() => config.value.actions?.map(action => action.label).join(),
|
|
224
|
+
() => props.job === undefined,
|
|
225
|
+
() => props.job === null,
|
|
226
|
+
showExperiment,
|
|
227
|
+
() => config.value.pills?.length,
|
|
228
|
+
],
|
|
229
|
+
() => compactTracker?.reset(),
|
|
230
|
+
)
|
|
231
|
+
const hasContext = computed(() =>
|
|
232
|
+
showStandaloneBadge.value
|
|
233
|
+
|| showRunSlot.value
|
|
234
|
+
|| !!config.value.actions?.length
|
|
235
|
+
|| !!slots.actions
|
|
236
|
+
|| !!onboarding?.enabled.value,
|
|
237
|
+
)
|
|
171
238
|
const hasTitleGroup = computed(
|
|
172
239
|
() =>
|
|
173
240
|
!hasPageSelector.value &&
|
|
@@ -202,11 +269,24 @@ const normalizedSettingsTabs = computed<SettingsTab[]>(() =>
|
|
|
202
269
|
:class="[
|
|
203
270
|
'mint-topbar',
|
|
204
271
|
`mint-topbar--${config.variant}`,
|
|
272
|
+
{ 'mint-topbar--compact': compact },
|
|
205
273
|
]"
|
|
206
274
|
>
|
|
207
|
-
|
|
275
|
+
<!-- Below 1024px of bar width a bar with pills or context stacks into two rows (CSS container query). -->
|
|
276
|
+
<div :class="['mint-topbar__container', { 'mint-topbar__container--stackable': hasPillNav || $slots.center || hasContext }]">
|
|
208
277
|
<!-- Left: brand -->
|
|
209
278
|
<div class="mint-topbar__start">
|
|
279
|
+
<!-- Sidebar drawer button of the surrounding AppLayout; CSS shows it with the drawer (< 1024px). -->
|
|
280
|
+
<button
|
|
281
|
+
v-if="drawer"
|
|
282
|
+
type="button"
|
|
283
|
+
class="mint-topbar__icon-btn mint-topbar__sidebar-toggle"
|
|
284
|
+
:aria-label="drawer.open.value ? drawer.closeLabel.value : drawer.openLabel.value"
|
|
285
|
+
:aria-expanded="drawer.open.value"
|
|
286
|
+
@click="drawer.toggle()"
|
|
287
|
+
>
|
|
288
|
+
<component :is="drawer.open.value ? X : Menu" class="mint-topbar__icon-btn-icon" aria-hidden="true" />
|
|
289
|
+
</button>
|
|
210
290
|
<div class="mint-topbar__brand">
|
|
211
291
|
<a
|
|
212
292
|
v-if="config.homePath && (config.homePath.startsWith('http') || config.homePath.startsWith('/'))"
|
|
@@ -348,104 +428,157 @@ const normalizedSettingsTabs = computed<SettingsTab[]>(() =>
|
|
|
348
428
|
</slot>
|
|
349
429
|
</div>
|
|
350
430
|
|
|
351
|
-
<!-- Right section -->
|
|
431
|
+
<!-- Right section: the context group, then the fixed tail -->
|
|
352
432
|
<div class="mint-topbar__right">
|
|
353
|
-
<
|
|
354
|
-
|
|
355
|
-
|
|
433
|
+
<div v-if="hasContext" class="mint-topbar__context">
|
|
434
|
+
<span v-if="showStandaloneBadge" class="mint-topbar__standalone-badge">
|
|
435
|
+
{{ config.standaloneLabel }}
|
|
436
|
+
</span>
|
|
356
437
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
>
|
|
364
|
-
<ExperimentPopover
|
|
365
|
-
v-if="showExperiment && appExperiment"
|
|
366
|
-
v-bind="job ? { ...appExperiment.popover.value, showSave: false } : appExperiment.popover.value"
|
|
367
|
-
@select="appExperiment.openModal()"
|
|
368
|
-
@save="appExperiment.handleSave()"
|
|
369
|
-
@detach="appExperiment.handleDetach()"
|
|
370
|
-
/>
|
|
371
|
-
<!-- Status words only, so a moving percent does not repeat. -->
|
|
372
|
-
<span class="mint-topbar__job-status" aria-live="polite">{{ job ? JOB_STATUS_TEXT[job.status] : '' }}</span>
|
|
373
|
-
<!-- appear: the slot can mount together with its first job; before-enter then joins it. -->
|
|
374
|
-
<Transition
|
|
375
|
-
name="mint-topbar-job"
|
|
376
|
-
mode="out-in"
|
|
377
|
-
appear
|
|
378
|
-
@before-enter="syncRunSlotJoined"
|
|
379
|
-
@after-leave="syncRunSlotJoined"
|
|
438
|
+
<!-- Run slot: while a job shows, its progress takes the place of the experiment name and Save:
|
|
439
|
+
[ EXP-0142 ▾ | job ]. The idle Jobs button stands apart. -->
|
|
440
|
+
<div
|
|
441
|
+
v-if="showRunSlot"
|
|
442
|
+
:class="['mint-topbar__run-slot', { 'mint-topbar__run-slot--joined': runSlotJoined }]"
|
|
443
|
+
data-onboarding="topbar-run"
|
|
380
444
|
>
|
|
381
|
-
<
|
|
382
|
-
v-if="
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
445
|
+
<ExperimentPopover
|
|
446
|
+
v-if="showExperiment && appExperiment"
|
|
447
|
+
v-bind="job ? { ...appExperiment.popover.value, showSave: false } : appExperiment.popover.value"
|
|
448
|
+
@select="appExperiment.openModal()"
|
|
449
|
+
@save="appExperiment.handleSave()"
|
|
450
|
+
@detach="appExperiment.handleDetach()"
|
|
451
|
+
/>
|
|
452
|
+
<!-- Status words only, so a moving percent does not repeat. -->
|
|
453
|
+
<span class="mint-topbar__job-status" aria-live="polite">{{ job ? JOB_STATUS_TEXT[job.status] : '' }}</span>
|
|
454
|
+
<!-- appear: the slot can mount together with its first job; before-enter then joins it. -->
|
|
455
|
+
<Transition
|
|
456
|
+
name="mint-topbar-job"
|
|
457
|
+
mode="out-in"
|
|
458
|
+
appear
|
|
459
|
+
@before-enter="syncRunSlotJoined"
|
|
460
|
+
@after-leave="syncRunSlotJoined"
|
|
391
461
|
>
|
|
392
|
-
<
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
462
|
+
<button
|
|
463
|
+
v-if="job"
|
|
464
|
+
key="run"
|
|
465
|
+
type="button"
|
|
466
|
+
:class="['mint-topbar__job', `mint-topbar__job--${JOB_TONES[job.status]}`]"
|
|
467
|
+
:title="jobName"
|
|
468
|
+
:aria-label="jobName"
|
|
469
|
+
:aria-expanded="jobsOpen"
|
|
470
|
+
:aria-controls="jobsPanelId"
|
|
471
|
+
@click="emit('job-click')"
|
|
472
|
+
>
|
|
473
|
+
<component
|
|
474
|
+
:is="JOB_ICONS[job.status]"
|
|
475
|
+
:class="['mint-topbar__job-icon', { 'mint-topbar__job-icon--spin': job.status === 'running' || job.status === 'queued' }]"
|
|
476
|
+
:size="14"
|
|
477
|
+
aria-hidden="true"
|
|
478
|
+
/>
|
|
479
|
+
<span v-if="jobPercent != null" class="mint-topbar__job-bar" aria-hidden="true">
|
|
480
|
+
<span :style="{ width: `${jobPercent}%` }" />
|
|
481
|
+
</span>
|
|
482
|
+
</button>
|
|
483
|
+
<button
|
|
484
|
+
v-else-if="job === null"
|
|
485
|
+
key="idle"
|
|
486
|
+
type="button"
|
|
487
|
+
class="mint-topbar__job mint-topbar__job--idle"
|
|
488
|
+
:aria-label="jobsDone ? `Jobs, ${jobsDone} completed` : 'Jobs'"
|
|
489
|
+
title="Jobs"
|
|
490
|
+
:aria-expanded="jobsOpen"
|
|
491
|
+
:aria-controls="jobsPanelId"
|
|
492
|
+
@click="emit('job-click')"
|
|
493
|
+
>
|
|
494
|
+
<ListChecks class="mint-topbar__job-icon" :size="14" aria-hidden="true" />
|
|
495
|
+
<span class="mint-topbar__job-label">Jobs</span>
|
|
496
|
+
<span v-if="jobsDone" class="mint-topbar__job-count" aria-hidden="true">{{ jobsDone }}</span>
|
|
497
|
+
</button>
|
|
498
|
+
</Transition>
|
|
499
|
+
</div>
|
|
500
|
+
|
|
501
|
+
<!-- Onboarding: checklist chip, Help button and tour layer, when the app provides onboarding -->
|
|
502
|
+
<AppTopBarOnboardingInternal v-if="onboarding?.enabled.value" :state="onboarding" />
|
|
503
|
+
|
|
504
|
+
<!-- Plugin buttons: spec.actions (two, then More); the deprecated #actions slot is clipped to a fixed width -->
|
|
505
|
+
<template v-for="action in visibleActions" :key="action.id">
|
|
402
506
|
<button
|
|
403
|
-
v-
|
|
404
|
-
key="idle"
|
|
507
|
+
v-if="action.icon"
|
|
405
508
|
type="button"
|
|
406
|
-
class="mint-
|
|
407
|
-
:aria-label="
|
|
408
|
-
:
|
|
409
|
-
:
|
|
410
|
-
|
|
509
|
+
class="mint-topbar__icon-btn"
|
|
510
|
+
:aria-label="action.label"
|
|
511
|
+
:title="action.title ?? action.label"
|
|
512
|
+
:disabled="action.disabled || action.loading"
|
|
513
|
+
:aria-busy="action.loading || undefined"
|
|
514
|
+
:data-action="action.id"
|
|
515
|
+
@click="action.onSelect()"
|
|
411
516
|
>
|
|
412
|
-
<
|
|
413
|
-
<
|
|
414
|
-
<span v-if="
|
|
517
|
+
<LoaderCircle v-if="action.loading" class="mint-topbar__icon-btn-icon mint-topbar__job-icon--spin" aria-hidden="true" />
|
|
518
|
+
<LucideIconInternal v-else :name="action.icon" :size="16" />
|
|
519
|
+
<span v-if="action.dot" :class="['mint-topbar__icon-btn-dot', `mint-topbar__icon-btn-dot--${action.dot}`]" aria-hidden="true" />
|
|
415
520
|
</button>
|
|
416
|
-
|
|
521
|
+
<BaseButton
|
|
522
|
+
v-else
|
|
523
|
+
variant="secondary"
|
|
524
|
+
size="sm"
|
|
525
|
+
:title="action.title"
|
|
526
|
+
:loading="action.loading"
|
|
527
|
+
:disabled="action.disabled"
|
|
528
|
+
:data-action="action.id"
|
|
529
|
+
@click="action.onSelect()"
|
|
530
|
+
>
|
|
531
|
+
{{ action.label }}
|
|
532
|
+
</BaseButton>
|
|
533
|
+
</template>
|
|
534
|
+
<ActionMenu
|
|
535
|
+
v-if="overflowMenuItems.length"
|
|
536
|
+
label="More actions"
|
|
537
|
+
:items="overflowMenuItems"
|
|
538
|
+
@select="selectOverflowAction"
|
|
539
|
+
>
|
|
540
|
+
<template #trigger="{ open, toggle, keydown }">
|
|
541
|
+
<button
|
|
542
|
+
type="button"
|
|
543
|
+
class="mint-topbar__icon-btn"
|
|
544
|
+
aria-label="More actions"
|
|
545
|
+
title="More actions"
|
|
546
|
+
aria-haspopup="menu"
|
|
547
|
+
:aria-expanded="open"
|
|
548
|
+
@click="toggle"
|
|
549
|
+
@keydown="keydown"
|
|
550
|
+
>
|
|
551
|
+
<Ellipsis class="mint-topbar__icon-btn-icon" aria-hidden="true" />
|
|
552
|
+
</button>
|
|
553
|
+
</template>
|
|
554
|
+
</ActionMenu>
|
|
555
|
+
<span v-if="$slots.actions" class="mint-topbar__legacy-actions"><slot name="actions" /></span>
|
|
417
556
|
</div>
|
|
418
557
|
|
|
419
|
-
<
|
|
558
|
+
<div class="mint-topbar__tail">
|
|
559
|
+
<ThemeToggle size="sm" />
|
|
420
560
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
aria-label="Open settings"
|
|
433
|
-
data-onboarding="topbar-settings"
|
|
434
|
-
@click="settingsOpen = true"
|
|
435
|
-
>
|
|
436
|
-
<svg class="mint-topbar__settings-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
437
|
-
<path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /><circle cx="12" cy="12" r="3" />
|
|
438
|
-
</svg>
|
|
439
|
-
</button>
|
|
561
|
+
<button
|
|
562
|
+
type="button"
|
|
563
|
+
class="mint-topbar__settings-btn"
|
|
564
|
+
aria-label="Open settings"
|
|
565
|
+
data-onboarding="topbar-settings"
|
|
566
|
+
@click="settingsOpen = true"
|
|
567
|
+
>
|
|
568
|
+
<svg class="mint-topbar__settings-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
569
|
+
<path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /><circle cx="12" cy="12" r="3" />
|
|
570
|
+
</svg>
|
|
571
|
+
</button>
|
|
440
572
|
|
|
441
|
-
|
|
442
|
-
|
|
573
|
+
<!-- Account: the host application renders its account controls here -->
|
|
574
|
+
<slot name="account" />
|
|
575
|
+
</div>
|
|
443
576
|
</div>
|
|
444
577
|
</div>
|
|
445
578
|
</header>
|
|
446
579
|
|
|
447
580
|
<SettingsModal
|
|
448
|
-
v-if="
|
|
581
|
+
v-if="settingsOpen"
|
|
449
582
|
v-model:open="settingsOpen"
|
|
450
583
|
:title="settingsConfig?.title"
|
|
451
584
|
:tabs="normalizedSettingsTabs"
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/** Open modals, oldest first; shared by every BaseModal instance. */
|
|
3
3
|
const openModals: object[] = []
|
|
4
|
+
/** Body overflow from before the first modal opened, restored when the last one closes. */
|
|
5
|
+
let bodyOverflowBeforeLock = ''
|
|
6
|
+
|
|
7
|
+
/** Body scroll follows the stack: locked while any modal is open. */
|
|
8
|
+
function syncBodyScrollLock(wasEmpty: boolean) {
|
|
9
|
+
if (wasEmpty && openModals.length > 0) {
|
|
10
|
+
bodyOverflowBeforeLock = document.body.style.overflow
|
|
11
|
+
document.body.style.overflow = 'hidden'
|
|
12
|
+
} else if (!wasEmpty && openModals.length === 0) {
|
|
13
|
+
document.body.style.overflow = bodyOverflowBeforeLock
|
|
14
|
+
}
|
|
15
|
+
}
|
|
4
16
|
</script>
|
|
5
17
|
|
|
6
18
|
<script setup lang="ts">
|
|
@@ -95,16 +107,14 @@ const containerRef = ref<HTMLElement | null>(null)
|
|
|
95
107
|
|
|
96
108
|
useFocusTrap({ containerRef, active: toRef(props, 'open') })
|
|
97
109
|
|
|
98
|
-
watch(() => props.open, (isOpen) => {
|
|
99
|
-
document.body.style.overflow = isOpen ? 'hidden' : ''
|
|
100
|
-
})
|
|
101
|
-
|
|
102
110
|
// Every open modal listens on document, so only the most recently opened one answers Escape.
|
|
103
111
|
const self = {}
|
|
104
112
|
watch(() => props.open, (isOpen) => {
|
|
113
|
+
const wasEmpty = openModals.length === 0
|
|
105
114
|
const index = openModals.indexOf(self)
|
|
106
115
|
if (isOpen && index === -1) openModals.push(self)
|
|
107
116
|
if (!isOpen && index !== -1) openModals.splice(index, 1)
|
|
117
|
+
syncBodyScrollLock(wasEmpty)
|
|
108
118
|
}, { immediate: true })
|
|
109
119
|
|
|
110
120
|
// A control inside the modal that handles Escape (an open select) prevents the default first.
|
|
@@ -130,9 +140,10 @@ function handleOverlayClick(event: MouseEvent) {
|
|
|
130
140
|
useEventListener(() => document, 'keydown', handleEscapeKeydown)
|
|
131
141
|
|
|
132
142
|
onUnmounted(() => {
|
|
133
|
-
|
|
143
|
+
const wasEmpty = openModals.length === 0
|
|
134
144
|
const index = openModals.indexOf(self)
|
|
135
145
|
if (index !== -1) openModals.splice(index, 1)
|
|
146
|
+
syncBodyScrollLock(wasEmpty)
|
|
136
147
|
})
|
|
137
148
|
</script>
|
|
138
149
|
|
|
@@ -282,8 +282,8 @@ function initPlayground() {
|
|
|
282
282
|
/>
|
|
283
283
|
</div>
|
|
284
284
|
<div style="text-align: center; font-size: 0.75rem; color: var(--text-muted); padding: 0 1rem 1rem;">
|
|
285
|
-
Green surface with check icon. Success state
|
|
286
|
-
just shows the moment.
|
|
285
|
+
Green surface with check icon. Success state lasts while saveSuccessMessage is set; the parent clears it
|
|
286
|
+
(useAppExperiment after 3s). The story just shows the moment.
|
|
287
287
|
</div>
|
|
288
288
|
</Variant>
|
|
289
289
|
|