@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,165 @@
1
+ import { computed, onBeforeUnmount, shallowRef, toValue, watch, type MaybeRefOrGetter } from 'vue'
2
+
3
+ export interface ResizePanelWidthConstraints {
4
+ minWidth: number
5
+ maxWidth: number
6
+ mainMinWidth?: number
7
+ layoutGap?: number
8
+ }
9
+
10
+ export interface ResizePercentConstraints {
11
+ minPercent: number
12
+ maxPercent: number
13
+ }
14
+
15
+ function clamp(value: number, min: number, max: number): number {
16
+ return Math.min(max, Math.max(min, value))
17
+ }
18
+
19
+ function availablePanelWidth(
20
+ containerWidth: number,
21
+ constraints: ResizePanelWidthConstraints,
22
+ ): number {
23
+ return Math.min(
24
+ constraints.maxWidth,
25
+ containerWidth - (constraints.mainMinWidth ?? 0) - (constraints.layoutGap ?? 0),
26
+ )
27
+ }
28
+
29
+ export function resizedLeadingPanelWidth(
30
+ startWidth: number,
31
+ deltaX: number,
32
+ containerWidth: number,
33
+ constraints: ResizePanelWidthConstraints,
34
+ ): number {
35
+ return Math.round(clamp(
36
+ startWidth + deltaX,
37
+ constraints.minWidth,
38
+ Math.max(constraints.minWidth, availablePanelWidth(containerWidth, constraints)),
39
+ ))
40
+ }
41
+
42
+ export function resizedTrailingPanelWidth(
43
+ startWidth: number,
44
+ deltaX: number,
45
+ containerWidth: number,
46
+ constraints: ResizePanelWidthConstraints,
47
+ ): number {
48
+ return resizedLeadingPanelWidth(startWidth, -deltaX, containerWidth, constraints)
49
+ }
50
+
51
+ export function resizedVerticalSplitPercent(
52
+ startPercent: number,
53
+ deltaY: number,
54
+ containerHeight: number,
55
+ constraints: ResizePercentConstraints,
56
+ ): number {
57
+ if (containerHeight <= 0) return startPercent
58
+ return Math.round(clamp(
59
+ startPercent + (deltaY / containerHeight) * 100,
60
+ constraints.minPercent,
61
+ constraints.maxPercent,
62
+ ))
63
+ }
64
+
65
+ export interface ManualLayoutResizeContext<TKind extends string> {
66
+ kind: TKind
67
+ startValue: number
68
+ deltaX: number
69
+ deltaY: number
70
+ containerRect: DOMRectReadOnly
71
+ secondaryRect: DOMRectReadOnly | null
72
+ }
73
+
74
+ export interface ManualLayoutResizeTarget<TKind extends string> {
75
+ cursor?: string
76
+ getContainerElement: () => HTMLElement | null
77
+ getSecondaryElement?: () => HTMLElement | null
78
+ getStartValue: () => number
79
+ onResize: (context: ManualLayoutResizeContext<TKind>) => void
80
+ }
81
+
82
+ /** Coordinates pointer-driven resizing for one or more layout splitters. */
83
+ export function useManualLayoutResize<TKind extends string>(options: {
84
+ enabled: MaybeRefOrGetter<boolean>
85
+ resolveTarget: (kind: TKind) => ManualLayoutResizeTarget<TKind> | null
86
+ }) {
87
+ const activeResize = shallowRef<{
88
+ kind: TKind
89
+ startClientX: number
90
+ startClientY: number
91
+ startValue: number
92
+ containerRect: DOMRectReadOnly
93
+ secondaryRect: DOMRectReadOnly | null
94
+ cursor: string
95
+ } | null>(null)
96
+ const isResizing = computed(() => activeResize.value !== null)
97
+ let previousBodyCursor = ''
98
+ let previousBodyUserSelect = ''
99
+
100
+ function stopResize(): void {
101
+ if (!activeResize.value) return
102
+ activeResize.value = null
103
+ window.removeEventListener('pointermove', handlePointerMove)
104
+ window.removeEventListener('pointerup', stopResize)
105
+ window.removeEventListener('pointercancel', stopResize)
106
+ if (typeof document !== 'undefined') {
107
+ document.body.style.cursor = previousBodyCursor
108
+ document.body.style.userSelect = previousBodyUserSelect
109
+ }
110
+ }
111
+
112
+ function handlePointerMove(event: PointerEvent): void {
113
+ const state = activeResize.value
114
+ if (!state) return
115
+ options.resolveTarget(state.kind)?.onResize({
116
+ kind: state.kind,
117
+ startValue: state.startValue,
118
+ deltaX: event.clientX - state.startClientX,
119
+ deltaY: event.clientY - state.startClientY,
120
+ containerRect: state.containerRect,
121
+ secondaryRect: state.secondaryRect,
122
+ })
123
+ }
124
+
125
+ function startResize(kind: TKind, event: PointerEvent): void {
126
+ if (!toValue(options.enabled) || event.button !== 0) return
127
+ const target = options.resolveTarget(kind)
128
+ const containerRect = target?.getContainerElement()?.getBoundingClientRect()
129
+ if (!target || !containerRect) return
130
+
131
+ event.preventDefault()
132
+ if (event.currentTarget instanceof HTMLElement) {
133
+ event.currentTarget.setPointerCapture(event.pointerId)
134
+ }
135
+ const secondary = target.getSecondaryElement?.() ?? null
136
+ activeResize.value = {
137
+ kind,
138
+ startClientX: event.clientX,
139
+ startClientY: event.clientY,
140
+ startValue: target.getStartValue(),
141
+ containerRect,
142
+ secondaryRect: secondary?.getBoundingClientRect() ?? null,
143
+ cursor: target.cursor ?? 'col-resize',
144
+ }
145
+
146
+ if (typeof document !== 'undefined') {
147
+ previousBodyCursor = document.body.style.cursor
148
+ previousBodyUserSelect = document.body.style.userSelect
149
+ document.body.style.cursor = activeResize.value.cursor
150
+ document.body.style.userSelect = 'none'
151
+ }
152
+ window.addEventListener('pointermove', handlePointerMove)
153
+ window.addEventListener('pointerup', stopResize)
154
+ window.addEventListener('pointercancel', stopResize)
155
+ }
156
+
157
+ function isResizingKind(kind: TKind): boolean {
158
+ return activeResize.value?.kind === kind
159
+ }
160
+
161
+ watch(() => toValue(options.enabled), enabled => { if (!enabled) stopResize() })
162
+ onBeforeUnmount(stopResize)
163
+
164
+ return { activeResize, isResizing, isResizingKind, startResize, stopResize }
165
+ }
@@ -167,7 +167,6 @@ export function usePluginWorkspace(options: UsePluginWorkspaceOptions): UsePlugi
167
167
  title: props.sidebarTitle,
168
168
  subtitle: props.sidebarSubtitle,
169
169
  badge: props.sidebarBadge,
170
- variant: props.sidebarVariant ?? 'analysis',
171
170
  panels: props.panels ?? {},
172
171
  activeView: navigationState.resolvedActiveView.value,
173
172
  floating: false,
@@ -187,7 +186,7 @@ export function usePluginWorkspace(options: UsePluginWorkspaceOptions): UsePlugi
187
186
  formDisabled: props.formDisabled ?? false,
188
187
  formReadonly: props.formReadonly ?? false,
189
188
  formSize: props.formSize ?? 'sm',
190
- collapsible: props.sidebarCollapsible,
189
+ collapsible: props.sidebarCollapsible ?? true,
191
190
  collapsed: props.sidebarCollapsed,
192
191
  defaultCollapsed: props.defaultSidebarCollapsed ?? false,
193
192
  collapsedWidth: props.sidebarCollapsedWidth ?? '3rem',
@@ -1,15 +1,31 @@
1
1
  import { ref } from 'vue'
2
- import type { Toast } from '../types'
2
+ import type { Toast, ToastOptions } from '../types'
3
3
 
4
4
  const toasts = ref<Toast[]>([])
5
5
  let nextId = 0
6
6
 
7
7
  /** Reactive global toast queue with success/error/warning/info variants and auto-dismiss timers. */
8
8
  export function useToast() {
9
- function show(message: string, type: Toast['type'] = 'success', duration = 3500) {
9
+ /** Push a structured toast. `progress: 'indeterminate'` is the in-progress form and never auto-dismisses. */
10
+ function push(options: ToastOptions): number {
10
11
  const id = nextId++
11
- toasts.value.push({ id, message, type, duration })
12
- setTimeout(() => dismiss(id), duration)
12
+ const duration = options.duration ?? 3500
13
+ toasts.value.push({
14
+ id,
15
+ type: 'success',
16
+ ...options,
17
+ message: options.message ?? options.title,
18
+ duration,
19
+ actions: options.actions?.slice(0, 2),
20
+ })
21
+ if (options.progress !== 'indeterminate') {
22
+ setTimeout(() => dismiss(id), duration)
23
+ }
24
+ return id
25
+ }
26
+
27
+ function show(message: string, type: Toast['type'] = 'success', duration = 3500) {
28
+ push({ title: message, type, duration })
13
29
  }
14
30
 
15
31
  function success(message: string, duration = 3500) {
@@ -36,5 +52,5 @@ export function useToast() {
36
52
  toasts.value = []
37
53
  }
38
54
 
39
- return { toasts, show, success, error, warning, info, dismiss, clear }
55
+ return { toasts, push, show, success, error, warning, info, dismiss, clear }
40
56
  }
@@ -38,8 +38,9 @@ export function generatedJobFormSchema(definition: GeneratedJobDefinition): Form
38
38
  export function generatedJobDefaults(definition: GeneratedJobDefinition): Record<string, unknown> {
39
39
  return Object.fromEntries(
40
40
  Object.entries(definition.input_schema.properties ?? {})
41
- .filter(([, schema]) => hasOwnDefault(schema))
42
- .map(([name, schema]) => [name, structuredClone(schema.default)]),
41
+ .map(([name, schema]) => [name, resolveGeneratedField(definition.input_schema, schema)] as const)
42
+ .filter(([, field]) => field.hasDefault)
43
+ .map(([name, field]) => [name, structuredClone(field.defaultValue)]),
43
44
  )
44
45
  }
45
46
 
@@ -51,7 +52,17 @@ export function normalizeGeneratedJobInput(
51
52
  for (const [name, value] of Object.entries(values)) {
52
53
  const field = definition.input_schema.properties?.[name]
53
54
  if (!field) continue
54
- normalized[name] = value
55
+ const { schema, reference } = resolveGeneratedField(definition.input_schema, field)
56
+ if (reference !== 'path' && Array.isArray(value) && ['integer', 'number'].includes(String(schema.items?.type))) {
57
+ normalized[name] = value.map(item => {
58
+ const numeric = typeof item === 'string' && item.trim() !== '' ? Number(item) : item
59
+ return typeof numeric === 'number' && Number.isFinite(numeric) ? numeric : item
60
+ })
61
+ } else if (reference !== 'path' && Array.isArray(value) && schema.items?.type === 'boolean') {
62
+ normalized[name] = value.map(item => item === 'true' ? true : item === 'false' ? false : item)
63
+ } else {
64
+ normalized[name] = value === undefined && allowsNull(schema) ? null : value
65
+ }
55
66
  }
56
67
  return normalized
57
68
  }
@@ -63,7 +74,12 @@ function toFormField(
63
74
  ): FormFieldSchema {
64
75
  const { schema, reference } = resolved
65
76
  const validation: NonNullable<FormFieldSchema['validation']> = {}
66
- if (required) validation.required = true
77
+ const allowsEmpty = schema.type === 'array' || (
78
+ schema.type === 'string' && reference !== 'path'
79
+ && !(schema.minLength && schema.minLength > 0)
80
+ && (!schema.enum || schema.enum.includes(''))
81
+ )
82
+ if (required && !allowsNull(schema) && !allowsEmpty) validation.required = true
67
83
  if (schema.minimum !== undefined) validation.min = schema.minimum
68
84
  if (schema.maximum !== undefined) validation.max = schema.maximum
69
85
  if (schema.minLength !== undefined) validation.minLength = schema.minLength
@@ -71,15 +87,37 @@ function toFormField(
71
87
 
72
88
  const props: Record<string, unknown> = {}
73
89
  if (schema.enum) props.options = [...schema.enum]
90
+ if (schema.type === 'integer') {
91
+ if (schema.exclusiveMinimum !== undefined) validation.min = Math.max(Number(validation.min ?? -Infinity), Math.floor(schema.exclusiveMinimum) + 1)
92
+ if (schema.exclusiveMaximum !== undefined) validation.max = Math.min(Number(validation.max ?? Infinity), Math.ceil(schema.exclusiveMaximum) - 1)
93
+ if (validation.min !== undefined) validation.min = Math.ceil(Number(validation.min))
94
+ if (validation.max !== undefined) validation.max = Math.floor(Number(validation.max))
95
+ if (schema.multipleOf && Number.isInteger(schema.multipleOf)) {
96
+ if (validation.min !== undefined) validation.min = Math.ceil(Number(validation.min) / schema.multipleOf) * schema.multipleOf
97
+ if (validation.max !== undefined) validation.max = Math.floor(Number(validation.max) / schema.multipleOf) * schema.multipleOf
98
+ }
99
+ props.step = 1
100
+ }
101
+ if (schema.type === 'integer' || schema.type === 'number') {
102
+ if (validation.min !== undefined) props.min = validation.min
103
+ if (validation.max !== undefined) props.max = validation.max
104
+ if (schema.multipleOf !== undefined) props.step = schema.multipleOf
105
+ }
106
+ if (schema.type === 'array' && reference !== 'path') {
107
+ if (schema.items?.enum) props.options = [...schema.items.enum]
108
+ else props.allowDuplicates = true
109
+ }
74
110
  if (reference === 'path' && schema.type === 'array') props.multiple = true
75
111
  return {
76
112
  name,
77
113
  label: schema.title || humanize(name),
78
114
  type: fieldType(schema, reference),
79
115
  defaultValue: resolved.hasDefault ? structuredClone(resolved.defaultValue) : undefined,
116
+ disabled: Object.prototype.hasOwnProperty.call(schema, 'const') || undefined,
80
117
  hint: schema.description,
81
118
  validation,
82
119
  props,
120
+ itemType: schema.items?.type === 'number' || schema.items?.type === 'integer' ? 'number' : undefined,
83
121
  }
84
122
  }
85
123
 
@@ -87,27 +125,24 @@ function resolveGeneratedField(
87
125
  root: GeneratedJsonSchema,
88
126
  field: GeneratedJsonSchema,
89
127
  ): ResolvedGeneratedField {
90
- const hasDefault = hasOwnDefault(field)
91
128
  const candidate = field.anyOf?.find(item => item.type !== 'null')
92
129
  ?? field.allOf?.[0]
93
130
  ?? field
131
+ let schema = { ...candidate, ...field }
94
132
  if (candidate.$ref) {
95
133
  const referenceParts = candidate.$ref.split('/')
96
134
  const key = referenceParts[referenceParts.length - 1]
97
- const schema = key ? root.$defs?.[key] : undefined
98
- if (!schema) throw new Error(`Generated field reference is missing: ${candidate.$ref}`)
99
- return {
100
- schema: { ...schema, title: field.title ?? schema.title },
101
- reference: field['x-mint-component'] ?? schema['x-mint-component'],
102
- hasDefault,
103
- defaultValue: field.default,
104
- }
135
+ const referenced = key ? root.$defs?.[key] : undefined
136
+ if (!referenced) throw new Error(`Generated field reference is missing: ${candidate.$ref}`)
137
+ schema = { ...referenced, ...candidate, ...field }
105
138
  }
139
+ if (schema.items) schema.items = resolveGeneratedField(root, schema.items).schema
140
+ const constant = Object.prototype.hasOwnProperty.call(schema, 'const')
106
141
  return {
107
- schema: candidate,
108
- reference: field['x-mint-component'] ?? candidate['x-mint-component'],
109
- hasDefault,
110
- defaultValue: field.default,
142
+ schema,
143
+ reference: schema['x-mint-component'],
144
+ hasDefault: constant || hasOwnDefault(field) || allowsNull(schema),
145
+ defaultValue: constant ? schema.const : hasOwnDefault(field) ? field.default : null,
111
146
  }
112
147
  }
113
148
 
@@ -117,7 +152,7 @@ function fieldType(schema: GeneratedJsonSchema, reference?: GeneratedReference):
117
152
  const type = Array.isArray(schema.type) ? schema.type.find(value => value !== 'null') : schema.type
118
153
  if (type === 'boolean') return 'toggle'
119
154
  if (type === 'number' || type === 'integer') return 'number'
120
- if (type === 'array') return 'tags'
155
+ if (type === 'array') return schema.items?.enum ? 'multiselect' : 'tags'
121
156
  return 'text'
122
157
  }
123
158
 
@@ -125,6 +160,11 @@ function hasOwnDefault(schema: GeneratedJsonSchema): boolean {
125
160
  return Object.prototype.hasOwnProperty.call(schema, 'default')
126
161
  }
127
162
 
163
+ function allowsNull(schema: GeneratedJsonSchema): boolean {
164
+ return schema.type === 'null' || (Array.isArray(schema.type) && schema.type.includes('null'))
165
+ || !!schema.anyOf?.some(item => item.type === 'null')
166
+ }
167
+
128
168
  function humanize(value: string): string {
129
169
  return value.replace(/[_-]+/g, ' ').replace(/^\w/, character => character.toUpperCase())
130
170
  }
@@ -0,0 +1,45 @@
1
+ .mint-adduct-text {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ flex-shrink: 0;
5
+ max-width: 100%;
6
+ overflow: hidden;
7
+ color: var(--text-muted);
8
+ font-variant-numeric: tabular-nums;
9
+ font-weight: 600;
10
+ letter-spacing: 0;
11
+ text-overflow: ellipsis;
12
+ white-space: nowrap;
13
+ }
14
+
15
+ .mint-adduct-text--mono { font-family: var(--font-mono); font-feature-settings: 'liga' 0, 'calt' 0; }
16
+ .mint-adduct-text--sans { font-family: var(--font-sans); font-feature-settings: 'liga' 1, 'calt' 1; font-weight: 650; }
17
+
18
+ .mint-adduct-text--xs {
19
+ font-size: 0.625rem;
20
+ line-height: 1rem;
21
+ }
22
+
23
+ .mint-adduct-text--inherit {
24
+ font-size: inherit;
25
+ line-height: inherit;
26
+ }
27
+
28
+ .mint-adduct-text--badge {
29
+ padding: 0.125rem 0.25rem;
30
+ border-radius: var(--radius-sm);
31
+ background: var(--bg-tertiary);
32
+ }
33
+
34
+ .mint-adduct-text--on-primary {
35
+ color: rgb(255 255 255 / 0.82);
36
+ }
37
+
38
+ .mint-adduct-text--badge.mint-adduct-text--on-primary {
39
+ background: rgb(255 255 255 / 0.2);
40
+ }
41
+
42
+ .mint-adduct-text--muted,
43
+ .mint-adduct-text--empty {
44
+ color: var(--text-muted);
45
+ }