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.
- package/README.md +291 -8
- package/dist/_app/env.js +1 -0
- package/dist/_app/immutable/assets/0.DtiRW3lO.css +1 -0
- package/dist/_app/immutable/assets/DynamicControlEditor.BkVTzFZ-.css +1 -0
- package/dist/_app/immutable/chunks/7x_q-1ab.js +1 -0
- package/dist/_app/immutable/chunks/B19gt6-g.js +2 -0
- package/dist/_app/immutable/chunks/BR-0Dorr.js +1 -0
- package/dist/_app/immutable/chunks/B_3ksxz5.js +2 -0
- package/dist/_app/immutable/chunks/Bg_R1qWi.js +3 -0
- package/dist/_app/immutable/chunks/D3aNP_lg.js +1 -0
- package/dist/_app/immutable/chunks/D4Q_ObIy.js +1 -0
- package/dist/_app/immutable/chunks/DsnmJJEf.js +1 -0
- package/dist/_app/immutable/chunks/XY2j_owG.js +66 -0
- package/dist/_app/immutable/chunks/rzN25oDf.js +1 -0
- package/dist/_app/immutable/entry/app.r0uOd9qg.js +2 -0
- package/dist/_app/immutable/entry/start.DvoqR0rc.js +1 -0
- package/dist/_app/immutable/nodes/0.Ct6FAss_.js +1 -0
- package/dist/_app/immutable/nodes/1.DLoKuy8Q.js +1 -0
- package/dist/_app/immutable/nodes/2.IRkwSmiB.js +1 -0
- package/dist/_app/immutable/nodes/3.BrTg-ZHv.js +1 -0
- package/dist/_app/immutable/nodes/4.Blq-4WQS.js +9 -0
- package/dist/_app/version.json +1 -0
- package/dist/cli/commands/crawl.js +128 -0
- package/dist/cli/commands/ui.js +2769 -0
- package/dist/cli/commands/version.js +30 -0
- package/dist/cli/server/index.js +2713 -0
- package/dist/cli/server/server.js +2702 -0
- package/dist/cli/server/serverState.js +1199 -0
- package/dist/cli/server/spreadsheetRoutes.js +788 -0
- package/dist/cli/server/types.js +0 -0
- package/dist/cli/server/websocketServer.js +2625 -0
- package/dist/cli/utils/debug.js +24 -0
- package/dist/favicon.svg +1 -0
- package/dist/index.html +38 -0
- package/dist/index.js +2924 -37
- package/dist/lula.png +0 -0
- package/dist/lula2 +2 -0
- package/package.json +120 -72
- package/src/app.css +192 -0
- package/src/app.d.ts +13 -0
- package/src/app.html +13 -0
- package/src/lib/actions/fadeWhenScrollable.ts +39 -0
- package/src/lib/actions/modal.ts +230 -0
- package/src/lib/actions/tooltip.ts +82 -0
- package/src/lib/components/control-sets/ControlSetInfo.svelte +20 -0
- package/src/lib/components/control-sets/ControlSetSelector.svelte +46 -0
- package/src/lib/components/control-sets/index.ts +5 -0
- package/src/lib/components/controls/ControlDetailsPanel.svelte +235 -0
- package/src/lib/components/controls/ControlsList.svelte +608 -0
- package/src/lib/components/controls/DynamicControlEditor.svelte +298 -0
- package/src/lib/components/controls/MappingCard.svelte +105 -0
- package/src/lib/components/controls/MappingForm.svelte +188 -0
- package/src/lib/components/controls/index.ts +9 -0
- package/src/lib/components/controls/renderers/EditableFieldRenderer.svelte +103 -0
- package/src/lib/components/controls/renderers/FieldRenderer.svelte +49 -0
- package/src/lib/components/controls/renderers/index.ts +5 -0
- package/src/lib/components/controls/tabs/CustomFieldsTab.svelte +130 -0
- package/src/lib/components/controls/tabs/ImplementationTab.svelte +127 -0
- package/src/lib/components/controls/tabs/MappingsTab.svelte +182 -0
- package/src/lib/components/controls/tabs/OverviewTab.svelte +151 -0
- package/src/lib/components/controls/tabs/TimelineTab.svelte +41 -0
- package/src/lib/components/controls/tabs/index.ts +8 -0
- package/src/lib/components/controls/utils/ProcessedTextRenderer.svelte +63 -0
- package/src/lib/components/controls/utils/textProcessor.ts +164 -0
- package/src/lib/components/forms/DynamicControlForm.svelte +340 -0
- package/src/lib/components/forms/DynamicField.svelte +494 -0
- package/src/lib/components/forms/FormField.svelte +107 -0
- package/src/lib/components/forms/index.ts +6 -0
- package/src/lib/components/setup/ExistingControlSets.svelte +284 -0
- package/src/lib/components/setup/SpreadsheetImport.svelte +968 -0
- package/src/lib/components/setup/index.ts +5 -0
- package/src/lib/components/ui/Dropdown.svelte +107 -0
- package/src/lib/components/ui/EmptyState.svelte +80 -0
- package/src/lib/components/ui/FeatureToggle.svelte +50 -0
- package/src/lib/components/ui/SearchBar.svelte +73 -0
- package/src/lib/components/ui/StatusBadge.svelte +79 -0
- package/src/lib/components/ui/TabNavigation.svelte +48 -0
- package/src/lib/components/ui/Tooltip.svelte +120 -0
- package/src/lib/components/ui/index.ts +10 -0
- package/src/lib/components/version-control/DiffViewer.svelte +292 -0
- package/src/lib/components/version-control/TimelineItem.svelte +107 -0
- package/src/lib/components/version-control/YamlDiffViewer.svelte +428 -0
- package/src/lib/components/version-control/index.ts +6 -0
- package/src/lib/form-types.ts +57 -0
- package/src/lib/formatUtils.ts +17 -0
- package/src/lib/index.ts +5 -0
- package/src/lib/types.ts +180 -0
- package/src/lib/websocket.ts +359 -0
- package/src/routes/+layout.svelte +236 -0
- package/src/routes/+page.svelte +38 -0
- package/src/routes/control/[id]/+page.svelte +112 -0
- package/src/routes/setup/+page.svelte +241 -0
- package/src/stores/compliance.ts +95 -0
- package/src/styles/highlightjs.css +20 -0
- package/src/styles/modal.css +58 -0
- package/src/styles/tables.css +111 -0
- package/src/styles/tooltip.css +65 -0
- package/dist/controls/index.d.ts +0 -18
- package/dist/controls/index.d.ts.map +0 -1
- package/dist/controls/index.js +0 -18
- package/dist/crawl.d.ts +0 -62
- package/dist/crawl.d.ts.map +0 -1
- package/dist/crawl.js +0 -172
- package/dist/index.d.ts +0 -8
- package/dist/index.d.ts.map +0 -1
- package/src/controls/index.ts +0 -19
- package/src/crawl.ts +0 -227
- 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';
|