@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.
Files changed (152) hide show
  1. package/dist/{ExperimentSelectorModal-DCXmwKNS.js → ExperimentSelectorModal-DHE9k8TP.js} +3 -3
  2. package/dist/{ExperimentSelectorModal-DCXmwKNS.js.map → ExperimentSelectorModal-DHE9k8TP.js.map} +1 -1
  3. package/dist/{SettingsModal-BYR20I4c.js → SettingsModal-DELMkUFR.js} +39 -16
  4. package/dist/SettingsModal-DELMkUFR.js.map +1 -0
  5. package/dist/{Skeleton-BAF3CKY7.js → Skeleton-jhF9wUkU.js} +17 -7
  6. package/dist/Skeleton-jhF9wUkU.js.map +1 -0
  7. package/dist/__tests__/components/ChartContainer.test.d.ts +1 -0
  8. package/dist/__tests__/components/ChemicalFormula.test.d.ts +1 -0
  9. package/dist/__tests__/components/FormField.test.d.ts +1 -0
  10. package/dist/__tests__/components/LayoutResizeHandle.test.d.ts +1 -0
  11. package/dist/__tests__/components/NumberInput.test.d.ts +1 -0
  12. package/dist/__tests__/components/SearchableSelect.test.d.ts +1 -0
  13. package/dist/__tests__/components/WellPlate.decoration.test.d.ts +1 -0
  14. package/dist/__tests__/composables/localFiles.test.d.ts +1 -0
  15. package/dist/__tests__/composables/useManualLayoutResize.test.d.ts +1 -0
  16. package/dist/__tests__/composables/usePickerSelection.test.d.ts +1 -0
  17. package/dist/__tests__/styles/tokenOnlyColors.test.d.ts +1 -0
  18. package/dist/components/AdductText.vue.d.ts +17 -0
  19. package/dist/components/AppSidebar.vue.d.ts +7 -7
  20. package/dist/components/BasePill.vue.d.ts +3 -0
  21. package/dist/components/BaseTabs.vue.d.ts +1 -4
  22. package/dist/components/ChartContainer.vue.d.ts +19 -5
  23. package/dist/components/ChemicalFormula.vue.d.ts +28 -2
  24. package/dist/components/DataFrame.vue.d.ts +1 -1
  25. package/dist/components/FileUploader.vue.d.ts +3 -0
  26. package/dist/components/FormField.vue.d.ts +8 -1
  27. package/dist/components/LayoutResizeHandle.vue.d.ts +23 -0
  28. package/dist/components/PlotlyChart.vue.d.ts +34 -6
  29. package/dist/components/PluginWorkspaceView.props.d.ts +3 -3
  30. package/dist/components/SearchableSelect.vue.d.ts +57 -0
  31. package/dist/components/WellPlate.vue.d.ts +5 -1
  32. package/dist/components/index.d.ts +3 -1
  33. package/dist/components/index.js +6 -6
  34. package/dist/{components-DIum5hkx.js → components-CxQVQCpP.js} +1215 -546
  35. package/dist/components-CxQVQCpP.js.map +1 -0
  36. package/dist/composables/index.d.ts +1 -0
  37. package/dist/composables/index.js +8 -8
  38. package/dist/composables/pluginWorkspaceModel.d.ts +0 -3
  39. package/dist/composables/useManualLayoutResize.d.ts +56 -0
  40. package/dist/composables/useToast.d.ts +22 -1
  41. package/dist/{composables-mFXsTA4g.js → composables-DAqOU8Aq.js} +97 -8
  42. package/dist/composables-DAqOU8Aq.js.map +1 -0
  43. package/dist/index.js +11 -11
  44. package/dist/install.js +2 -2
  45. package/dist/styles.css +855 -320
  46. package/dist/templates/index.js +2 -2
  47. package/dist/{templates-CmGfmBRA.js → templates-NuaNHohp.js} +2 -2
  48. package/dist/{templates-CmGfmBRA.js.map → templates-NuaNHohp.js.map} +1 -1
  49. package/dist/types/componentLabTypes.d.ts +3 -0
  50. package/dist/types/components.d.ts +26 -1
  51. package/dist/types/form-builder.d.ts +2 -0
  52. package/dist/types/generated-ui.d.ts +2 -0
  53. package/dist/types/index.d.ts +1 -1
  54. package/dist/{useControlSchema-DldpxNOP.js → useControlSchema-vLiRPUmj.js} +181 -71
  55. package/dist/useControlSchema-vLiRPUmj.js.map +1 -0
  56. package/dist/{useExperimentSelector-CEcIaueJ.js → useExperimentSelector-G_CAOFTX.js} +2 -2
  57. package/dist/{useExperimentSelector-CEcIaueJ.js.map → useExperimentSelector-G_CAOFTX.js.map} +1 -1
  58. package/dist/{useFormBuilder-z5L_dUIy.js → useFormBuilder-D37ajgS8.js} +2 -2
  59. package/dist/{useFormBuilder-z5L_dUIy.js.map → useFormBuilder-D37ajgS8.js.map} +1 -1
  60. package/dist/{validation-uwoJGEb6.js → validation-ChMyixAf.js} +21 -9
  61. package/dist/validation-ChMyixAf.js.map +1 -0
  62. package/package.json +1 -1
  63. package/src/__tests__/components/AppSidebar.test.ts +84 -94
  64. package/src/__tests__/components/AppToastContainer.test.ts +55 -1
  65. package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +0 -21
  66. package/src/__tests__/components/ChartContainer.test.ts +24 -0
  67. package/src/__tests__/components/ChemicalFormula.test.ts +27 -0
  68. package/src/__tests__/components/ControlWorkspaceView.test.ts +0 -2
  69. package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +0 -2
  70. package/src/__tests__/components/FormBuilder.test.ts +26 -0
  71. package/src/__tests__/components/FormField.test.ts +26 -0
  72. package/src/__tests__/components/GeneratedPathInput.test.ts +18 -0
  73. package/src/__tests__/components/LayoutResizeHandle.test.ts +19 -0
  74. package/src/__tests__/components/NumberInput.test.ts +149 -0
  75. package/src/__tests__/components/PlotlyChart.test.ts +48 -0
  76. package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +1 -2
  77. package/src/__tests__/components/PluginWorkspaceView.test.ts +1 -2
  78. package/src/__tests__/components/RackEditor.test.ts +9 -0
  79. package/src/__tests__/components/SearchableSelect.test.ts +61 -0
  80. package/src/__tests__/components/WellPlate.decoration.test.ts +28 -0
  81. package/src/__tests__/composables/localFiles.test.ts +57 -0
  82. package/src/__tests__/composables/useManualLayoutResize.test.ts +21 -0
  83. package/src/__tests__/composables/usePickerSelection.test.ts +98 -0
  84. package/src/__tests__/generatedUi.test.ts +111 -0
  85. package/src/__tests__/styles/tokenOnlyColors.test.ts +16 -0
  86. package/src/components/AdductText.story.vue +15 -0
  87. package/src/components/AdductText.vue +40 -0
  88. package/src/components/AppLayout.story.vue +1 -1
  89. package/src/components/AppSidebar.story.vue +7 -10
  90. package/src/components/AppSidebar.vue +178 -58
  91. package/src/components/AppToastContainer.vue +65 -23
  92. package/src/components/BasePill.story.vue +2 -0
  93. package/src/components/BasePill.vue +4 -0
  94. package/src/components/BaseTabs.story.vue +0 -42
  95. package/src/components/BaseTabs.vue +3 -6
  96. package/src/components/ChartContainer.story.vue +21 -0
  97. package/src/components/ChartContainer.vue +27 -6
  98. package/src/components/ChemicalFormula.story.vue +7 -0
  99. package/src/components/ChemicalFormula.vue +40 -11
  100. package/src/components/FileUploader.vue +10 -1
  101. package/src/components/FormField.vue +10 -3
  102. package/src/components/GeneratedPathInput.vue +11 -7
  103. package/src/components/LayoutResizeHandle.story.vue +26 -0
  104. package/src/components/LayoutResizeHandle.vue +62 -0
  105. package/src/components/NumberInput.story.vue +32 -13
  106. package/src/components/NumberInput.vue +145 -56
  107. package/src/components/PlotlyChart.vue +77 -11
  108. package/src/components/PluginWorkspaceView.props.ts +3 -3
  109. package/src/components/RackEditor.vue +38 -32
  110. package/src/components/SearchableSelect.story.vue +24 -0
  111. package/src/components/SearchableSelect.vue +223 -0
  112. package/src/components/WellPlate.story.vue +16 -0
  113. package/src/components/WellPlate.vue +17 -8
  114. package/src/components/index.ts +3 -1
  115. package/src/components/internal/FormFieldRendererInternal.vue +17 -0
  116. package/src/components/internal/WorkspaceSidebarInternal.vue +1 -2
  117. package/src/composables/filePicker/localFiles.ts +21 -14
  118. package/src/composables/filePicker/usePickerSelection.ts +15 -4
  119. package/src/composables/index.ts +10 -0
  120. package/src/composables/pluginWorkspaceModel.ts +0 -3
  121. package/src/composables/useManualLayoutResize.ts +165 -0
  122. package/src/composables/usePluginWorkspace.ts +1 -2
  123. package/src/composables/useToast.ts +21 -5
  124. package/src/generated/schema.ts +58 -18
  125. package/src/styles/components/adduct-text.css +45 -0
  126. package/src/styles/components/app-sidebar.css +282 -121
  127. package/src/styles/components/chart-container.css +92 -0
  128. package/src/styles/components/chemical-formula.css +34 -4
  129. package/src/styles/components/form-builder.css +1 -1
  130. package/src/styles/components/form-field.css +28 -0
  131. package/src/styles/components/layout-resize-handle.css +60 -0
  132. package/src/styles/components/number-input.css +75 -91
  133. package/src/styles/components/pill.css +49 -51
  134. package/src/styles/components/rack-editor.css +31 -9
  135. package/src/styles/components/searchable-select.css +63 -0
  136. package/src/styles/components/segmented-control.css +16 -11
  137. package/src/styles/components/tabs.css +18 -34
  138. package/src/styles/components/toast.css +136 -27
  139. package/src/styles/components/well-plate.css +9 -0
  140. package/src/styles/index.css +3 -0
  141. package/src/styles/variables.css +25 -5
  142. package/src/types/componentLabTypes.ts +6 -0
  143. package/src/types/components.ts +30 -0
  144. package/src/types/form-builder.ts +2 -0
  145. package/src/types/generated-ui.ts +2 -0
  146. package/src/types/index.ts +5 -0
  147. package/dist/SettingsModal-BYR20I4c.js.map +0 -1
  148. package/dist/Skeleton-BAF3CKY7.js.map +0 -1
  149. package/dist/components-DIum5hkx.js.map +0 -1
  150. package/dist/composables-mFXsTA4g.js.map +0 -1
  151. package/dist/useControlSchema-DldpxNOP.js.map +0 -1
  152. 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 :model-value="42" :size="size" />
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 :model-value="96" :min="0" :max="384" :step="1" placeholder="Samples" />
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 :model-value="37" :min="0" :max="100" :step="0.5" />
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 :model-value="100" :min="0" :max="1000" :step="10" />
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 :model-value="999" :max="100" error />
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="Hover-Reveal Steppers (default)">
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
- Hover or focus to reveal the ↑/↓ steppers. Rest = clean.
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 :model-value="12.5" unit="mg" :step="0.1" />
110
- <NumberInput :model-value="7.4" unit="pH" :step="0.1" :min="0" :max="14" />
111
- <NumberInput :model-value="96" unit="wells" :step="1" />
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 :model-value="7.4" :min="0" :max="14" :step="0.1" unit="pH" />
118
- <NumberInput :model-value="45" :min="0" :max="100" :step="1" unit="%" />
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 increment/decrement buttons, optional unit label, and slider progress track. */
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 handleSliderInput(event: Event) {
69
- const target = event.target as HTMLInputElement
70
- const value = Number(target.value)
71
- if (!isNaN(value)) {
72
- emit('update:modelValue', clamp(value))
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
- if (props.disabled || !canDecrement.value) return
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
- if (props.disabled || !canIncrement.value) return
84
- const current = props.modelValue ?? (props.min ?? 0)
85
- emit('update:modelValue', clamp(current + props.step))
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
- <input
101
- type="number"
102
- :value="modelValue"
103
- :min="min"
104
- :max="max"
105
- :step="step"
106
- :disabled="disabled"
107
- :placeholder="placeholder"
108
- :class="[
109
- 'mint-number-input__input',
110
- `mint-number-input__input--${size}`,
111
- ]"
112
- @input="handleInput"
113
- />
114
-
115
- <span v-if="unit" class="mint-number-input__unit">{{ unit }}</span>
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="Increase value"
122
- :disabled="disabled || !canIncrement"
123
- class="mint-number-input__stepper mint-number-input__stepper--up"
124
- @click="increment"
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 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
127
- <path d="M1 5l4-4 4 4" />
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="Decrease value"
134
- :disabled="disabled || !canDecrement"
135
- class="mint-number-input__stepper mint-number-input__stepper--down"
136
- @click="decrement"
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 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
139
- <path d="M1 1l4 4 4-4" />
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 visual preset. `analysis` matches the LEAF-style MINT analysis sidebar language. */
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 variant width. */
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 AppSidebar's variant behavior. */
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