@morscherlab/mint-sdk 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ExperimentSelectorModal-DCXmwKNS.js → ExperimentSelectorModal-DHE9k8TP.js} +3 -3
- package/dist/{ExperimentSelectorModal-DCXmwKNS.js.map → ExperimentSelectorModal-DHE9k8TP.js.map} +1 -1
- package/dist/{SettingsModal-BYR20I4c.js → SettingsModal-DOcCf0Vo.js} +39 -16
- package/dist/SettingsModal-DOcCf0Vo.js.map +1 -0
- package/dist/{Skeleton-BAF3CKY7.js → Skeleton-jhF9wUkU.js} +17 -7
- package/dist/Skeleton-jhF9wUkU.js.map +1 -0
- package/dist/__tests__/components/FormField.test.d.ts +1 -0
- package/dist/__tests__/components/NumberInput.test.d.ts +1 -0
- package/dist/__tests__/composables/localFiles.test.d.ts +1 -0
- package/dist/__tests__/composables/usePickerSelection.test.d.ts +1 -0
- package/dist/__tests__/styles/tokenOnlyColors.test.d.ts +1 -0
- package/dist/components/AppSidebar.vue.d.ts +7 -7
- package/dist/components/BasePill.vue.d.ts +3 -0
- package/dist/components/BaseTabs.vue.d.ts +1 -4
- package/dist/components/FileUploader.vue.d.ts +3 -0
- package/dist/components/FormField.vue.d.ts +8 -1
- package/dist/components/PluginWorkspaceView.props.d.ts +3 -3
- package/dist/components/index.js +5 -5
- package/dist/{components-DIum5hkx.js → components-Cp8DcNrU.js} +472 -259
- package/dist/components-Cp8DcNrU.js.map +1 -0
- package/dist/composables/index.js +7 -7
- package/dist/composables/pluginWorkspaceModel.d.ts +0 -3
- package/dist/composables/useToast.d.ts +22 -1
- package/dist/{composables-mFXsTA4g.js → composables-D_4qqOlp.js} +6 -6
- package/dist/{composables-mFXsTA4g.js.map → composables-D_4qqOlp.js.map} +1 -1
- package/dist/index.js +10 -10
- package/dist/install.js +2 -2
- package/dist/styles.css +560 -303
- package/dist/templates/index.js +2 -2
- package/dist/{templates-CmGfmBRA.js → templates-Dd9bBSs6.js} +2 -2
- package/dist/{templates-CmGfmBRA.js.map → templates-Dd9bBSs6.js.map} +1 -1
- package/dist/types/components.d.ts +21 -0
- package/dist/types/form-builder.d.ts +2 -0
- package/dist/types/generated-ui.d.ts +2 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/{useControlSchema-DldpxNOP.js → useControlSchema-BB2GRPhW.js} +151 -71
- package/dist/useControlSchema-BB2GRPhW.js.map +1 -0
- package/dist/{useExperimentSelector-CEcIaueJ.js → useExperimentSelector-G_CAOFTX.js} +2 -2
- package/dist/{useExperimentSelector-CEcIaueJ.js.map → useExperimentSelector-G_CAOFTX.js.map} +1 -1
- package/dist/{useFormBuilder-z5L_dUIy.js → useFormBuilder-BnGSdSRG.js} +2 -2
- package/dist/{useFormBuilder-z5L_dUIy.js.map → useFormBuilder-BnGSdSRG.js.map} +1 -1
- package/dist/{validation-uwoJGEb6.js → validation-CTxuzLIX.js} +21 -9
- package/dist/validation-CTxuzLIX.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/AppSidebar.test.ts +84 -94
- package/src/__tests__/components/AppToastContainer.test.ts +55 -1
- package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +0 -21
- package/src/__tests__/components/ControlWorkspaceView.test.ts +0 -2
- package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +0 -2
- package/src/__tests__/components/FormBuilder.test.ts +26 -0
- package/src/__tests__/components/FormField.test.ts +26 -0
- package/src/__tests__/components/GeneratedPathInput.test.ts +18 -0
- package/src/__tests__/components/NumberInput.test.ts +105 -0
- package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +1 -2
- package/src/__tests__/components/PluginWorkspaceView.test.ts +1 -2
- package/src/__tests__/composables/localFiles.test.ts +57 -0
- package/src/__tests__/composables/usePickerSelection.test.ts +98 -0
- package/src/__tests__/generatedUi.test.ts +111 -0
- package/src/__tests__/styles/tokenOnlyColors.test.ts +16 -0
- package/src/components/AppLayout.story.vue +1 -1
- package/src/components/AppSidebar.story.vue +7 -10
- package/src/components/AppSidebar.vue +178 -58
- package/src/components/AppToastContainer.vue +65 -23
- package/src/components/BasePill.story.vue +2 -0
- package/src/components/BasePill.vue +4 -0
- package/src/components/BaseTabs.story.vue +0 -42
- package/src/components/BaseTabs.vue +3 -6
- package/src/components/FileUploader.vue +10 -1
- package/src/components/FormField.vue +10 -3
- package/src/components/GeneratedPathInput.vue +11 -7
- package/src/components/NumberInput.story.vue +32 -13
- package/src/components/NumberInput.vue +116 -55
- package/src/components/PluginWorkspaceView.props.ts +3 -3
- package/src/components/internal/FormFieldRendererInternal.vue +17 -0
- package/src/components/internal/WorkspaceSidebarInternal.vue +1 -2
- package/src/composables/filePicker/localFiles.ts +21 -14
- package/src/composables/filePicker/usePickerSelection.ts +15 -4
- package/src/composables/pluginWorkspaceModel.ts +0 -3
- package/src/composables/usePluginWorkspace.ts +1 -2
- package/src/composables/useToast.ts +21 -5
- package/src/generated/schema.ts +58 -18
- package/src/styles/components/app-sidebar.css +282 -121
- package/src/styles/components/form-builder.css +1 -1
- package/src/styles/components/form-field.css +28 -0
- package/src/styles/components/number-input.css +73 -91
- package/src/styles/components/pill.css +49 -51
- package/src/styles/components/segmented-control.css +16 -11
- package/src/styles/components/tabs.css +18 -34
- package/src/styles/components/toast.css +136 -27
- package/src/styles/variables.css +25 -5
- package/src/types/components.ts +23 -0
- package/src/types/form-builder.ts +2 -0
- package/src/types/generated-ui.ts +2 -0
- package/src/types/index.ts +2 -0
- package/dist/SettingsModal-BYR20I4c.js.map +0 -1
- package/dist/Skeleton-BAF3CKY7.js.map +0 -1
- package/dist/components-DIum5hkx.js.map +0 -1
- package/dist/useControlSchema-DldpxNOP.js.map +0 -1
- package/dist/validation-uwoJGEb6.js.map +0 -1
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* AppSidebar - Context-sensitive toolkit panel
|
|
4
4
|
*
|
|
5
5
|
* Shows tool sections relevant to the active view. Sections are defined via
|
|
6
|
-
* the `panels` config and rendered as
|
|
7
|
-
* provided through named slots or
|
|
6
|
+
* the `panels` config and rendered as flat groups (header row + hairline,
|
|
7
|
+
* no nested card). Controls can be provided through named slots or
|
|
8
|
+
* auto-rendered from FormBuilder schemas.
|
|
8
9
|
*
|
|
9
10
|
* When activeView is omitted, the first non-empty panel view is selected.
|
|
10
11
|
* When no view has matching panels, the sidebar hides entirely.
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
* ```
|
|
16
17
|
*/
|
|
17
18
|
import { computed, ref, useSlots, type Slots } from 'vue'
|
|
18
|
-
import type { PillNavItem, SidebarToolSection } from '../types'
|
|
19
|
+
import type { PillNavItem, SidebarToolSection, SidebarToolSectionAction } from '../types'
|
|
19
20
|
import type { FormEnhancements, FormSchema } from '../types/form-builder'
|
|
20
21
|
import {
|
|
21
22
|
controlsToSectionFormSchemas,
|
|
@@ -28,7 +29,6 @@ import {
|
|
|
28
29
|
type ControlSchema,
|
|
29
30
|
type ControlWorkspaceOptions,
|
|
30
31
|
} from '../composables/useControlSchema'
|
|
31
|
-
import CollapsibleCard from './CollapsibleCard.vue'
|
|
32
32
|
import FormBuilder from './FormBuilder.vue'
|
|
33
33
|
|
|
34
34
|
interface Props {
|
|
@@ -38,17 +38,17 @@ interface Props {
|
|
|
38
38
|
subtitle?: string
|
|
39
39
|
/** Optional compact badge/count rendered in the chrome header. */
|
|
40
40
|
badge?: string | number
|
|
41
|
-
/** Visual preset for common plugin sidebars. `analysis` preserves the LEAF-style MINT analysis sidebar design language. */
|
|
42
|
-
variant?: 'default' | 'analysis'
|
|
43
41
|
/** Map of view IDs to their tool sections */
|
|
44
42
|
panels?: Record<string, SidebarToolSection[]>
|
|
45
43
|
/** Which view's panels to display. Defaults to the first non-empty panel view. */
|
|
46
44
|
activeView?: string
|
|
47
|
-
/** Floating
|
|
45
|
+
/** Floating shell with absolute positioning. Prefer letting `AppLayout` own this; it stays for standalone use. */
|
|
48
46
|
floating?: boolean
|
|
49
|
-
/**
|
|
47
|
+
/** Section density. `compact` (default) is the 36px header row; `comfortable` restores the roomier spacing. */
|
|
48
|
+
density?: 'compact' | 'comfortable'
|
|
49
|
+
/** @deprecated Compact is now the default. Pass `density="comfortable"` for the roomier layout. */
|
|
50
50
|
dense?: boolean
|
|
51
|
-
/** Width when visible.
|
|
51
|
+
/** Width when visible. */
|
|
52
52
|
width?: string
|
|
53
53
|
/** Position sidebar on left or right side */
|
|
54
54
|
side?: 'left' | 'right'
|
|
@@ -88,7 +88,7 @@ interface Props {
|
|
|
88
88
|
formReadonly?: boolean
|
|
89
89
|
/** Size passed to auto-rendered section forms */
|
|
90
90
|
formSize?: 'sm' | 'md' | 'lg'
|
|
91
|
-
/** Show a built-in collapse/expand button in the sidebar chrome.
|
|
91
|
+
/** Show a built-in collapse/expand button in the sidebar chrome. */
|
|
92
92
|
collapsible?: boolean
|
|
93
93
|
/** Controlled collapsed state. */
|
|
94
94
|
collapsed?: boolean
|
|
@@ -106,12 +106,12 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
106
106
|
title: undefined,
|
|
107
107
|
subtitle: undefined,
|
|
108
108
|
badge: undefined,
|
|
109
|
-
variant: 'default',
|
|
110
109
|
panels: () => ({}),
|
|
111
110
|
activeView: '',
|
|
112
|
-
floating:
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
floating: true,
|
|
112
|
+
density: undefined,
|
|
113
|
+
dense: undefined,
|
|
114
|
+
width: '280px',
|
|
115
115
|
side: 'left',
|
|
116
116
|
toggleState: () => ({}),
|
|
117
117
|
forms: () => ({}),
|
|
@@ -129,7 +129,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
129
129
|
formDisabled: false,
|
|
130
130
|
formReadonly: false,
|
|
131
131
|
formSize: 'sm',
|
|
132
|
-
collapsible:
|
|
132
|
+
collapsible: false,
|
|
133
133
|
collapsed: undefined,
|
|
134
134
|
defaultCollapsed: false,
|
|
135
135
|
collapsedWidth: '3rem',
|
|
@@ -157,10 +157,20 @@ const collapsedModel = computed({
|
|
|
157
157
|
},
|
|
158
158
|
})
|
|
159
159
|
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
const resolvedDensity = computed(() =>
|
|
161
|
+
props.density ?? (props.dense === false ? 'comfortable' : 'compact'),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
// Per-section open state; falls back to `defaultOpen` until the user toggles.
|
|
165
|
+
const openState = ref<Record<string, boolean>>({})
|
|
166
|
+
|
|
167
|
+
function isSectionOpen(section: SidebarToolSection): boolean {
|
|
168
|
+
return openState.value[section.id] ?? section.defaultOpen !== false
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function toggleSection(section: SidebarToolSection) {
|
|
172
|
+
openState.value[section.id] = !isSectionOpen(section)
|
|
173
|
+
}
|
|
164
174
|
|
|
165
175
|
const resolvedModel = computed<ControlModelBinding | undefined>(() => {
|
|
166
176
|
return resolveControlModel(props.model)
|
|
@@ -224,16 +234,15 @@ const isVisible = computed<boolean>(() =>
|
|
|
224
234
|
const sidebarClasses = computed<string[]>(() => [
|
|
225
235
|
'mint-sidebar',
|
|
226
236
|
`mint-sidebar--${props.side}`,
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
props.
|
|
230
|
-
resolvedCollapsible.value ? 'mint-sidebar--collapsible' : '',
|
|
237
|
+
props.floating ? 'mint-sidebar--floating' : 'mint-sidebar--static',
|
|
238
|
+
resolvedDensity.value === 'comfortable' ? 'mint-sidebar--comfortable' : '',
|
|
239
|
+
props.collapsible ? 'mint-sidebar--collapsible' : '',
|
|
231
240
|
collapsedModel.value ? 'mint-sidebar--collapsed' : '',
|
|
232
241
|
!isVisible.value ? 'mint-sidebar--hidden' : '',
|
|
233
242
|
])
|
|
234
243
|
|
|
235
244
|
const sidebarStyle = computed<Record<string, string>>(() => ({
|
|
236
|
-
width: collapsedModel.value ? props.collapsedWidth :
|
|
245
|
+
width: collapsedModel.value ? props.collapsedWidth : props.width,
|
|
237
246
|
}))
|
|
238
247
|
|
|
239
248
|
function handleFormUpdate(values: Record<string, unknown>) {
|
|
@@ -282,6 +291,17 @@ function badgeToneClass(section: SidebarToolSection): string {
|
|
|
282
291
|
return `mint-sidebar__rail-badge--${section.badgeTone ?? 'cta'}`
|
|
283
292
|
}
|
|
284
293
|
|
|
294
|
+
function tagToneClass(section: SidebarToolSection): string {
|
|
295
|
+
return `mint-sidebar__tag--${section.badgeTone ?? 'cta'}`
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function actionClasses(action: SidebarToolSectionAction): string[] {
|
|
299
|
+
return [
|
|
300
|
+
'mint-sidebar__group-action',
|
|
301
|
+
action.tone ? `mint-sidebar__group-action--${action.tone}` : '',
|
|
302
|
+
]
|
|
303
|
+
}
|
|
304
|
+
|
|
285
305
|
function mergeSidebarPanels(
|
|
286
306
|
generated: Record<string, SidebarToolSection[]>,
|
|
287
307
|
explicit: Record<string, SidebarToolSection[]>,
|
|
@@ -319,7 +339,7 @@ function firstVisibleViewId(panels: Record<string, SidebarToolSection[]>): strin
|
|
|
319
339
|
>
|
|
320
340
|
<!-- Header slot / built-in chrome -->
|
|
321
341
|
<div
|
|
322
|
-
v-if="$slots.header || title || subtitle || badge !== undefined ||
|
|
342
|
+
v-if="$slots.header || title || subtitle || badge !== undefined || collapsible"
|
|
323
343
|
class="mint-sidebar__header"
|
|
324
344
|
>
|
|
325
345
|
<slot
|
|
@@ -335,7 +355,7 @@ function firstVisibleViewId(panels: Record<string, SidebarToolSection[]>): strin
|
|
|
335
355
|
<span v-if="badge !== undefined" class="mint-sidebar__badge">{{ badge }}</span>
|
|
336
356
|
</div>
|
|
337
357
|
<button
|
|
338
|
-
v-if="
|
|
358
|
+
v-if="collapsible"
|
|
339
359
|
type="button"
|
|
340
360
|
class="mint-sidebar__collapse-button"
|
|
341
361
|
:aria-label="collapsedModel ? expandButtonLabel : collapseButtonLabel"
|
|
@@ -365,41 +385,141 @@ function firstVisibleViewId(panels: Record<string, SidebarToolSection[]>): strin
|
|
|
365
385
|
:id="contentId"
|
|
366
386
|
class="mint-sidebar__sections"
|
|
367
387
|
>
|
|
368
|
-
<
|
|
388
|
+
<div
|
|
369
389
|
v-for="section in activeSections"
|
|
370
390
|
:key="section.id"
|
|
371
|
-
:
|
|
372
|
-
:subtitle="section.subtitle"
|
|
373
|
-
:icon="section.icon"
|
|
374
|
-
:icon-color="section.iconColor"
|
|
375
|
-
:icon-bg="section.iconBg"
|
|
376
|
-
:badge="section.badge"
|
|
377
|
-
:badge-tone="section.badgeTone"
|
|
378
|
-
:actions="section.actions"
|
|
379
|
-
:dense="dense"
|
|
380
|
-
:default-open="section.defaultOpen !== false"
|
|
381
|
-
:show-toggle="section.showToggle"
|
|
382
|
-
:toggle-value="toggleState[section.id] ?? false"
|
|
383
|
-
@action="handleSectionAction(section.id, $event)"
|
|
384
|
-
@update:toggle-value="emit('update:toggle', section.id, $event)"
|
|
391
|
+
:class="['mint-sidebar__group', isSectionOpen(section) ? 'mint-sidebar__group--open' : 'mint-sidebar__group--closed']"
|
|
385
392
|
>
|
|
386
|
-
<
|
|
387
|
-
<
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
:
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
393
|
+
<div class="mint-sidebar__group-head">
|
|
394
|
+
<button
|
|
395
|
+
type="button"
|
|
396
|
+
class="mint-sidebar__group-toggle"
|
|
397
|
+
:aria-expanded="isSectionOpen(section)"
|
|
398
|
+
@click="toggleSection(section)"
|
|
399
|
+
>
|
|
400
|
+
<span
|
|
401
|
+
v-if="section.icon"
|
|
402
|
+
class="mint-sidebar__group-icon"
|
|
403
|
+
:style="railIconStyle(section)"
|
|
404
|
+
>
|
|
405
|
+
<svg
|
|
406
|
+
v-if="isSvgIcon(section.icon)"
|
|
407
|
+
class="mint-sidebar__group-svg"
|
|
408
|
+
viewBox="0 0 24 24"
|
|
409
|
+
fill="none"
|
|
410
|
+
stroke="currentColor"
|
|
411
|
+
stroke-width="2"
|
|
412
|
+
stroke-linecap="round"
|
|
413
|
+
stroke-linejoin="round"
|
|
414
|
+
aria-hidden="true"
|
|
415
|
+
>
|
|
416
|
+
<template v-if="Array.isArray(section.icon)">
|
|
417
|
+
<path v-for="(d, i) in section.icon" :key="i" :d="d" />
|
|
418
|
+
</template>
|
|
419
|
+
<path v-else :d="section.icon" />
|
|
420
|
+
</svg>
|
|
421
|
+
<span v-else class="mint-sidebar__group-text-icon">{{ section.icon }}</span>
|
|
422
|
+
</span>
|
|
423
|
+
<span class="mint-sidebar__group-copy">
|
|
424
|
+
<span class="mint-sidebar__group-label">{{ section.label }}</span>
|
|
425
|
+
<span v-if="section.subtitle" class="mint-sidebar__group-subtitle">{{ section.subtitle }}</span>
|
|
426
|
+
</span>
|
|
427
|
+
</button>
|
|
428
|
+
|
|
429
|
+
<div class="mint-sidebar__group-tail">
|
|
430
|
+
<span
|
|
431
|
+
v-if="section.badge !== undefined"
|
|
432
|
+
:class="['mint-sidebar__tag', tagToneClass(section)]"
|
|
433
|
+
>
|
|
434
|
+
{{ section.badge }}
|
|
435
|
+
</span>
|
|
436
|
+
|
|
437
|
+
<button
|
|
438
|
+
v-for="action in section.actions ?? []"
|
|
439
|
+
:key="action.id"
|
|
440
|
+
type="button"
|
|
441
|
+
:class="actionClasses(action)"
|
|
442
|
+
:style="action.iconColor ? { color: action.iconColor } : undefined"
|
|
443
|
+
:disabled="action.disabled"
|
|
444
|
+
:aria-label="action.label"
|
|
445
|
+
:title="action.label"
|
|
446
|
+
@click="handleSectionAction(section.id, action.id)"
|
|
447
|
+
>
|
|
448
|
+
<svg
|
|
449
|
+
v-if="action.icon && isSvgIcon(action.icon)"
|
|
450
|
+
class="mint-sidebar__group-action-icon"
|
|
451
|
+
viewBox="0 0 24 24"
|
|
452
|
+
fill="none"
|
|
453
|
+
stroke="currentColor"
|
|
454
|
+
stroke-width="2"
|
|
455
|
+
stroke-linecap="round"
|
|
456
|
+
stroke-linejoin="round"
|
|
457
|
+
aria-hidden="true"
|
|
458
|
+
>
|
|
459
|
+
<template v-if="Array.isArray(action.icon)">
|
|
460
|
+
<path v-for="(d, i) in action.icon" :key="i" :d="d" />
|
|
461
|
+
</template>
|
|
462
|
+
<path v-else :d="action.icon" />
|
|
463
|
+
</svg>
|
|
464
|
+
<span v-else-if="action.icon">{{ action.icon }}</span>
|
|
465
|
+
</button>
|
|
466
|
+
|
|
467
|
+
<button
|
|
468
|
+
v-if="section.showToggle"
|
|
469
|
+
type="button"
|
|
470
|
+
role="switch"
|
|
471
|
+
:aria-checked="toggleState[section.id] ?? false"
|
|
472
|
+
:aria-label="`Enable ${section.label}`"
|
|
473
|
+
:class="['mint-sidebar__switch', (toggleState[section.id] ?? false) ? 'mint-sidebar__switch--on' : '']"
|
|
474
|
+
@click="emit('update:toggle', section.id, !(toggleState[section.id] ?? false))"
|
|
475
|
+
>
|
|
476
|
+
<span class="mint-sidebar__switch-knob" />
|
|
477
|
+
</button>
|
|
478
|
+
|
|
479
|
+
<button
|
|
480
|
+
type="button"
|
|
481
|
+
class="mint-sidebar__chevron-button"
|
|
482
|
+
:aria-label="isSectionOpen(section) ? 'Collapse section' : 'Expand section'"
|
|
483
|
+
:aria-expanded="isSectionOpen(section)"
|
|
484
|
+
@click="toggleSection(section)"
|
|
485
|
+
>
|
|
486
|
+
<svg
|
|
487
|
+
:class="['mint-sidebar__chevron', isSectionOpen(section) ? 'mint-sidebar__chevron--open' : '']"
|
|
488
|
+
viewBox="0 0 24 24"
|
|
489
|
+
fill="none"
|
|
490
|
+
stroke="currentColor"
|
|
491
|
+
stroke-width="2"
|
|
492
|
+
stroke-linecap="round"
|
|
493
|
+
stroke-linejoin="round"
|
|
494
|
+
aria-hidden="true"
|
|
495
|
+
>
|
|
496
|
+
<path d="m6 9 6 6 6-6" />
|
|
497
|
+
</svg>
|
|
498
|
+
</button>
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
|
|
502
|
+
<Transition name="collapse">
|
|
503
|
+
<div v-show="isSectionOpen(section)" class="mint-sidebar__group-body">
|
|
504
|
+
<slot :name="`section-${section.id}`">
|
|
505
|
+
<FormBuilder
|
|
506
|
+
v-if="resolvedForms[section.id]"
|
|
507
|
+
:model-value="resolvedValues"
|
|
508
|
+
:schema="resolvedForms[section.id]"
|
|
509
|
+
:enhancements="formEnhancements"
|
|
510
|
+
:loading="formLoading"
|
|
511
|
+
:disabled="formDisabled"
|
|
512
|
+
:readonly="formReadonly"
|
|
513
|
+
:size="formSize"
|
|
514
|
+
:show-actions="showFormActions"
|
|
515
|
+
@update:model-value="handleFormUpdate"
|
|
516
|
+
@submit="handleFormSubmit(section.id, $event)"
|
|
517
|
+
@cancel="handleFormCancel(section.id)"
|
|
518
|
+
/>
|
|
519
|
+
</slot>
|
|
520
|
+
</div>
|
|
521
|
+
</Transition>
|
|
522
|
+
</div>
|
|
403
523
|
|
|
404
524
|
<slot
|
|
405
525
|
:sections="activeSections"
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
/** Preferred toast container driven by useToast,
|
|
2
|
+
/** Preferred toast container driven by useToast: title/detail, up to two actions, TTL or indeterminate bar, click-to-close. */
|
|
3
3
|
import { useToast } from '../composables/useToast'
|
|
4
|
-
import type { Toast } from '../types'
|
|
4
|
+
import type { Toast, ToastAction } from '../types'
|
|
5
5
|
|
|
6
6
|
const { toasts, dismiss } = useToast()
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
function title(toast: Toast): string {
|
|
9
|
+
return toast.title ?? toast.message
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function actions(toast: Toast): ToastAction[] {
|
|
13
|
+
return (toast.actions ?? []).slice(0, 2)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function progress(toast: Toast): NonNullable<Toast['progress']> {
|
|
17
|
+
return toast.progress ?? 'ttl'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function runAction(toast: Toast, action: ToastAction) {
|
|
21
|
+
action.onClick()
|
|
22
|
+
dismiss(toast.id)
|
|
15
23
|
}
|
|
16
24
|
</script>
|
|
17
25
|
|
|
@@ -22,36 +30,70 @@ const accentColors: Record<Toast['type'], string> = {
|
|
|
22
30
|
<div
|
|
23
31
|
v-for="toast in toasts"
|
|
24
32
|
:key="toast.id"
|
|
25
|
-
:class="[
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
:class="[
|
|
34
|
+
'mint-toast__item',
|
|
35
|
+
`mint-toast__item--${toast.type}`,
|
|
36
|
+
(toast.count ?? 1) > 1 ? 'mint-toast__item--stacked' : '',
|
|
37
|
+
]"
|
|
38
|
+
:role="toast.type === 'error' || toast.type === 'warning' ? 'alert' : 'status'"
|
|
30
39
|
@click="dismiss(toast.id)"
|
|
31
40
|
>
|
|
32
41
|
<!-- Lucide circle-check -->
|
|
33
|
-
<svg v-if="toast.type === 'success'" class="mint-toast__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
42
|
+
<svg v-if="toast.type === 'success'" class="mint-toast__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
34
43
|
<circle cx="12" cy="12" r="10" /><path d="m9 12 2 2 4-4" />
|
|
35
44
|
</svg>
|
|
36
45
|
<!-- Lucide circle-x -->
|
|
37
|
-
<svg v-else-if="toast.type === 'error'" class="mint-toast__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
46
|
+
<svg v-else-if="toast.type === 'error'" class="mint-toast__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
38
47
|
<circle cx="12" cy="12" r="10" /><path d="m15 9-6 6" /><path d="m9 9 6 6" />
|
|
39
48
|
</svg>
|
|
40
49
|
<!-- Lucide triangle-alert -->
|
|
41
|
-
<svg v-else-if="toast.type === 'warning'" class="mint-toast__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
50
|
+
<svg v-else-if="toast.type === 'warning'" class="mint-toast__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
42
51
|
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /><path d="M12 9v4" /><path d="M12 17h.01" />
|
|
43
52
|
</svg>
|
|
44
53
|
<!-- Lucide info -->
|
|
45
|
-
<svg v-else class="mint-toast__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
54
|
+
<svg v-else class="mint-toast__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
46
55
|
<circle cx="12" cy="12" r="10" /><path d="M12 16v-4" /><path d="M12 8h.01" />
|
|
47
56
|
</svg>
|
|
48
|
-
|
|
57
|
+
|
|
58
|
+
<div class="mint-toast__body">
|
|
59
|
+
<div class="mint-toast__title">
|
|
60
|
+
<span class="mint-toast__message">{{ title(toast) }}</span>
|
|
61
|
+
<span v-if="(toast.count ?? 1) > 1" class="mint-toast__count">×{{ toast.count }}</span>
|
|
62
|
+
</div>
|
|
63
|
+
<div v-if="toast.detail" class="mint-toast__detail">{{ toast.detail }}</div>
|
|
64
|
+
<div v-if="actions(toast).length" class="mint-toast__actions">
|
|
65
|
+
<button
|
|
66
|
+
v-for="action in actions(toast)"
|
|
67
|
+
:key="action.label"
|
|
68
|
+
type="button"
|
|
69
|
+
:class="['mint-toast__action', action.primary ? 'mint-toast__action--primary' : '']"
|
|
70
|
+
@click.stop="runAction(toast, action)"
|
|
71
|
+
>
|
|
72
|
+
{{ action.label }}
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<button
|
|
78
|
+
v-if="toast.dismissible !== false"
|
|
79
|
+
type="button"
|
|
80
|
+
class="mint-toast__close"
|
|
81
|
+
aria-label="Dismiss"
|
|
82
|
+
@click.stop="dismiss(toast.id)"
|
|
83
|
+
>
|
|
84
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true">
|
|
85
|
+
<path d="M18 6 6 18" /><path d="m6 6 12 12" />
|
|
86
|
+
</svg>
|
|
87
|
+
</button>
|
|
88
|
+
|
|
49
89
|
<span
|
|
90
|
+
v-if="progress(toast) === 'ttl'"
|
|
50
91
|
class="mint-toast__progress"
|
|
51
|
-
:style="{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
92
|
+
:style="{ animationDuration: `${toast.duration ?? 3500}ms` }"
|
|
93
|
+
/>
|
|
94
|
+
<span
|
|
95
|
+
v-else-if="progress(toast) === 'indeterminate'"
|
|
96
|
+
class="mint-toast__progress mint-toast__progress--indeterminate"
|
|
55
97
|
/>
|
|
56
98
|
</div>
|
|
57
99
|
</TransitionGroup>
|
|
@@ -16,6 +16,7 @@ const sizes: PillSize[] = ['sm', 'md', 'lg']
|
|
|
16
16
|
:size="state.size"
|
|
17
17
|
:removable="state.removable"
|
|
18
18
|
:disabled="state.disabled"
|
|
19
|
+
:dot="state.dot"
|
|
19
20
|
@remove="() => console.log('Remove clicked')"
|
|
20
21
|
>
|
|
21
22
|
{{ state.label }}
|
|
@@ -37,6 +38,7 @@ const sizes: PillSize[] = ['sm', 'md', 'lg']
|
|
|
37
38
|
/>
|
|
38
39
|
<HstCheckbox v-model="state.removable" title="Removable" />
|
|
39
40
|
<HstCheckbox v-model="state.disabled" title="Disabled" />
|
|
41
|
+
<HstCheckbox v-model="state.dot" title="Status dot" />
|
|
40
42
|
</template>
|
|
41
43
|
</Variant>
|
|
42
44
|
|
|
@@ -28,6 +28,8 @@ interface Props {
|
|
|
28
28
|
disabled?: boolean
|
|
29
29
|
/** Show icon slot */
|
|
30
30
|
icon?: boolean
|
|
31
|
+
/** Leading status dot in the pill's tone color */
|
|
32
|
+
dot?: boolean
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -37,6 +39,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
37
39
|
removable: false,
|
|
38
40
|
disabled: false,
|
|
39
41
|
icon: false,
|
|
42
|
+
dot: false,
|
|
40
43
|
})
|
|
41
44
|
|
|
42
45
|
/**
|
|
@@ -64,6 +67,7 @@ function handleRemove(event: MouseEvent) {
|
|
|
64
67
|
{ 'mint-pill--disabled': disabled, 'mint-pill--with-icon': icon },
|
|
65
68
|
]"
|
|
66
69
|
>
|
|
70
|
+
<span v-if="dot" class="mint-pill__dot" aria-hidden="true" />
|
|
67
71
|
<span v-if="icon" class="mint-pill__icon">
|
|
68
72
|
<slot name="icon" />
|
|
69
73
|
</span>
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref } from 'vue'
|
|
3
2
|
import BaseTabs from './BaseTabs.vue'
|
|
4
3
|
import type { TabItem } from '../types'
|
|
5
4
|
|
|
6
|
-
const activeTab = ref('overview')
|
|
7
5
|
|
|
8
6
|
const basicTabs: TabItem[] = [
|
|
9
7
|
{ id: 'overview', label: 'Overview' },
|
|
@@ -25,12 +23,9 @@ const tabsWithIcons: TabItem[] = [
|
|
|
25
23
|
{ id: 'analysis', label: 'Analysis', icon: '\u{1F52C}' },
|
|
26
24
|
]
|
|
27
25
|
|
|
28
|
-
const variants = ['underline', 'pills'] as const
|
|
29
|
-
|
|
30
26
|
function initPlayground() {
|
|
31
27
|
return {
|
|
32
28
|
activeTab: 'overview',
|
|
33
|
-
variant: 'underline' as const,
|
|
34
29
|
}
|
|
35
30
|
}
|
|
36
31
|
</script>
|
|
@@ -43,7 +38,6 @@ function initPlayground() {
|
|
|
43
38
|
<BaseTabs
|
|
44
39
|
v-model="state.activeTab"
|
|
45
40
|
:tabs="basicTabs"
|
|
46
|
-
:variant="state.variant"
|
|
47
41
|
/>
|
|
48
42
|
<p style="margin-top: 1rem; font-size: 0.875rem; color: var(--text-muted, #94a3b8);">
|
|
49
43
|
Active tab: <code>{{ state.activeTab }}</code>
|
|
@@ -52,11 +46,6 @@ function initPlayground() {
|
|
|
52
46
|
</template>
|
|
53
47
|
|
|
54
48
|
<template #controls="{ state }">
|
|
55
|
-
<HstSelect
|
|
56
|
-
v-model="state.variant"
|
|
57
|
-
title="Variant"
|
|
58
|
-
:options="variants.map(v => ({ label: v, value: v }))"
|
|
59
|
-
/>
|
|
60
49
|
<HstSelect
|
|
61
50
|
v-model="state.activeTab"
|
|
62
51
|
title="Active Tab"
|
|
@@ -65,27 +54,11 @@ function initPlayground() {
|
|
|
65
54
|
</template>
|
|
66
55
|
</Variant>
|
|
67
56
|
|
|
68
|
-
<Variant title="All Variants">
|
|
69
|
-
<div style="padding: 2rem; display: flex; flex-direction: column; gap: 2rem;">
|
|
70
|
-
<div v-for="variant in variants" :key="variant">
|
|
71
|
-
<p style="margin: 0 0 0.5rem; font-size: 0.75rem; color: var(--text-muted, #94a3b8); text-transform: uppercase; letter-spacing: 0.05em;">
|
|
72
|
-
{{ variant }}
|
|
73
|
-
</p>
|
|
74
|
-
<BaseTabs
|
|
75
|
-
v-model="activeTab"
|
|
76
|
-
:tabs="basicTabs"
|
|
77
|
-
:variant="variant"
|
|
78
|
-
/>
|
|
79
|
-
</div>
|
|
80
|
-
</div>
|
|
81
|
-
</Variant>
|
|
82
|
-
|
|
83
57
|
<Variant title="With Badges">
|
|
84
58
|
<div style="padding: 2rem;">
|
|
85
59
|
<BaseTabs
|
|
86
60
|
model-value="inbox"
|
|
87
61
|
:tabs="tabsWithBadges"
|
|
88
|
-
variant="underline"
|
|
89
62
|
/>
|
|
90
63
|
</div>
|
|
91
64
|
</Variant>
|
|
@@ -95,7 +68,6 @@ function initPlayground() {
|
|
|
95
68
|
<BaseTabs
|
|
96
69
|
model-value="plate"
|
|
97
70
|
:tabs="tabsWithIcons"
|
|
98
|
-
variant="pills"
|
|
99
71
|
/>
|
|
100
72
|
</div>
|
|
101
73
|
</Variant>
|
|
@@ -105,23 +77,9 @@ function initPlayground() {
|
|
|
105
77
|
<BaseTabs
|
|
106
78
|
model-value="inbox"
|
|
107
79
|
:tabs="tabsWithBadges"
|
|
108
|
-
variant="underline"
|
|
109
80
|
/>
|
|
110
81
|
</div>
|
|
111
82
|
</Variant>
|
|
112
83
|
|
|
113
|
-
<Variant title="Pills — matches SegmentedControl.card">
|
|
114
|
-
<div style="padding: 2rem; display: flex; justify-content: center;">
|
|
115
|
-
<BaseTabs
|
|
116
|
-
v-model="activeTab"
|
|
117
|
-
variant="pills"
|
|
118
|
-
:tabs="[
|
|
119
|
-
{ id: 'overview', label: 'Overview' },
|
|
120
|
-
{ id: 'samples', label: 'Samples' },
|
|
121
|
-
{ id: 'results', label: 'Results' },
|
|
122
|
-
]"
|
|
123
|
-
/>
|
|
124
|
-
</div>
|
|
125
|
-
</Variant>
|
|
126
84
|
</Story>
|
|
127
85
|
</template>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
/** Renders
|
|
2
|
+
/** Renders an underline tab bar with mono count badges, icons, and disabled tab support. */
|
|
3
3
|
import { computed, type ComponentPublicInstance } from 'vue'
|
|
4
4
|
import type { TabItem, TabItemInput } from '../types'
|
|
5
5
|
import { normalizeItemInput } from '../utils/items'
|
|
@@ -8,12 +8,9 @@ import { useRovingFocus } from '../composables/useRovingFocus'
|
|
|
8
8
|
interface Props {
|
|
9
9
|
modelValue: string
|
|
10
10
|
tabs: TabItemInput[]
|
|
11
|
-
variant?: 'underline' | 'pills'
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
const props =
|
|
15
|
-
variant: 'underline',
|
|
16
|
-
})
|
|
13
|
+
const props = defineProps<Props>()
|
|
17
14
|
|
|
18
15
|
const emit = defineEmits<{
|
|
19
16
|
'update:modelValue': [value: string]
|
|
@@ -55,7 +52,7 @@ const { handleKeydown } = useRovingFocus({
|
|
|
55
52
|
</script>
|
|
56
53
|
|
|
57
54
|
<template>
|
|
58
|
-
<div
|
|
55
|
+
<div class="mint-tabs mint-tabs--underline" role="tablist">
|
|
59
56
|
<button
|
|
60
57
|
v-for="tab in normalizedTabs"
|
|
61
58
|
:id="`mint-tab-${tab.id}`"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/** Drag-and-drop file upload zone with type filtering, size validation, and folder mode. */
|
|
3
|
-
import { ref, computed } from 'vue'
|
|
3
|
+
import { ref, computed, watch } from 'vue'
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
accept?: string
|
|
@@ -11,6 +11,9 @@ interface Props {
|
|
|
11
11
|
mode?: 'file' | 'folder' | 'file+folder'
|
|
12
12
|
/** Hide the selection list when a parent presents the files itself. */
|
|
13
13
|
showFiles?: boolean
|
|
14
|
+
/** Restore selections owned by a parent form; omitted for internal selection state. */
|
|
15
|
+
files?: File[]
|
|
16
|
+
rootName?: string
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -32,6 +35,12 @@ const inputRef = ref<HTMLInputElement>()
|
|
|
32
35
|
const selectedFiles = ref<File[]>([])
|
|
33
36
|
const folderName = ref<string | null>(null)
|
|
34
37
|
|
|
38
|
+
watch(() => [props.files, props.rootName] as const, ([files, rootName]) => {
|
|
39
|
+
if (files === undefined) return
|
|
40
|
+
selectedFiles.value = [...files]
|
|
41
|
+
folderName.value = rootName ?? extractFolderName(files)
|
|
42
|
+
}, { immediate: true })
|
|
43
|
+
|
|
35
44
|
const isFolder = computed(() => props.mode === 'folder')
|
|
36
45
|
const isMixed = computed(() => props.mode === 'file+folder')
|
|
37
46
|
|