@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.
- package/LICENSE +21 -0
- package/README.md +39 -0
- package/admin/ColumnList/ColumnList.svelte +68 -0
- package/admin/ColumnList/component.json +13 -0
- package/admin/MetadataSettings/MetadataSettings.svelte +62 -0
- package/admin/MetadataSettings/component.json +9 -0
- package/admin/RelationshipManager/RelationshipManager.svelte +86 -0
- package/admin/RelationshipManager/component.json +10 -0
- package/admin/RoleManager/RoleManager.svelte +167 -0
- package/admin/RoleManager/component.json +17 -0
- package/admin/TableDesigner/TableDesigner.svelte +37 -0
- package/admin/TableDesigner/component.json +14 -0
- package/ai/AIFeedback/AIFeedback.svelte +43 -0
- package/ai/AIInsightWidget/AIInsightWidget.svelte +56 -0
- package/ai/AIQueryBuilder/AIQueryBuilder.svelte +91 -0
- package/ai/Omnisearch/Omnisearch.svelte +112 -0
- package/ai/VoiceSearch/VoiceSearch.svelte +73 -0
- package/attachments/AttachmentManager/AttachmentManager.svelte +175 -0
- package/attachments/AttachmentManager/component.json +21 -0
- package/charts/SimpleBarChart/SimpleBarChart.svelte +28 -0
- package/charts/SimpleBarChart/component.json +14 -0
- package/common/AddressInput/AddressInput.svelte +88 -0
- package/common/AddressInput/component.json +12 -0
- package/common/Alert/Alert.svelte +20 -0
- package/common/Alert/component.json +15 -0
- package/common/Button/Button.svelte +28 -0
- package/common/Button/component.json +16 -0
- package/common/Card/Card.svelte +25 -0
- package/common/Card/component.json +13 -0
- package/common/DynamicDataTable/DynamicDataTable.svelte +84 -0
- package/common/DynamicDataTable/component.json +14 -0
- package/common/Input/Input.svelte +21 -0
- package/common/Input/component.json +16 -0
- package/common/Loading/Loading.svelte +12 -0
- package/common/Loading/component.json +12 -0
- package/common/Modal/Modal.svelte +31 -0
- package/common/Modal/component.json +14 -0
- package/common/Pagination/Pagination.svelte +40 -0
- package/common/Pagination/component.json +1 -0
- package/common/PermissionGuard/PermissionGuard.svelte +53 -0
- package/common/PermissionGuard/component.json +21 -0
- package/common/SearchableSelect/SearchableSelect.svelte +136 -0
- package/common/SearchableSelect/component.json +17 -0
- package/common/StatusBadge/StatusBadge.svelte +22 -0
- package/common/StatusBadge/component.json +1 -0
- package/dashboard/RecentActivity/RecentActivity.svelte +70 -0
- package/dashboard/RecentActivity/component.json +1 -0
- package/forms/FormField/FormField.svelte +39 -0
- package/forms/FormField/component.json +17 -0
- package/navigation/NavGroup/NavGroup.svelte +40 -0
- package/navigation/NavGroup/component.json +1 -0
- package/navigation/NavLink/NavLink.svelte +18 -0
- package/navigation/NavLink/component.json +15 -0
- package/navigation/SmartNavbar/SmartNavbar.svelte +184 -0
- package/navigation/SmartNavbar/component.json +20 -0
- package/package.json +53 -0
- package/pages/Settings/Settings.svelte +154 -0
- package/pages/Settings/component.json +11 -0
- package/src/lib/index.ts +53 -0
- package/views/ListView/ListView.svelte +19 -0
- 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>
|