@morscherlab/mint-sdk 1.2.0 → 1.2.2
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-DELMkUFR.js} +39 -16
- package/dist/SettingsModal-DELMkUFR.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/ChartContainer.test.d.ts +1 -0
- package/dist/__tests__/components/ChemicalFormula.test.d.ts +1 -0
- package/dist/__tests__/components/FormField.test.d.ts +1 -0
- package/dist/__tests__/components/LayoutResizeHandle.test.d.ts +1 -0
- package/dist/__tests__/components/NumberInput.test.d.ts +1 -0
- package/dist/__tests__/components/SearchableSelect.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.decoration.test.d.ts +1 -0
- package/dist/__tests__/composables/localFiles.test.d.ts +1 -0
- package/dist/__tests__/composables/useManualLayoutResize.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/AdductText.vue.d.ts +17 -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/ChartContainer.vue.d.ts +19 -5
- package/dist/components/ChemicalFormula.vue.d.ts +28 -2
- package/dist/components/DataFrame.vue.d.ts +1 -1
- package/dist/components/FileUploader.vue.d.ts +3 -0
- package/dist/components/FormField.vue.d.ts +8 -1
- package/dist/components/LayoutResizeHandle.vue.d.ts +23 -0
- package/dist/components/PlotlyChart.vue.d.ts +34 -6
- package/dist/components/PluginWorkspaceView.props.d.ts +3 -3
- package/dist/components/SearchableSelect.vue.d.ts +57 -0
- package/dist/components/WellPlate.vue.d.ts +5 -1
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.js +6 -6
- package/dist/{components-DIum5hkx.js → components-CxQVQCpP.js} +1215 -546
- package/dist/components-CxQVQCpP.js.map +1 -0
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/index.js +8 -8
- package/dist/composables/pluginWorkspaceModel.d.ts +0 -3
- package/dist/composables/useManualLayoutResize.d.ts +56 -0
- package/dist/composables/useToast.d.ts +22 -1
- package/dist/{composables-mFXsTA4g.js → composables-DAqOU8Aq.js} +97 -8
- package/dist/composables-DAqOU8Aq.js.map +1 -0
- package/dist/index.js +11 -11
- package/dist/install.js +2 -2
- package/dist/styles.css +855 -320
- package/dist/templates/index.js +2 -2
- package/dist/{templates-CmGfmBRA.js → templates-NuaNHohp.js} +2 -2
- package/dist/{templates-CmGfmBRA.js.map → templates-NuaNHohp.js.map} +1 -1
- package/dist/types/componentLabTypes.d.ts +3 -0
- package/dist/types/components.d.ts +26 -1
- 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-vLiRPUmj.js} +181 -71
- package/dist/useControlSchema-vLiRPUmj.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-D37ajgS8.js} +2 -2
- package/dist/{useFormBuilder-z5L_dUIy.js.map → useFormBuilder-D37ajgS8.js.map} +1 -1
- package/dist/{validation-uwoJGEb6.js → validation-ChMyixAf.js} +21 -9
- package/dist/validation-ChMyixAf.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/ChartContainer.test.ts +24 -0
- package/src/__tests__/components/ChemicalFormula.test.ts +27 -0
- 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/LayoutResizeHandle.test.ts +19 -0
- package/src/__tests__/components/NumberInput.test.ts +149 -0
- package/src/__tests__/components/PlotlyChart.test.ts +48 -0
- package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +1 -2
- package/src/__tests__/components/PluginWorkspaceView.test.ts +1 -2
- package/src/__tests__/components/RackEditor.test.ts +9 -0
- package/src/__tests__/components/SearchableSelect.test.ts +61 -0
- package/src/__tests__/components/WellPlate.decoration.test.ts +28 -0
- package/src/__tests__/composables/localFiles.test.ts +57 -0
- package/src/__tests__/composables/useManualLayoutResize.test.ts +21 -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/AdductText.story.vue +15 -0
- package/src/components/AdductText.vue +40 -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/ChartContainer.story.vue +21 -0
- package/src/components/ChartContainer.vue +27 -6
- package/src/components/ChemicalFormula.story.vue +7 -0
- package/src/components/ChemicalFormula.vue +40 -11
- package/src/components/FileUploader.vue +10 -1
- package/src/components/FormField.vue +10 -3
- package/src/components/GeneratedPathInput.vue +11 -7
- package/src/components/LayoutResizeHandle.story.vue +26 -0
- package/src/components/LayoutResizeHandle.vue +62 -0
- package/src/components/NumberInput.story.vue +32 -13
- package/src/components/NumberInput.vue +145 -56
- package/src/components/PlotlyChart.vue +77 -11
- package/src/components/PluginWorkspaceView.props.ts +3 -3
- package/src/components/RackEditor.vue +38 -32
- package/src/components/SearchableSelect.story.vue +24 -0
- package/src/components/SearchableSelect.vue +223 -0
- package/src/components/WellPlate.story.vue +16 -0
- package/src/components/WellPlate.vue +17 -8
- package/src/components/index.ts +3 -1
- 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/index.ts +10 -0
- package/src/composables/pluginWorkspaceModel.ts +0 -3
- package/src/composables/useManualLayoutResize.ts +165 -0
- 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/adduct-text.css +45 -0
- package/src/styles/components/app-sidebar.css +282 -121
- package/src/styles/components/chart-container.css +92 -0
- package/src/styles/components/chemical-formula.css +34 -4
- package/src/styles/components/form-builder.css +1 -1
- package/src/styles/components/form-field.css +28 -0
- package/src/styles/components/layout-resize-handle.css +60 -0
- package/src/styles/components/number-input.css +75 -91
- package/src/styles/components/pill.css +49 -51
- package/src/styles/components/rack-editor.css +31 -9
- package/src/styles/components/searchable-select.css +63 -0
- 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/components/well-plate.css +9 -0
- package/src/styles/index.css +3 -0
- package/src/styles/variables.css +25 -5
- package/src/types/componentLabTypes.ts +6 -0
- package/src/types/components.ts +30 -0
- package/src/types/form-builder.ts +2 -0
- package/src/types/generated-ui.ts +2 -0
- package/src/types/index.ts +5 -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/composables-mFXsTA4g.js.map +0 -1
- package/dist/useControlSchema-DldpxNOP.js.map +0 -1
- package/dist/validation-uwoJGEb6.js.map +0 -1
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
normalizeGeneratedJobInput,
|
|
7
7
|
} from '../generated/schema'
|
|
8
8
|
import type { GeneratedJobDefinition } from '../types/generated-ui'
|
|
9
|
+
import { validateFieldValue } from '../composables/useFormValidation'
|
|
10
|
+
import { buildRules } from '../composables/formBuilderSchema'
|
|
9
11
|
|
|
10
12
|
const definition: GeneratedJobDefinition = {
|
|
11
13
|
id: 'summarize',
|
|
@@ -30,6 +32,115 @@ const definition: GeneratedJobDefinition = {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
describe('generated UI schema adapter', () => {
|
|
35
|
+
it('allows present empty strings, arrays, nullable values and false', () => {
|
|
36
|
+
const requiredDefinition: GeneratedJobDefinition = {
|
|
37
|
+
...definition,
|
|
38
|
+
input_schema: {
|
|
39
|
+
properties: {
|
|
40
|
+
nullable: { anyOf: [{ type: 'string' }, { type: 'null' }] },
|
|
41
|
+
text: { type: 'string' },
|
|
42
|
+
labels: { type: 'array', items: { type: 'string' } },
|
|
43
|
+
enabled: { type: 'boolean' },
|
|
44
|
+
nonempty: { type: 'string', minLength: 1 },
|
|
45
|
+
},
|
|
46
|
+
required: ['nullable', 'text', 'labels', 'enabled', 'nonempty'],
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
const fields = generatedJobFormSchema(requiredDefinition).sections[0]!.fields
|
|
50
|
+
const rules = buildRules(fields)
|
|
51
|
+
const values = { nullable: null, text: '', labels: [], enabled: false, nonempty: 'valid' }
|
|
52
|
+
expect(fields.map(field => validateFieldValue(values[field.name as keyof typeof values], rules[field.name], values)))
|
|
53
|
+
.toEqual([null, null, null, null, null])
|
|
54
|
+
expect(generatedJobDefaults(requiredDefinition)).toEqual({ nullable: null })
|
|
55
|
+
expect(validateFieldValue('', rules.nonempty, {})).not.toBeNull()
|
|
56
|
+
expect(normalizeGeneratedJobInput(requiredDefinition, values)).toEqual(values)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('supplies fixed literal values and resolves array enums and numeric items', () => {
|
|
60
|
+
const arrayDefinition: GeneratedJobDefinition = {
|
|
61
|
+
...definition,
|
|
62
|
+
input_schema: {
|
|
63
|
+
$defs: { Choice: { type: 'integer', enum: [1, 2] } },
|
|
64
|
+
properties: {
|
|
65
|
+
kind: { type: 'string', const: 'fixed' },
|
|
66
|
+
choices: { type: 'array', items: { $ref: '#/$defs/Choice' } },
|
|
67
|
+
counts: { type: 'array', items: { type: 'integer' } },
|
|
68
|
+
scales: { type: 'array', items: { type: 'number' } },
|
|
69
|
+
labels: { type: 'array', items: { type: 'string' } },
|
|
70
|
+
flags: { type: 'array', items: { type: 'boolean' } },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
expect(generatedJobDefaults(arrayDefinition)).toEqual({ kind: 'fixed' })
|
|
75
|
+
expect(generatedJobFormSchema(arrayDefinition).sections[0]!.fields).toMatchObject([
|
|
76
|
+
{ name: 'kind', defaultValue: 'fixed', disabled: true },
|
|
77
|
+
{ name: 'choices', type: 'multiselect', props: { options: [1, 2] } },
|
|
78
|
+
{ name: 'counts', type: 'tags' },
|
|
79
|
+
{ name: 'scales', type: 'tags' },
|
|
80
|
+
{ name: 'labels', type: 'tags', props: { allowDuplicates: true } },
|
|
81
|
+
{ name: 'flags', type: 'tags' },
|
|
82
|
+
])
|
|
83
|
+
expect(normalizeGeneratedJobInput(arrayDefinition, {
|
|
84
|
+
counts: ['1', '2'], scales: ['0.5', '1.5'], choices: [1, 2], labels: ['1', '1'],
|
|
85
|
+
})).toEqual({ counts: [1, 2], scales: [0.5, 1.5], choices: [1, 2], labels: ['1', '1'] })
|
|
86
|
+
expect(normalizeGeneratedJobInput(arrayDefinition, { counts: ['bad', '1.5'] }))
|
|
87
|
+
.toEqual({ counts: ['bad', 1.5] })
|
|
88
|
+
expect(normalizeGeneratedJobInput(arrayDefinition, { flags: ['true', 'false', true, 'bad'] }))
|
|
89
|
+
.toEqual({ flags: [true, false, true, 'bad'] })
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('applies integer bounds and numeric steps to the existing controls', () => {
|
|
93
|
+
const boundedDefinition: GeneratedJobDefinition = {
|
|
94
|
+
...definition,
|
|
95
|
+
input_schema: { properties: {
|
|
96
|
+
count: { type: 'integer', exclusiveMinimum: 0, exclusiveMaximum: 10, multipleOf: 2 },
|
|
97
|
+
fraction: { type: 'number', minimum: 0, maximum: 1, multipleOf: 0.1 },
|
|
98
|
+
} },
|
|
99
|
+
}
|
|
100
|
+
const fields = generatedJobFormSchema(boundedDefinition).sections[0]!.fields
|
|
101
|
+
const rules = buildRules(fields)
|
|
102
|
+
expect(fields).toMatchObject([
|
|
103
|
+
{ validation: { min: 2, max: 8 }, props: { min: 2, max: 8, step: 2 } },
|
|
104
|
+
{ validation: { min: 0, max: 1 }, props: { min: 0, max: 1, step: 0.1 } },
|
|
105
|
+
])
|
|
106
|
+
expect(validateFieldValue(0, rules.count, {})).not.toBeNull()
|
|
107
|
+
expect(validateFieldValue(10, rules.count, {})).not.toBeNull()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('preserves field labels and hints outside nullable schemas and enum references', () => {
|
|
111
|
+
const annotatedDefinition: GeneratedJobDefinition = {
|
|
112
|
+
...definition,
|
|
113
|
+
input_schema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
$defs: { Method: { type: 'string', enum: ['fast', 'robust'], title: 'Method' } },
|
|
116
|
+
properties: {
|
|
117
|
+
count: {
|
|
118
|
+
anyOf: [{ type: 'integer', minimum: 1 }, { type: 'null' }],
|
|
119
|
+
title: 'Optional sample count',
|
|
120
|
+
description: 'Leave empty to use all samples',
|
|
121
|
+
default: null,
|
|
122
|
+
},
|
|
123
|
+
method: {
|
|
124
|
+
$ref: '#/$defs/Method',
|
|
125
|
+
title: 'Analysis method',
|
|
126
|
+
description: 'Choose the analysis method',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
expect(generatedJobFormSchema(annotatedDefinition).sections[0]?.fields).toMatchObject([
|
|
133
|
+
{
|
|
134
|
+
name: 'count', type: 'number', label: 'Optional sample count',
|
|
135
|
+
hint: 'Leave empty to use all samples', defaultValue: null, validation: { min: 1 },
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'method', type: 'select', label: 'Analysis method',
|
|
139
|
+
hint: 'Choose the analysis method', props: { options: ['fast', 'robust'] },
|
|
140
|
+
},
|
|
141
|
+
])
|
|
142
|
+
})
|
|
143
|
+
|
|
33
144
|
it('maps the supported JSON Schema subset to standard FormBuilder fields', () => {
|
|
34
145
|
expect(generatedJobFormSchema(definition).sections[0]?.fields).toMatchObject([
|
|
35
146
|
{ name: 'threshold', type: 'number', validation: { required: true, min: 0, max: 10 } },
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, resolve } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
|
|
6
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
const componentsDir = resolve(here, '../../styles/components')
|
|
8
|
+
|
|
9
|
+
// Every color in these sheets must resolve from a token so dark mode remaps it.
|
|
10
|
+
const TOKEN_ONLY = ['pill.css', 'tabs.css', 'segmented-control.css', 'toast.css']
|
|
11
|
+
|
|
12
|
+
describe('token-only component stylesheets', () => {
|
|
13
|
+
it.each(TOKEN_ONLY)('%s has no rgba() literal', (file) => {
|
|
14
|
+
expect(readFileSync(join(componentsDir, file), 'utf-8')).not.toMatch(/rgba?\(/)
|
|
15
|
+
})
|
|
16
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import AdductText from './AdductText.vue'
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<Story title="Data Display/AdductText">
|
|
7
|
+
<Variant title="Inline and badge">
|
|
8
|
+
<div style="display: flex; align-items: center; gap: 1rem; padding: 2rem; color: var(--text-primary);">
|
|
9
|
+
<AdductText value="[M+H]+" />
|
|
10
|
+
<AdductText value="[M+Na]+" variant="badge" />
|
|
11
|
+
<AdductText />
|
|
12
|
+
</div>
|
|
13
|
+
</Variant>
|
|
14
|
+
</Story>
|
|
15
|
+
</template>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** Compact, overflow-safe display for mass-spectrometry adduct notation. */
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
value?: string | null
|
|
7
|
+
emptyText?: string
|
|
8
|
+
variant?: 'inline' | 'badge'
|
|
9
|
+
tone?: 'default' | 'on-primary' | 'muted'
|
|
10
|
+
size?: 'xs' | 'inherit'
|
|
11
|
+
font?: 'mono' | 'sans'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
15
|
+
value: null,
|
|
16
|
+
emptyText: '-',
|
|
17
|
+
variant: 'inline',
|
|
18
|
+
tone: 'default',
|
|
19
|
+
size: 'xs',
|
|
20
|
+
font: 'mono',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const value = computed(() => props.value?.trim() ?? '')
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<span
|
|
28
|
+
:class="[
|
|
29
|
+
'mint-adduct-text',
|
|
30
|
+
`mint-adduct-text--${variant}`,
|
|
31
|
+
`mint-adduct-text--${tone}`,
|
|
32
|
+
`mint-adduct-text--${size}`,
|
|
33
|
+
`mint-adduct-text--${font}`,
|
|
34
|
+
{ 'mint-adduct-text--empty': !value },
|
|
35
|
+
]"
|
|
36
|
+
:title="value || undefined"
|
|
37
|
+
>
|
|
38
|
+
{{ value || emptyText }}
|
|
39
|
+
</span>
|
|
40
|
+
</template>
|
|
@@ -16,7 +16,7 @@ import { useBioTemplateWorkspace } from '../composables/useBioTemplateWorkspace'
|
|
|
16
16
|
import { createWellPlateScreenCollection } from '../templates'
|
|
17
17
|
import type { SidebarToolSection } from '../types'
|
|
18
18
|
|
|
19
|
-
type
|
|
19
|
+
type SidebarDensity = 'compact' | 'comfortable'
|
|
20
20
|
|
|
21
21
|
/* --- Icon paths (Lucide-style) --- */
|
|
22
22
|
const icons = {
|
|
@@ -256,7 +256,7 @@ function handleToggle(id: string, value: boolean) {
|
|
|
256
256
|
function initSimpleToolkit() {
|
|
257
257
|
return {
|
|
258
258
|
activeView: 'analysis',
|
|
259
|
-
|
|
259
|
+
density: 'compact' as SidebarDensity,
|
|
260
260
|
floating: false,
|
|
261
261
|
width: '20rem',
|
|
262
262
|
side: 'left' as const,
|
|
@@ -351,7 +351,6 @@ function initSimpleToolkit() {
|
|
|
351
351
|
<Variant title="Schema Driven Controls">
|
|
352
352
|
<div style="padding: 2rem; height: 520px; position: relative; background: var(--bg-primary, #f1f5f9);">
|
|
353
353
|
<AppSidebar
|
|
354
|
-
variant="analysis"
|
|
355
354
|
title="Peak Picking"
|
|
356
355
|
subtitle="Current experiment"
|
|
357
356
|
:controls="schemaControls"
|
|
@@ -365,7 +364,6 @@ function initSimpleToolkit() {
|
|
|
365
364
|
<div style="padding: 2rem; height: 560px; position: relative; background: var(--bg-primary, #f1f5f9);">
|
|
366
365
|
<AppSidebar
|
|
367
366
|
v-model="modelDrivenValues"
|
|
368
|
-
variant="analysis"
|
|
369
367
|
title="Peak Picking"
|
|
370
368
|
subtitle="Current experiment"
|
|
371
369
|
:model="modelDrivenSidebarModel"
|
|
@@ -376,7 +374,6 @@ function initSimpleToolkit() {
|
|
|
376
374
|
<Variant title="Route Owned Shell">
|
|
377
375
|
<div style="padding: 2rem; height: 560px; display: flex; gap: 1rem; background: var(--bg-primary, #f1f5f9);">
|
|
378
376
|
<AppSidebar
|
|
379
|
-
variant="analysis"
|
|
380
377
|
title="Sequence"
|
|
381
378
|
subtitle="Acquisition run"
|
|
382
379
|
content-id="seqgen-sidebar"
|
|
@@ -466,7 +463,7 @@ function initSimpleToolkit() {
|
|
|
466
463
|
</div>
|
|
467
464
|
<AppSidebar
|
|
468
465
|
:panels="simplePanels"
|
|
469
|
-
:
|
|
466
|
+
:density="state.density"
|
|
470
467
|
:active-view="state.activeView"
|
|
471
468
|
:floating="state.floating"
|
|
472
469
|
:width="state.width"
|
|
@@ -531,11 +528,11 @@ function initSimpleToolkit() {
|
|
|
531
528
|
:options="['analysis', 'results', 'settings'].map(v => ({ label: v, value: v }))"
|
|
532
529
|
/>
|
|
533
530
|
<HstSelect
|
|
534
|
-
v-model="state.
|
|
535
|
-
title="
|
|
531
|
+
v-model="state.density"
|
|
532
|
+
title="Density"
|
|
536
533
|
:options="[
|
|
537
|
-
{ label: '
|
|
538
|
-
{ label: '
|
|
534
|
+
{ label: 'Compact', value: 'compact' },
|
|
535
|
+
{ label: 'Comfortable', value: 'comfortable' },
|
|
539
536
|
]"
|
|
540
537
|
/>
|
|
541
538
|
<HstCheckbox v-model="state.floating" title="Floating" />
|
|
@@ -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"
|