@morscherlab/mld-sdk 0.6.5 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/__tests__/composables/formBuilderRegistry.test.d.ts +1 -0
  2. package/dist/__tests__/composables/useFormBuilder.test.d.ts +1 -0
  3. package/dist/components/BaseButton.vue.d.ts +1 -1
  4. package/dist/components/BasePill.vue.d.ts +1 -1
  5. package/dist/components/DropdownButton.vue.d.ts +1 -1
  6. package/dist/components/FormActions.vue.d.ts +33 -0
  7. package/dist/components/FormActions.vue.js +76 -0
  8. package/dist/components/FormActions.vue.js.map +1 -0
  9. package/dist/components/FormActions.vue3.js +6 -0
  10. package/dist/components/FormActions.vue3.js.map +1 -0
  11. package/dist/components/FormBuilder.vue.js +205 -0
  12. package/dist/components/FormBuilder.vue.js.map +1 -0
  13. package/dist/components/FormBuilder.vue3.js +6 -0
  14. package/dist/components/FormBuilder.vue3.js.map +1 -0
  15. package/dist/components/FormFieldRenderer.vue.d.ts +31 -0
  16. package/dist/components/FormFieldRenderer.vue.js +48 -0
  17. package/dist/components/FormFieldRenderer.vue.js.map +1 -0
  18. package/dist/components/FormFieldRenderer.vue2.js +5 -0
  19. package/dist/components/FormFieldRenderer.vue2.js.map +1 -0
  20. package/dist/components/FormSection.vue.d.ts +43 -0
  21. package/dist/components/FormSection.vue.js +117 -0
  22. package/dist/components/FormSection.vue.js.map +1 -0
  23. package/dist/components/FormSection.vue3.js +6 -0
  24. package/dist/components/FormSection.vue3.js.map +1 -0
  25. package/dist/components/IconButton.vue.d.ts +1 -1
  26. package/dist/components/LoadingSpinner.vue.d.ts +1 -1
  27. package/dist/components/ProgressBar.vue.d.ts +1 -1
  28. package/dist/components/ReagentList.vue.d.ts +2 -2
  29. package/dist/components/ResourceCard.vue.d.ts +1 -1
  30. package/dist/components/SegmentedControl.vue.d.ts +1 -1
  31. package/dist/components/WellEditPopup.vue.d.ts +2 -2
  32. package/dist/components/index.d.ts +4 -0
  33. package/dist/components/index.js +19 -8
  34. package/dist/components/index.js.map +1 -1
  35. package/dist/composables/formBuilderRegistry.d.ts +13 -0
  36. package/dist/composables/formBuilderRegistry.js +87 -0
  37. package/dist/composables/formBuilderRegistry.js.map +1 -0
  38. package/dist/composables/index.d.ts +3 -0
  39. package/dist/composables/index.js +8 -0
  40. package/dist/composables/index.js.map +1 -1
  41. package/dist/composables/useFormBuilder.d.ts +23 -0
  42. package/dist/composables/useFormBuilder.js +264 -0
  43. package/dist/composables/useFormBuilder.js.map +1 -0
  44. package/dist/composables/usePluginConfig.d.ts +12 -0
  45. package/dist/composables/usePluginConfig.js +77 -0
  46. package/dist/composables/usePluginConfig.js.map +1 -0
  47. package/dist/index.d.ts +1 -1
  48. package/dist/index.js +2 -0
  49. package/dist/index.js.map +1 -1
  50. package/dist/styles.css +247 -6
  51. package/dist/types/form-builder.d.ts +167 -0
  52. package/dist/types/index.d.ts +1 -0
  53. package/package.json +1 -1
  54. package/src/__tests__/composables/formBuilderRegistry.test.ts +187 -0
  55. package/src/__tests__/composables/useFormBuilder.test.ts +917 -0
  56. package/src/components/FormActions.vue +92 -0
  57. package/src/components/FormBuilder.vue +214 -0
  58. package/src/components/FormFieldRenderer.vue +58 -0
  59. package/src/components/FormSection.vue +90 -0
  60. package/src/components/index.ts +6 -0
  61. package/src/composables/formBuilderRegistry.ts +79 -0
  62. package/src/composables/index.ts +7 -0
  63. package/src/composables/useFormBuilder.ts +382 -0
  64. package/src/composables/usePluginConfig.ts +92 -0
  65. package/src/index.ts +3 -0
  66. package/src/styles/components/app-container.css +1 -0
  67. package/src/styles/components/app-layout.css +1 -2
  68. package/src/styles/components/form-builder.css +69 -0
  69. package/src/styles/components/number-input.css +4 -1
  70. package/src/styles/index.css +1 -0
  71. package/src/types/form-builder.ts +197 -0
  72. package/src/types/index.ts +14 -0
@@ -0,0 +1,382 @@
1
+ import { ref, computed, watch } from 'vue'
2
+ import { useForm, type FieldRules, type UseFormReturn } from './useForm'
3
+ import { getFieldRegistryEntry, getTypeDefault } from './formBuilderRegistry'
4
+ import type {
5
+ FormSchema,
6
+ FormFieldSchema,
7
+ FormSectionSchema,
8
+ FieldCondition,
9
+ FieldValidation,
10
+ FormEnhancements,
11
+ UseFormBuilderReturn,
12
+ } from '../types/form-builder'
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Condition evaluator
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /**
19
+ * Evaluate a JSON-serializable field condition against the current form data.
20
+ *
21
+ * Supports logical operators (`and`, `or`, `not`) and comparison operators
22
+ * (`eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `notIn`, `truthy`, `falsy`,
23
+ * `contains`). Returns `true` if the condition passes.
24
+ */
25
+ export function evaluateCondition(
26
+ condition: FieldCondition,
27
+ data: Record<string, unknown>,
28
+ ): boolean {
29
+ if ('and' in condition) {
30
+ return condition.and.every((c) => evaluateCondition(c, data))
31
+ }
32
+ if ('or' in condition) {
33
+ return condition.or.some((c) => evaluateCondition(c, data))
34
+ }
35
+ if ('not' in condition) {
36
+ return !evaluateCondition(condition.not, data)
37
+ }
38
+
39
+ const value = data[condition.field]
40
+
41
+ if ('eq' in condition) return value === condition.eq
42
+ if ('neq' in condition) return value !== condition.neq
43
+ if ('gt' in condition) return typeof value === 'number' && value > condition.gt
44
+ if ('lt' in condition) return typeof value === 'number' && value < condition.lt
45
+ if ('gte' in condition) return typeof value === 'number' && value >= condition.gte
46
+ if ('lte' in condition) return typeof value === 'number' && value <= condition.lte
47
+ if ('in' in condition) return condition.in.includes(value)
48
+ if ('notIn' in condition) return !condition.notIn.includes(value)
49
+ if ('truthy' in condition) return !!value
50
+ if ('falsy' in condition) return !value
51
+ if ('contains' in condition) {
52
+ if (typeof value === 'string') return value.includes(condition.contains)
53
+ if (Array.isArray(value)) return value.includes(condition.contains)
54
+ return false
55
+ }
56
+
57
+ return true
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Schema helpers
62
+ // ---------------------------------------------------------------------------
63
+
64
+ /** Return all sections across steps (wizard) or directly from a flat schema. */
65
+ function collectSections(schema: FormSchema): FormSectionSchema[] {
66
+ return schema.steps ? schema.steps.flatMap((step) => step.sections) : schema.sections
67
+ }
68
+
69
+ /** Return all field schemas in schema order, flattening sections and steps. */
70
+ function flattenFields(schema: FormSchema): FormFieldSchema[] {
71
+ return collectSections(schema).flatMap((section) => section.fields)
72
+ }
73
+
74
+ /** Convert a JSON-safe `FieldValidation` descriptor to a runtime `FieldRules` object. */
75
+ function convertValidation(v: FieldValidation): FieldRules {
76
+ const rules: FieldRules = {}
77
+
78
+ if (v.required !== undefined) rules.required = v.required
79
+ if (v.minLength !== undefined) rules.minLength = v.minLength
80
+ if (v.maxLength !== undefined) rules.maxLength = v.maxLength
81
+ if (v.min !== undefined) rules.min = v.min
82
+ if (v.max !== undefined) rules.max = v.max
83
+ if (v.email !== undefined) rules.email = v.email
84
+
85
+ if (v.pattern !== undefined) {
86
+ rules.pattern =
87
+ typeof v.pattern === 'string'
88
+ ? new RegExp(v.pattern)
89
+ : { value: new RegExp(v.pattern.value), message: v.pattern.message }
90
+ }
91
+
92
+ return rules
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // useFormBuilder
97
+ // ---------------------------------------------------------------------------
98
+
99
+ /**
100
+ * Drive a `FormSchema` as reactive form state.
101
+ *
102
+ * Builds initial values from schema defaults and `initialData`, derives
103
+ * validation rules from `FieldValidation` descriptors and enhancement
104
+ * validators, evaluates `FieldCondition` expressions for field/section
105
+ * visibility, and wires wizard step navigation when `schema.steps` is set.
106
+ *
107
+ * @param schema - Declarative form or wizard schema.
108
+ * @param initialData - Values that override schema defaults.
109
+ * @param enhancements - TypeScript-only callbacks (dynamic options, validators,
110
+ * submit handler, transform, field-change watcher).
111
+ */
112
+ export function useFormBuilder<T extends Record<string, unknown> = Record<string, unknown>>(
113
+ schema: FormSchema,
114
+ initialData?: Partial<T>,
115
+ enhancements?: FormEnhancements<T>,
116
+ ): UseFormBuilderReturn<T> {
117
+ const fields = flattenFields(schema)
118
+ const sections = collectSections(schema)
119
+
120
+ // -- Build initial values --------------------------------------------------
121
+ const initialValues = {} as Record<string, unknown>
122
+ for (const field of fields) {
123
+ const key = field.name
124
+ if (initialData && key in initialData) {
125
+ initialValues[key] = (initialData as Record<string, unknown>)[key]
126
+ } else if (field.defaultValue !== undefined) {
127
+ initialValues[key] = field.defaultValue
128
+ } else {
129
+ initialValues[key] = getTypeDefault(field.type)
130
+ }
131
+ }
132
+
133
+ // -- Build validation rules ------------------------------------------------
134
+ const rules: Partial<Record<string, FieldRules>> = {}
135
+ for (const field of fields) {
136
+ const base: FieldRules = field.validation ? convertValidation(field.validation) : {}
137
+ const enhancement = enhancements?.fields?.[field.name as keyof T]
138
+
139
+ // Wrap validators to skip hidden fields
140
+ const customValidators: Array<(value: unknown, formData: Record<string, unknown>) => string | undefined | null> = []
141
+
142
+ if (enhancement?.validate) {
143
+ const fn = enhancement.validate
144
+ customValidators.push((value, formData) => fn(value, formData as T))
145
+ }
146
+
147
+ if (customValidators.length > 0) {
148
+ base.custom = customValidators
149
+ }
150
+
151
+ // Wrap all rules to skip hidden fields
152
+ if (Object.keys(base).length > 0) {
153
+ rules[field.name] = base
154
+ }
155
+ }
156
+
157
+ // -- Create form -----------------------------------------------------------
158
+ const form = useForm(initialValues as T, rules as Partial<Record<keyof T, FieldRules>>)
159
+
160
+ // -- Visibility ------------------------------------------------------------
161
+ function isFieldVisible(name: string): boolean {
162
+ const enhancement = enhancements?.fields?.[name as keyof T]
163
+ if (enhancement?.visible) {
164
+ return enhancement.visible(form.data as T)
165
+ }
166
+
167
+ const field = fields.find((f) => f.name === name)
168
+ if (field?.condition) {
169
+ return evaluateCondition(field.condition, form.data as Record<string, unknown>)
170
+ }
171
+
172
+ return true
173
+ }
174
+
175
+ function isSectionVisible(id: string): boolean {
176
+ const section = sections.find((s) => s.id === id)
177
+ if (section?.condition) {
178
+ return evaluateCondition(section.condition, form.data as Record<string, unknown>)
179
+ }
180
+ return true
181
+ }
182
+
183
+ // -- Resolved field props --------------------------------------------------
184
+ function getResolvedFieldProps(field: FormFieldSchema): Record<string, unknown> {
185
+ const entry = getFieldRegistryEntry(field.type)
186
+ const formProps = form.getFieldProps(field.name as keyof T)
187
+
188
+ const merged: Record<string, unknown> = {
189
+ ...entry.defaults,
190
+ ...(field.props ?? {}),
191
+ }
192
+
193
+ // Dynamic enhancement props
194
+ const enhancement = enhancements?.fields?.[field.name as keyof T]
195
+ if (enhancement?.props) {
196
+ Object.assign(merged, enhancement.props(form.data as T))
197
+ }
198
+
199
+ // Dynamic options
200
+ const options = getFieldOptions(field.name)
201
+ if (options) {
202
+ merged.options = options
203
+ }
204
+
205
+ // Form field bindings
206
+ if (entry.vModel) {
207
+ merged.modelValue = formProps.modelValue
208
+ merged['onUpdate:modelValue'] = formProps['onUpdate:modelValue']
209
+ }
210
+ merged.onBlur = formProps.onBlur
211
+
212
+ // Error as boolean for components that use boolean error prop
213
+ const errorMsg = formProps.error
214
+ if (errorMsg) {
215
+ merged.error = true
216
+ }
217
+
218
+ // Schema-level props
219
+ if (field.placeholder) merged.placeholder = field.placeholder
220
+ if (field.size) merged.size = field.size
221
+ if (field.disabled) merged.disabled = true
222
+ if (field.readonly) merged.readonly = true
223
+
224
+ // Radio group needs a name prop
225
+ if (field.type === 'radio' && !merged.name) {
226
+ merged.name = field.name
227
+ }
228
+
229
+ return merged
230
+ }
231
+
232
+ function getFieldOptions(name: string): { label: string; value: unknown }[] | undefined {
233
+ const enhancement = enhancements?.fields?.[name as keyof T]
234
+ if (enhancement?.options) {
235
+ return enhancement.options(form.data as T)
236
+ }
237
+
238
+ const field = fields.find((f) => f.name === name)
239
+ if (field?.props?.options) {
240
+ return field.props.options as { label: string; value: unknown }[]
241
+ }
242
+
243
+ return undefined
244
+ }
245
+
246
+ // -- Wizard state ----------------------------------------------------------
247
+ const currentStep = ref(0)
248
+
249
+ const isCurrentStepValid = computed(() => {
250
+ if (!schema.steps) return form.isValid.value
251
+
252
+ const step = schema.steps[currentStep.value]
253
+ if (!step) return true
254
+
255
+ for (const section of step.sections) {
256
+ if (!isSectionVisible(section.id)) continue
257
+ for (const field of section.fields) {
258
+ if (!isFieldVisible(field.name)) continue
259
+ if (!form.validateField(field.name)) return false
260
+ }
261
+ }
262
+ return true
263
+ })
264
+
265
+ function goNext(): boolean {
266
+ if (!schema.steps) return false
267
+
268
+ // Touch and validate all visible fields in current step
269
+ const step = schema.steps[currentStep.value]
270
+ if (!step) return false
271
+
272
+ let valid = true
273
+ for (const section of step.sections) {
274
+ if (!isSectionVisible(section.id)) continue
275
+ for (const field of section.fields) {
276
+ if (!isFieldVisible(field.name)) continue
277
+ form.setFieldTouched(field.name, true)
278
+ if (!form.validateField(field.name)) valid = false
279
+ }
280
+ }
281
+
282
+ if (!valid) return false
283
+
284
+ if (currentStep.value < schema.steps.length - 1) {
285
+ currentStep.value++
286
+ }
287
+ return true
288
+ }
289
+
290
+ function goBack(): void {
291
+ if (currentStep.value > 0) {
292
+ currentStep.value--
293
+ }
294
+ }
295
+
296
+ function goToStep(index: number): void {
297
+ if (!schema.steps) return
298
+ if (index >= 0 && index < schema.steps.length) {
299
+ currentStep.value = index
300
+ }
301
+ }
302
+
303
+ // -- Validate (skip hidden fields) -----------------------------------------
304
+ function validate(): boolean {
305
+ let allValid = true
306
+ for (const field of fields) {
307
+ if (!isFieldVisible(field.name)) continue
308
+ form.setFieldTouched(field.name, true)
309
+ if (!form.validateField(field.name)) {
310
+ allValid = false
311
+ }
312
+ }
313
+ return allValid
314
+ }
315
+
316
+ // -- Submit ----------------------------------------------------------------
317
+ async function submit(): Promise<void> {
318
+ if (!validate()) return
319
+
320
+ // Build submission data excluding hidden fields
321
+ let submitData = {} as Record<string, unknown>
322
+ for (const field of fields) {
323
+ if (isFieldVisible(field.name)) {
324
+ submitData[field.name] = (form.data as Record<string, unknown>)[field.name]
325
+ }
326
+ }
327
+
328
+ if (enhancements?.transform) {
329
+ submitData = enhancements.transform(submitData as T) as Record<string, unknown>
330
+ }
331
+
332
+ if (enhancements?.onSubmit) {
333
+ form.isSubmitting.value = true
334
+ try {
335
+ await enhancements.onSubmit(submitData as T)
336
+ } finally {
337
+ form.isSubmitting.value = false
338
+ }
339
+ }
340
+ }
341
+
342
+ // -- Reset -----------------------------------------------------------------
343
+ function reset(values?: Partial<T>): void {
344
+ form.reset(values)
345
+ currentStep.value = 0
346
+ }
347
+
348
+ // -- onFieldChange wiring --------------------------------------------------
349
+ if (enhancements?.onFieldChange) {
350
+ const callback = enhancements.onFieldChange
351
+ watch(
352
+ () => ({ ...(form.data as Record<string, unknown>) }),
353
+ (newData, oldData) => {
354
+ if (!oldData) return
355
+ for (const key of Object.keys(newData)) {
356
+ if (newData[key] !== oldData[key]) {
357
+ callback(key as keyof T, newData[key], newData as T)
358
+ }
359
+ }
360
+ },
361
+ { deep: true },
362
+ )
363
+ }
364
+
365
+ return {
366
+ form: form as UseFormReturn<T>,
367
+ rules: rules as Partial<Record<keyof T, FieldRules>>,
368
+ isFieldVisible,
369
+ isSectionVisible,
370
+ fields,
371
+ getResolvedFieldProps,
372
+ getFieldOptions,
373
+ currentStep,
374
+ isCurrentStepValid,
375
+ goNext,
376
+ goBack,
377
+ goToStep,
378
+ validate,
379
+ reset,
380
+ submit,
381
+ }
382
+ }
@@ -0,0 +1,92 @@
1
+ import { ref, computed, onMounted, type Ref, type ComputedRef } from 'vue'
2
+ import { useApi } from './useApi'
3
+ import { usePlatformContext } from './usePlatformContext'
4
+
5
+ export interface UsePluginConfigReturn {
6
+ config: Ref<Record<string, unknown>>
7
+ isLoading: Ref<boolean>
8
+ isSaving: Ref<boolean>
9
+ error: Ref<string | null>
10
+ isDirty: ComputedRef<boolean>
11
+ load: () => Promise<void>
12
+ save: () => Promise<boolean>
13
+ reset: () => void
14
+ }
15
+
16
+ export function usePluginConfig(pluginName?: string): UsePluginConfigReturn {
17
+ const api = useApi()
18
+ const { plugin } = usePlatformContext()
19
+
20
+ const resolvedName = computed(() => pluginName ?? plugin.value?.name ?? '')
21
+
22
+ const config = ref<Record<string, unknown>>({})
23
+ const savedConfig = ref<Record<string, unknown>>({})
24
+ const isLoading = ref(false)
25
+ const isSaving = ref(false)
26
+ const error = ref<string | null>(null)
27
+
28
+ const isDirty = computed(() => {
29
+ return JSON.stringify(config.value) !== JSON.stringify(savedConfig.value)
30
+ })
31
+
32
+ async function load(): Promise<void> {
33
+ const name = resolvedName.value
34
+ if (!name) return
35
+
36
+ isLoading.value = true
37
+ error.value = null
38
+ try {
39
+ const response = await api.get<{ plugin_name: string; config: Record<string, unknown> }>(
40
+ `/api/plugins/${encodeURIComponent(name)}/config`,
41
+ )
42
+ config.value = { ...response.config }
43
+ savedConfig.value = { ...response.config }
44
+ } catch (e) {
45
+ error.value = e instanceof Error ? e.message : 'Failed to load plugin config'
46
+ } finally {
47
+ isLoading.value = false
48
+ }
49
+ }
50
+
51
+ async function save(): Promise<boolean> {
52
+ const name = resolvedName.value
53
+ if (!name) return false
54
+
55
+ isSaving.value = true
56
+ error.value = null
57
+ try {
58
+ const response = await api.put<{ plugin_name: string; config: Record<string, unknown> }>(
59
+ `/api/plugins/${encodeURIComponent(name)}/config`,
60
+ { config: config.value },
61
+ )
62
+ config.value = { ...response.config }
63
+ savedConfig.value = { ...response.config }
64
+ return true
65
+ } catch (e) {
66
+ error.value = e instanceof Error ? e.message : 'Failed to save plugin config'
67
+ return false
68
+ } finally {
69
+ isSaving.value = false
70
+ }
71
+ }
72
+
73
+ function reset(): void {
74
+ config.value = { ...savedConfig.value }
75
+ error.value = null
76
+ }
77
+
78
+ onMounted(() => {
79
+ load()
80
+ })
81
+
82
+ return {
83
+ config,
84
+ isLoading,
85
+ isSaving,
86
+ error,
87
+ isDirty,
88
+ load,
89
+ save,
90
+ reset,
91
+ }
92
+ }
package/src/index.ts CHANGED
@@ -133,6 +133,9 @@ export {
133
133
  compareTime,
134
134
  // Schedule drag
135
135
  useScheduleDrag,
136
+ // Plugin config
137
+ usePluginConfig,
138
+ type UsePluginConfigReturn,
136
139
  } from './composables'
137
140
 
138
141
  // Stores
@@ -12,6 +12,7 @@
12
12
  box-shadow: var(--shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.05));
13
13
  border: 1px solid var(--border-color, var(--mld-border, #e5e7eb));
14
14
  background-color: var(--bg-card, var(--mld-bg-card, #ffffff));
15
+ padding: 1rem;
15
16
  }
16
17
 
17
18
  .mld-container--scrollable {
@@ -62,7 +62,6 @@
62
62
 
63
63
  .mld-layout--floating .mld-layout__topbar {
64
64
  border-radius: var(--radius-lg, 0.75rem);
65
- overflow: hidden;
66
65
  box-shadow: var(--shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.05));
67
66
  border: 1px solid var(--border-color, var(--mld-border, #e5e7eb));
68
67
  }
@@ -94,6 +93,6 @@
94
93
  border-bottom: none;
95
94
  margin: 0;
96
95
  width: 100%;
97
- border-radius: 0;
96
+ border-radius: var(--radius-lg, 0.75rem);
98
97
  box-shadow: none;
99
98
  }
@@ -0,0 +1,69 @@
1
+ /* FormBuilder */
2
+ .mld-form-builder {
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: 1.5rem;
6
+ }
7
+
8
+ .mld-form-builder--sm {
9
+ gap: 1rem;
10
+ }
11
+
12
+ .mld-form-builder--lg {
13
+ gap: 2rem;
14
+ }
15
+
16
+ .mld-form-builder__step {
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: 1.5rem;
20
+ }
21
+
22
+ /* FormSection */
23
+ .mld-form-section {
24
+ display: flex;
25
+ flex-direction: column;
26
+ gap: 0.5rem;
27
+ }
28
+
29
+ .mld-form-section--static {
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: 1rem;
33
+ }
34
+
35
+ .mld-form-section__header {
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: 0.25rem;
39
+ margin-bottom: 0.5rem;
40
+ }
41
+
42
+ .mld-form-section__title {
43
+ font-size: var(--font-size-base, 0.9375rem);
44
+ font-weight: 600;
45
+ color: var(--color-text, #1a1a2e);
46
+ margin: 0;
47
+ line-height: 1.4;
48
+ }
49
+
50
+ .mld-form-section__description {
51
+ font-size: var(--font-size-sm, 0.8125rem);
52
+ color: var(--color-text-secondary, #6b7280);
53
+ margin: 0;
54
+ line-height: 1.5;
55
+ }
56
+
57
+ .mld-form-section__grid {
58
+ display: grid;
59
+ gap: 1rem;
60
+ }
61
+
62
+ /* FormActions */
63
+ .mld-form-actions {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 0.75rem;
67
+ padding-top: 1rem;
68
+ border-top: 1px solid var(--color-border, #e5e7eb);
69
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  .mld-number-input {
4
4
  display: inline-flex;
5
+ max-width: 100%;
5
6
  border-radius: 0.5rem;
6
7
  border: 1px solid var(--border-color);
7
8
  overflow: hidden;
@@ -42,6 +43,7 @@
42
43
  display: flex;
43
44
  align-items: center;
44
45
  justify-content: center;
46
+ flex-shrink: 0;
45
47
  background-color: var(--bg-tertiary);
46
48
  color: var(--text-muted);
47
49
  border: none;
@@ -85,7 +87,8 @@
85
87
  }
86
88
 
87
89
  .mld-number-input__input {
88
- width: 5rem;
90
+ flex: 1;
91
+ min-width: 0;
89
92
  text-align: center;
90
93
  background-color: var(--bg-secondary);
91
94
  color: var(--text-primary);
@@ -78,3 +78,4 @@
78
78
  @import './components/time-range-input.css';
79
79
  @import './components/schedule-calendar.css';
80
80
  @import './components/resource-card.css';
81
+ @import './components/form-builder.css';