@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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** Accessible pointer and keyboard handle for resizable workbench panes. */
|
|
3
|
+
interface Props {
|
|
4
|
+
modelValue: number
|
|
5
|
+
min: number
|
|
6
|
+
max: number
|
|
7
|
+
label: string
|
|
8
|
+
orientation: 'vertical' | 'horizontal'
|
|
9
|
+
edge?: 'left' | 'right'
|
|
10
|
+
step?: number
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
15
|
+
edge: undefined,
|
|
16
|
+
step: 10,
|
|
17
|
+
disabled: false,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const emit = defineEmits<{
|
|
21
|
+
'update:modelValue': [value: number]
|
|
22
|
+
'resize-start': [event: PointerEvent]
|
|
23
|
+
}>()
|
|
24
|
+
|
|
25
|
+
function update(value: number): void {
|
|
26
|
+
emit('update:modelValue', Math.min(props.max, Math.max(props.min, value)))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function handleKeydown(event: KeyboardEvent): void {
|
|
30
|
+
if (props.disabled) return
|
|
31
|
+
const decrement = props.orientation === 'vertical' ? 'ArrowLeft' : 'ArrowUp'
|
|
32
|
+
const increment = props.orientation === 'vertical' ? 'ArrowRight' : 'ArrowDown'
|
|
33
|
+
if (event.key === decrement) update(props.modelValue - props.step)
|
|
34
|
+
else if (event.key === increment) update(props.modelValue + props.step)
|
|
35
|
+
else if (event.key === 'Home') update(props.min)
|
|
36
|
+
else if (event.key === 'End') update(props.max)
|
|
37
|
+
else return
|
|
38
|
+
event.preventDefault()
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<div
|
|
44
|
+
role="separator"
|
|
45
|
+
:tabindex="disabled ? -1 : 0"
|
|
46
|
+
:class="[
|
|
47
|
+
'mint-layout-resize-handle',
|
|
48
|
+
`mint-layout-resize-handle--${orientation}`,
|
|
49
|
+
edge ? `mint-layout-resize-handle--edge-${edge}` : '',
|
|
50
|
+
]"
|
|
51
|
+
:aria-label="label"
|
|
52
|
+
:aria-orientation="orientation"
|
|
53
|
+
:aria-valuemin="min"
|
|
54
|
+
:aria-valuemax="max"
|
|
55
|
+
:aria-valuenow="modelValue"
|
|
56
|
+
:aria-disabled="disabled || undefined"
|
|
57
|
+
@pointerdown="!disabled && emit('resize-start', $event)"
|
|
58
|
+
@keydown="handleKeydown"
|
|
59
|
+
>
|
|
60
|
+
<span class="mint-layout-resize-handle__bar" aria-hidden="true" />
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
2
3
|
import NumberInput from './NumberInput.vue'
|
|
3
4
|
|
|
4
5
|
const sizes = ['sm', 'md', 'lg'] as const
|
|
6
|
+
|
|
7
|
+
const sizeValues = ref<Record<string, number | undefined>>({ sm: 42, md: 42, lg: 42 })
|
|
8
|
+
const samples = ref<number | undefined>(96)
|
|
9
|
+
const temperature = ref<number | undefined>(37)
|
|
10
|
+
const volume = ref<number | undefined>(100)
|
|
11
|
+
const overMax = ref<number | undefined>(999)
|
|
12
|
+
const empty = ref<number | undefined>(undefined)
|
|
13
|
+
const mass = ref<number | undefined>(12.5)
|
|
14
|
+
const ph = ref<number | undefined>(7.4)
|
|
15
|
+
const wells = ref<number | undefined>(96)
|
|
16
|
+
const sliderPh = ref<number | undefined>(7.4)
|
|
17
|
+
const percent = ref<number | undefined>(45)
|
|
5
18
|
</script>
|
|
6
19
|
|
|
7
20
|
<template>
|
|
@@ -39,6 +52,9 @@ const sizes = ['sm', 'md', 'lg'] as const
|
|
|
39
52
|
<HstCheckbox v-model="state.disabled" title="Disabled" />
|
|
40
53
|
<HstCheckbox v-model="state.error" title="Error" />
|
|
41
54
|
</template>
|
|
55
|
+
<template #initState>
|
|
56
|
+
{{ { value: 42, min: 0, max: 100, step: 1, size: 'md', placeholder: '', disabled: false, error: false } }}
|
|
57
|
+
</template>
|
|
42
58
|
</Variant>
|
|
43
59
|
|
|
44
60
|
<Variant title="Sizes">
|
|
@@ -47,7 +63,7 @@ const sizes = ['sm', 'md', 'lg'] as const
|
|
|
47
63
|
<p style="margin: 0 0 0.5rem; font-size: 0.75rem; color: var(--text-muted, #94a3b8); text-transform: uppercase; letter-spacing: 0.05em;">
|
|
48
64
|
{{ size }}
|
|
49
65
|
</p>
|
|
50
|
-
<NumberInput
|
|
66
|
+
<NumberInput v-model="sizeValues[size]" :size="size" />
|
|
51
67
|
</div>
|
|
52
68
|
</div>
|
|
53
69
|
</Variant>
|
|
@@ -56,22 +72,22 @@ const sizes = ['sm', 'md', 'lg'] as const
|
|
|
56
72
|
<div style="padding: 2rem; max-width: 300px; margin: 0 auto; display: flex; flex-direction: column; gap: 1.5rem;">
|
|
57
73
|
<div>
|
|
58
74
|
<p style="margin: 0 0 0.5rem; font-size: 0.875rem; color: var(--text-primary, #e2e8f0);">Sample count (0-384)</p>
|
|
59
|
-
<NumberInput
|
|
75
|
+
<NumberInput v-model="samples" :min="0" :max="384" :step="1" placeholder="Samples" />
|
|
60
76
|
</div>
|
|
61
77
|
<div>
|
|
62
78
|
<p style="margin: 0 0 0.5rem; font-size: 0.875rem; color: var(--text-primary, #e2e8f0);">Temperature (step 0.5)</p>
|
|
63
|
-
<NumberInput
|
|
79
|
+
<NumberInput v-model="temperature" :min="0" :max="100" :step="0.5" />
|
|
64
80
|
</div>
|
|
65
81
|
<div>
|
|
66
82
|
<p style="margin: 0 0 0.5rem; font-size: 0.875rem; color: var(--text-primary, #e2e8f0);">Volume in uL (step 10)</p>
|
|
67
|
-
<NumberInput
|
|
83
|
+
<NumberInput v-model="volume" :min="0" :max="1000" :step="10" />
|
|
68
84
|
</div>
|
|
69
85
|
</div>
|
|
70
86
|
</Variant>
|
|
71
87
|
|
|
72
88
|
<Variant title="Error State">
|
|
73
89
|
<div style="padding: 2rem; max-width: 300px; margin: 0 auto;">
|
|
74
|
-
<NumberInput
|
|
90
|
+
<NumberInput v-model="overMax" :max="100" error />
|
|
75
91
|
</div>
|
|
76
92
|
</Variant>
|
|
77
93
|
|
|
@@ -83,16 +99,16 @@ const sizes = ['sm', 'md', 'lg'] as const
|
|
|
83
99
|
|
|
84
100
|
<Variant title="Empty with Placeholder">
|
|
85
101
|
<div style="padding: 2rem; max-width: 300px; margin: 0 auto;">
|
|
86
|
-
<NumberInput placeholder="Enter value..." />
|
|
102
|
+
<NumberInput v-model="empty" placeholder="Enter value..." />
|
|
87
103
|
</div>
|
|
88
104
|
</Variant>
|
|
89
105
|
|
|
90
|
-
<Variant title="
|
|
106
|
+
<Variant title="Steppers (always visible)">
|
|
91
107
|
<template #default="{ state }">
|
|
92
108
|
<div style="padding: 2rem; max-width: 320px; display: flex; flex-direction: column; gap: 1rem;">
|
|
93
109
|
<NumberInput v-model="state.value" :step="0.5" />
|
|
94
110
|
<p style="margin: 0; font-size: 0.8125rem; color: var(--text-muted);">
|
|
95
|
-
|
|
111
|
+
Two horizontal steppers, hairline-divided. Arrow keys, PageUp/PageDown also step.
|
|
96
112
|
</p>
|
|
97
113
|
</div>
|
|
98
114
|
</template>
|
|
@@ -106,16 +122,19 @@ const sizes = ['sm', 'md', 'lg'] as const
|
|
|
106
122
|
|
|
107
123
|
<Variant title="With unit suffix">
|
|
108
124
|
<div style="padding: 2rem; max-width: 320px; display: flex; flex-direction: column; gap: 0.75rem;">
|
|
109
|
-
<NumberInput
|
|
110
|
-
<NumberInput
|
|
111
|
-
<NumberInput
|
|
125
|
+
<NumberInput v-model="mass" unit="mg" :step="0.1" />
|
|
126
|
+
<NumberInput v-model="ph" unit="pH" :step="0.1" :min="0" :max="14" />
|
|
127
|
+
<NumberInput v-model="wells" unit="wells" :step="1" />
|
|
112
128
|
</div>
|
|
113
129
|
</Variant>
|
|
114
130
|
|
|
115
131
|
<Variant title="Slider mode (min + max provided)">
|
|
116
132
|
<div style="padding: 2rem; max-width: 320px; display: flex; flex-direction: column; gap: 1rem;">
|
|
117
|
-
<NumberInput
|
|
118
|
-
<NumberInput
|
|
133
|
+
<NumberInput v-model="sliderPh" :min="0" :max="14" :step="0.1" unit="pH" />
|
|
134
|
+
<NumberInput v-model="percent" :min="0" :max="100" :step="1" unit="%" />
|
|
135
|
+
<p style="margin: 0; font-size: 0.8125rem; color: var(--text-muted);">
|
|
136
|
+
Drag the number left or right to adjust; click to type. Home/End on the slider jump to min/max.
|
|
137
|
+
</p>
|
|
119
138
|
</div>
|
|
120
139
|
</Variant>
|
|
121
140
|
</Story>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
/** Numeric input with
|
|
3
|
-
import { computed } from 'vue'
|
|
2
|
+
/** Numeric input with always-visible steppers, optional unit label, and (with min+max) an in-field drag-to-scrub range fill. */
|
|
3
|
+
import { computed, onBeforeUnmount } from 'vue'
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
modelValue?: number
|
|
@@ -33,7 +33,7 @@ const sliderPercent = computed(() => {
|
|
|
33
33
|
if (min === undefined || max === undefined || props.modelValue === undefined) return 0
|
|
34
34
|
const range = max - min
|
|
35
35
|
if (range === 0) return 0
|
|
36
|
-
return ((props.modelValue - min) / range) * 100
|
|
36
|
+
return Math.min(100, Math.max(0, ((props.modelValue - min) / range) * 100))
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
const canDecrement = computed(() => {
|
|
@@ -55,6 +55,17 @@ function clamp(value: number): number {
|
|
|
55
55
|
return result
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/** Clamp and trim float noise (0.1 + 0.2 → 0.3) without losing real precision. */
|
|
59
|
+
function settle(value: number): number {
|
|
60
|
+
return Number(clamp(value).toFixed(10))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Snap to the step grid anchored at `min`, for pointer scrubbing. */
|
|
64
|
+
function snap(value: number): number {
|
|
65
|
+
const origin = props.min ?? 0
|
|
66
|
+
return settle(origin + Math.round((value - origin) / props.step) * props.step)
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
function handleInput(event: Event) {
|
|
59
70
|
const target = event.target as HTMLInputElement
|
|
60
71
|
const value = target.value === '' ? undefined : Number(target.value)
|
|
@@ -65,24 +76,98 @@ function handleInput(event: Event) {
|
|
|
65
76
|
}
|
|
66
77
|
}
|
|
67
78
|
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (!
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
function nudge(delta: number) {
|
|
80
|
+
if (props.disabled) return
|
|
81
|
+
if (delta < 0 && !canDecrement.value) return
|
|
82
|
+
if (delta > 0 && !canIncrement.value) return
|
|
83
|
+
const current = props.modelValue ?? (delta < 0 ? props.max ?? 0 : props.min ?? 0)
|
|
84
|
+
emit('update:modelValue', settle(current + delta))
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
function decrement() {
|
|
77
|
-
|
|
78
|
-
const current = props.modelValue ?? (props.max ?? 0)
|
|
79
|
-
emit('update:modelValue', clamp(current - props.step))
|
|
88
|
+
nudge(-props.step)
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
function increment() {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
nudge(props.step)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Home/End only apply on the slider surface: inside the text input they must
|
|
96
|
+
// keep moving the caret.
|
|
97
|
+
function handleKeydown(event: KeyboardEvent, allowJump = false) {
|
|
98
|
+
if (props.disabled) return
|
|
99
|
+
switch (event.key) {
|
|
100
|
+
case 'ArrowUp': increment(); break
|
|
101
|
+
case 'ArrowDown': decrement(); break
|
|
102
|
+
case 'PageUp': nudge(props.step * 10); break
|
|
103
|
+
case 'PageDown': nudge(-props.step * 10); break
|
|
104
|
+
case 'Home':
|
|
105
|
+
if (!allowJump || props.min === undefined) return
|
|
106
|
+
emit('update:modelValue', props.min)
|
|
107
|
+
break
|
|
108
|
+
case 'End':
|
|
109
|
+
if (!allowJump || props.max === undefined) return
|
|
110
|
+
emit('update:modelValue', props.max)
|
|
111
|
+
break
|
|
112
|
+
default:
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
event.preventDefault()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The surrounding range uses absolute position; dragging the number uses a
|
|
119
|
+
// relative offset so grabbing it never jumps away from the current value.
|
|
120
|
+
function scrubTo(clientX: number, area: HTMLElement) {
|
|
121
|
+
const rect = area.getBoundingClientRect()
|
|
122
|
+
if (rect.width === 0 || props.min === undefined || props.max === undefined) return
|
|
123
|
+
const ratio = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width))
|
|
124
|
+
emit('update:modelValue', snap(props.min + ratio * (props.max - props.min)))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let stopScrubbing: (() => void) | undefined
|
|
128
|
+
onBeforeUnmount(() => stopScrubbing?.())
|
|
129
|
+
|
|
130
|
+
function handleScrubStart(event: PointerEvent) {
|
|
131
|
+
const target = event.target as HTMLElement
|
|
132
|
+
const onNumber = target.tagName === 'INPUT'
|
|
133
|
+
if (props.disabled || event.button !== 0 || (!onNumber && !isSliderMode.value)) return
|
|
134
|
+
stopScrubbing?.()
|
|
135
|
+
const area = event.currentTarget as HTMLElement
|
|
136
|
+
const startValue = props.modelValue ?? props.min ?? 0
|
|
137
|
+
const width = area.getBoundingClientRect().width
|
|
138
|
+
const unitsPerPixel = isSliderMode.value && width > 0
|
|
139
|
+
? (props.max! - props.min!) / width
|
|
140
|
+
: props.step / 8
|
|
141
|
+
// Preserve ordinary clicks and double-clicks for caret placement and typing.
|
|
142
|
+
if (!onNumber) {
|
|
143
|
+
event.preventDefault()
|
|
144
|
+
scrubTo(event.clientX, area)
|
|
145
|
+
}
|
|
146
|
+
target.setPointerCapture?.(event.pointerId)
|
|
147
|
+
let dragging = false
|
|
148
|
+
const move = (e: PointerEvent) => {
|
|
149
|
+
if (e.pointerId !== event.pointerId) return
|
|
150
|
+
if (props.disabled) { stop(); return }
|
|
151
|
+
if (!onNumber) { scrubTo(e.clientX, area); return }
|
|
152
|
+
const delta = e.clientX - event.clientX
|
|
153
|
+
if (!dragging && Math.abs(delta) < 4) return
|
|
154
|
+
dragging = true
|
|
155
|
+
e.preventDefault()
|
|
156
|
+
emit('update:modelValue', settle(startValue + Math.round(delta * unitsPerPixel / props.step) * props.step))
|
|
157
|
+
}
|
|
158
|
+
const stop = () => {
|
|
159
|
+
area.removeEventListener('pointermove', move)
|
|
160
|
+
area.removeEventListener('pointerup', stop)
|
|
161
|
+
area.removeEventListener('pointercancel', stop)
|
|
162
|
+
area.removeEventListener('lostpointercapture', stop)
|
|
163
|
+
if (target.hasPointerCapture?.(event.pointerId)) target.releasePointerCapture(event.pointerId)
|
|
164
|
+
stopScrubbing = undefined
|
|
165
|
+
}
|
|
166
|
+
stopScrubbing = stop
|
|
167
|
+
area.addEventListener('pointermove', move)
|
|
168
|
+
area.addEventListener('pointerup', stop)
|
|
169
|
+
area.addEventListener('pointercancel', stop)
|
|
170
|
+
area.addEventListener('lostpointercapture', stop)
|
|
86
171
|
}
|
|
87
172
|
</script>
|
|
88
173
|
|
|
@@ -97,62 +182,66 @@ function increment() {
|
|
|
97
182
|
]"
|
|
98
183
|
>
|
|
99
184
|
<div class="mint-number-input__field">
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
:
|
|
103
|
-
:
|
|
104
|
-
:
|
|
105
|
-
:
|
|
106
|
-
:
|
|
107
|
-
:
|
|
108
|
-
:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
185
|
+
<div
|
|
186
|
+
class="mint-number-input__scrub"
|
|
187
|
+
:style="isSliderMode ? { '--mint-number-input-fill': `${sliderPercent}%` } : undefined"
|
|
188
|
+
:role="isSliderMode ? 'slider' : undefined"
|
|
189
|
+
:tabindex="isSliderMode && !disabled ? 0 : undefined"
|
|
190
|
+
:aria-valuenow="isSliderMode ? modelValue : undefined"
|
|
191
|
+
:aria-valuemin="isSliderMode ? min : undefined"
|
|
192
|
+
:aria-valuemax="isSliderMode ? max : undefined"
|
|
193
|
+
:aria-orientation="isSliderMode ? 'horizontal' : undefined"
|
|
194
|
+
@pointerdown="handleScrubStart"
|
|
195
|
+
@keydown.self="isSliderMode && handleKeydown($event, true)"
|
|
196
|
+
>
|
|
197
|
+
<template v-if="isSliderMode">
|
|
198
|
+
<span class="mint-number-input__wash" aria-hidden="true" />
|
|
199
|
+
<span class="mint-number-input__rule" aria-hidden="true" />
|
|
200
|
+
</template>
|
|
201
|
+
<input
|
|
202
|
+
type="number"
|
|
203
|
+
:value="modelValue"
|
|
204
|
+
:min="min"
|
|
205
|
+
:max="max"
|
|
206
|
+
:step="step"
|
|
207
|
+
:disabled="disabled"
|
|
208
|
+
:placeholder="placeholder"
|
|
209
|
+
:class="[
|
|
210
|
+
'mint-number-input__input',
|
|
211
|
+
`mint-number-input__input--${size}`,
|
|
212
|
+
]"
|
|
213
|
+
@input="handleInput"
|
|
214
|
+
@keydown="handleKeydown"
|
|
215
|
+
/>
|
|
216
|
+
<span v-if="unit" class="mint-number-input__unit">{{ unit }}</span>
|
|
217
|
+
</div>
|
|
116
218
|
|
|
117
219
|
<div class="mint-number-input__steppers" aria-hidden="true">
|
|
118
220
|
<button
|
|
119
221
|
type="button"
|
|
120
222
|
tabindex="-1"
|
|
121
|
-
aria-label="
|
|
122
|
-
:disabled="disabled || !
|
|
123
|
-
class="mint-number-input__stepper mint-number-input__stepper--
|
|
124
|
-
@click="
|
|
223
|
+
aria-label="Decrease value"
|
|
224
|
+
:disabled="disabled || !canDecrement"
|
|
225
|
+
class="mint-number-input__stepper mint-number-input__stepper--down"
|
|
226
|
+
@click="decrement"
|
|
125
227
|
>
|
|
126
|
-
<svg viewBox="0 0 10
|
|
127
|
-
<path d="
|
|
228
|
+
<svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
|
|
229
|
+
<path d="M2 5h6" />
|
|
128
230
|
</svg>
|
|
129
231
|
</button>
|
|
130
232
|
<button
|
|
131
233
|
type="button"
|
|
132
234
|
tabindex="-1"
|
|
133
|
-
aria-label="
|
|
134
|
-
:disabled="disabled || !
|
|
135
|
-
class="mint-number-input__stepper mint-number-input__stepper--
|
|
136
|
-
@click="
|
|
235
|
+
aria-label="Increase value"
|
|
236
|
+
:disabled="disabled || !canIncrement"
|
|
237
|
+
class="mint-number-input__stepper mint-number-input__stepper--up"
|
|
238
|
+
@click="increment"
|
|
137
239
|
>
|
|
138
|
-
<svg viewBox="0 0 10
|
|
139
|
-
<path d="
|
|
240
|
+
<svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
|
|
241
|
+
<path d="M5 2v6" /><path d="M2 5h6" />
|
|
140
242
|
</svg>
|
|
141
243
|
</button>
|
|
142
244
|
</div>
|
|
143
245
|
</div>
|
|
144
|
-
|
|
145
|
-
<input
|
|
146
|
-
v-if="isSliderMode"
|
|
147
|
-
type="range"
|
|
148
|
-
:value="modelValue ?? min"
|
|
149
|
-
:min="min"
|
|
150
|
-
:max="max"
|
|
151
|
-
:step="step"
|
|
152
|
-
:disabled="disabled"
|
|
153
|
-
:style="{ '--mint-slider-fill': `${sliderPercent}%` }"
|
|
154
|
-
class="mint-number-input__slider"
|
|
155
|
-
@input="handleSliderInput"
|
|
156
|
-
/>
|
|
157
246
|
</div>
|
|
158
247
|
</template>
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
import type * as PlotlyType from 'plotly.js'
|
|
19
|
-
import { nextTick, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
|
|
19
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'
|
|
20
20
|
|
|
21
21
|
import ChartContainer from './ChartContainer.vue'
|
|
22
22
|
|
|
@@ -30,6 +30,16 @@ interface Props {
|
|
|
30
30
|
empty?: boolean
|
|
31
31
|
emptyMessage?: string
|
|
32
32
|
ariaLabel?: string
|
|
33
|
+
/** Use a custom Plotly build instead of the SDK's lazy-loaded default bundle. */
|
|
34
|
+
plotly?: typeof PlotlyType
|
|
35
|
+
/** Pause rendering and resize work while a tab or panel is inactive. */
|
|
36
|
+
active?: boolean
|
|
37
|
+
/** Plot canvas height. Numbers are interpreted as pixels. */
|
|
38
|
+
height?: string | number
|
|
39
|
+
/** Forward Plotly point clicks through the `plotly-click` event. */
|
|
40
|
+
clickEvents?: boolean
|
|
41
|
+
/** Shared chart container chrome. `frame` fills a workbench panel. */
|
|
42
|
+
variant?: 'card' | 'frame'
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -39,11 +49,28 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
39
49
|
empty: false,
|
|
40
50
|
emptyMessage: 'No data available',
|
|
41
51
|
ariaLabel: 'Interactive Plotly chart',
|
|
52
|
+
active: true,
|
|
53
|
+
clickEvents: false,
|
|
54
|
+
variant: 'card',
|
|
42
55
|
})
|
|
43
56
|
|
|
57
|
+
const emit = defineEmits<{
|
|
58
|
+
'plotly-click': [event: PlotlyType.PlotMouseEvent]
|
|
59
|
+
}>()
|
|
60
|
+
|
|
61
|
+
defineSlots<{
|
|
62
|
+
header?: () => unknown
|
|
63
|
+
toolbar?: () => unknown
|
|
64
|
+
subhead?: () => unknown
|
|
65
|
+
empty?: () => unknown
|
|
66
|
+
error?: () => unknown
|
|
67
|
+
legend?: () => unknown
|
|
68
|
+
footer?: () => unknown
|
|
69
|
+
}>()
|
|
70
|
+
|
|
44
71
|
const chartElement = ref<HTMLDivElement | null>(null)
|
|
45
72
|
const failed = ref(false)
|
|
46
|
-
let plotly: typeof PlotlyType | null = null
|
|
73
|
+
let plotly: typeof PlotlyType | null = props.plotly ?? null
|
|
47
74
|
let resizeObserver: ResizeObserver | null = null
|
|
48
75
|
let themeObserver: MutationObserver | null = null
|
|
49
76
|
let renderedElement: HTMLDivElement | null = null
|
|
@@ -52,10 +79,24 @@ let disposed = false
|
|
|
52
79
|
let revision = 0
|
|
53
80
|
|
|
54
81
|
async function loadPlotly(): Promise<typeof PlotlyType> {
|
|
82
|
+
if (props.plotly) return props.plotly
|
|
55
83
|
const module = await import('plotly.js-dist-min')
|
|
56
84
|
return ('default' in module ? module.default : module) as unknown as typeof PlotlyType
|
|
57
85
|
}
|
|
58
86
|
|
|
87
|
+
type PlotlyEventHost = HTMLDivElement & {
|
|
88
|
+
on?: (eventName: string, callback: (event: PlotlyType.PlotMouseEvent) => void) => void
|
|
89
|
+
removeAllListeners?: (eventName: string) => void
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function bindEvents(element: HTMLDivElement): void {
|
|
93
|
+
const host = element as PlotlyEventHost
|
|
94
|
+
host.removeAllListeners?.('plotly_click')
|
|
95
|
+
if (props.clickEvents) {
|
|
96
|
+
host.on?.('plotly_click', event => emit('plotly-click', event))
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
59
100
|
function cssValue(name: string, fallback: string): string {
|
|
60
101
|
const value = getComputedStyle(chartElement.value ?? document.documentElement).getPropertyValue(name).trim()
|
|
61
102
|
return value || getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback
|
|
@@ -98,17 +139,17 @@ function purgeChart(): void {
|
|
|
98
139
|
|
|
99
140
|
async function renderChart(): Promise<void> {
|
|
100
141
|
revision++
|
|
101
|
-
if (rendering || disposed || props.loading || props.empty) return
|
|
142
|
+
if (rendering || disposed || !props.active || props.loading || props.empty) return
|
|
102
143
|
rendering = true
|
|
103
144
|
try {
|
|
104
|
-
while (!disposed && !props.loading && !props.empty) {
|
|
145
|
+
while (!disposed && props.active && !props.loading && !props.empty) {
|
|
105
146
|
const currentRevision = revision
|
|
106
147
|
failed.value = false
|
|
107
148
|
await nextTick()
|
|
108
|
-
if (disposed || props.loading || props.empty) break
|
|
149
|
+
if (disposed || !props.active || props.loading || props.empty) break
|
|
109
150
|
try {
|
|
110
151
|
plotly ??= await loadPlotly()
|
|
111
|
-
if (disposed || props.loading || props.empty) break
|
|
152
|
+
if (disposed || !props.active || props.loading || props.empty) break
|
|
112
153
|
if (currentRevision !== revision) continue
|
|
113
154
|
const element = chartElement.value
|
|
114
155
|
if (!element) break
|
|
@@ -122,7 +163,8 @@ async function renderChart(): Promise<void> {
|
|
|
122
163
|
)
|
|
123
164
|
if (disposed || props.loading || props.empty || element !== chartElement.value) {
|
|
124
165
|
purgeChart()
|
|
125
|
-
} else {
|
|
166
|
+
} else if (props.active) {
|
|
167
|
+
bindEvents(element)
|
|
126
168
|
resizeObserver?.observe(element)
|
|
127
169
|
}
|
|
128
170
|
} catch {
|
|
@@ -140,7 +182,7 @@ async function renderChart(): Promise<void> {
|
|
|
140
182
|
onMounted(() => {
|
|
141
183
|
if (typeof ResizeObserver !== 'undefined') {
|
|
142
184
|
resizeObserver = new ResizeObserver(() => {
|
|
143
|
-
if (rendering || !plotly || !renderedElement) return
|
|
185
|
+
if (rendering || !props.active || !plotly || !renderedElement) return
|
|
144
186
|
// A tab can become hidden or unmount before Plotly's async resize finishes.
|
|
145
187
|
void Promise.resolve(plotly.Plots.resize(renderedElement)).catch(() => {})
|
|
146
188
|
})
|
|
@@ -157,16 +199,29 @@ onMounted(() => {
|
|
|
157
199
|
})
|
|
158
200
|
|
|
159
201
|
watch(
|
|
160
|
-
[() => props.data, () => props.layout, () => props.config, () => props.loading, () => props.empty],
|
|
202
|
+
[() => props.data, () => props.layout, () => props.config, () => props.loading, () => props.empty, () => props.active],
|
|
161
203
|
() => {
|
|
162
|
-
if (props.loading || props.empty) {
|
|
204
|
+
if (props.loading || props.empty || !props.active) {
|
|
163
205
|
resizeObserver?.disconnect()
|
|
164
|
-
if (!rendering) purgeChart()
|
|
206
|
+
if ((props.loading || props.empty) && !rendering) purgeChart()
|
|
165
207
|
}
|
|
166
208
|
void renderChart()
|
|
167
209
|
},
|
|
168
210
|
)
|
|
169
211
|
|
|
212
|
+
watch(
|
|
213
|
+
() => props.clickEvents,
|
|
214
|
+
() => {
|
|
215
|
+
if (renderedElement) bindEvents(renderedElement)
|
|
216
|
+
},
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
const chartStyle = computed(() => {
|
|
220
|
+
if (props.height === undefined) return undefined
|
|
221
|
+
const height = typeof props.height === 'number' ? `${props.height}px` : props.height
|
|
222
|
+
return { height, minHeight: height }
|
|
223
|
+
})
|
|
224
|
+
|
|
170
225
|
onBeforeUnmount(() => {
|
|
171
226
|
disposed = true
|
|
172
227
|
resizeObserver?.disconnect()
|
|
@@ -183,10 +238,17 @@ onBeforeUnmount(() => {
|
|
|
183
238
|
:loading="loading"
|
|
184
239
|
:empty="empty"
|
|
185
240
|
:empty-message="emptyMessage"
|
|
241
|
+
:variant="variant"
|
|
186
242
|
>
|
|
243
|
+
<template v-if="$slots.header" #header>
|
|
244
|
+
<slot name="header" />
|
|
245
|
+
</template>
|
|
187
246
|
<template v-if="$slots.toolbar" #toolbar>
|
|
188
247
|
<slot name="toolbar" />
|
|
189
248
|
</template>
|
|
249
|
+
<template v-if="$slots.subhead" #subhead>
|
|
250
|
+
<slot name="subhead" />
|
|
251
|
+
</template>
|
|
190
252
|
<template v-if="$slots.empty" #empty>
|
|
191
253
|
<slot name="empty" />
|
|
192
254
|
</template>
|
|
@@ -204,11 +266,15 @@ onBeforeUnmount(() => {
|
|
|
204
266
|
class="mint-plotly-chart__plot"
|
|
205
267
|
role="group"
|
|
206
268
|
:aria-label="ariaLabel"
|
|
269
|
+
:style="chartStyle"
|
|
207
270
|
/>
|
|
208
271
|
|
|
209
272
|
<template v-if="$slots.legend" #legend>
|
|
210
273
|
<slot name="legend" />
|
|
211
274
|
</template>
|
|
275
|
+
<template v-if="$slots.footer" #footer>
|
|
276
|
+
<slot name="footer" />
|
|
277
|
+
</template>
|
|
212
278
|
</ChartContainer>
|
|
213
279
|
</template>
|
|
214
280
|
|
|
@@ -97,9 +97,9 @@ export interface PluginWorkspaceViewProps {
|
|
|
97
97
|
sidebarSubtitle?: string
|
|
98
98
|
/** Optional compact badge/count rendered in the sidebar header. */
|
|
99
99
|
sidebarBadge?: string | number
|
|
100
|
-
/** AppSidebar
|
|
100
|
+
/** @deprecated No-op. AppSidebar has one look now; the former `analysis` preset is the default. */
|
|
101
101
|
sidebarVariant?: PluginWorkspaceSidebarVariant
|
|
102
|
-
/** Sidebar CSS width. Defaults to AppSidebar's
|
|
102
|
+
/** Sidebar CSS width. Defaults to AppSidebar's width. */
|
|
103
103
|
sidebarWidth?: string
|
|
104
104
|
/** Sidebar position in AppLayout. */
|
|
105
105
|
sidebarPosition?: 'left' | 'right'
|
|
@@ -113,7 +113,7 @@ export interface PluginWorkspaceViewProps {
|
|
|
113
113
|
sidebarContentId?: string
|
|
114
114
|
/** Compact AppSidebar density. */
|
|
115
115
|
dense?: boolean
|
|
116
|
-
/** Show a built-in collapse/expand button in the sidebar chrome. Defaults to
|
|
116
|
+
/** Show a built-in collapse/expand button in the sidebar chrome. Defaults to true. */
|
|
117
117
|
sidebarCollapsible?: boolean
|
|
118
118
|
/** Sidebar width when collapsed. */
|
|
119
119
|
sidebarCollapsedWidth?: string
|