@htlkg/components 0.0.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 (79) hide show
  1. package/dist/composables/index.js +388 -0
  2. package/dist/composables/index.js.map +1 -0
  3. package/package.json +41 -0
  4. package/src/composables/index.ts +6 -0
  5. package/src/composables/useForm.test.ts +229 -0
  6. package/src/composables/useForm.ts +130 -0
  7. package/src/composables/useFormValidation.test.ts +189 -0
  8. package/src/composables/useFormValidation.ts +83 -0
  9. package/src/composables/useModal.property.test.ts +164 -0
  10. package/src/composables/useModal.ts +43 -0
  11. package/src/composables/useNotifications.test.ts +166 -0
  12. package/src/composables/useNotifications.ts +81 -0
  13. package/src/composables/useTable.property.test.ts +198 -0
  14. package/src/composables/useTable.ts +134 -0
  15. package/src/composables/useTabs.property.test.ts +247 -0
  16. package/src/composables/useTabs.ts +101 -0
  17. package/src/data/Chart.demo.vue +340 -0
  18. package/src/data/Chart.md +525 -0
  19. package/src/data/Chart.vue +133 -0
  20. package/src/data/DataList.md +80 -0
  21. package/src/data/DataList.test.ts +69 -0
  22. package/src/data/DataList.vue +46 -0
  23. package/src/data/SearchableSelect.md +107 -0
  24. package/src/data/SearchableSelect.vue +124 -0
  25. package/src/data/Table.demo.vue +296 -0
  26. package/src/data/Table.md +588 -0
  27. package/src/data/Table.property.test.ts +548 -0
  28. package/src/data/Table.test.ts +562 -0
  29. package/src/data/Table.unit.test.ts +544 -0
  30. package/src/data/Table.vue +321 -0
  31. package/src/data/index.ts +5 -0
  32. package/src/domain/BrandCard.md +81 -0
  33. package/src/domain/BrandCard.vue +63 -0
  34. package/src/domain/BrandSelector.md +84 -0
  35. package/src/domain/BrandSelector.vue +65 -0
  36. package/src/domain/ProductBadge.md +60 -0
  37. package/src/domain/ProductBadge.vue +47 -0
  38. package/src/domain/UserAvatar.md +84 -0
  39. package/src/domain/UserAvatar.vue +60 -0
  40. package/src/domain/domain-components.property.test.ts +449 -0
  41. package/src/domain/index.ts +4 -0
  42. package/src/forms/DateRange.demo.vue +273 -0
  43. package/src/forms/DateRange.md +337 -0
  44. package/src/forms/DateRange.vue +110 -0
  45. package/src/forms/JsonSchemaForm.demo.vue +549 -0
  46. package/src/forms/JsonSchemaForm.md +112 -0
  47. package/src/forms/JsonSchemaForm.property.test.ts +817 -0
  48. package/src/forms/JsonSchemaForm.test.ts +601 -0
  49. package/src/forms/JsonSchemaForm.unit.test.ts +801 -0
  50. package/src/forms/JsonSchemaForm.vue +615 -0
  51. package/src/forms/index.ts +3 -0
  52. package/src/index.ts +17 -0
  53. package/src/navigation/Breadcrumbs.demo.vue +142 -0
  54. package/src/navigation/Breadcrumbs.md +102 -0
  55. package/src/navigation/Breadcrumbs.test.ts +69 -0
  56. package/src/navigation/Breadcrumbs.vue +58 -0
  57. package/src/navigation/Stepper.demo.vue +337 -0
  58. package/src/navigation/Stepper.md +174 -0
  59. package/src/navigation/Stepper.vue +146 -0
  60. package/src/navigation/Tabs.demo.vue +293 -0
  61. package/src/navigation/Tabs.md +163 -0
  62. package/src/navigation/Tabs.test.ts +176 -0
  63. package/src/navigation/Tabs.vue +104 -0
  64. package/src/navigation/index.ts +5 -0
  65. package/src/overlays/Alert.demo.vue +377 -0
  66. package/src/overlays/Alert.md +248 -0
  67. package/src/overlays/Alert.test.ts +166 -0
  68. package/src/overlays/Alert.vue +70 -0
  69. package/src/overlays/Drawer.md +140 -0
  70. package/src/overlays/Drawer.test.ts +92 -0
  71. package/src/overlays/Drawer.vue +76 -0
  72. package/src/overlays/Modal.demo.vue +149 -0
  73. package/src/overlays/Modal.md +385 -0
  74. package/src/overlays/Modal.test.ts +128 -0
  75. package/src/overlays/Modal.vue +86 -0
  76. package/src/overlays/Notification.md +150 -0
  77. package/src/overlays/Notification.test.ts +96 -0
  78. package/src/overlays/Notification.vue +58 -0
  79. package/src/overlays/index.ts +4 -0
@@ -0,0 +1,615 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue';
3
+ import Ajv from 'ajv';
4
+ import addFormats from 'ajv-formats';
5
+ import {
6
+ uiInput,
7
+ uiSelect,
8
+ uiToggle,
9
+ uiRangeSlider,
10
+ uiTextArea,
11
+ uiButton
12
+ } from '@hotelinking/ui';
13
+
14
+ // Initialize AJV for JSON Schema validation with format support
15
+ const ajv = new Ajv({ allErrors: true });
16
+ addFormats(ajv); // Add support for format keywords like "email", "uri", "date", etc.
17
+
18
+ interface JsonSchema {
19
+ type: string;
20
+ properties?: Record<string, any>;
21
+ required?: string[];
22
+ title?: string;
23
+ description?: string;
24
+ }
25
+
26
+ interface Props {
27
+ schema: JsonSchema;
28
+ modelValue?: Record<string, any>;
29
+ uiSchema?: Record<string, any>;
30
+ loading?: boolean;
31
+ }
32
+
33
+ const props = withDefaults(defineProps<Props>(), {
34
+ modelValue: () => ({}),
35
+ uiSchema: () => ({}),
36
+ loading: false
37
+ });
38
+
39
+ const emit = defineEmits<{
40
+ 'update:modelValue': [value: Record<string, any>];
41
+ 'validation-error': [errors: Array<{ field: string; message: string }>];
42
+ 'submit': [value: Record<string, any>];
43
+ }>();
44
+
45
+ // Internal form data
46
+ const formData = computed({
47
+ get: () => props.modelValue,
48
+ set: (value: Record<string, any>) => {
49
+ emit('update:modelValue', value);
50
+ }
51
+ });
52
+
53
+ const errors = ref<Record<string, string>>({});
54
+ const touched = ref<Record<string, boolean>>({});
55
+
56
+ // Get widget type from UI schema or infer from field schema
57
+ function getWidget(fieldName: string): string {
58
+ const uiSchemaField = props.uiSchema[fieldName];
59
+ if (uiSchemaField?.['ui:widget']) {
60
+ return uiSchemaField['ui:widget'];
61
+ }
62
+
63
+ const fieldSchema = props.schema.properties?.[fieldName];
64
+ if (!fieldSchema) return 'text';
65
+
66
+ // Infer widget from schema
67
+ if (fieldSchema.type === 'boolean') return 'toggle';
68
+ if (fieldSchema.type === 'number' || fieldSchema.type === 'integer') {
69
+ // Only use slider if explicitly specified in uiSchema
70
+ // Don't auto-infer from min/max presence
71
+ return 'number';
72
+ }
73
+ if (fieldSchema.type === 'string') {
74
+ if (fieldSchema.enum) return 'select';
75
+ if (fieldSchema.format === 'email') return 'email';
76
+ if (fieldSchema.format === 'uri') return 'url';
77
+ if (fieldSchema.minLength && fieldSchema.minLength > 100) return 'textarea';
78
+ }
79
+ if (fieldSchema.type === 'array') return 'array';
80
+ if (fieldSchema.type === 'object') return 'object';
81
+
82
+ return 'text';
83
+ }
84
+
85
+ // Get input type for uiInput component
86
+ function getInputType(fieldName: string): 'text' | 'email' | 'password' | 'number' | 'date' | 'datetime-local' {
87
+ const widget = getWidget(fieldName);
88
+
89
+ if (widget === 'password') return 'password';
90
+ if (widget === 'email') return 'email';
91
+ if (widget === 'number') return 'number';
92
+ if (widget === 'date') return 'date';
93
+ if (widget === 'datetime-local') return 'datetime-local';
94
+
95
+ return 'text';
96
+ }
97
+
98
+ // Get field color based on validation state (computed for reactivity)
99
+ function getFieldColor(fieldName: string): 'gray' | 'green' | 'red' | 'yellow' {
100
+ // Check for errors first (most important)
101
+ if (errors.value[fieldName]) return 'red';
102
+
103
+ // If field has been touched and has a valid value, show green
104
+ if (touched.value[fieldName]) {
105
+ const value = formData.value[fieldName];
106
+ if (isRequired(fieldName) && value !== undefined && value !== null && value !== '') {
107
+ return 'green';
108
+ }
109
+ }
110
+
111
+ // Default to gray
112
+ return 'gray';
113
+ }
114
+
115
+ // Check if field is required
116
+ function isRequired(fieldName: string): boolean {
117
+ return props.schema.required?.includes(fieldName) ?? false;
118
+ }
119
+
120
+ // Get field label
121
+ function getFieldLabel(fieldName: string): string {
122
+ const fieldSchema = props.schema.properties?.[fieldName];
123
+ return fieldSchema?.title || fieldName;
124
+ }
125
+
126
+ // Get field description
127
+ function getFieldDescription(fieldName: string): string {
128
+ const fieldSchema = props.schema.properties?.[fieldName];
129
+ return fieldSchema?.description || '';
130
+ }
131
+
132
+ // Get placeholder from UI schema
133
+ function getPlaceholder(fieldName: string): string {
134
+ return props.uiSchema[fieldName]?.['ui:placeholder'] || '';
135
+ }
136
+
137
+ // Get autocomplete attribute for better UX (especially for passwords)
138
+ // Following https://www.chromium.org/developers/design-documents/create-amazing-password-forms/
139
+ function getAutocomplete(fieldName: string): string | undefined {
140
+ const widget = getWidget(fieldName);
141
+ const fieldSchema = props.schema.properties?.[fieldName];
142
+
143
+ // Check if explicitly set in uiSchema
144
+ if (props.uiSchema[fieldName]?.['ui:autocomplete']) {
145
+ return props.uiSchema[fieldName]['ui:autocomplete'];
146
+ }
147
+
148
+ // Auto-detect based on field name and type
149
+ const lowerFieldName = fieldName.toLowerCase();
150
+
151
+ if (widget === 'email' || fieldSchema?.format === 'email') {
152
+ return 'email';
153
+ }
154
+
155
+ if (widget === 'password') {
156
+ // Check for common password field patterns
157
+ if (lowerFieldName.includes('new') || lowerFieldName.includes('confirm')) {
158
+ return 'new-password';
159
+ }
160
+ if (lowerFieldName.includes('current') || lowerFieldName === 'password') {
161
+ return 'current-password';
162
+ }
163
+ return 'current-password'; // Default for password fields
164
+ }
165
+
166
+ // Common autocomplete values
167
+ if (lowerFieldName.includes('username') || lowerFieldName === 'user') {
168
+ return 'username';
169
+ }
170
+ if (lowerFieldName.includes('name')) {
171
+ if (lowerFieldName.includes('first')) return 'given-name';
172
+ if (lowerFieldName.includes('last')) return 'family-name';
173
+ return 'name';
174
+ }
175
+ if (lowerFieldName.includes('phone') || lowerFieldName.includes('tel')) {
176
+ return 'tel';
177
+ }
178
+ if (lowerFieldName.includes('address')) {
179
+ return 'street-address';
180
+ }
181
+ if (lowerFieldName.includes('city')) {
182
+ return 'address-level2';
183
+ }
184
+ if (lowerFieldName.includes('country')) {
185
+ return 'country-name';
186
+ }
187
+ if (lowerFieldName.includes('postal') || lowerFieldName.includes('zip')) {
188
+ return 'postal-code';
189
+ }
190
+
191
+ return undefined;
192
+ }
193
+
194
+ // Validate form data against schema using AJV
195
+ function validate(): Array<{ field: string; message: string }> {
196
+ const validationErrors: Array<{ field: string; message: string }> = [];
197
+ errors.value = {};
198
+
199
+ // Compile and validate using AJV
200
+ const validateFn = ajv.compile(props.schema);
201
+ const valid = validateFn(formData.value);
202
+
203
+ if (!valid && validateFn.errors) {
204
+ for (const error of validateFn.errors) {
205
+ // Extract field name from instancePath (e.g., "/fieldName" -> "fieldName")
206
+ const field = error.instancePath.replace(/^\//, '') || error.params.missingProperty || 'form';
207
+
208
+ // Generate user-friendly error message
209
+ let message = '';
210
+
211
+ if (error.keyword === 'required') {
212
+ const missingField = error.params.missingProperty;
213
+ message = `${getFieldLabel(missingField)} is required`;
214
+ errors.value[missingField] = message;
215
+ validationErrors.push({ field: missingField, message });
216
+ } else if (error.keyword === 'type') {
217
+ message = `${getFieldLabel(field)} must be a ${error.params.type}`;
218
+ errors.value[field] = message;
219
+ validationErrors.push({ field, message });
220
+ } else if (error.keyword === 'minimum') {
221
+ message = `${getFieldLabel(field)} must be at least ${error.params.limit}`;
222
+ errors.value[field] = message;
223
+ validationErrors.push({ field, message });
224
+ } else if (error.keyword === 'maximum') {
225
+ message = `${getFieldLabel(field)} must be at most ${error.params.limit}`;
226
+ errors.value[field] = message;
227
+ validationErrors.push({ field, message });
228
+ } else if (error.keyword === 'minLength') {
229
+ message = `${getFieldLabel(field)} must be at least ${error.params.limit} characters`;
230
+ errors.value[field] = message;
231
+ validationErrors.push({ field, message });
232
+ } else if (error.keyword === 'maxLength') {
233
+ message = `${getFieldLabel(field)} must be at most ${error.params.limit} characters`;
234
+ errors.value[field] = message;
235
+ validationErrors.push({ field, message });
236
+ } else if (error.keyword === 'format') {
237
+ message = `${getFieldLabel(field)} must be a valid ${error.params.format}`;
238
+ errors.value[field] = message;
239
+ validationErrors.push({ field, message });
240
+ } else if (error.keyword === 'enum') {
241
+ message = `${getFieldLabel(field)} must be one of: ${error.params.allowedValues.join(', ')}`;
242
+ errors.value[field] = message;
243
+ validationErrors.push({ field, message });
244
+ } else {
245
+ // Generic error message
246
+ message = error.message || 'Invalid value';
247
+ errors.value[field] = message;
248
+ validationErrors.push({ field, message });
249
+ }
250
+ }
251
+ }
252
+
253
+ return validationErrors;
254
+ }
255
+
256
+ // Handle form submission
257
+ function handleSubmit(event: Event) {
258
+ event.preventDefault();
259
+
260
+ const validationErrors = validate();
261
+ if (validationErrors.length > 0) {
262
+ emit('validation-error', validationErrors);
263
+ return;
264
+ }
265
+
266
+ emit('submit', formData.value);
267
+ }
268
+
269
+ // Update field value
270
+ function updateField(field: string, value: any) {
271
+ touched.value[field] = true;
272
+
273
+ // Update formData by emitting the new value
274
+ const newData = {
275
+ ...formData.value,
276
+ [field]: value
277
+ };
278
+ emit('update:modelValue', newData);
279
+
280
+ // Validate field on change with the new value
281
+ validateFieldWithValue(field, value);
282
+ }
283
+
284
+ // Validate a single field using AJV (reads from formData)
285
+ function validateField(fieldName: string) {
286
+ validateFieldWithValue(fieldName, formData.value[fieldName]);
287
+ }
288
+
289
+ // Validate a single field with a specific value
290
+ function validateFieldWithValue(fieldName: string, fieldValue: any) {
291
+ const fieldSchema = props.schema.properties?.[fieldName];
292
+ if (!fieldSchema) return;
293
+
294
+ // Clear existing error
295
+ delete errors.value[fieldName];
296
+
297
+ // If field is empty/null/undefined and required, show required error
298
+ if (isRequired(fieldName) && (fieldValue === undefined || fieldValue === null || fieldValue === '')) {
299
+ errors.value[fieldName] = `${getFieldLabel(fieldName)} is required`;
300
+ return;
301
+ }
302
+
303
+ // If field is empty and not required, no validation needed
304
+ if (fieldValue === undefined || fieldValue === null || fieldValue === '') {
305
+ return;
306
+ }
307
+
308
+ // Create a mini schema for just this field (without required)
309
+ const singleFieldSchema = {
310
+ type: 'object',
311
+ properties: {
312
+ [fieldName]: fieldSchema
313
+ }
314
+ };
315
+
316
+ // Validate just this field
317
+ const validateFn = ajv.compile(singleFieldSchema);
318
+ const valid = validateFn({ [fieldName]: fieldValue });
319
+
320
+ if (!valid && validateFn.errors) {
321
+ for (const error of validateFn.errors) {
322
+ let message = '';
323
+
324
+ if (error.keyword === 'type') {
325
+ message = `${getFieldLabel(fieldName)} must be a ${error.params.type}`;
326
+ } else if (error.keyword === 'minimum') {
327
+ message = `${getFieldLabel(fieldName)} must be at least ${error.params.limit}`;
328
+ } else if (error.keyword === 'maximum') {
329
+ message = `${getFieldLabel(fieldName)} must be at most ${error.params.limit}`;
330
+ } else if (error.keyword === 'minLength') {
331
+ message = `${getFieldLabel(fieldName)} must be at least ${error.params.limit} characters`;
332
+ } else if (error.keyword === 'maxLength') {
333
+ message = `${getFieldLabel(fieldName)} must be at most ${error.params.limit} characters`;
334
+ } else if (error.keyword === 'format') {
335
+ message = `${getFieldLabel(fieldName)} must be a valid ${error.params.format}`;
336
+ } else if (error.keyword === 'enum') {
337
+ message = `${getFieldLabel(fieldName)} must be one of: ${error.params.allowedValues.join(', ')}`;
338
+ } else {
339
+ message = error.message || 'Invalid value';
340
+ }
341
+
342
+ errors.value[fieldName] = message;
343
+ break; // Only show first error per field
344
+ }
345
+ }
346
+ }
347
+
348
+ // Get enum options for select
349
+ function getSelectOptions(fieldName: string) {
350
+ const fieldSchema = props.schema.properties?.[fieldName];
351
+ if (!fieldSchema?.enum) return [];
352
+
353
+ return fieldSchema.enum.map((value: any) => ({
354
+ id: String(value),
355
+ name: String(value),
356
+ label: String(value)
357
+ }));
358
+ }
359
+
360
+ // Get selected option for select
361
+ function getSelectedOption(fieldName: string) {
362
+ const value = formData.value[fieldName];
363
+ if (!value) {
364
+ // Return first option as default if available
365
+ const options = getSelectOptions(fieldName);
366
+ return options.length > 0 ? options[0] : { id: '', name: '', label: '' };
367
+ }
368
+
369
+ return {
370
+ id: String(value),
371
+ name: String(value),
372
+ label: String(value)
373
+ };
374
+ }
375
+
376
+ // Array field helpers
377
+ function addArrayItem(fieldName: string) {
378
+ const currentArray = formData.value[fieldName] || [];
379
+ updateField(fieldName, [...currentArray, '']);
380
+ }
381
+
382
+ function removeArrayItem(fieldName: string, index: number) {
383
+ const currentArray = formData.value[fieldName] || [];
384
+ const newArray = currentArray.filter((_: any, i: number) => i !== index);
385
+ updateField(fieldName, newArray);
386
+ }
387
+
388
+ function updateArrayItem(fieldName: string, index: number, value: any) {
389
+ const currentArray = formData.value[fieldName] || [];
390
+ const newArray = [...currentArray];
391
+ newArray[index] = value;
392
+ updateField(fieldName, newArray);
393
+ }
394
+
395
+ // Expose methods
396
+ defineExpose({
397
+ validate,
398
+ reset: () => {
399
+ emit('update:modelValue', {});
400
+ errors.value = {};
401
+ touched.value = {};
402
+ }
403
+ });
404
+ </script>
405
+
406
+ <template>
407
+ <form @submit="handleSubmit" class="space-y-6">
408
+ <!-- Form Title and Description -->
409
+ <div v-if="schema.title || schema.description" class="mb-6">
410
+ <h2 v-if="schema.title" class="text-2xl font-bold text-gray-900 mb-2">
411
+ {{ schema.title }}
412
+ </h2>
413
+ <p v-if="schema.description" class="text-gray-600">
414
+ {{ schema.description }}
415
+ </p>
416
+ </div>
417
+
418
+ <!-- Form Fields -->
419
+ <div v-if="schema.properties" class="space-y-6">
420
+ <div
421
+ v-for="(fieldSchema, fieldName) in schema.properties"
422
+ :key="fieldName"
423
+ >
424
+ <!-- Text/Email/URL/Password Input -->
425
+ <uiInput
426
+ v-if="['text', 'email', 'url', 'password'].includes(getWidget(fieldName))"
427
+ :name="fieldName"
428
+ :label="getFieldLabel(fieldName)"
429
+ :type="getInputType(fieldName)"
430
+ :value="formData[fieldName] || ''"
431
+ :placeholder="getPlaceholder(fieldName)"
432
+ :error="errors[fieldName]"
433
+ :color="getFieldColor(fieldName)"
434
+ :loading="loading"
435
+ :required-text="isRequired(fieldName) ? '*' : undefined"
436
+ @input-changed="updateField(fieldName, $event.value)"
437
+ />
438
+
439
+ <!-- Number Input -->
440
+ <uiInput
441
+ v-else-if="getWidget(fieldName) === 'number'"
442
+ :name="fieldName"
443
+ :label="getFieldLabel(fieldName)"
444
+ type="number"
445
+ :value="String(formData[fieldName] ?? '')"
446
+ :placeholder="getPlaceholder(fieldName)"
447
+ :error="errors[fieldName]"
448
+ :color="getFieldColor(fieldName)"
449
+ :loading="loading"
450
+ :required-text="isRequired(fieldName) ? '*' : undefined"
451
+ :min="schema.properties?.[fieldName]?.minimum"
452
+ :max="schema.properties?.[fieldName]?.maximum"
453
+ @input-changed="updateField(fieldName, $event.value === '' ? null : Number($event.value))"
454
+ />
455
+
456
+ <!-- Textarea -->
457
+ <uiTextArea
458
+ v-else-if="getWidget(fieldName) === 'textarea'"
459
+ :name="fieldName"
460
+ :label="getFieldLabel(fieldName)"
461
+ :value="formData[fieldName] || ''"
462
+ :placeholder="getPlaceholder(fieldName)"
463
+ :error="errors[fieldName]"
464
+ :color="getFieldColor(fieldName)"
465
+ :loading="loading"
466
+ :required-text="isRequired(fieldName) ? '*' : undefined"
467
+ :rows="4"
468
+ @input-changed="updateField(fieldName, $event.value)"
469
+ />
470
+
471
+ <!-- Select Dropdown -->
472
+ <uiSelect
473
+ v-else-if="getWidget(fieldName) === 'select'"
474
+ :label="getFieldLabel(fieldName)"
475
+ :items="getSelectOptions(fieldName)"
476
+ :select="getSelectedOption(fieldName)"
477
+ :placeholder="getPlaceholder(fieldName) || 'Select an option'"
478
+ :error="errors[fieldName]"
479
+ :color="getFieldColor(fieldName)"
480
+ :loading="loading"
481
+ :required-text="isRequired(fieldName) ? '*' : undefined"
482
+ @select-changed="(item: any) => updateField(fieldName, item.id)"
483
+ />
484
+
485
+ <!-- Toggle (Boolean) -->
486
+ <uiToggle
487
+ v-else-if="getWidget(fieldName) === 'toggle'"
488
+ :item="{
489
+ title: getFieldLabel(fieldName),
490
+ subtitle: getFieldDescription(fieldName),
491
+ action: fieldName
492
+ }"
493
+ :checked="formData[fieldName]"
494
+ :loading="loading"
495
+ @toggle-changed="updateField(fieldName, $event.active)"
496
+ />
497
+
498
+ <!-- Checkbox (Boolean alternative) -->
499
+ <div v-else-if="getWidget(fieldName) === 'checkbox'" class="flex items-start">
500
+ <input
501
+ :id="fieldName"
502
+ type="checkbox"
503
+ :checked="formData[fieldName]"
504
+ :disabled="loading"
505
+ @change="updateField(fieldName, ($event.target as HTMLInputElement).checked)"
506
+ class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mt-1"
507
+ />
508
+ <label :for="fieldName" class="ml-2 block">
509
+ <span class="text-sm font-medium text-gray-700">
510
+ {{ getFieldLabel(fieldName) }}
511
+ <span v-if="isRequired(fieldName)" class="text-red-500">*</span>
512
+ </span>
513
+ <p v-if="getFieldDescription(fieldName)" class="text-sm text-gray-500">
514
+ {{ getFieldDescription(fieldName) }}
515
+ </p>
516
+ </label>
517
+ </div>
518
+
519
+ <!-- Range Slider -->
520
+ <uiRangeSlider
521
+ v-else-if="getWidget(fieldName) === 'slider'"
522
+ :label="getFieldLabel(fieldName)"
523
+ :min="schema.properties[fieldName].minimum ?? 0"
524
+ :max="schema.properties[fieldName].maximum ?? 100"
525
+ :slider-value="formData[fieldName] ?? schema.properties[fieldName].minimum ?? 0"
526
+ :loading="loading"
527
+ :required-text="isRequired(fieldName) ? '*' : undefined"
528
+ @sliderUpdated="updateField(fieldName, $event)"
529
+ />
530
+
531
+ <!-- Array (Simple list) -->
532
+ <div v-else-if="getWidget(fieldName) === 'array'" class="space-y-2">
533
+ <label class="block text-sm font-medium text-gray-700">
534
+ {{ getFieldLabel(fieldName) }}
535
+ <span v-if="isRequired(fieldName)" class="text-red-500">*</span>
536
+ </label>
537
+ <p v-if="getFieldDescription(fieldName)" class="text-sm text-gray-500 mb-2">
538
+ {{ getFieldDescription(fieldName) }}
539
+ </p>
540
+ <div v-if="errors[fieldName]" class="text-sm text-red-600 mb-2">
541
+ {{ errors[fieldName] }}
542
+ </div>
543
+ <div class="space-y-2">
544
+ <div
545
+ v-for="(item, index) in (formData[fieldName] || [])"
546
+ :key="index"
547
+ class="flex gap-2"
548
+ >
549
+ <input
550
+ type="text"
551
+ :value="item"
552
+ :disabled="loading"
553
+ @input="updateArrayItem(fieldName, index, ($event.target as HTMLInputElement).value)"
554
+ class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
555
+ />
556
+ <button
557
+ type="button"
558
+ @click="removeArrayItem(fieldName, index)"
559
+ :disabled="loading"
560
+ class="px-3 py-2 text-red-600 hover:text-red-800 border border-red-300 rounded-md"
561
+ >
562
+ Remove
563
+ </button>
564
+ </div>
565
+ </div>
566
+ <button
567
+ type="button"
568
+ @click="addArrayItem(fieldName)"
569
+ :disabled="loading"
570
+ class="mt-2 px-4 py-2 text-sm text-blue-600 hover:text-blue-800 border border-blue-300 rounded-md"
571
+ >
572
+ + Add Item
573
+ </button>
574
+ </div>
575
+
576
+ <!-- Nested Object (Simple rendering) -->
577
+ <div v-else-if="getWidget(fieldName) === 'object' || getWidget(fieldName) === 'card'" class="border border-gray-200 rounded-lg p-4 space-y-4">
578
+ <div>
579
+ <h3 class="text-lg font-semibold text-gray-900 mb-1">
580
+ {{ getFieldLabel(fieldName) }}
581
+ </h3>
582
+ <p v-if="getFieldDescription(fieldName)" class="text-sm text-gray-600 mb-4">
583
+ {{ getFieldDescription(fieldName) }}
584
+ </p>
585
+ </div>
586
+ <div class="space-y-4 pl-4 border-l-2 border-gray-200">
587
+ <!-- Recursively render nested fields -->
588
+ <div
589
+ v-for="(subSchema, subName) in fieldSchema.properties"
590
+ :key="subName"
591
+ >
592
+ <!-- Nested field rendering would go here -->
593
+ <p class="text-sm text-gray-500">
594
+ Nested field: {{ subName }} (type: {{ subSchema.type }})
595
+ </p>
596
+ </div>
597
+ </div>
598
+ </div>
599
+ </div>
600
+ </div>
601
+
602
+ <!-- Form Actions -->
603
+ <div class="flex justify-end gap-3 pt-6 border-t">
604
+ <slot name="actions">
605
+ <uiButton
606
+ type="submit"
607
+ color="primary"
608
+ :loading="loading"
609
+ >
610
+ Submit
611
+ </uiButton>
612
+ </slot>
613
+ </div>
614
+ </form>
615
+ </template>
@@ -0,0 +1,3 @@
1
+ export { default as JsonSchemaForm } from './JsonSchemaForm.vue';
2
+ export { default as DateRange } from './DateRange.vue';
3
+ export type { DateRangeValue, DateRangeLiterals } from './DateRange.vue';
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ // Data components
2
+ export * from './data';
3
+
4
+ // Form components
5
+ export * from './forms';
6
+
7
+ // Navigation components
8
+ export * from './navigation';
9
+
10
+ // Overlay components
11
+ export * from './overlays';
12
+
13
+ // Domain components
14
+ export * from './domain';
15
+
16
+ // Composables
17
+ export * from './composables';