lula2 0.0.5 → 0.0.6

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 (108) hide show
  1. package/README.md +291 -8
  2. package/dist/_app/env.js +1 -0
  3. package/dist/_app/immutable/assets/0.DtiRW3lO.css +1 -0
  4. package/dist/_app/immutable/assets/DynamicControlEditor.BkVTzFZ-.css +1 -0
  5. package/dist/_app/immutable/chunks/7x_q-1ab.js +1 -0
  6. package/dist/_app/immutable/chunks/B19gt6-g.js +2 -0
  7. package/dist/_app/immutable/chunks/BR-0Dorr.js +1 -0
  8. package/dist/_app/immutable/chunks/B_3ksxz5.js +2 -0
  9. package/dist/_app/immutable/chunks/Bg_R1qWi.js +3 -0
  10. package/dist/_app/immutable/chunks/D3aNP_lg.js +1 -0
  11. package/dist/_app/immutable/chunks/D4Q_ObIy.js +1 -0
  12. package/dist/_app/immutable/chunks/DsnmJJEf.js +1 -0
  13. package/dist/_app/immutable/chunks/XY2j_owG.js +66 -0
  14. package/dist/_app/immutable/chunks/rzN25oDf.js +1 -0
  15. package/dist/_app/immutable/entry/app.r0uOd9qg.js +2 -0
  16. package/dist/_app/immutable/entry/start.DvoqR0rc.js +1 -0
  17. package/dist/_app/immutable/nodes/0.Ct6FAss_.js +1 -0
  18. package/dist/_app/immutable/nodes/1.DLoKuy8Q.js +1 -0
  19. package/dist/_app/immutable/nodes/2.IRkwSmiB.js +1 -0
  20. package/dist/_app/immutable/nodes/3.BrTg-ZHv.js +1 -0
  21. package/dist/_app/immutable/nodes/4.Blq-4WQS.js +9 -0
  22. package/dist/_app/version.json +1 -0
  23. package/dist/cli/commands/crawl.js +128 -0
  24. package/dist/cli/commands/ui.js +2769 -0
  25. package/dist/cli/commands/version.js +30 -0
  26. package/dist/cli/server/index.js +2713 -0
  27. package/dist/cli/server/server.js +2702 -0
  28. package/dist/cli/server/serverState.js +1199 -0
  29. package/dist/cli/server/spreadsheetRoutes.js +788 -0
  30. package/dist/cli/server/types.js +0 -0
  31. package/dist/cli/server/websocketServer.js +2625 -0
  32. package/dist/cli/utils/debug.js +24 -0
  33. package/dist/favicon.svg +1 -0
  34. package/dist/index.html +38 -0
  35. package/dist/index.js +2924 -37
  36. package/dist/lula.png +0 -0
  37. package/dist/lula2 +2 -0
  38. package/package.json +120 -72
  39. package/src/app.css +192 -0
  40. package/src/app.d.ts +13 -0
  41. package/src/app.html +13 -0
  42. package/src/lib/actions/fadeWhenScrollable.ts +39 -0
  43. package/src/lib/actions/modal.ts +230 -0
  44. package/src/lib/actions/tooltip.ts +82 -0
  45. package/src/lib/components/control-sets/ControlSetInfo.svelte +20 -0
  46. package/src/lib/components/control-sets/ControlSetSelector.svelte +46 -0
  47. package/src/lib/components/control-sets/index.ts +5 -0
  48. package/src/lib/components/controls/ControlDetailsPanel.svelte +235 -0
  49. package/src/lib/components/controls/ControlsList.svelte +608 -0
  50. package/src/lib/components/controls/DynamicControlEditor.svelte +298 -0
  51. package/src/lib/components/controls/MappingCard.svelte +105 -0
  52. package/src/lib/components/controls/MappingForm.svelte +188 -0
  53. package/src/lib/components/controls/index.ts +9 -0
  54. package/src/lib/components/controls/renderers/EditableFieldRenderer.svelte +103 -0
  55. package/src/lib/components/controls/renderers/FieldRenderer.svelte +49 -0
  56. package/src/lib/components/controls/renderers/index.ts +5 -0
  57. package/src/lib/components/controls/tabs/CustomFieldsTab.svelte +130 -0
  58. package/src/lib/components/controls/tabs/ImplementationTab.svelte +127 -0
  59. package/src/lib/components/controls/tabs/MappingsTab.svelte +182 -0
  60. package/src/lib/components/controls/tabs/OverviewTab.svelte +151 -0
  61. package/src/lib/components/controls/tabs/TimelineTab.svelte +41 -0
  62. package/src/lib/components/controls/tabs/index.ts +8 -0
  63. package/src/lib/components/controls/utils/ProcessedTextRenderer.svelte +63 -0
  64. package/src/lib/components/controls/utils/textProcessor.ts +164 -0
  65. package/src/lib/components/forms/DynamicControlForm.svelte +340 -0
  66. package/src/lib/components/forms/DynamicField.svelte +494 -0
  67. package/src/lib/components/forms/FormField.svelte +107 -0
  68. package/src/lib/components/forms/index.ts +6 -0
  69. package/src/lib/components/setup/ExistingControlSets.svelte +284 -0
  70. package/src/lib/components/setup/SpreadsheetImport.svelte +968 -0
  71. package/src/lib/components/setup/index.ts +5 -0
  72. package/src/lib/components/ui/Dropdown.svelte +107 -0
  73. package/src/lib/components/ui/EmptyState.svelte +80 -0
  74. package/src/lib/components/ui/FeatureToggle.svelte +50 -0
  75. package/src/lib/components/ui/SearchBar.svelte +73 -0
  76. package/src/lib/components/ui/StatusBadge.svelte +79 -0
  77. package/src/lib/components/ui/TabNavigation.svelte +48 -0
  78. package/src/lib/components/ui/Tooltip.svelte +120 -0
  79. package/src/lib/components/ui/index.ts +10 -0
  80. package/src/lib/components/version-control/DiffViewer.svelte +292 -0
  81. package/src/lib/components/version-control/TimelineItem.svelte +107 -0
  82. package/src/lib/components/version-control/YamlDiffViewer.svelte +428 -0
  83. package/src/lib/components/version-control/index.ts +6 -0
  84. package/src/lib/form-types.ts +57 -0
  85. package/src/lib/formatUtils.ts +17 -0
  86. package/src/lib/index.ts +5 -0
  87. package/src/lib/types.ts +180 -0
  88. package/src/lib/websocket.ts +359 -0
  89. package/src/routes/+layout.svelte +236 -0
  90. package/src/routes/+page.svelte +38 -0
  91. package/src/routes/control/[id]/+page.svelte +112 -0
  92. package/src/routes/setup/+page.svelte +241 -0
  93. package/src/stores/compliance.ts +95 -0
  94. package/src/styles/highlightjs.css +20 -0
  95. package/src/styles/modal.css +58 -0
  96. package/src/styles/tables.css +111 -0
  97. package/src/styles/tooltip.css +65 -0
  98. package/dist/controls/index.d.ts +0 -18
  99. package/dist/controls/index.d.ts.map +0 -1
  100. package/dist/controls/index.js +0 -18
  101. package/dist/crawl.d.ts +0 -62
  102. package/dist/crawl.d.ts.map +0 -1
  103. package/dist/crawl.js +0 -172
  104. package/dist/index.d.ts +0 -8
  105. package/dist/index.d.ts.map +0 -1
  106. package/src/controls/index.ts +0 -19
  107. package/src/crawl.ts +0 -227
  108. package/src/index.ts +0 -46
@@ -0,0 +1,494 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ <!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
3
+
4
+ <script lang="ts">
5
+ import type { FieldDefinition } from '$lib/form-types';
6
+
7
+ interface Props {
8
+ field: FieldDefinition;
9
+ value: any;
10
+ readonly?: boolean;
11
+ error?: string;
12
+ onChange?: () => void;
13
+ }
14
+
15
+ let { field, value = $bindable(), readonly = false, error, onChange }: Props = $props();
16
+
17
+ const baseInputClass =
18
+ 'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-blue-500';
19
+ const readonlyClass = readonly ? 'bg-gray-50 dark:bg-gray-900 cursor-not-allowed' : '';
20
+ const errorClass = error ? 'border-red-500 focus:ring-red-500' : '';
21
+
22
+ function handleChange() {
23
+ onChange?.();
24
+ }
25
+
26
+ // Convert value to appropriate type for boolean fields
27
+ let booleanValue = $derived.by(() => {
28
+ if (field.type === 'boolean') {
29
+ if (typeof value === 'boolean') return value;
30
+ if (typeof value === 'string') {
31
+ return value.toLowerCase() === 'true' || value === '1' || value === 'yes';
32
+ }
33
+ return Boolean(value);
34
+ }
35
+ return value;
36
+ });
37
+
38
+ function handleBooleanChange(newValue: boolean) {
39
+ value = newValue;
40
+ handleChange();
41
+ }
42
+
43
+ // Array handling functions
44
+ function ensureArray() {
45
+ if (!Array.isArray(value)) {
46
+ value = [];
47
+ }
48
+ return value;
49
+ }
50
+
51
+ function addStringItem() {
52
+ const arr = ensureArray();
53
+ arr.push('');
54
+ value = [...arr];
55
+ handleChange();
56
+ }
57
+
58
+ function removeStringItem(index: number) {
59
+ const arr = ensureArray();
60
+ arr.splice(index, 1);
61
+ value = [...arr];
62
+ handleChange();
63
+ }
64
+
65
+ function updateStringItem(index: number, newValue: string) {
66
+ const arr = ensureArray();
67
+ arr[index] = newValue;
68
+ value = [...arr];
69
+ handleChange();
70
+ }
71
+
72
+ function addObjectItem() {
73
+ const arr = ensureArray();
74
+ const newItem: any = {};
75
+
76
+ // Initialize with default values based on schema
77
+ if (field.arraySchema) {
78
+ Object.entries(field.arraySchema).forEach(([key, schema]: [string, any]) => {
79
+ if (schema.type === 'string-array') {
80
+ newItem[key] = [];
81
+ } else {
82
+ newItem[key] = '';
83
+ }
84
+ });
85
+ }
86
+
87
+ arr.push(newItem);
88
+ value = [...arr];
89
+ handleChange();
90
+ }
91
+
92
+ function removeObjectItem(index: number) {
93
+ const arr = ensureArray();
94
+ arr.splice(index, 1);
95
+ value = [...arr];
96
+ handleChange();
97
+ }
98
+
99
+ function updateObjectItem(index: number, key: string, newValue: any) {
100
+ const arr = ensureArray();
101
+ if (!arr[index]) arr[index] = {};
102
+ arr[index][key] = newValue;
103
+ value = [...arr];
104
+ handleChange();
105
+ }
106
+ </script>
107
+
108
+ <div class="space-y-1">
109
+ <label for={field.id} class="block text-sm font-medium text-gray-700 dark:text-gray-300">
110
+ {field.label}
111
+ {#if field.required}
112
+ <span class="text-red-500 ml-1">*</span>
113
+ {/if}
114
+ </label>
115
+
116
+ {#if field.description}
117
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
118
+ {field.description}
119
+ </p>
120
+ {/if}
121
+
122
+ {#if field.type === 'textarea'}
123
+ <textarea
124
+ id={field.id}
125
+ bind:value
126
+ rows={field.rows || 4}
127
+ placeholder={field.placeholder}
128
+ class="{baseInputClass} {readonlyClass} {errorClass} resize-vertical"
129
+ {readonly}
130
+ onchange={handleChange}
131
+ ></textarea>
132
+ {:else if field.type === 'select' && field.options?.length}
133
+ <select
134
+ id={field.id}
135
+ bind:value
136
+ class="{baseInputClass} {readonlyClass} {errorClass}"
137
+ disabled={readonly}
138
+ onchange={handleChange}
139
+ >
140
+ {#if !field.required}
141
+ <option value="">-- Select an option --</option>
142
+ {/if}
143
+ {#each field.options as option}
144
+ <option value={option}>{option}</option>
145
+ {/each}
146
+ </select>
147
+ {:else if field.type === 'boolean'}
148
+ <div class="flex items-center space-x-3">
149
+ <label class="flex items-center">
150
+ <input
151
+ id="{field.id}-true"
152
+ type="radio"
153
+ name={field.id}
154
+ checked={booleanValue === true}
155
+ disabled={readonly}
156
+ onchange={() => handleBooleanChange(true)}
157
+ class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 dark:border-gray-600"
158
+ />
159
+ <span class="ml-2 text-sm text-gray-900 dark:text-white">Yes</span>
160
+ </label>
161
+ <label class="flex items-center">
162
+ <input
163
+ id="{field.id}-false"
164
+ type="radio"
165
+ name={field.id}
166
+ checked={booleanValue === false}
167
+ disabled={readonly}
168
+ onchange={() => handleBooleanChange(false)}
169
+ class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 dark:border-gray-600"
170
+ />
171
+ <span class="ml-2 text-sm text-gray-900 dark:text-white">No</span>
172
+ </label>
173
+ </div>
174
+ {:else if field.type === 'date'}
175
+ <input
176
+ id={field.id}
177
+ bind:value
178
+ type="date"
179
+ placeholder={field.placeholder}
180
+ class="{baseInputClass} {readonlyClass} {errorClass}"
181
+ {readonly}
182
+ onchange={handleChange}
183
+ />
184
+ {:else if field.type === 'string-array'}
185
+ <div class="space-y-3">
186
+ {#if ensureArray().length === 0 && !readonly}
187
+ <!-- Empty state with Flowbite styling -->
188
+ <div
189
+ class="flex flex-col items-center justify-center py-8 px-4 bg-gray-50 dark:bg-gray-800 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg"
190
+ >
191
+ <svg
192
+ class="w-8 h-8 text-gray-400 dark:text-gray-500 mb-4"
193
+ fill="none"
194
+ stroke="currentColor"
195
+ viewBox="0 0 24 24"
196
+ >
197
+ <path
198
+ stroke-linecap="round"
199
+ stroke-linejoin="round"
200
+ stroke-width="2"
201
+ d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
202
+ ></path>
203
+ </svg>
204
+ <p class="text-sm text-gray-500 dark:text-gray-400 mb-3">
205
+ No {field.label.toLowerCase()} added yet
206
+ </p>
207
+ <button
208
+ type="button"
209
+ onclick={addStringItem}
210
+ class="inline-flex items-center px-4 py-2 text-sm font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-300 dark:bg-blue-900 dark:text-blue-400 dark:border-blue-800 dark:hover:bg-blue-800 dark:hover:text-blue-300"
211
+ >
212
+ <svg class="w-4 h-4 me-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
213
+ <path
214
+ stroke-linecap="round"
215
+ stroke-linejoin="round"
216
+ stroke-width="2"
217
+ d="M12 6v6m0 0v6m0-6h6m-6 0H6"
218
+ />
219
+ </svg>
220
+ Add {field.label.replace(/s$/, '')}
221
+ </button>
222
+ </div>
223
+ {:else}
224
+ {#each ensureArray() as item, index}
225
+ <div
226
+ class="group relative p-4 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm hover:shadow-md transition-shadow"
227
+ >
228
+ <!-- Item header with drag handle and remove button -->
229
+ <div class="flex items-center justify-between mb-3">
230
+ <div class="flex items-center space-x-3">
231
+ <!-- Drag handle -->
232
+ <div class="cursor-move text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
233
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
234
+ <path
235
+ stroke-linecap="round"
236
+ stroke-linejoin="round"
237
+ stroke-width="2"
238
+ d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"
239
+ ></path>
240
+ </svg>
241
+ </div>
242
+ <span class="text-sm font-medium text-gray-700 dark:text-gray-300">
243
+ {field.label.replace(/s$/, '')}
244
+ {index + 1}
245
+ </span>
246
+ </div>
247
+ {#if !readonly}
248
+ <button
249
+ type="button"
250
+ onclick={() => removeStringItem(index)}
251
+ class="opacity-0 group-hover:opacity-100 transition-opacity p-1.5 text-red-500 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-md"
252
+ title="Remove item"
253
+ aria-label="Remove {field.label.replace(/s$/, '')} {index + 1}"
254
+ >
255
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
256
+ <path
257
+ stroke-linecap="round"
258
+ stroke-linejoin="round"
259
+ stroke-width="2"
260
+ d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
261
+ ></path>
262
+ </svg>
263
+ </button>
264
+ {/if}
265
+ </div>
266
+
267
+ <!-- Input field - use textarea for longer fields like objectives -->
268
+ {#if field.id === 'objectives' || field.id === 'guidance' || field.id === 'references' || (field.placeholder && field.placeholder.length > 50)}
269
+ <textarea
270
+ value={item}
271
+ placeholder={field.placeholder ||
272
+ `Enter ${field.label.replace(/s$/, '').toLowerCase()}`}
273
+ rows="3"
274
+ class="{baseInputClass} {readonlyClass} {errorClass} resize-none"
275
+ {readonly}
276
+ onchange={(e) => updateStringItem(index, e.currentTarget.value)}
277
+ ></textarea>
278
+ {:else}
279
+ <input
280
+ type="text"
281
+ value={item}
282
+ placeholder={field.placeholder ||
283
+ `Enter ${field.label.replace(/s$/, '').toLowerCase()}`}
284
+ class="{baseInputClass} {readonlyClass} {errorClass}"
285
+ {readonly}
286
+ onchange={(e) => updateStringItem(index, e.currentTarget.value)}
287
+ />
288
+ {/if}
289
+ </div>
290
+ {/each}
291
+
292
+ {#if !readonly}
293
+ <!-- Add button -->
294
+ <button
295
+ type="button"
296
+ onclick={addStringItem}
297
+ class="w-full flex items-center justify-center px-4 py-3 text-sm font-medium text-blue-600 bg-blue-50 border-2 border-dashed border-blue-300 rounded-lg hover:bg-blue-100 hover:border-blue-400 focus:z-10 focus:ring-2 focus:ring-blue-300 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-blue-900/50 dark:hover:border-blue-500 transition-all"
298
+ >
299
+ <svg class="w-5 h-5 me-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
300
+ <path
301
+ stroke-linecap="round"
302
+ stroke-linejoin="round"
303
+ stroke-width="2"
304
+ d="M12 6v6m0 0v6m0-6h6m-6 0H6"
305
+ />
306
+ </svg>
307
+ Add {field.label.replace(/s$/, '')}
308
+ </button>
309
+ {/if}
310
+ {/if}
311
+ </div>
312
+ {:else if field.type === 'object-array'}
313
+ <div class="space-y-4">
314
+ {#if ensureArray().length === 0 && !readonly}
315
+ <!-- Empty state for object arrays -->
316
+ <div
317
+ class="flex flex-col items-center justify-center py-10 px-4 bg-gray-50 dark:bg-gray-800 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg"
318
+ >
319
+ <svg
320
+ class="w-10 h-10 text-gray-400 dark:text-gray-500 mb-4"
321
+ fill="none"
322
+ stroke="currentColor"
323
+ viewBox="0 0 24 24"
324
+ >
325
+ <path
326
+ stroke-linecap="round"
327
+ stroke-linejoin="round"
328
+ stroke-width="2"
329
+ d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
330
+ ></path>
331
+ </svg>
332
+ <p class="text-sm text-gray-500 dark:text-gray-400 mb-4 text-center">
333
+ No {field.label.toLowerCase()} created yet
334
+ </p>
335
+ <button
336
+ type="button"
337
+ onclick={addObjectItem}
338
+ class="inline-flex items-center px-5 py-2.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:ring-2 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 transition-colors"
339
+ >
340
+ <svg class="w-4 h-4 me-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
341
+ <path
342
+ stroke-linecap="round"
343
+ stroke-linejoin="round"
344
+ stroke-width="2"
345
+ d="M12 6v6m0 0v6m0-6h6m-6 0H6"
346
+ />
347
+ </svg>
348
+ Create {field.label.replace(/s$/, '')}
349
+ </button>
350
+ </div>
351
+ {:else}
352
+ {#each ensureArray() as item, index}
353
+ <div
354
+ class="group relative bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm hover:shadow-md transition-all duration-200"
355
+ >
356
+ <!-- Card header -->
357
+ <div
358
+ class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 rounded-t-xl"
359
+ >
360
+ <div class="flex items-center space-x-3">
361
+ <!-- Drag handle -->
362
+ <div class="cursor-move text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
363
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
364
+ <path
365
+ stroke-linecap="round"
366
+ stroke-linejoin="round"
367
+ stroke-width="2"
368
+ d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"
369
+ ></path>
370
+ </svg>
371
+ </div>
372
+ <div class="flex items-center space-x-2">
373
+ <div
374
+ class="flex-shrink-0 w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center"
375
+ >
376
+ <span class="text-sm font-semibold text-blue-600 dark:text-blue-400"
377
+ >{index + 1}</span
378
+ >
379
+ </div>
380
+ <h4 class="text-sm font-semibold text-gray-900 dark:text-white">
381
+ {field.label.replace(/s$/, '')}
382
+ {index + 1}
383
+ </h4>
384
+ </div>
385
+ </div>
386
+ {#if !readonly}
387
+ <div class="flex items-center space-x-2">
388
+ <button
389
+ type="button"
390
+ onclick={() => removeObjectItem(index)}
391
+ class="opacity-0 group-hover:opacity-100 transition-opacity p-2 text-red-500 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg"
392
+ aria-label="Remove {field.label.replace(/s$/, '')} {index + 1}"
393
+ title="Remove item"
394
+ >
395
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
396
+ <path
397
+ stroke-linecap="round"
398
+ stroke-linejoin="round"
399
+ stroke-width="2"
400
+ d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
401
+ ></path>
402
+ </svg>
403
+ </button>
404
+ </div>
405
+ {/if}
406
+ </div>
407
+
408
+ <!-- Card content -->
409
+ <div class="p-4">
410
+ {#if field.arraySchema}
411
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
412
+ {#each Object.entries(field.arraySchema) as [key, schemaObj]}
413
+ {@const schema = schemaObj as any}
414
+ <div class="space-y-2">
415
+ <label
416
+ for="{field.id}-{index}-{key}"
417
+ class="block text-sm font-medium text-gray-700 dark:text-gray-300"
418
+ >
419
+ {schema.label || key.charAt(0).toUpperCase() + key.slice(1)}
420
+ {#if schema.required}<span class="text-red-500 ml-1">*</span>{/if}
421
+ </label>
422
+ {#if schema.type === 'textarea'}
423
+ <textarea
424
+ id="{field.id}-{index}-{key}"
425
+ value={item[key] || ''}
426
+ placeholder={schema.placeholder || `Enter ${schema.label || key}`}
427
+ rows={schema.rows || 3}
428
+ class="{baseInputClass} {readonlyClass} text-sm resize-none"
429
+ {readonly}
430
+ onchange={(e) => updateObjectItem(index, key, e.currentTarget.value)}
431
+ ></textarea>
432
+ {:else}
433
+ <input
434
+ id="{field.id}-{index}-{key}"
435
+ type="text"
436
+ value={item[key] || ''}
437
+ placeholder={schema.placeholder || `Enter ${schema.label || key}`}
438
+ class="{baseInputClass} {readonlyClass} text-sm"
439
+ {readonly}
440
+ onchange={(e) => updateObjectItem(index, key, e.currentTarget.value)}
441
+ />
442
+ {/if}
443
+ </div>
444
+ {/each}
445
+ </div>
446
+ {/if}
447
+ </div>
448
+ </div>
449
+ {/each}
450
+
451
+ {#if !readonly}
452
+ <!-- Enhanced add button -->
453
+ <button
454
+ type="button"
455
+ onclick={addObjectItem}
456
+ class="group w-full flex items-center justify-center px-4 py-4 text-sm font-medium text-blue-600 bg-blue-50 border-2 border-dashed border-blue-300 rounded-xl hover:bg-blue-100 hover:border-blue-400 focus:z-10 focus:ring-2 focus:ring-blue-300 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-600 dark:hover:bg-blue-900/50 dark:hover:border-blue-500 transition-all duration-200"
457
+ >
458
+ <div class="flex items-center space-x-3">
459
+ <div
460
+ class="p-2 bg-blue-100 dark:bg-blue-800 rounded-full group-hover:bg-blue-200 dark:group-hover:bg-blue-700 transition-colors"
461
+ >
462
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
463
+ <path
464
+ stroke-linecap="round"
465
+ stroke-linejoin="round"
466
+ stroke-width="2"
467
+ d="M12 6v6m0 0v6m0-6h6m-6 0H6"
468
+ />
469
+ </svg>
470
+ </div>
471
+ <span>Add {field.label.replace(/s$/, '')}</span>
472
+ </div>
473
+ </button>
474
+ {/if}
475
+ {/if}
476
+ </div>
477
+ {:else}
478
+ <input
479
+ id={field.id}
480
+ bind:value
481
+ type="text"
482
+ placeholder={field.placeholder}
483
+ class="{baseInputClass} {readonlyClass} {errorClass}"
484
+ {readonly}
485
+ onchange={handleChange}
486
+ />
487
+ {/if}
488
+
489
+ {#if error}
490
+ <p class="text-sm text-red-600 dark:text-red-400">
491
+ {error}
492
+ </p>
493
+ {/if}
494
+ </div>
@@ -0,0 +1,107 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ <!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
3
+
4
+ <script lang="ts">
5
+ interface Props {
6
+ id: string;
7
+ label: string;
8
+ type?: 'text' | 'textarea' | 'select';
9
+ value: string;
10
+ options?: string[];
11
+ rows?: number;
12
+ placeholder?: string;
13
+ required?: boolean;
14
+ error?: string;
15
+ onChange?: () => void;
16
+ }
17
+
18
+ let {
19
+ id,
20
+ label,
21
+ type = 'text',
22
+ value = $bindable(),
23
+ options = [],
24
+ rows = 4,
25
+ placeholder,
26
+ required = false,
27
+ error,
28
+ onChange
29
+ }: Props = $props();
30
+
31
+ const baseInputClass =
32
+ 'w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors';
33
+ const errorClass = error ? 'border-red-400 focus:ring-red-500 focus:border-red-500' : '';
34
+ const successClass = '';
35
+ </script>
36
+
37
+ <div class="space-y-2">
38
+ <label for={id} class="block text-sm font-medium text-gray-700 dark:text-gray-300">
39
+ {label}{#if required}<span class="text-red-500 ml-1">*</span>{/if}
40
+ </label>
41
+
42
+ {#if type === 'textarea'}
43
+ <div class="relative">
44
+ <textarea
45
+ {id}
46
+ bind:value
47
+ {rows}
48
+ {placeholder}
49
+ class="{baseInputClass} {errorClass} {successClass} resize-vertical min-h-[100px]"
50
+ onchange={onChange}
51
+ ></textarea>
52
+ {#if value}
53
+ <div class="absolute bottom-3 right-3 text-xs text-gray-400">
54
+ {value.length} characters
55
+ </div>
56
+ {/if}
57
+ </div>
58
+ {:else if type === 'select' && options.length > 0}
59
+ <div class="relative">
60
+ <select
61
+ {id}
62
+ bind:value
63
+ class="{baseInputClass} {errorClass} {successClass} cursor-pointer"
64
+ onchange={onChange}
65
+ >
66
+ {#each options as option}
67
+ <option value={option}>{option}</option>
68
+ {/each}
69
+ </select>
70
+ <div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
71
+ <svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
72
+ <path
73
+ stroke-linecap="round"
74
+ stroke-linejoin="round"
75
+ stroke-width="2"
76
+ d="M8 9l4-4 4 4m0 6l-4 4-4-4"
77
+ ></path>
78
+ </svg>
79
+ </div>
80
+ </div>
81
+ {:else}
82
+ <input
83
+ {id}
84
+ bind:value
85
+ type="text"
86
+ {placeholder}
87
+ class="{baseInputClass} {errorClass} {successClass}"
88
+ onchange={onChange}
89
+ />
90
+ {/if}
91
+
92
+ {#if error}
93
+ <div class="flex items-center space-x-1">
94
+ <svg class="w-4 h-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
95
+ <path
96
+ stroke-linecap="round"
97
+ stroke-linejoin="round"
98
+ stroke-width="2"
99
+ d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
100
+ ></path>
101
+ </svg>
102
+ <p class="text-sm text-red-600 dark:text-red-400">
103
+ {error}
104
+ </p>
105
+ </div>
106
+ {/if}
107
+ </div>
@@ -0,0 +1,6 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Lula Authors
3
+
4
+ export { default as FormField } from './FormField.svelte';
5
+ export { default as DynamicField } from './DynamicField.svelte';
6
+ export { default as DynamicControlForm } from './DynamicControlForm.svelte';