@d34dman/flowdrop 0.0.22 → 0.0.24

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 (44) hide show
  1. package/dist/components/App.svelte +26 -25
  2. package/dist/components/ConfigForm.svelte +141 -520
  3. package/dist/components/ConfigForm.svelte.d.ts +5 -3
  4. package/dist/components/form/FormArray.svelte +1049 -0
  5. package/dist/components/form/FormArray.svelte.d.ts +22 -0
  6. package/dist/components/form/FormCheckboxGroup.svelte +152 -0
  7. package/dist/components/form/FormCheckboxGroup.svelte.d.ts +15 -0
  8. package/dist/components/form/FormField.svelte +297 -0
  9. package/dist/components/form/FormField.svelte.d.ts +18 -0
  10. package/dist/components/form/FormFieldWrapper.svelte +133 -0
  11. package/dist/components/form/FormFieldWrapper.svelte.d.ts +18 -0
  12. package/dist/components/form/FormNumberField.svelte +109 -0
  13. package/dist/components/form/FormNumberField.svelte.d.ts +23 -0
  14. package/dist/components/form/FormRangeField.svelte +252 -0
  15. package/dist/components/form/FormRangeField.svelte.d.ts +21 -0
  16. package/dist/components/form/FormSelect.svelte +126 -0
  17. package/dist/components/form/FormSelect.svelte.d.ts +18 -0
  18. package/dist/components/form/FormTextField.svelte +88 -0
  19. package/dist/components/form/FormTextField.svelte.d.ts +17 -0
  20. package/dist/components/form/FormTextarea.svelte +94 -0
  21. package/dist/components/form/FormTextarea.svelte.d.ts +19 -0
  22. package/dist/components/form/FormToggle.svelte +123 -0
  23. package/dist/components/form/FormToggle.svelte.d.ts +17 -0
  24. package/dist/components/form/index.d.ts +42 -0
  25. package/dist/components/form/index.js +46 -0
  26. package/dist/components/form/types.d.ts +224 -0
  27. package/dist/components/form/types.js +29 -0
  28. package/dist/components/nodes/GatewayNode.svelte +76 -16
  29. package/dist/components/nodes/SimpleNode.svelte +41 -5
  30. package/dist/components/nodes/SimpleNode.svelte.d.ts +2 -1
  31. package/dist/components/nodes/SquareNode.svelte +41 -5
  32. package/dist/components/nodes/SquareNode.svelte.d.ts +2 -1
  33. package/dist/components/nodes/WorkflowNode.svelte +88 -5
  34. package/dist/index.d.ts +2 -3
  35. package/dist/index.js +1 -3
  36. package/dist/stores/workflowStore.d.ts +15 -0
  37. package/dist/stores/workflowStore.js +28 -0
  38. package/dist/types/index.d.ts +176 -1
  39. package/dist/types/index.js +16 -0
  40. package/package.json +3 -3
  41. package/dist/config/demo.d.ts +0 -58
  42. package/dist/config/demo.js +0 -142
  43. package/dist/data/samples.d.ts +0 -51
  44. package/dist/data/samples.js +0 -3245
@@ -0,0 +1,1049 @@
1
+ <!--
2
+ FormArray Component
3
+ Dynamic array field that allows adding, removing, and reordering items
4
+ Generates sub-forms for each item based on the schema's items property
5
+
6
+ Features:
7
+ - Add/remove items with animated transitions
8
+ - Supports simple types (string, number, boolean) and complex types (objects)
9
+ - Recursively uses FormField for item rendering
10
+ - Drag handle for future reordering support
11
+ - Collapsible items for complex object arrays
12
+ - Empty state with helpful prompt
13
+
14
+ Accessibility:
15
+ - Proper ARIA labels for add/remove buttons
16
+ - Keyboard navigation support
17
+ - Screen reader friendly item descriptions
18
+ -->
19
+
20
+ <script lang="ts">
21
+ import Icon from '@iconify/svelte';
22
+ import type { FieldSchema } from './types.js';
23
+
24
+ interface Props {
25
+ /** Field identifier */
26
+ id: string;
27
+ /** Current array value */
28
+ value: unknown[];
29
+ /** Schema for array items */
30
+ itemSchema: FieldSchema;
31
+ /** Minimum number of items required */
32
+ minItems?: number;
33
+ /** Maximum number of items allowed */
34
+ maxItems?: number;
35
+ /** Label for add button */
36
+ addLabel?: string;
37
+ /** Whether the field is disabled */
38
+ disabled?: boolean;
39
+ /** Callback when value changes */
40
+ onChange: (value: unknown[]) => void;
41
+ }
42
+
43
+ let {
44
+ id,
45
+ value = [],
46
+ itemSchema,
47
+ minItems = 0,
48
+ maxItems,
49
+ addLabel = 'Add Item',
50
+ disabled = false,
51
+ onChange
52
+ }: Props = $props();
53
+
54
+ /**
55
+ * Ensure value is always an array
56
+ */
57
+ const items = $derived(Array.isArray(value) ? value : []);
58
+
59
+ /**
60
+ * Check if we can add more items
61
+ */
62
+ const canAddItem = $derived(maxItems === undefined || items.length < maxItems);
63
+
64
+ /**
65
+ * Check if we can remove items
66
+ */
67
+ const canRemoveItem = $derived(items.length > minItems);
68
+
69
+ /**
70
+ * Determine if items are simple (primitive) or complex (objects)
71
+ */
72
+ const isSimpleType = $derived(
73
+ itemSchema.type === 'string' ||
74
+ itemSchema.type === 'number' ||
75
+ itemSchema.type === 'integer' ||
76
+ itemSchema.type === 'boolean'
77
+ );
78
+
79
+ /**
80
+ * Get the default value for a new item based on schema
81
+ */
82
+ function getDefaultValue(): unknown {
83
+ if (itemSchema.default !== undefined) {
84
+ return itemSchema.default;
85
+ }
86
+
87
+ switch (itemSchema.type) {
88
+ case 'string':
89
+ return '';
90
+ case 'number':
91
+ case 'integer':
92
+ return 0;
93
+ case 'boolean':
94
+ return false;
95
+ case 'object':
96
+ // Create default object from properties
97
+ if (itemSchema.properties) {
98
+ const defaultObj: Record<string, unknown> = {};
99
+ Object.entries(itemSchema.properties).forEach(([key, propSchema]) => {
100
+ if (propSchema.default !== undefined) {
101
+ defaultObj[key] = propSchema.default;
102
+ } else {
103
+ defaultObj[key] = getDefaultForType(propSchema.type);
104
+ }
105
+ });
106
+ return defaultObj;
107
+ }
108
+ return {};
109
+ case 'array':
110
+ return [];
111
+ default:
112
+ return '';
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Get default value for a specific type
118
+ */
119
+ function getDefaultForType(type: string | undefined): unknown {
120
+ switch (type) {
121
+ case 'string':
122
+ return '';
123
+ case 'number':
124
+ case 'integer':
125
+ return 0;
126
+ case 'boolean':
127
+ return false;
128
+ case 'object':
129
+ return {};
130
+ case 'array':
131
+ return [];
132
+ default:
133
+ return '';
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Add a new item to the array
139
+ */
140
+ function addItem(): void {
141
+ if (!canAddItem || disabled) return;
142
+ const newValue = [...items, getDefaultValue()];
143
+ onChange(newValue);
144
+ }
145
+
146
+ /**
147
+ * Remove an item at the specified index
148
+ */
149
+ function removeItem(index: number): void {
150
+ if (!canRemoveItem || disabled) return;
151
+ const newValue = items.filter((_, i) => i !== index);
152
+ onChange(newValue);
153
+ }
154
+
155
+ /**
156
+ * Update an item at the specified index
157
+ */
158
+ function updateItem(index: number, newItemValue: unknown): void {
159
+ const newValue = items.map((item, i) => (i === index ? newItemValue : item));
160
+ onChange(newValue);
161
+ }
162
+
163
+ /**
164
+ * Update a property of an object item
165
+ */
166
+ function updateObjectProperty(index: number, propertyKey: string, propertyValue: unknown): void {
167
+ const currentItem = items[index] as Record<string, unknown>;
168
+ const updatedItem = { ...currentItem, [propertyKey]: propertyValue };
169
+ updateItem(index, updatedItem);
170
+ }
171
+
172
+ /**
173
+ * Move an item up in the array
174
+ */
175
+ function moveItemUp(index: number): void {
176
+ if (index === 0 || disabled) return;
177
+ const newValue = [...items];
178
+ [newValue[index - 1], newValue[index]] = [newValue[index], newValue[index - 1]];
179
+ onChange(newValue);
180
+ }
181
+
182
+ /**
183
+ * Move an item down in the array
184
+ */
185
+ function moveItemDown(index: number): void {
186
+ if (index === items.length - 1 || disabled) return;
187
+ const newValue = [...items];
188
+ [newValue[index], newValue[index + 1]] = [newValue[index + 1], newValue[index]];
189
+ onChange(newValue);
190
+ }
191
+
192
+ /**
193
+ * Get item label for display
194
+ */
195
+ function getItemLabel(index: number, item: unknown): string {
196
+ if (isSimpleType) {
197
+ const itemStr = String(item);
198
+ return itemStr.length > 30
199
+ ? `${itemStr.substring(0, 30)}...`
200
+ : itemStr || `Item ${index + 1}`;
201
+ }
202
+
203
+ // For objects, try to find a name/label/title property
204
+ if (typeof item === 'object' && item !== null) {
205
+ const obj = item as Record<string, unknown>;
206
+ const labelKey = Object.keys(obj).find((k) =>
207
+ ['name', 'label', 'title', 'id'].includes(k.toLowerCase())
208
+ );
209
+ if (labelKey && obj[labelKey]) {
210
+ return String(obj[labelKey]);
211
+ }
212
+ }
213
+
214
+ return `Item ${index + 1}`;
215
+ }
216
+
217
+ /**
218
+ * Track collapsed state for complex items
219
+ */
220
+ let collapsedItems = $state<Set<number>>(new Set());
221
+
222
+ /**
223
+ * Toggle collapsed state for an item
224
+ */
225
+ function toggleCollapse(index: number): void {
226
+ const newCollapsed = new Set(collapsedItems);
227
+ if (newCollapsed.has(index)) {
228
+ newCollapsed.delete(index);
229
+ } else {
230
+ newCollapsed.add(index);
231
+ }
232
+ collapsedItems = newCollapsed;
233
+ }
234
+
235
+ /**
236
+ * Check if an item is collapsed
237
+ */
238
+ function isCollapsed(index: number): boolean {
239
+ return collapsedItems.has(index);
240
+ }
241
+ </script>
242
+
243
+ <div class="form-array" class:form-array--disabled={disabled}>
244
+ <!-- Array Items -->
245
+ {#if items.length > 0}
246
+ <div class="form-array__items">
247
+ {#each items as item, index (index)}
248
+ <div
249
+ class="form-array__item"
250
+ class:form-array__item--simple={isSimpleType}
251
+ class:form-array__item--complex={!isSimpleType}
252
+ style="animation-delay: {index * 50}ms"
253
+ >
254
+ <!-- Item Header -->
255
+ <div class="form-array__item-header">
256
+ <!-- Item index/label -->
257
+ {#if !isSimpleType}
258
+ <button
259
+ type="button"
260
+ class="form-array__item-toggle"
261
+ onclick={() => toggleCollapse(index)}
262
+ aria-expanded={!isCollapsed(index)}
263
+ aria-label={isCollapsed(index) ? 'Expand item' : 'Collapse item'}
264
+ >
265
+ <Icon
266
+ icon={isCollapsed(index) ? 'heroicons:chevron-right' : 'heroicons:chevron-down'}
267
+ class="form-array__toggle-icon"
268
+ />
269
+ <span class="form-array__item-label">{getItemLabel(index, item)}</span>
270
+ </button>
271
+ {:else}
272
+ <span class="form-array__item-number">#{index + 1}</span>
273
+ {/if}
274
+
275
+ <!-- Action buttons group -->
276
+ <div class="form-array__actions">
277
+ <!-- Move Up button -->
278
+ <button
279
+ type="button"
280
+ class="form-array__action-btn form-array__action-btn--move"
281
+ onclick={() => moveItemUp(index)}
282
+ disabled={index === 0 || disabled}
283
+ aria-label="Move item {index + 1} up"
284
+ title="Move up"
285
+ >
286
+ <Icon icon="heroicons:arrow-up" />
287
+ </button>
288
+
289
+ <!-- Move Down button -->
290
+ <button
291
+ type="button"
292
+ class="form-array__action-btn form-array__action-btn--move"
293
+ onclick={() => moveItemDown(index)}
294
+ disabled={index === items.length - 1 || disabled}
295
+ aria-label="Move item {index + 1} down"
296
+ title="Move down"
297
+ >
298
+ <Icon icon="heroicons:arrow-down" />
299
+ </button>
300
+
301
+ <!-- Delete button -->
302
+ <button
303
+ type="button"
304
+ class="form-array__action-btn form-array__action-btn--delete"
305
+ onclick={() => removeItem(index)}
306
+ disabled={!canRemoveItem || disabled}
307
+ aria-label="Delete item {index + 1}"
308
+ title="Delete item"
309
+ >
310
+ <Icon icon="heroicons:trash" />
311
+ </button>
312
+ </div>
313
+ </div>
314
+
315
+ <!-- Item Content -->
316
+ <div
317
+ class="form-array__item-content"
318
+ class:form-array__item-content--collapsed={!isSimpleType && isCollapsed(index)}
319
+ >
320
+ {#if isSimpleType}
321
+ <!-- Simple type: render inline input -->
322
+ {#if itemSchema.type === 'string'}
323
+ {#if itemSchema.format === 'multiline'}
324
+ <textarea
325
+ class="form-array__input form-array__textarea"
326
+ value={String(item ?? '')}
327
+ placeholder={itemSchema.placeholder ?? ''}
328
+ rows={3}
329
+ oninput={(e) => updateItem(index, e.currentTarget.value)}
330
+ {disabled}
331
+ ></textarea>
332
+ {:else}
333
+ <input
334
+ type="text"
335
+ class="form-array__input"
336
+ value={String(item ?? '')}
337
+ placeholder={itemSchema.placeholder ?? ''}
338
+ oninput={(e) => updateItem(index, e.currentTarget.value)}
339
+ {disabled}
340
+ />
341
+ {/if}
342
+ {:else if itemSchema.type === 'number' || itemSchema.type === 'integer'}
343
+ <input
344
+ type="number"
345
+ class="form-array__input form-array__input--number"
346
+ value={item as number}
347
+ placeholder={itemSchema.placeholder ?? ''}
348
+ min={itemSchema.minimum}
349
+ max={itemSchema.maximum}
350
+ oninput={(e) => {
351
+ const val = e.currentTarget.value;
352
+ updateItem(index, val === '' ? '' : Number(val));
353
+ }}
354
+ {disabled}
355
+ />
356
+ {:else if itemSchema.type === 'boolean'}
357
+ <label class="form-array__toggle-wrapper">
358
+ <input
359
+ type="checkbox"
360
+ class="form-array__checkbox-input"
361
+ checked={Boolean(item)}
362
+ onchange={(e) => updateItem(index, e.currentTarget.checked)}
363
+ {disabled}
364
+ />
365
+ <span class="form-array__toggle-track">
366
+ <span class="form-array__toggle-thumb"></span>
367
+ </span>
368
+ <span class="form-array__toggle-label">
369
+ {item ? 'Yes' : 'No'}
370
+ </span>
371
+ </label>
372
+ {:else if itemSchema.enum}
373
+ <!-- Enum: render select -->
374
+ <select
375
+ class="form-array__select"
376
+ value={String(item ?? '')}
377
+ onchange={(e) => updateItem(index, e.currentTarget.value)}
378
+ {disabled}
379
+ >
380
+ {#each itemSchema.enum as option}
381
+ <option value={String(option)}>{String(option)}</option>
382
+ {/each}
383
+ </select>
384
+ {:else}
385
+ <!-- Fallback to text -->
386
+ <input
387
+ type="text"
388
+ class="form-array__input"
389
+ value={String(item ?? '')}
390
+ placeholder={itemSchema.placeholder ?? ''}
391
+ oninput={(e) => updateItem(index, e.currentTarget.value)}
392
+ {disabled}
393
+ />
394
+ {/if}
395
+ {:else if itemSchema.type === 'object' && itemSchema.properties}
396
+ <!-- Complex type: render sub-form for object properties -->
397
+ {#if !isCollapsed(index)}
398
+ <div class="form-array__subform">
399
+ {#each Object.entries(itemSchema.properties) as [propKey, propSchema], propIndex (propKey)}
400
+ {@const propValue = (item as Record<string, unknown>)?.[propKey]}
401
+ {@const isRequired = itemSchema.required?.includes(propKey) ?? false}
402
+ {@const propFieldSchema = propSchema as FieldSchema}
403
+
404
+ <div
405
+ class="form-array__subform-field"
406
+ style="animation-delay: {propIndex * 20}ms"
407
+ >
408
+ <label class="form-array__subform-label" for="{id}-{index}-{propKey}">
409
+ <span class="form-array__subform-label-text">
410
+ {propFieldSchema.title ?? propKey}
411
+ </span>
412
+ {#if isRequired}
413
+ <span class="form-array__required">*</span>
414
+ {/if}
415
+ </label>
416
+
417
+ <div class="form-array__subform-input">
418
+ {#if propFieldSchema.enum}
419
+ <select
420
+ id="{id}-{index}-{propKey}"
421
+ class="form-array__select"
422
+ value={String(propValue ?? '')}
423
+ onchange={(e) =>
424
+ updateObjectProperty(index, propKey, e.currentTarget.value)}
425
+ {disabled}
426
+ >
427
+ {#each propFieldSchema.enum as option}
428
+ <option value={String(option)}>{String(option)}</option>
429
+ {/each}
430
+ </select>
431
+ {:else if propFieldSchema.type === 'string' && propFieldSchema.format === 'multiline'}
432
+ <textarea
433
+ id="{id}-{index}-{propKey}"
434
+ class="form-array__input form-array__textarea"
435
+ value={String(propValue ?? '')}
436
+ placeholder={propFieldSchema.placeholder ?? ''}
437
+ rows={3}
438
+ oninput={(e) =>
439
+ updateObjectProperty(index, propKey, e.currentTarget.value)}
440
+ {disabled}
441
+ ></textarea>
442
+ {:else if propFieldSchema.type === 'string'}
443
+ <input
444
+ id="{id}-{index}-{propKey}"
445
+ type="text"
446
+ class="form-array__input"
447
+ value={String(propValue ?? '')}
448
+ placeholder={propFieldSchema.placeholder ?? ''}
449
+ oninput={(e) =>
450
+ updateObjectProperty(index, propKey, e.currentTarget.value)}
451
+ {disabled}
452
+ />
453
+ {:else if propFieldSchema.type === 'number' || propFieldSchema.type === 'integer'}
454
+ <input
455
+ id="{id}-{index}-{propKey}"
456
+ type="number"
457
+ class="form-array__input form-array__input--number"
458
+ value={propValue as number}
459
+ placeholder={propFieldSchema.placeholder ?? ''}
460
+ min={propFieldSchema.minimum}
461
+ max={propFieldSchema.maximum}
462
+ oninput={(e) => {
463
+ const val = e.currentTarget.value;
464
+ updateObjectProperty(index, propKey, val === '' ? '' : Number(val));
465
+ }}
466
+ {disabled}
467
+ />
468
+ {:else if propFieldSchema.type === 'boolean'}
469
+ <label class="form-array__toggle-wrapper">
470
+ <input
471
+ id="{id}-{index}-{propKey}"
472
+ type="checkbox"
473
+ class="form-array__checkbox-input"
474
+ checked={Boolean(propValue)}
475
+ onchange={(e) =>
476
+ updateObjectProperty(index, propKey, e.currentTarget.checked)}
477
+ {disabled}
478
+ />
479
+ <span class="form-array__toggle-track">
480
+ <span class="form-array__toggle-thumb"></span>
481
+ </span>
482
+ <span class="form-array__toggle-label">
483
+ {propValue ? 'Yes' : 'No'}
484
+ </span>
485
+ </label>
486
+ {:else}
487
+ <input
488
+ id="{id}-{index}-{propKey}"
489
+ type="text"
490
+ class="form-array__input"
491
+ value={String(propValue ?? '')}
492
+ placeholder={propFieldSchema.placeholder ?? ''}
493
+ oninput={(e) =>
494
+ updateObjectProperty(index, propKey, e.currentTarget.value)}
495
+ {disabled}
496
+ />
497
+ {/if}
498
+ </div>
499
+
500
+ {#if propFieldSchema.description && propFieldSchema.title}
501
+ <p class="form-array__subform-description">{propFieldSchema.description}</p>
502
+ {/if}
503
+ </div>
504
+ {/each}
505
+ </div>
506
+ {/if}
507
+ {:else}
508
+ <!-- Unknown complex type -->
509
+ <div class="form-array__unsupported">
510
+ <p>Complex item type "{itemSchema.type}" is not fully supported.</p>
511
+ </div>
512
+ {/if}
513
+ </div>
514
+ </div>
515
+ {/each}
516
+ </div>
517
+ {:else}
518
+ <!-- Empty State -->
519
+ <div class="form-array__empty">
520
+ <Icon icon="heroicons:squares-plus" class="form-array__empty-icon" />
521
+ <p class="form-array__empty-text">No items yet</p>
522
+ </div>
523
+ {/if}
524
+
525
+ <!-- Add Button -->
526
+ <button
527
+ type="button"
528
+ class="form-array__add-btn"
529
+ onclick={addItem}
530
+ disabled={!canAddItem || disabled}
531
+ aria-label={addLabel}
532
+ >
533
+ <Icon icon="heroicons:plus" />
534
+ <span>{addLabel}</span>
535
+ </button>
536
+
537
+ <!-- Item count and limits -->
538
+ {#if minItems > 0 || maxItems !== undefined}
539
+ <div class="form-array__info">
540
+ <span class="form-array__count">{items.length} item{items.length !== 1 ? 's' : ''}</span>
541
+ {#if minItems > 0}
542
+ <span class="form-array__limit">Min: {minItems}</span>
543
+ {/if}
544
+ {#if maxItems !== undefined}
545
+ <span class="form-array__limit">Max: {maxItems}</span>
546
+ {/if}
547
+ </div>
548
+ {/if}
549
+ </div>
550
+
551
+ <style>
552
+ /* ============================================
553
+ FORM ARRAY CONTAINER
554
+ ============================================ */
555
+
556
+ .form-array {
557
+ display: flex;
558
+ flex-direction: column;
559
+ gap: 0.75rem;
560
+ }
561
+
562
+ .form-array--disabled {
563
+ opacity: 0.6;
564
+ pointer-events: none;
565
+ }
566
+
567
+ /* ============================================
568
+ ITEMS CONTAINER
569
+ ============================================ */
570
+
571
+ .form-array__items {
572
+ display: flex;
573
+ flex-direction: column;
574
+ gap: 0.5rem;
575
+ }
576
+
577
+ /* ============================================
578
+ INDIVIDUAL ITEM
579
+ ============================================ */
580
+
581
+ .form-array__item {
582
+ display: flex;
583
+ flex-direction: column;
584
+ background-color: var(--color-ref-gray-50, #f9fafb);
585
+ border: 1px solid var(--color-ref-gray-200, #e5e7eb);
586
+ border-radius: 0.5rem;
587
+ overflow: hidden;
588
+ animation: itemFadeIn 0.25s ease-out forwards;
589
+ opacity: 0;
590
+ transform: translateY(-8px);
591
+ }
592
+
593
+ @keyframes itemFadeIn {
594
+ to {
595
+ opacity: 1;
596
+ transform: translateY(0);
597
+ }
598
+ }
599
+
600
+ .form-array__item--simple .form-array__item-content {
601
+ padding: 0.5rem 0.75rem 0.75rem;
602
+ }
603
+
604
+ .form-array__item--complex .form-array__item-content {
605
+ padding: 0;
606
+ }
607
+
608
+ /* ============================================
609
+ ITEM HEADER
610
+ ============================================ */
611
+
612
+ .form-array__item-header {
613
+ display: flex;
614
+ align-items: center;
615
+ gap: 0.625rem;
616
+ padding: 0.625rem 0.75rem;
617
+ background-color: var(--color-ref-gray-100, #f3f4f6);
618
+ border-bottom: 1px solid var(--color-ref-gray-200, #e5e7eb);
619
+ }
620
+
621
+ .form-array__item--simple .form-array__item-header {
622
+ padding: 0.5rem 0.625rem;
623
+ }
624
+
625
+ /* ============================================
626
+ ITEM NUMBER/LABEL
627
+ ============================================ */
628
+
629
+ .form-array__item-number {
630
+ font-size: 0.75rem;
631
+ font-weight: 600;
632
+ color: var(--color-ref-gray-600, #4b5563);
633
+ min-width: 1.75rem;
634
+ padding: 0.125rem 0.375rem;
635
+ background-color: var(--color-ref-gray-200, #e5e7eb);
636
+ border-radius: 0.25rem;
637
+ text-align: center;
638
+ }
639
+
640
+ .form-array__item-toggle {
641
+ display: flex;
642
+ align-items: center;
643
+ gap: 0.5rem;
644
+ flex: 1;
645
+ padding: 0.375rem 0.5rem;
646
+ margin: -0.25rem;
647
+ border: 1px solid transparent;
648
+ background: transparent;
649
+ cursor: pointer;
650
+ text-align: left;
651
+ border-radius: 0.375rem;
652
+ transition: all 0.15s;
653
+ }
654
+
655
+ .form-array__item-toggle:hover {
656
+ background-color: var(--color-ref-gray-200, #e5e7eb);
657
+ }
658
+
659
+ .form-array__item-toggle:focus-visible {
660
+ outline: none;
661
+ border-color: var(--color-ref-blue-500, #3b82f6);
662
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
663
+ }
664
+
665
+ .form-array__item-toggle :global(svg) {
666
+ width: 1.125rem;
667
+ height: 1.125rem;
668
+ color: var(--color-ref-gray-500, #6b7280);
669
+ transition: transform 0.2s;
670
+ }
671
+
672
+ .form-array__item-label {
673
+ font-size: 0.8125rem;
674
+ font-weight: 600;
675
+ color: var(--color-ref-gray-700, #374151);
676
+ }
677
+
678
+ /* ============================================
679
+ ACTION BUTTONS GROUP
680
+ ============================================ */
681
+
682
+ .form-array__actions {
683
+ display: flex;
684
+ align-items: center;
685
+ gap: 0.375rem;
686
+ margin-left: auto;
687
+ }
688
+
689
+ .form-array__action-btn {
690
+ display: flex;
691
+ align-items: center;
692
+ justify-content: center;
693
+ width: 2rem;
694
+ height: 2rem;
695
+ padding: 0;
696
+ border: 1px solid transparent;
697
+ border-radius: 0.375rem;
698
+ cursor: pointer;
699
+ transition: all 0.15s;
700
+ }
701
+
702
+ .form-array__action-btn :global(svg) {
703
+ width: 1rem;
704
+ height: 1rem;
705
+ }
706
+
707
+ .form-array__action-btn:focus-visible {
708
+ outline: none;
709
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
710
+ }
711
+
712
+ .form-array__action-btn:disabled {
713
+ opacity: 0.35;
714
+ cursor: not-allowed;
715
+ }
716
+
717
+ /* Move Up/Down buttons - Blue semantic color */
718
+ .form-array__action-btn--move {
719
+ background-color: var(--color-ref-blue-50, #eff6ff);
720
+ border-color: var(--color-ref-blue-200, #bfdbfe);
721
+ color: var(--color-ref-blue-600, #2563eb);
722
+ }
723
+
724
+ .form-array__action-btn--move:hover:not(:disabled) {
725
+ background-color: var(--color-ref-blue-100, #dbeafe);
726
+ border-color: var(--color-ref-blue-300, #93c5fd);
727
+ color: var(--color-ref-blue-700, #1d4ed8);
728
+ }
729
+
730
+ .form-array__action-btn--move:active:not(:disabled) {
731
+ background-color: var(--color-ref-blue-200, #bfdbfe);
732
+ }
733
+
734
+ /* Delete button - Red/Warning semantic color */
735
+ .form-array__action-btn--delete {
736
+ background-color: var(--color-ref-red-50, #fef2f2);
737
+ border-color: var(--color-ref-red-200, #fecaca);
738
+ color: var(--color-ref-red-600, #dc2626);
739
+ }
740
+
741
+ .form-array__action-btn--delete:hover:not(:disabled) {
742
+ background-color: var(--color-ref-red-100, #fee2e2);
743
+ border-color: var(--color-ref-red-300, #fca5a5);
744
+ color: var(--color-ref-red-700, #b91c1c);
745
+ }
746
+
747
+ .form-array__action-btn--delete:active:not(:disabled) {
748
+ background-color: var(--color-ref-red-200, #fecaca);
749
+ }
750
+
751
+ /* ============================================
752
+ ITEM CONTENT
753
+ ============================================ */
754
+
755
+ .form-array__item-content {
756
+ transition: all 0.2s ease-out;
757
+ }
758
+
759
+ .form-array__item-content--collapsed {
760
+ height: 0;
761
+ overflow: hidden;
762
+ padding: 0 !important;
763
+ }
764
+
765
+ /* ============================================
766
+ INPUTS (Simple Types)
767
+ ============================================ */
768
+
769
+ .form-array__input {
770
+ width: 100%;
771
+ padding: 0.5rem 0.75rem;
772
+ border: 1px solid var(--color-ref-gray-200, #e5e7eb);
773
+ border-radius: 0.375rem;
774
+ font-size: 0.875rem;
775
+ font-family: inherit;
776
+ color: var(--color-ref-gray-900, #111827);
777
+ background-color: #ffffff;
778
+ transition: all 0.2s;
779
+ }
780
+
781
+ .form-array__input::placeholder {
782
+ color: var(--color-ref-gray-400, #9ca3af);
783
+ }
784
+
785
+ .form-array__input:hover {
786
+ border-color: var(--color-ref-gray-300, #d1d5db);
787
+ }
788
+
789
+ .form-array__input:focus {
790
+ outline: none;
791
+ border-color: var(--color-ref-blue-500, #3b82f6);
792
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
793
+ }
794
+
795
+ .form-array__input--number {
796
+ font-variant-numeric: tabular-nums;
797
+ }
798
+
799
+ .form-array__textarea {
800
+ resize: vertical;
801
+ min-height: 4rem;
802
+ line-height: 1.5;
803
+ }
804
+
805
+ .form-array__select {
806
+ width: 100%;
807
+ padding: 0.5rem 2rem 0.5rem 0.75rem;
808
+ border: 1px solid var(--color-ref-gray-200, #e5e7eb);
809
+ border-radius: 0.375rem;
810
+ font-size: 0.875rem;
811
+ font-family: inherit;
812
+ color: var(--color-ref-gray-900, #111827);
813
+ background-color: #ffffff;
814
+ cursor: pointer;
815
+ appearance: none;
816
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%239ca3af'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
817
+ background-repeat: no-repeat;
818
+ background-position: right 0.5rem center;
819
+ background-size: 1rem;
820
+ }
821
+
822
+ .form-array__select:hover {
823
+ border-color: var(--color-ref-gray-300, #d1d5db);
824
+ }
825
+
826
+ .form-array__select:focus {
827
+ outline: none;
828
+ border-color: var(--color-ref-blue-500, #3b82f6);
829
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
830
+ }
831
+
832
+ /* ============================================
833
+ TOGGLE (Boolean in Array)
834
+ ============================================ */
835
+
836
+ .form-array__toggle-wrapper {
837
+ display: flex;
838
+ align-items: center;
839
+ gap: 0.625rem;
840
+ cursor: pointer;
841
+ }
842
+
843
+ .form-array__checkbox-input {
844
+ position: absolute;
845
+ opacity: 0;
846
+ width: 0;
847
+ height: 0;
848
+ }
849
+
850
+ .form-array__toggle-track {
851
+ position: relative;
852
+ width: 2.25rem;
853
+ height: 1.25rem;
854
+ background-color: var(--color-ref-gray-300, #d1d5db);
855
+ border-radius: 0.625rem;
856
+ transition: background-color 0.2s;
857
+ flex-shrink: 0;
858
+ }
859
+
860
+ .form-array__toggle-thumb {
861
+ position: absolute;
862
+ top: 0.125rem;
863
+ left: 0.125rem;
864
+ width: 1rem;
865
+ height: 1rem;
866
+ background-color: #ffffff;
867
+ border-radius: 50%;
868
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
869
+ transition: transform 0.2s;
870
+ }
871
+
872
+ .form-array__checkbox-input:checked + .form-array__toggle-track {
873
+ background-color: var(--color-ref-blue-500, #3b82f6);
874
+ }
875
+
876
+ .form-array__checkbox-input:checked + .form-array__toggle-track .form-array__toggle-thumb {
877
+ transform: translateX(1rem);
878
+ }
879
+
880
+ .form-array__toggle-label {
881
+ font-size: 0.8125rem;
882
+ color: var(--color-ref-gray-600, #4b5563);
883
+ }
884
+
885
+ /* ============================================
886
+ SUBFORM (Complex Types)
887
+ ============================================ */
888
+
889
+ .form-array__subform {
890
+ display: flex;
891
+ flex-direction: column;
892
+ gap: 0.75rem;
893
+ padding: 0.75rem;
894
+ background-color: #ffffff;
895
+ }
896
+
897
+ .form-array__subform-field {
898
+ display: flex;
899
+ flex-direction: column;
900
+ gap: 0.375rem;
901
+ animation: subfieldFadeIn 0.2s ease-out forwards;
902
+ opacity: 0;
903
+ }
904
+
905
+ @keyframes subfieldFadeIn {
906
+ to {
907
+ opacity: 1;
908
+ }
909
+ }
910
+
911
+ .form-array__subform-label {
912
+ display: flex;
913
+ align-items: center;
914
+ gap: 0.25rem;
915
+ font-size: 0.75rem;
916
+ font-weight: 600;
917
+ color: var(--color-ref-gray-600, #4b5563);
918
+ }
919
+
920
+ .form-array__subform-label-text {
921
+ line-height: 1.4;
922
+ }
923
+
924
+ .form-array__required {
925
+ color: var(--color-ref-red-500, #ef4444);
926
+ font-weight: 500;
927
+ }
928
+
929
+ .form-array__subform-description {
930
+ margin: 0;
931
+ font-size: 0.6875rem;
932
+ color: var(--color-ref-gray-500, #6b7280);
933
+ line-height: 1.4;
934
+ }
935
+
936
+ /* ============================================
937
+ EMPTY STATE
938
+ ============================================ */
939
+
940
+ .form-array__empty {
941
+ display: flex;
942
+ flex-direction: column;
943
+ align-items: center;
944
+ justify-content: center;
945
+ padding: 2rem 1rem;
946
+ background-color: var(--color-ref-gray-50, #f9fafb);
947
+ border: 2px dashed var(--color-ref-gray-300, #d1d5db);
948
+ border-radius: 0.625rem;
949
+ }
950
+
951
+ .form-array__empty :global(svg) {
952
+ width: 2.5rem;
953
+ height: 2.5rem;
954
+ color: var(--color-ref-gray-400, #9ca3af);
955
+ margin-bottom: 0.625rem;
956
+ }
957
+
958
+ .form-array__empty-text {
959
+ margin: 0;
960
+ font-size: 0.875rem;
961
+ font-weight: 500;
962
+ color: var(--color-ref-gray-500, #6b7280);
963
+ }
964
+
965
+ /* ============================================
966
+ ADD BUTTON
967
+ ============================================ */
968
+
969
+ .form-array__add-btn {
970
+ display: inline-flex;
971
+ align-items: center;
972
+ justify-content: center;
973
+ gap: 0.5rem;
974
+ padding: 0.625rem 1rem;
975
+ border: 1px solid var(--color-ref-green-300, #86efac);
976
+ border-radius: 0.5rem;
977
+ background-color: var(--color-ref-green-50, #f0fdf4);
978
+ color: var(--color-ref-green-700, #15803d);
979
+ font-size: 0.8125rem;
980
+ font-weight: 600;
981
+ font-family: inherit;
982
+ cursor: pointer;
983
+ transition: all 0.15s;
984
+ }
985
+
986
+ .form-array__add-btn:hover:not(:disabled) {
987
+ background-color: var(--color-ref-green-100, #dcfce7);
988
+ border-color: var(--color-ref-green-400, #4ade80);
989
+ color: var(--color-ref-green-800, #166534);
990
+ }
991
+
992
+ .form-array__add-btn:focus-visible {
993
+ outline: none;
994
+ box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.3);
995
+ }
996
+
997
+ .form-array__add-btn:active:not(:disabled) {
998
+ background-color: var(--color-ref-green-200, #bbf7d0);
999
+ }
1000
+
1001
+ .form-array__add-btn:disabled {
1002
+ opacity: 0.5;
1003
+ cursor: not-allowed;
1004
+ }
1005
+
1006
+ .form-array__add-btn :global(svg) {
1007
+ width: 1.125rem;
1008
+ height: 1.125rem;
1009
+ }
1010
+
1011
+ /* ============================================
1012
+ INFO BAR
1013
+ ============================================ */
1014
+
1015
+ .form-array__info {
1016
+ display: flex;
1017
+ align-items: center;
1018
+ gap: 0.75rem;
1019
+ font-size: 0.6875rem;
1020
+ color: var(--color-ref-gray-500, #6b7280);
1021
+ }
1022
+
1023
+ .form-array__count {
1024
+ font-weight: 500;
1025
+ }
1026
+
1027
+ .form-array__limit {
1028
+ padding: 0.125rem 0.375rem;
1029
+ background-color: var(--color-ref-gray-100, #f3f4f6);
1030
+ border-radius: 0.25rem;
1031
+ }
1032
+
1033
+ /* ============================================
1034
+ UNSUPPORTED TYPE
1035
+ ============================================ */
1036
+
1037
+ .form-array__unsupported {
1038
+ padding: 0.75rem;
1039
+ background-color: var(--color-ref-amber-50, #fffbeb);
1040
+ border: 1px solid var(--color-ref-amber-200, #fde68a);
1041
+ border-radius: 0.375rem;
1042
+ color: var(--color-ref-amber-800, #92400e);
1043
+ font-size: 0.75rem;
1044
+ }
1045
+
1046
+ .form-array__unsupported p {
1047
+ margin: 0;
1048
+ }
1049
+ </style>