@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.
- package/dist/{AlertBox-CIhqsWbN.js → AlertBox-Cai0w7gF.js} +3 -3
- package/dist/{AlertBox-CIhqsWbN.js.map → AlertBox-Cai0w7gF.js.map} +1 -1
- package/dist/{AppSidebar-BDrd7Kwn.js → AppSidebar-C-Vq5x4I.js} +410 -127
- package/dist/AppSidebar-C-Vq5x4I.js.map +1 -0
- package/dist/{ConfirmDialog-CjWoxAD1.js → ConfirmDialog-BjFsOpBe.js} +3 -3
- package/dist/ConfirmDialog-BjFsOpBe.js.map +1 -0
- package/dist/{ExperimentPopover-BM92LlrQ.js → ExperimentPopover-DYy_s0kI.js} +25 -37
- package/dist/ExperimentPopover-DYy_s0kI.js.map +1 -0
- package/dist/{ExperimentSelectorModal-LsB1ABmh.js → ExperimentSelectorModal-Bfh5bdkv.js} +37 -8
- package/dist/ExperimentSelectorModal-Bfh5bdkv.js.map +1 -0
- package/dist/{SegmentedControl-Dz41vAFM.js → SegmentedControl-B8KgyOjd.js} +2 -2
- package/dist/{SegmentedControl-Dz41vAFM.js.map → SegmentedControl-B8KgyOjd.js.map} +1 -1
- package/dist/{SettingsModal-C-MSR-bk.js → SettingsModal-DwfrD1ER.js} +7 -7
- package/dist/{SettingsModal-C-MSR-bk.js.map → SettingsModal-DwfrD1ER.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/FormBuilder.vue.d.ts +1 -1
- package/dist/components/PluginIcon.vue.d.ts +2 -2
- package/dist/components/PluginWorkspaceView.vue.d.ts +4 -0
- package/dist/components/SectionCard.vue.d.ts +13 -1
- package/dist/components/SettingsModal.vue.d.ts +1 -1
- package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -2
- package/dist/components/internal/FormSchemaInternal.vue.d.ts +9 -9
- package/dist/components/internal/FormSectionRenderer.vue.d.ts +4 -4
- package/dist/components/internal/LucideIconInternal.vue.d.ts +17 -0
- package/dist/composables/appLayoutSidebar.d.ts +9 -0
- package/dist/composables/useExperimentSelector.d.ts +2 -0
- package/dist/composables/useOnboarding.d.ts +1 -1
- package/dist/composables/usePluginWorkspace.d.ts +2 -0
- package/dist/composables/useVersionedSave.d.ts +5 -2
- package/dist/{controlSchemaModel-BbXbMgc6.js → controlSchemaModel-BtFNZj8t.js} +2 -2
- package/dist/{controlSchemaModel-BbXbMgc6.js.map → controlSchemaModel-BtFNZj8t.js.map} +1 -1
- package/dist/{data-LzdkEZJ-.js → data-CJztFns8.js} +31 -15
- package/dist/{data-LzdkEZJ-.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.d.ts +0 -1
- package/dist/index.js +15 -20
- package/dist/index.js.map +1 -1
- package/dist/{install-UTqrAgHe.js → install-Bk02tJ5M.js} +15 -40
- package/dist/install-Bk02tJ5M.js.map +1 -0
- package/dist/install.js +1 -1
- package/dist/{lab-CbdDLAVr.js → lab-BYhb_cJI.js} +47 -117
- package/dist/lab-BYhb_cJI.js.map +1 -0
- package/dist/lab.js +4 -4
- package/dist/{records-CWRztE5M.js → records-GtiVMlTQ.js} +207 -171
- package/dist/records-GtiVMlTQ.js.map +1 -0
- package/dist/{runtime-DrL6lKAb.js → runtime-aZPUPxoB.js} +112 -54
- package/dist/runtime-aZPUPxoB.js.map +1 -0
- package/dist/runtime.js +6 -6
- package/dist/{shell-Ch7cJnjo.js → shell-Bk9mD4Lc.js} +5 -5
- package/dist/{shell-Ch7cJnjo.js.map → shell-Bk9mD4Lc.js.map} +1 -1
- package/dist/shell.js +5 -5
- package/dist/styles.css +315 -134
- package/dist/styles.d.ts +1 -0
- package/dist/types/components.d.ts +22 -2
- package/dist/types/generated-ui.d.ts +2 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/instrument.d.ts +5 -17
- package/dist/{useDropdownState-uFIz6Yv8.js → useDropdownState-CcBzUYZ6.js} +14 -5
- package/dist/useDropdownState-CcBzUYZ6.js.map +1 -0
- package/dist/{useExperimentSamples-Cz2v3V_q.js → useExperimentSamples-CpNXvpUn.js} +3 -3
- package/dist/{useExperimentSamples-Cz2v3V_q.js.map → useExperimentSamples-CpNXvpUn.js.map} +1 -1
- package/dist/{useListReorder-QqJc1dpX.js → useListReorder-Bp4EAI2w.js} +16 -4
- package/dist/useListReorder-Bp4EAI2w.js.map +1 -0
- package/dist/{useRackEditor-BHGM1Ngz.js → useRackEditor-i2bdPUFf.js} +156 -27
- package/dist/useRackEditor-i2bdPUFf.js.map +1 -0
- package/dist/{useRealtime-CNQbrS01.js → useRealtime-BLQEjbuq.js} +3 -3
- package/dist/{useRealtime-CNQbrS01.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 +7 -4
- 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/AppTopBarOnboarding.test.ts +10 -0
- package/src/__tests__/components/AutoGroupModal.experimentLoad.test.ts +58 -0
- package/src/__tests__/components/BaseModal.test.ts +33 -2
- package/src/__tests__/components/ConfirmDialog.test.ts +2 -2
- package/src/__tests__/components/ExperimentPopover.test.ts +32 -0
- package/src/__tests__/components/ExperimentSelectorModal.test.ts +16 -0
- package/src/__tests__/components/PluginWorkspaceView.test.ts +20 -1
- package/src/__tests__/components/SectionCard.test.ts +33 -0
- package/src/__tests__/components/deprecationTags.test.ts +4 -18
- package/src/__tests__/composables/useJobCenter.test.ts +35 -0
- package/src/__tests__/composables/useJobsEndpoint.test.ts +50 -0
- package/src/__tests__/composables/useOnboarding.test.ts +2 -2
- package/src/__tests__/composables/useVersionedSave.test.ts +71 -6
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +3 -3
- package/src/__tests__/styles/menusShellRefresh.test.ts +10 -0
- package/src/components/AppLayout.story.vue +5 -5
- package/src/components/AppLayout.vue +13 -1
- package/src/components/AppSidebar.vue +2 -2
- 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/CollapsibleCard.vue +1 -1
- package/src/components/ConfirmDialog.vue +1 -1
- package/src/components/ExperimentPopover.story.vue +2 -2
- package/src/components/ExperimentPopover.vue +6 -22
- package/src/components/ExperimentSelectorModal.vue +35 -0
- package/src/components/FileUploader.vue +3 -35
- package/src/components/GeneratedPluginView.vue +34 -81
- package/src/components/GeneratedResultRenderer.vue +2 -1
- package/src/components/InstrumentStatusPanels.vue +3 -3
- package/src/components/PlotlyChart.vue +1 -1
- package/src/components/PluginIcon.story.vue +2 -2
- package/src/components/PluginIcon.vue +2 -2
- package/src/components/PluginWorkspaceView.vue +10 -1
- package/src/components/SectionCard.vue +42 -6
- package/src/components/internal/AppTopBarOnboardingInternal.vue +30 -14
- 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/index.ts +1 -2
- package/src/composables/useExperimentSelector.ts +4 -0
- package/src/composables/useJobCenter.ts +21 -1
- package/src/composables/useJobsEndpoint.ts +15 -4
- package/src/composables/useOnboarding.ts +3 -3
- package/src/composables/usePluginWorkspace.ts +15 -4
- package/src/composables/useVersionedSave.ts +32 -7
- package/src/generated-ui/main.ts +3 -1
- package/src/index.ts +0 -3
- package/src/styles/components/alert-box.css +1 -1
- package/src/styles/components/app-layout.css +5 -0
- package/src/styles/components/app-sidebar.css +5 -5
- package/src/styles/components/app-top-bar.css +130 -9
- package/src/styles/components/button.css +3 -3
- package/src/styles/components/checkbox.css +1 -1
- package/src/styles/components/collapsible-card.css +1 -1
- package/src/styles/components/confirm-dialog.css +2 -2
- package/src/styles/components/file-uploader.css +3 -3
- package/src/styles/components/icon-button.css +3 -3
- package/src/styles/components/loading-spinner.css +1 -1
- package/src/styles/components/onboarding.css +39 -0
- package/src/styles/components/pill.css +1 -1
- package/src/styles/components/plugin-icon.css +2 -2
- package/src/styles/components/presence-avatars.css +4 -4
- package/src/styles/components/progress-bar.css +1 -1
- package/src/styles/components/section-card.css +65 -6
- package/src/styles/components/sequence-input.css +1 -1
- package/src/styles/components/slider.css +1 -1
- package/src/styles/components/toggle.css +1 -1
- package/src/styles/variables.css +10 -4
- package/src/types/components.ts +23 -2
- package/src/types/generated-ui.ts +2 -0
- package/src/types/index.ts +1 -1
- package/src/types/instrument.ts +5 -18
- 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-BDrd7Kwn.js.map +0 -1
- package/dist/ConfirmDialog-CjWoxAD1.js.map +0 -1
- package/dist/ExperimentPopover-BM92LlrQ.js.map +0 -1
- package/dist/ExperimentSelectorModal-LsB1ABmh.js.map +0 -1
- package/dist/deprecatedRootExports.d.ts +0 -191
- package/dist/install-UTqrAgHe.js.map +0 -1
- package/dist/lab-CbdDLAVr.js.map +0 -1
- package/dist/records-CWRztE5M.js.map +0 -1
- package/dist/runtime-DrL6lKAb.js.map +0 -1
- package/dist/useDropdownState-uFIz6Yv8.js.map +0 -1
- package/dist/useListReorder-QqJc1dpX.js.map +0 -1
- package/dist/useRackEditor-BHGM1Ngz.js.map +0 -1
- package/dist/useRequest-Bfsh7gIm.js +0 -215
- package/dist/useRequest-Bfsh7gIm.js.map +0 -1
- package/src/deprecatedRootExports.ts +0 -226
|
@@ -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
|
|
|
@@ -41,7 +41,7 @@ function handleConfirm() {
|
|
|
41
41
|
|
|
42
42
|
const confirmVariant = computed<ButtonVariant>(() => {
|
|
43
43
|
if (props.tone === 'error') return 'danger'
|
|
44
|
-
if (props.tone === 'warning' || props.tone === 'cta') return '
|
|
44
|
+
if (props.tone === 'warning' || props.tone === 'cta') return 'primary'
|
|
45
45
|
if (props.tone === 'success') return 'success'
|
|
46
46
|
return 'primary'
|
|
47
47
|
})
|
|
@@ -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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/** Top-bar control for the attached experiment: a trigger joined to an optional Save button, with a summary panel (change, open, detach). */
|
|
3
|
-
import { ref, computed, watch, nextTick,
|
|
3
|
+
import { ref, computed, watch, nextTick, useTemplateRef } from 'vue'
|
|
4
4
|
import { ArrowLeftRight, Check, ChevronDown, CircleAlert, CloudUpload, ExternalLink, Link, LoaderCircle, Unlink } from 'lucide-vue-next'
|
|
5
5
|
import { useDropdownState } from '../composables/useDropdownState'
|
|
6
6
|
import { format as formatStatus } from '../utils/experimentStatus'
|
|
@@ -78,7 +78,8 @@ const { isOpen, close, toggle } = useDropdownState({
|
|
|
78
78
|
triggerRef: useTemplateRef<HTMLElement>('trigger'),
|
|
79
79
|
closeOnEscape: false,
|
|
80
80
|
})
|
|
81
|
-
|
|
81
|
+
// The parent owns the success timer; the indicator lasts as long as its message.
|
|
82
|
+
const showSuccess = computed(() => !!props.saveSuccessMessage)
|
|
82
83
|
const showConfirm = ref(false)
|
|
83
84
|
|
|
84
85
|
// The panel hangs from the group's left edge; when that would cross the
|
|
@@ -95,7 +96,7 @@ watch(isOpen, async (open) => {
|
|
|
95
96
|
|
|
96
97
|
const saveTitle = computed(() => {
|
|
97
98
|
if (props.saveDisabled && props.saveDisabledMessage) return props.saveDisabledMessage
|
|
98
|
-
if (
|
|
99
|
+
if (props.saveSuccessMessage) return props.saveSuccessMessage
|
|
99
100
|
return `${props.saveLabel} to experiment`
|
|
100
101
|
})
|
|
101
102
|
|
|
@@ -135,6 +136,7 @@ function handleSave() {
|
|
|
135
136
|
|
|
136
137
|
function handleConfirmSave() {
|
|
137
138
|
showConfirm.value = false
|
|
139
|
+
if (props.saveDisabled || props.saveLoading) return
|
|
138
140
|
emit('save')
|
|
139
141
|
}
|
|
140
142
|
|
|
@@ -142,24 +144,6 @@ function handleDetach() {
|
|
|
142
144
|
emit('detach')
|
|
143
145
|
close()
|
|
144
146
|
}
|
|
145
|
-
|
|
146
|
-
let successTimer: ReturnType<typeof setTimeout> | null = null
|
|
147
|
-
|
|
148
|
-
// Show success state when saveSuccessMessage changes from empty to a value
|
|
149
|
-
watch(() => props.saveSuccessMessage, (msg) => {
|
|
150
|
-
if (successTimer) clearTimeout(successTimer)
|
|
151
|
-
if (msg) {
|
|
152
|
-
showSuccess.value = true
|
|
153
|
-
successTimer = setTimeout(() => {
|
|
154
|
-
showSuccess.value = false
|
|
155
|
-
successTimer = null
|
|
156
|
-
}, 3000)
|
|
157
|
-
}
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
onUnmounted(() => {
|
|
161
|
-
if (successTimer) clearTimeout(successTimer)
|
|
162
|
-
})
|
|
163
147
|
</script>
|
|
164
148
|
|
|
165
149
|
<template>
|
|
@@ -221,7 +205,7 @@ onUnmounted(() => {
|
|
|
221
205
|
<LoaderCircle v-if="saveLoading" class="mint-experiment-popover__save-spinner" :size="14" aria-hidden="true" />
|
|
222
206
|
<Check v-else-if="showSuccess" :size="14" aria-hidden="true" />
|
|
223
207
|
<CloudUpload v-else :size="14" aria-hidden="true" />
|
|
224
|
-
<span>{{ saveLoading ? 'Saving…' : saveLabel }}</span>
|
|
208
|
+
<span class="mint-experiment-popover__save-label">{{ saveLoading ? 'Saving…' : saveLabel }}</span>
|
|
225
209
|
<span v-if="dirty && !saveLoading" class="mint-experiment-popover__dirty" aria-hidden="true" />
|
|
226
210
|
</button>
|
|
227
211
|
|
|
@@ -53,6 +53,28 @@ function close() {
|
|
|
53
53
|
|
|
54
54
|
const experimentStore = useExperimentStore()
|
|
55
55
|
|
|
56
|
+
// The filter choices (not the search) survive a close or a page reload, so a
|
|
57
|
+
// user who works on their own ongoing experiments does not pick them again.
|
|
58
|
+
// One key for every plugin: all plugin pages share the platform origin. The
|
|
59
|
+
// experiment type is left out, since each plugin allows its own types.
|
|
60
|
+
const FILTERS_STORAGE_KEY = 'mint.experimentSelector.filters'
|
|
61
|
+
const REMEMBERED_FILTERS = ['status', 'project', 'datePreset', 'mine'] as const
|
|
62
|
+
|
|
63
|
+
function readRememberedFilters(): Partial<ExperimentFilters> {
|
|
64
|
+
try {
|
|
65
|
+
const saved: unknown = JSON.parse(globalThis.localStorage?.getItem(FILTERS_STORAGE_KEY) ?? '{}')
|
|
66
|
+
if (!saved || typeof saved !== 'object') return {}
|
|
67
|
+
const restored: Record<string, unknown> = {}
|
|
68
|
+
for (const key of REMEMBERED_FILTERS) {
|
|
69
|
+
const value = (saved as Record<string, unknown>)[key]
|
|
70
|
+
if (typeof value === 'string' || value === true) restored[key] = value
|
|
71
|
+
}
|
|
72
|
+
return restored as Partial<ExperimentFilters>
|
|
73
|
+
} catch {
|
|
74
|
+
return {}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
56
78
|
const {
|
|
57
79
|
experiments,
|
|
58
80
|
filters,
|
|
@@ -68,8 +90,21 @@ const {
|
|
|
68
90
|
experimentType: props.experimentType,
|
|
69
91
|
allowedExperimentTypes: props.allowedExperimentTypes,
|
|
70
92
|
withStatusCounts: true,
|
|
93
|
+
initialFilters: readRememberedFilters(),
|
|
71
94
|
})
|
|
72
95
|
|
|
96
|
+
watch(
|
|
97
|
+
() => REMEMBERED_FILTERS.map(key => filters[key]),
|
|
98
|
+
() => {
|
|
99
|
+
const saved = Object.fromEntries(REMEMBERED_FILTERS.filter(key => filters[key]).map(key => [key, filters[key]]))
|
|
100
|
+
try {
|
|
101
|
+
globalThis.localStorage?.setItem(FILTERS_STORAGE_KEY, JSON.stringify(saved))
|
|
102
|
+
} catch {
|
|
103
|
+
// Storage blocked (private mode): the filters last only while the page is open.
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
|
|
73
108
|
// Re-mounting the list on open clears its keyboard highlight.
|
|
74
109
|
const openCount = ref(0)
|
|
75
110
|
|
|
@@ -63,35 +63,8 @@ const maxSizeLabel = computed(() => {
|
|
|
63
63
|
return `${props.maxSize} bytes`
|
|
64
64
|
})
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
tsv: { color: '#10B981', label: 'TSV' },
|
|
69
|
-
xlsx: { color: '#10B981', label: 'XLSX' },
|
|
70
|
-
xls: { color: '#10B981', label: 'XLS' },
|
|
71
|
-
pdf: { color: '#EF4444', label: 'PDF' },
|
|
72
|
-
png: { color: '#8B5CF6', label: 'PNG' },
|
|
73
|
-
jpg: { color: '#8B5CF6', label: 'JPG' },
|
|
74
|
-
jpeg: { color: '#8B5CF6', label: 'JPEG' },
|
|
75
|
-
gif: { color: '#8B5CF6', label: 'GIF' },
|
|
76
|
-
svg: { color: '#8B5CF6', label: 'SVG' },
|
|
77
|
-
webp: { color: '#8B5CF6', label: 'WEBP' },
|
|
78
|
-
json: { color: '#F59E0B', label: 'JSON' },
|
|
79
|
-
xml: { color: '#F59E0B', label: 'XML' },
|
|
80
|
-
txt: { color: '#64748B', label: 'TXT' },
|
|
81
|
-
zip: { color: '#6366F1', label: 'ZIP' },
|
|
82
|
-
gz: { color: '#6366F1', label: 'GZ' },
|
|
83
|
-
py: { color: '#3B82F6', label: 'PY' },
|
|
84
|
-
js: { color: '#EAB308', label: 'JS' },
|
|
85
|
-
ts: { color: '#3B82F6', label: 'TS' },
|
|
86
|
-
fasta: { color: '#EC4899', label: 'FASTA' },
|
|
87
|
-
fastq: { color: '#EC4899', label: 'FASTQ' },
|
|
88
|
-
bam: { color: '#EC4899', label: 'BAM' },
|
|
89
|
-
vcf: { color: '#EC4899', label: 'VCF' },
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function getFileType(filename: string): { color: string; label: string } {
|
|
93
|
-
const ext = filename.split('.').pop()?.toLowerCase() || ''
|
|
94
|
-
return fileTypeMap[ext] || { color: '#94A3B8', label: ext.toUpperCase() || 'FILE' }
|
|
66
|
+
function fileTypeLabel(filename: string): string {
|
|
67
|
+
return filename.split('.').pop()?.toUpperCase() || 'FILE'
|
|
95
68
|
}
|
|
96
69
|
|
|
97
70
|
function validateFiles(files: FileList | File[]): File[] {
|
|
@@ -420,12 +393,7 @@ function formatFileSize(bytes: number): string {
|
|
|
420
393
|
class="mint-file-uploader__file"
|
|
421
394
|
>
|
|
422
395
|
<div class="mint-file-uploader__file-info">
|
|
423
|
-
<span
|
|
424
|
-
class="mint-file-uploader__file-badge"
|
|
425
|
-
:style="{ '--badge-color': getFileType(file.name).color }"
|
|
426
|
-
>
|
|
427
|
-
{{ getFileType(file.name).label }}
|
|
428
|
-
</span>
|
|
396
|
+
<span class="mint-file-uploader__file-badge">{{ fileTypeLabel(file.name) }}</span>
|
|
429
397
|
<div class="mint-file-uploader__file-meta">
|
|
430
398
|
<span class="mint-file-uploader__file-name">{{ file.name }}</span>
|
|
431
399
|
<span class="mint-file-uploader__file-size">{{ formatFileSize(file.size) }}</span>
|