@zveltio/components 1.0.0

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 (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -0
  3. package/admin/ColumnList/ColumnList.svelte +68 -0
  4. package/admin/ColumnList/component.json +13 -0
  5. package/admin/MetadataSettings/MetadataSettings.svelte +62 -0
  6. package/admin/MetadataSettings/component.json +9 -0
  7. package/admin/RelationshipManager/RelationshipManager.svelte +86 -0
  8. package/admin/RelationshipManager/component.json +10 -0
  9. package/admin/RoleManager/RoleManager.svelte +167 -0
  10. package/admin/RoleManager/component.json +17 -0
  11. package/admin/TableDesigner/TableDesigner.svelte +37 -0
  12. package/admin/TableDesigner/component.json +14 -0
  13. package/ai/AIFeedback/AIFeedback.svelte +43 -0
  14. package/ai/AIInsightWidget/AIInsightWidget.svelte +56 -0
  15. package/ai/AIQueryBuilder/AIQueryBuilder.svelte +91 -0
  16. package/ai/Omnisearch/Omnisearch.svelte +112 -0
  17. package/ai/VoiceSearch/VoiceSearch.svelte +73 -0
  18. package/attachments/AttachmentManager/AttachmentManager.svelte +175 -0
  19. package/attachments/AttachmentManager/component.json +21 -0
  20. package/charts/SimpleBarChart/SimpleBarChart.svelte +28 -0
  21. package/charts/SimpleBarChart/component.json +14 -0
  22. package/common/AddressInput/AddressInput.svelte +88 -0
  23. package/common/AddressInput/component.json +12 -0
  24. package/common/Alert/Alert.svelte +20 -0
  25. package/common/Alert/component.json +15 -0
  26. package/common/Button/Button.svelte +28 -0
  27. package/common/Button/component.json +16 -0
  28. package/common/Card/Card.svelte +25 -0
  29. package/common/Card/component.json +13 -0
  30. package/common/DynamicDataTable/DynamicDataTable.svelte +84 -0
  31. package/common/DynamicDataTable/component.json +14 -0
  32. package/common/Input/Input.svelte +21 -0
  33. package/common/Input/component.json +16 -0
  34. package/common/Loading/Loading.svelte +12 -0
  35. package/common/Loading/component.json +12 -0
  36. package/common/Modal/Modal.svelte +31 -0
  37. package/common/Modal/component.json +14 -0
  38. package/common/Pagination/Pagination.svelte +40 -0
  39. package/common/Pagination/component.json +1 -0
  40. package/common/PermissionGuard/PermissionGuard.svelte +53 -0
  41. package/common/PermissionGuard/component.json +21 -0
  42. package/common/SearchableSelect/SearchableSelect.svelte +136 -0
  43. package/common/SearchableSelect/component.json +17 -0
  44. package/common/StatusBadge/StatusBadge.svelte +22 -0
  45. package/common/StatusBadge/component.json +1 -0
  46. package/dashboard/RecentActivity/RecentActivity.svelte +70 -0
  47. package/dashboard/RecentActivity/component.json +1 -0
  48. package/forms/FormField/FormField.svelte +39 -0
  49. package/forms/FormField/component.json +17 -0
  50. package/navigation/NavGroup/NavGroup.svelte +40 -0
  51. package/navigation/NavGroup/component.json +1 -0
  52. package/navigation/NavLink/NavLink.svelte +18 -0
  53. package/navigation/NavLink/component.json +15 -0
  54. package/navigation/SmartNavbar/SmartNavbar.svelte +184 -0
  55. package/navigation/SmartNavbar/component.json +20 -0
  56. package/package.json +53 -0
  57. package/pages/Settings/Settings.svelte +154 -0
  58. package/pages/Settings/component.json +11 -0
  59. package/src/lib/index.ts +53 -0
  60. package/views/ListView/ListView.svelte +19 -0
  61. package/views/ListView/component.json +13 -0
@@ -0,0 +1,91 @@
1
+ <script lang="ts">
2
+ interface QueryCondition {
3
+ field: string;
4
+ operator: string;
5
+ value: any;
6
+ }
7
+
8
+ let {
9
+ query = $bindable<QueryCondition[]>([]),
10
+ fields = [],
11
+ onGenerate = null
12
+ }: {
13
+ query?: QueryCondition[];
14
+ fields: Array<{name: string; type: string}>;
15
+ onGenerate?: ((naturalLanguage: string) => Promise<QueryCondition[]>) | null;
16
+ } = $props();
17
+
18
+ let naturalQuery = $state('');
19
+ let generating = $state(false);
20
+
21
+ const operators = {
22
+ text: ['equals', 'contains', 'starts with'],
23
+ number: ['equals', '>', '<', '>=', '<='],
24
+ date: ['equals', 'before', 'after']
25
+ };
26
+
27
+ function addCondition() {
28
+ query = [...query, { field: fields[0]?.name || '', operator: 'equals', value: '' }];
29
+ }
30
+
31
+ function removeCondition(index: number) {
32
+ query = query.filter((_, i) => i !== index);
33
+ }
34
+
35
+ async function generateFromNatural() {
36
+ if (!onGenerate || !naturalQuery.trim()) return;
37
+ generating = true;
38
+ try {
39
+ query = await onGenerate(naturalQuery);
40
+ } finally {
41
+ generating = false;
42
+ }
43
+ }
44
+ </script>
45
+
46
+ <div class="space-y-4">
47
+ {#if onGenerate}
48
+ <div class="flex gap-2">
49
+ <input
50
+ type="text"
51
+ bind:value={naturalQuery}
52
+ placeholder="Describe your query in plain English..."
53
+ class="input input-bordered flex-1"
54
+ />
55
+ <button class="btn btn-primary" onclick={generateFromNatural} disabled={generating}>
56
+ {#if generating}
57
+ <span class="loading loading-spinner loading-sm"></span>
58
+ {:else}
59
+ Generate
60
+ {/if}
61
+ </button>
62
+ </div>
63
+ <div class="divider">OR build manually</div>
64
+ {/if}
65
+
66
+ <div class="space-y-2">
67
+ {#each query as condition, i}
68
+ <div class="flex gap-2 items-start">
69
+ <select bind:value={condition.field} class="select select-sm select-bordered">
70
+ {#each fields as field}
71
+ <option value={field.name}>{field.name}</option>
72
+ {/each}
73
+ </select>
74
+ <select bind:value={condition.operator} class="select select-sm select-bordered">
75
+ {#each operators[fields.find(f => f.name === condition.field)?.type || 'text'] || [] as op}
76
+ <option value={op}>{op}</option>
77
+ {/each}
78
+ </select>
79
+ <input
80
+ type="text"
81
+ bind:value={condition.value}
82
+ class="input input-sm input-bordered flex-1"
83
+ placeholder="Value"
84
+ />
85
+ <button class="btn btn-sm btn-ghost btn-square text-error" onclick={() => removeCondition(i)}>✕</button>
86
+ </div>
87
+ {/each}
88
+ </div>
89
+
90
+ <button class="btn btn-sm btn-ghost" onclick={addCondition}>+ Add Condition</button>
91
+ </div>
@@ -0,0 +1,112 @@
1
+ <script lang="ts">
2
+ interface SearchResult {
3
+ id: string;
4
+ title: string;
5
+ description?: string;
6
+ category?: string;
7
+ href?: string;
8
+ }
9
+
10
+ let {
11
+ onSearch,
12
+ placeholder = 'Search everything...',
13
+ categories = []
14
+ }: {
15
+ onSearch: (query: string, category?: string) => Promise<SearchResult[]>;
16
+ placeholder?: string;
17
+ categories?: string[];
18
+ } = $props();
19
+
20
+ let query = $state('');
21
+ let results = $state<SearchResult[]>([]);
22
+ let loading = $state(false);
23
+ let selectedCategory = $state<string | null>(null);
24
+ let showResults = $state(false);
25
+
26
+ async function handleSearch() {
27
+ if (query.length < 2) {
28
+ results = [];
29
+ return;
30
+ }
31
+
32
+ loading = true;
33
+ try {
34
+ results = await onSearch(query, selectedCategory || undefined);
35
+ showResults = true;
36
+ } finally {
37
+ loading = false;
38
+ }
39
+ }
40
+
41
+ let debounceTimer: number;
42
+ function debounceSearch() {
43
+ clearTimeout(debounceTimer);
44
+ debounceTimer = setTimeout(handleSearch, 300) as any;
45
+ }
46
+ </script>
47
+
48
+ <div class="relative">
49
+ <div class="flex gap-2">
50
+ <div class="relative flex-1">
51
+ <input
52
+ type="text"
53
+ bind:value={query}
54
+ oninput={debounceSearch}
55
+ {placeholder}
56
+ class="input input-bordered w-full pr-10"
57
+ />
58
+ <div class="absolute right-3 top-3">
59
+ {#if loading}
60
+ <span class="loading loading-spinner loading-sm"></span>
61
+ {:else}
62
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
63
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
64
+ </svg>
65
+ {/if}
66
+ </div>
67
+ </div>
68
+
69
+ {#if categories.length > 0}
70
+ <select bind:value={selectedCategory} class="select select-bordered" onchange={handleSearch}>
71
+ <option value={null}>All</option>
72
+ {#each categories as cat}
73
+ <option value={cat}>{cat}</option>
74
+ {/each}
75
+ </select>
76
+ {/if}
77
+ </div>
78
+
79
+ {#if showResults && query.length >= 2}
80
+ <div class="absolute z-50 w-full mt-2 bg-base-100 border border-base-200 rounded-lg shadow-lg max-h-96 overflow-auto">
81
+ {#if results.length === 0}
82
+ <div class="p-4 text-center text-sm opacity-60">No results found</div>
83
+ {:else}
84
+ {#each results as result}
85
+ <a
86
+ href={result.href || '#'}
87
+ class="block p-3 hover:bg-base-200 transition"
88
+ onclick={() => showResults = false}
89
+ >
90
+ <div class="flex items-start justify-between">
91
+ <div class="flex-1">
92
+ <h4 class="font-semibold text-sm">{result.title}</h4>
93
+ {#if result.description}
94
+ <p class="text-xs opacity-60 mt-1">{result.description}</p>
95
+ {/if}
96
+ </div>
97
+ {#if result.category}
98
+ <span class="badge badge-xs">{result.category}</span>
99
+ {/if}
100
+ </div>
101
+ </a>
102
+ {/each}
103
+ {/if}
104
+ </div>
105
+
106
+ <button
107
+ class="fixed inset-0 z-40"
108
+ onclick={() => showResults = false}
109
+ tabindex="-1"
110
+ ></button>
111
+ {/if}
112
+ </div>
@@ -0,0 +1,73 @@
1
+ <script lang="ts">
2
+ let {
3
+ onTranscript,
4
+ onSearch = null
5
+ }: {
6
+ onTranscript: (text: string) => void;
7
+ onSearch?: ((text: string) => Promise<void>) | null;
8
+ } = $props();
9
+
10
+ let listening = $state(false);
11
+ let transcript = $state('');
12
+ let error = $state('');
13
+
14
+ function startListening() {
15
+ if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
16
+ error = 'Speech recognition not supported';
17
+ return;
18
+ }
19
+
20
+ const SpeechRecognition = (window as any).webkitSpeechRecognition || (window as any).SpeechRecognition;
21
+ const recognition = new SpeechRecognition();
22
+
23
+ recognition.continuous = false;
24
+ recognition.interimResults = false;
25
+ recognition.lang = 'en-US';
26
+
27
+ recognition.onstart = () => {
28
+ listening = true;
29
+ error = '';
30
+ };
31
+
32
+ recognition.onresult = (event: any) => {
33
+ const text = event.results[0][0].transcript;
34
+ transcript = text;
35
+ onTranscript(text);
36
+ if (onSearch) onSearch(text);
37
+ };
38
+
39
+ recognition.onerror = (event: any) => {
40
+ error = event.error;
41
+ listening = false;
42
+ };
43
+
44
+ recognition.onend = () => {
45
+ listening = false;
46
+ };
47
+
48
+ recognition.start();
49
+ }
50
+ </script>
51
+
52
+ <div class="flex items-center gap-2">
53
+ <button
54
+ class="btn btn-circle {listening ? 'btn-error animate-pulse' : 'btn-ghost'}"
55
+ onclick={startListening}
56
+ disabled={listening}
57
+ title="Voice search"
58
+ >
59
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
60
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
61
+ </svg>
62
+ </button>
63
+
64
+ {#if listening}
65
+ <span class="text-sm opacity-60">Listening...</span>
66
+ {:else if transcript}
67
+ <span class="text-sm">{transcript}</span>
68
+ {/if}
69
+
70
+ {#if error}
71
+ <span class="text-sm text-error">{error}</span>
72
+ {/if}
73
+ </div>
@@ -0,0 +1,175 @@
1
+ <script lang="ts">
2
+ /**
3
+ * AttachmentManager - File upload/management component
4
+ * Standalone, works with any storage backend
5
+ */
6
+
7
+ interface FileItem {
8
+ id: string;
9
+ name: string;
10
+ size: number;
11
+ type: string;
12
+ url?: string;
13
+ created_at: string;
14
+ }
15
+
16
+ let {
17
+ files = [],
18
+ onUpload,
19
+ onDelete,
20
+ onDownload = null,
21
+ acceptedTypes = '*',
22
+ readOnly = false,
23
+ maxFiles = 10
24
+ }: {
25
+ files: FileItem[];
26
+ onUpload: (files: FileList) => Promise<void>;
27
+ onDelete: (fileId: string) => Promise<void>;
28
+ onDownload?: ((fileId: string) => void) | null;
29
+ acceptedTypes?: string;
30
+ readOnly?: boolean;
31
+ maxFiles?: number;
32
+ } = $props();
33
+
34
+ let uploading = $state(false);
35
+ let dragOver = $state(false);
36
+ let fileInput: HTMLInputElement;
37
+
38
+ function formatSize(bytes: number): string {
39
+ if (bytes < 1024) return bytes + ' B';
40
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
41
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
42
+ }
43
+
44
+ function getFileIcon(type: string): string {
45
+ if (type.startsWith('image/')) return '🖼️';
46
+ if (type.startsWith('video/')) return '🎥';
47
+ if (type.startsWith('audio/')) return '🎵';
48
+ if (type.includes('pdf')) return '📄';
49
+ if (type.includes('zip') || type.includes('rar')) return '📦';
50
+ return '📁';
51
+ }
52
+
53
+ async function handleUpload(fileList: FileList | null) {
54
+ if (!fileList || fileList.length === 0 || readOnly) return;
55
+
56
+ if (files.length + fileList.length > maxFiles) {
57
+ alert(`Maximum ${maxFiles} files allowed`);
58
+ return;
59
+ }
60
+
61
+ uploading = true;
62
+ try {
63
+ await onUpload(fileList);
64
+ } finally {
65
+ uploading = false;
66
+ }
67
+ }
68
+
69
+ async function handleDelete(fileId: string) {
70
+ if (!confirm('Delete this file?')) return;
71
+ await onDelete(fileId);
72
+ }
73
+
74
+ function handleDrop(e: DragEvent) {
75
+ e.preventDefault();
76
+ dragOver = false;
77
+ if (e.dataTransfer?.files) {
78
+ handleUpload(e.dataTransfer.files);
79
+ }
80
+ }
81
+
82
+ function handleDragOver(e: DragEvent) {
83
+ e.preventDefault();
84
+ dragOver = true;
85
+ }
86
+ </script>
87
+
88
+ <div class="space-y-4">
89
+ <!-- Upload Area -->
90
+ {#if !readOnly}
91
+ <div
92
+ class="border-2 border-dashed rounded-lg p-8 text-center transition {dragOver ? 'border-primary bg-primary/5' : 'border-base-300'}"
93
+ ondrop={handleDrop}
94
+ ondragover={handleDragOver}
95
+ ondragleave={() => dragOver = false}
96
+ >
97
+ <input
98
+ bind:this={fileInput}
99
+ type="file"
100
+ multiple
101
+ accept={acceptedTypes}
102
+ onchange={(e) => handleUpload(e.currentTarget.files)}
103
+ class="hidden"
104
+ />
105
+
106
+ {#if uploading}
107
+ <div class="flex flex-col items-center gap-2">
108
+ <svg class="animate-spin h-8 w-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
109
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
110
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
111
+ </svg>
112
+ <span class="text-sm">Uploading...</span>
113
+ </div>
114
+ {:else}
115
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
116
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
117
+ </svg>
118
+ <p class="text-sm mb-2">Drag & drop files or click to browse</p>
119
+ <button class="btn btn-sm btn-primary" onclick={() => fileInput.click()}>
120
+ Choose Files
121
+ </button>
122
+ {/if}
123
+ </div>
124
+ {/if}
125
+
126
+ <!-- Files List -->
127
+ {#if files.length > 0}
128
+ <div class="space-y-2">
129
+ {#each files as file}
130
+ <div class="card bg-base-100 border border-base-200">
131
+ <div class="card-body p-4 flex-row items-center gap-4">
132
+ <!-- Icon -->
133
+ <div class="text-3xl">{getFileIcon(file.type)}</div>
134
+
135
+ <!-- Info -->
136
+ <div class="flex-1 min-w-0">
137
+ <p class="font-medium truncate">{file.name}</p>
138
+ <p class="text-sm opacity-60">{formatSize(file.size)} • {new Date(file.created_at).toLocaleDateString()}</p>
139
+ </div>
140
+
141
+ <!-- Actions -->
142
+ <div class="flex gap-2">
143
+ {#if onDownload}
144
+ <button
145
+ class="btn btn-sm btn-ghost btn-square"
146
+ onclick={() => onDownload?.(file.id)}
147
+ title="Download"
148
+ >
149
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
150
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
151
+ </svg>
152
+ </button>
153
+ {/if}
154
+ {#if !readOnly}
155
+ <button
156
+ class="btn btn-sm btn-ghost btn-square text-error"
157
+ onclick={() => handleDelete(file.id)}
158
+ title="Delete"
159
+ >
160
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
161
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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" />
162
+ </svg>
163
+ </button>
164
+ {/if}
165
+ </div>
166
+ </div>
167
+ </div>
168
+ {/each}
169
+ </div>
170
+ {:else}
171
+ <div class="text-center py-8 text-sm opacity-50">
172
+ No files uploaded yet
173
+ </div>
174
+ {/if}
175
+ </div>
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "AttachmentManager",
3
+ "category": "attachments",
4
+ "description": "File upload and management with drag & drop",
5
+ "dependencies": {
6
+ "packages": [],
7
+ "components": []
8
+ },
9
+ "props": {
10
+ "files": "FileItem[] - List of uploaded files",
11
+ "onUpload": "(files: FileList) => Promise<void> - Upload callback",
12
+ "onDelete": "(fileId: string) => Promise<void> - Delete callback",
13
+ "onDownload": "((fileId: string) => void) | null - Download callback",
14
+ "acceptedTypes": "string - MIME types (e.g., 'image/*')",
15
+ "readOnly": "boolean - Disable uploads/deletes",
16
+ "maxFiles": "number - Maximum file count"
17
+ },
18
+ "types": {
19
+ "FileItem": "{id: string; name: string; size: number; type: string; url?: string; created_at: string}"
20
+ }
21
+ }
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ let {
3
+ data = [],
4
+ xKey = 'name',
5
+ yKey = 'value'
6
+ }: {
7
+ data: any[];
8
+ xKey?: string;
9
+ yKey?: string;
10
+ } = $props();
11
+
12
+ const maxValue = $derived(Math.max(...data.map(d => d[yKey])));
13
+ </script>
14
+
15
+ <div class="space-y-2">
16
+ {#each data as item}
17
+ <div class="flex items-center gap-2">
18
+ <span class="text-sm w-24 truncate">{item[xKey]}</span>
19
+ <div class="flex-1 bg-base-200 rounded-full h-6 relative">
20
+ <div
21
+ class="bg-primary h-full rounded-full"
22
+ style="width: {(item[yKey] / maxValue) * 100}%"
23
+ ></div>
24
+ </div>
25
+ <span class="text-sm font-mono">{item[yKey]}</span>
26
+ </div>
27
+ {/each}
28
+ </div>
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "SimpleBarChart",
3
+ "category": "charts",
4
+ "description": "SimpleBarChart - Svelte 5 component",
5
+ "dependencies": {
6
+ "packages": [],
7
+ "components": []
8
+ },
9
+ "props": {
10
+ "data": "Array",
11
+ "xKey": "string",
12
+ "yKey": "string"
13
+ }
14
+ }
@@ -0,0 +1,88 @@
1
+ <script lang="ts">
2
+ /**
3
+ * AddressInput - Address field with geocoding
4
+ */
5
+ interface Address {
6
+ street?: string;
7
+ city?: string;
8
+ country?: string;
9
+ postal_code?: string;
10
+ lat?: number;
11
+ lng?: number;
12
+ }
13
+
14
+ let {
15
+ value = $bindable<Address>({}),
16
+ onSearch = null,
17
+ label = 'Address',
18
+ disabled = false
19
+ }: {
20
+ value?: Address;
21
+ onSearch?: ((query: string) => Promise<Address[]>) | null;
22
+ label?: string;
23
+ disabled?: boolean;
24
+ } = $props();
25
+
26
+ let searchResults = $state<Address[]>([]);
27
+ let searching = $state(false);
28
+ let showResults = $state(false);
29
+
30
+ async function handleSearch(query: string) {
31
+ if (!onSearch || query.length < 3) return;
32
+ searching = true;
33
+ try {
34
+ searchResults = await onSearch(query);
35
+ showResults = true;
36
+ } finally {
37
+ searching = false;
38
+ }
39
+ }
40
+
41
+ function selectAddress(addr: Address) {
42
+ value = addr;
43
+ showResults = false;
44
+ }
45
+ </script>
46
+
47
+ <div class="form-control">
48
+ <label class="label"><span class="label-text">{label}</span></label>
49
+
50
+ <div class="relative">
51
+ <input
52
+ type="text"
53
+ value={value.street || ''}
54
+ oninput={(e) => handleSearch(e.currentTarget.value)}
55
+ placeholder="Start typing address..."
56
+ {disabled}
57
+ class="input input-bordered w-full"
58
+ />
59
+
60
+ {#if searching}
61
+ <div class="absolute right-3 top-3">
62
+ <span class="loading loading-spinner loading-sm"></span>
63
+ </div>
64
+ {/if}
65
+
66
+ {#if showResults && searchResults.length > 0}
67
+ <div class="absolute z-10 w-full mt-1 bg-base-100 border rounded-lg shadow-lg max-h-60 overflow-auto">
68
+ {#each searchResults as result}
69
+ <button
70
+ type="button"
71
+ class="w-full text-left px-4 py-2 hover:bg-base-200"
72
+ onclick={() => selectAddress(result)}
73
+ >
74
+ <div class="font-medium">{result.street}</div>
75
+ <div class="text-sm opacity-60">{result.city}, {result.country}</div>
76
+ </button>
77
+ {/each}
78
+ </div>
79
+ {/if}
80
+ </div>
81
+
82
+ {#if value.city}
83
+ <div class="grid grid-cols-2 gap-2 mt-2">
84
+ <input type="text" bind:value={value.city} placeholder="City" class="input input-sm input-bordered" {disabled} />
85
+ <input type="text" bind:value={value.postal_code} placeholder="Postal" class="input input-sm input-bordered" {disabled} />
86
+ </div>
87
+ {/if}
88
+ </div>
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "AddressInput",
3
+ "category": "common",
4
+ "description": "Address input with geocoding search",
5
+ "dependencies": {"packages": [], "components": []},
6
+ "props": {
7
+ "value": "$bindable<Address>",
8
+ "onSearch": "(query: string) => Promise<Address[]>",
9
+ "label": "string",
10
+ "disabled": "boolean"
11
+ }
12
+ }
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ let {
3
+ type = 'info',
4
+ message = '',
5
+ dismissible = false,
6
+ onDismiss = null
7
+ }: {
8
+ type?: 'info' | 'success' | 'warning' | 'error';
9
+ message: string;
10
+ dismissible?: boolean;
11
+ onDismiss?: (() => void) | null;
12
+ } = $props();
13
+ </script>
14
+
15
+ <div class="alert alert-{type}">
16
+ <span>{message}</span>
17
+ {#if dismissible}
18
+ <button class="btn btn-sm btn-ghost btn-circle" onclick={onDismiss}>✕</button>
19
+ {/if}
20
+ </div>
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "Alert",
3
+ "category": "common",
4
+ "description": "Alert - Svelte 5 component",
5
+ "dependencies": {
6
+ "packages": [],
7
+ "components": []
8
+ },
9
+ "props": {
10
+ "type": "string",
11
+ "message": "string",
12
+ "dismissible": "boolean",
13
+ "onDismiss": "function"
14
+ }
15
+ }
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ let {
3
+ children,
4
+ variant = 'primary',
5
+ size = 'md',
6
+ disabled = false,
7
+ loading = false,
8
+ onclick = null
9
+ }: {
10
+ children: any;
11
+ variant?: 'primary' | 'secondary' | 'ghost' | 'error';
12
+ size?: 'xs' | 'sm' | 'md' | 'lg';
13
+ disabled?: boolean;
14
+ loading?: boolean;
15
+ onclick?: (() => void) | null;
16
+ } = $props();
17
+ </script>
18
+
19
+ <button
20
+ class="btn btn-{variant} btn-{size}"
21
+ {disabled}
22
+ {onclick}
23
+ >
24
+ {#if loading}
25
+ <span class="loading loading-spinner loading-sm"></span>
26
+ {/if}
27
+ {@render children()}
28
+ </button>