@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,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Button",
|
|
3
|
+
"category": "common",
|
|
4
|
+
"description": "Button - Svelte 5 component",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"packages": [],
|
|
7
|
+
"components": []
|
|
8
|
+
},
|
|
9
|
+
"props": {
|
|
10
|
+
"variant": "string",
|
|
11
|
+
"size": "string",
|
|
12
|
+
"disabled": "boolean",
|
|
13
|
+
"loading": "boolean",
|
|
14
|
+
"onclick": "function"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
children,
|
|
6
|
+
title = '',
|
|
7
|
+
actions = null
|
|
8
|
+
}: {
|
|
9
|
+
children: Snippet;
|
|
10
|
+
title?: string;
|
|
11
|
+
actions?: Snippet | null;
|
|
12
|
+
} = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div class="card bg-base-100 shadow-xl">
|
|
16
|
+
<div class="card-body">
|
|
17
|
+
{#if title || actions}
|
|
18
|
+
<div class="flex justify-between items-center mb-4">
|
|
19
|
+
{#if title}<h2 class="card-title">{title}</h2>{/if}
|
|
20
|
+
{#if actions}<div class="card-actions">{@render actions()}</div>{/if}
|
|
21
|
+
</div>
|
|
22
|
+
{/if}
|
|
23
|
+
{@render children()}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DynamicDataTable - Universal data table with sorting, filtering, actions
|
|
4
|
+
*/
|
|
5
|
+
interface Column {
|
|
6
|
+
key: string;
|
|
7
|
+
label: string;
|
|
8
|
+
sortable?: boolean;
|
|
9
|
+
format?: (value: any) => string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
columns,
|
|
14
|
+
data = [],
|
|
15
|
+
onRowClick = null,
|
|
16
|
+
onSort = null,
|
|
17
|
+
loading = false,
|
|
18
|
+
emptyMessage = 'No data'
|
|
19
|
+
}: {
|
|
20
|
+
columns: Column[];
|
|
21
|
+
data?: any[];
|
|
22
|
+
onRowClick?: ((row: any) => void) | null;
|
|
23
|
+
onSort?: ((column: string, direction: 'asc' | 'desc') => void) | null;
|
|
24
|
+
loading?: boolean;
|
|
25
|
+
emptyMessage?: string;
|
|
26
|
+
} = $props();
|
|
27
|
+
|
|
28
|
+
let sortColumn = $state<string | null>(null);
|
|
29
|
+
let sortDirection = $state<'asc' | 'desc'>('asc');
|
|
30
|
+
|
|
31
|
+
function handleSort(col: Column) {
|
|
32
|
+
if (!col.sortable) return;
|
|
33
|
+
if (sortColumn === col.key) {
|
|
34
|
+
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
|
35
|
+
} else {
|
|
36
|
+
sortColumn = col.key;
|
|
37
|
+
sortDirection = 'asc';
|
|
38
|
+
}
|
|
39
|
+
onSort?.(col.key, sortDirection);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getCellValue(row: any, col: Column) {
|
|
43
|
+
const val = row[col.key];
|
|
44
|
+
return col.format ? col.format(val) : val;
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<div class="overflow-x-auto">
|
|
49
|
+
<table class="table table-zebra">
|
|
50
|
+
<thead>
|
|
51
|
+
<tr>
|
|
52
|
+
{#each columns as col}
|
|
53
|
+
<th>
|
|
54
|
+
{#if col.sortable}
|
|
55
|
+
<button class="flex items-center gap-1" onclick={() => handleSort(col)}>
|
|
56
|
+
{col.label}
|
|
57
|
+
{#if sortColumn === col.key}
|
|
58
|
+
<span>{sortDirection === 'asc' ? '↑' : '↓'}</span>
|
|
59
|
+
{/if}
|
|
60
|
+
</button>
|
|
61
|
+
{:else}
|
|
62
|
+
{col.label}
|
|
63
|
+
{/if}
|
|
64
|
+
</th>
|
|
65
|
+
{/each}
|
|
66
|
+
</tr>
|
|
67
|
+
</thead>
|
|
68
|
+
<tbody>
|
|
69
|
+
{#if loading}
|
|
70
|
+
<tr><td colspan={columns.length} class="text-center"><span class="loading loading-spinner"></span></td></tr>
|
|
71
|
+
{:else if data.length === 0}
|
|
72
|
+
<tr><td colspan={columns.length} class="text-center opacity-50">{emptyMessage}</td></tr>
|
|
73
|
+
{:else}
|
|
74
|
+
{#each data as row}
|
|
75
|
+
<tr class={onRowClick ? 'cursor-pointer hover:bg-base-200' : ''} onclick={() => onRowClick?.(row)}>
|
|
76
|
+
{#each columns as col}
|
|
77
|
+
<td>{getCellValue(row, col)}</td>
|
|
78
|
+
{/each}
|
|
79
|
+
</tr>
|
|
80
|
+
{/each}
|
|
81
|
+
{/if}
|
|
82
|
+
</tbody>
|
|
83
|
+
</table>
|
|
84
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "DynamicDataTable",
|
|
3
|
+
"category": "common",
|
|
4
|
+
"description": "Universal data table with sorting and row actions",
|
|
5
|
+
"dependencies": {"packages": [], "components": []},
|
|
6
|
+
"props": {
|
|
7
|
+
"columns": "Column[] - {key, label, sortable?, format?}",
|
|
8
|
+
"data": "any[]",
|
|
9
|
+
"onRowClick": "(row: any) => void",
|
|
10
|
+
"onSort": "(column: string, direction: 'asc'|'desc') => void",
|
|
11
|
+
"loading": "boolean",
|
|
12
|
+
"emptyMessage": "string"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
value = $bindable(''),
|
|
4
|
+
type = 'text',
|
|
5
|
+
placeholder = '',
|
|
6
|
+
label = '',
|
|
7
|
+
error = ''
|
|
8
|
+
}: {
|
|
9
|
+
value?: string;
|
|
10
|
+
type?: string;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
} = $props();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div class="form-control w-full">
|
|
18
|
+
{#if label}<label class="label"><span class="label-text">{label}</span></label>{/if}
|
|
19
|
+
<input {type} bind:value {placeholder} class="input input-bordered {error ? 'input-error' : ''}" />
|
|
20
|
+
{#if error}<label class="label"><span class="label-text-alt text-error">{error}</span></label>{/if}
|
|
21
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Input",
|
|
3
|
+
"category": "common",
|
|
4
|
+
"description": "Input - Svelte 5 component",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"packages": [],
|
|
7
|
+
"components": []
|
|
8
|
+
},
|
|
9
|
+
"props": {
|
|
10
|
+
"value": "$bindable<string>",
|
|
11
|
+
"type": "string",
|
|
12
|
+
"placeholder": "string",
|
|
13
|
+
"label": "string",
|
|
14
|
+
"error": "string"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
message = 'Loading...'
|
|
4
|
+
}: {
|
|
5
|
+
message?: string;
|
|
6
|
+
} = $props();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<div class="flex flex-col items-center justify-center p-8">
|
|
10
|
+
<span class="loading loading-spinner loading-lg"></span>
|
|
11
|
+
{#if message}<p class="mt-4 text-sm opacity-60">{message}</p>{/if}
|
|
12
|
+
</div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
children,
|
|
6
|
+
open = $bindable(false),
|
|
7
|
+
title = '',
|
|
8
|
+
onClose = null
|
|
9
|
+
}: {
|
|
10
|
+
children: Snippet;
|
|
11
|
+
open?: boolean;
|
|
12
|
+
title?: string;
|
|
13
|
+
onClose?: (() => void) | null;
|
|
14
|
+
} = $props();
|
|
15
|
+
|
|
16
|
+
function close() {
|
|
17
|
+
open = false;
|
|
18
|
+
onClose?.();
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
{#if open}
|
|
23
|
+
<dialog class="modal modal-open">
|
|
24
|
+
<div class="modal-box">
|
|
25
|
+
<h3 class="font-bold text-lg">{title}</h3>
|
|
26
|
+
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick={close}>✕</button>
|
|
27
|
+
<div class="py-4">{@render children()}</div>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="modal-backdrop" onclick={close}></div>
|
|
30
|
+
</dialog>
|
|
31
|
+
{/if}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Modal",
|
|
3
|
+
"category": "common",
|
|
4
|
+
"description": "Modal - Svelte 5 component",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"packages": [],
|
|
7
|
+
"components": []
|
|
8
|
+
},
|
|
9
|
+
"props": {
|
|
10
|
+
"open": "$bindable<boolean>",
|
|
11
|
+
"title": "string",
|
|
12
|
+
"onClose": "function"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
currentPage = 1,
|
|
4
|
+
totalPages = 1,
|
|
5
|
+
onPageChange
|
|
6
|
+
}: {
|
|
7
|
+
currentPage?: number;
|
|
8
|
+
totalPages?: number;
|
|
9
|
+
onPageChange: (page: number) => void;
|
|
10
|
+
} = $props();
|
|
11
|
+
|
|
12
|
+
const visiblePages = $derived(() => {
|
|
13
|
+
const pages = [];
|
|
14
|
+
const start = Math.max(1, currentPage - 2);
|
|
15
|
+
const end = Math.min(totalPages, currentPage + 2);
|
|
16
|
+
for (let i = start; i <= end; i++) pages.push(i);
|
|
17
|
+
return pages;
|
|
18
|
+
});
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<div class="join">
|
|
22
|
+
<button
|
|
23
|
+
class="join-item btn btn-sm"
|
|
24
|
+
disabled={currentPage === 1}
|
|
25
|
+
onclick={() => onPageChange(currentPage - 1)}
|
|
26
|
+
>«</button>
|
|
27
|
+
|
|
28
|
+
{#each visiblePages() as page}
|
|
29
|
+
<button
|
|
30
|
+
class="join-item btn btn-sm {page === currentPage ? 'btn-active' : ''}"
|
|
31
|
+
onclick={() => onPageChange(page)}
|
|
32
|
+
>{page}</button>
|
|
33
|
+
{/each}
|
|
34
|
+
|
|
35
|
+
<button
|
|
36
|
+
class="join-item btn btn-sm"
|
|
37
|
+
disabled={currentPage === totalPages}
|
|
38
|
+
onclick={() => onPageChange(currentPage + 1)}
|
|
39
|
+
>»</button>
|
|
40
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"Pagination","category":"common","dependencies":{"packages":[],"components":[]},"props":{}}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* PermissionGuard - Standalone permission gatekeeper
|
|
4
|
+
*
|
|
5
|
+
* Props-based design: no external dependencies
|
|
6
|
+
* Compatible with Casbin, RBAC, or any auth system
|
|
7
|
+
*/
|
|
8
|
+
import type { Snippet } from 'svelte';
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
children,
|
|
12
|
+
fallback,
|
|
13
|
+
canAccess = true, // Parent decides permission
|
|
14
|
+
isLoading = false,
|
|
15
|
+
showFallback = false,
|
|
16
|
+
errorMessage = 'Access Denied'
|
|
17
|
+
}: {
|
|
18
|
+
children: Snippet;
|
|
19
|
+
fallback?: Snippet;
|
|
20
|
+
canAccess?: boolean;
|
|
21
|
+
isLoading?: boolean;
|
|
22
|
+
showFallback?: boolean;
|
|
23
|
+
errorMessage?: string;
|
|
24
|
+
} = $props();
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
{#if isLoading}
|
|
28
|
+
<div class="flex items-center gap-2 p-4 text-sm opacity-50">
|
|
29
|
+
<svg class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
30
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
31
|
+
<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>
|
|
32
|
+
</svg>
|
|
33
|
+
Verifying permissions...
|
|
34
|
+
</div>
|
|
35
|
+
{:else if canAccess}
|
|
36
|
+
{@render children()}
|
|
37
|
+
{:else if showFallback}
|
|
38
|
+
{#if fallback}
|
|
39
|
+
{@render fallback()}
|
|
40
|
+
{:else}
|
|
41
|
+
<div class="border-base-300 bg-base-200/50 flex flex-col items-center gap-4 rounded-3xl border-2 border-dashed p-8 text-center">
|
|
42
|
+
<div class="bg-error/10 text-error rounded-full p-3 shadow-inner">
|
|
43
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
44
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
45
|
+
</svg>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="max-w-xs">
|
|
48
|
+
<h3 class="text-lg font-bold">Access Restricted</h3>
|
|
49
|
+
<p class="text-xs opacity-60 mt-1">{errorMessage}</p>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
{/if}
|
|
53
|
+
{/if}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "PermissionGuard",
|
|
3
|
+
"category": "common",
|
|
4
|
+
"description": "Standalone permission gatekeeper compatible with any auth system (Casbin, RBAC, etc.)",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"packages": [],
|
|
7
|
+
"components": []
|
|
8
|
+
},
|
|
9
|
+
"props": {
|
|
10
|
+
"canAccess": "boolean - Permission check result from parent",
|
|
11
|
+
"isLoading": "boolean - Show loading state",
|
|
12
|
+
"showFallback": "boolean - Display fallback UI on denial",
|
|
13
|
+
"errorMessage": "string - Custom error message",
|
|
14
|
+
"children": "Snippet - Protected content",
|
|
15
|
+
"fallback": "Snippet? - Custom fallback UI"
|
|
16
|
+
},
|
|
17
|
+
"examples": [
|
|
18
|
+
"basic-usage.svelte",
|
|
19
|
+
"with-casbin.svelte"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SearchableSelect - Dropdown with server-side search
|
|
4
|
+
* Zero dependencies, callback-based
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
value = $bindable(),
|
|
9
|
+
options = [],
|
|
10
|
+
onSearch = null,
|
|
11
|
+
placeholder = 'Search...',
|
|
12
|
+
disabled = false,
|
|
13
|
+
label = ''
|
|
14
|
+
}: {
|
|
15
|
+
value?: any;
|
|
16
|
+
options?: Array<{value: any; label: string}>;
|
|
17
|
+
onSearch?: ((term: string) => Promise<Array<{value: any; label: string}>>) | null;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
label?: string;
|
|
21
|
+
} = $props();
|
|
22
|
+
|
|
23
|
+
let searchTerm = $state('');
|
|
24
|
+
let isOpen = $state(false);
|
|
25
|
+
let filteredOptions = $state<Array<{value: any; label: string}>>([]);
|
|
26
|
+
let loading = $state(false);
|
|
27
|
+
|
|
28
|
+
// Display label for selected value
|
|
29
|
+
const selectedLabel = $derived(
|
|
30
|
+
options.find(o => o.value === value)?.label || filteredOptions.find(o => o.value === value)?.label || ''
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
async function handleSearch(term: string) {
|
|
34
|
+
searchTerm = term;
|
|
35
|
+
|
|
36
|
+
if (onSearch) {
|
|
37
|
+
// Server-side search
|
|
38
|
+
loading = true;
|
|
39
|
+
try {
|
|
40
|
+
filteredOptions = await onSearch(term);
|
|
41
|
+
} finally {
|
|
42
|
+
loading = false;
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
// Client-side filter
|
|
46
|
+
filteredOptions = options.filter(o =>
|
|
47
|
+
o.label.toLowerCase().includes(term.toLowerCase())
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function selectOption(opt: {value: any; label: string}) {
|
|
53
|
+
value = opt.value;
|
|
54
|
+
isOpen = false;
|
|
55
|
+
searchTerm = '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Initialize options on mount
|
|
59
|
+
$effect(() => {
|
|
60
|
+
if (options.length > 0 && filteredOptions.length === 0) {
|
|
61
|
+
filteredOptions = options;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<div class="relative w-full">
|
|
67
|
+
{#if label}
|
|
68
|
+
<label class="label">
|
|
69
|
+
<span class="label-text font-semibold">{label}</span>
|
|
70
|
+
</label>
|
|
71
|
+
{/if}
|
|
72
|
+
|
|
73
|
+
<!-- Trigger Button -->
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
class="input input-bordered w-full flex items-center justify-between"
|
|
77
|
+
{disabled}
|
|
78
|
+
onclick={() => !disabled && (isOpen = !isOpen)}
|
|
79
|
+
>
|
|
80
|
+
<span class={selectedLabel ? '' : 'opacity-50'}>
|
|
81
|
+
{selectedLabel || placeholder}
|
|
82
|
+
</span>
|
|
83
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
84
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
85
|
+
</svg>
|
|
86
|
+
</button>
|
|
87
|
+
|
|
88
|
+
{#if isOpen}
|
|
89
|
+
<!-- Dropdown -->
|
|
90
|
+
<div class="absolute z-50 w-full mt-1 bg-base-100 border border-base-200 rounded-lg shadow-lg">
|
|
91
|
+
<!-- Search Input -->
|
|
92
|
+
<div class="p-2">
|
|
93
|
+
<input
|
|
94
|
+
type="text"
|
|
95
|
+
bind:value={searchTerm}
|
|
96
|
+
oninput={(e) => handleSearch(e.currentTarget.value)}
|
|
97
|
+
placeholder="Type to search..."
|
|
98
|
+
class="input input-sm input-bordered w-full"
|
|
99
|
+
autofocus
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- Options List -->
|
|
104
|
+
<div class="max-h-60 overflow-auto">
|
|
105
|
+
{#if loading}
|
|
106
|
+
<div class="p-4 text-center text-sm opacity-50">Loading...</div>
|
|
107
|
+
{:else if filteredOptions.length === 0}
|
|
108
|
+
<div class="p-4 text-center text-sm opacity-50">No results</div>
|
|
109
|
+
{:else}
|
|
110
|
+
{#each filteredOptions as opt}
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
class="w-full text-left px-4 py-2 hover:bg-base-200 transition flex items-center justify-between"
|
|
114
|
+
onclick={() => selectOption(opt)}
|
|
115
|
+
>
|
|
116
|
+
<span>{opt.label}</span>
|
|
117
|
+
{#if opt.value === value}
|
|
118
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
119
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
|
120
|
+
</svg>
|
|
121
|
+
{/if}
|
|
122
|
+
</button>
|
|
123
|
+
{/each}
|
|
124
|
+
{/if}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<!-- Backdrop for closing dropdown -->
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
class="fixed inset-0 z-40"
|
|
132
|
+
onclick={() => isOpen = false}
|
|
133
|
+
tabindex="-1"
|
|
134
|
+
></button>
|
|
135
|
+
{/if}
|
|
136
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "SearchableSelect",
|
|
3
|
+
"category": "common",
|
|
4
|
+
"description": "Dropdown with client or server-side search",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"packages": [],
|
|
7
|
+
"components": []
|
|
8
|
+
},
|
|
9
|
+
"props": {
|
|
10
|
+
"value": "$bindable - Selected value",
|
|
11
|
+
"options": "Array<{value, label}> - Static options",
|
|
12
|
+
"onSearch": "(term: string) => Promise<Array<{value, label}>> - Async search callback",
|
|
13
|
+
"placeholder": "string",
|
|
14
|
+
"disabled": "boolean",
|
|
15
|
+
"label": "string"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
status,
|
|
4
|
+
label = ''
|
|
5
|
+
}: {
|
|
6
|
+
status: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
} = $props();
|
|
9
|
+
|
|
10
|
+
const statusColors: Record<string, string> = {
|
|
11
|
+
active: 'badge-success',
|
|
12
|
+
pending: 'badge-warning',
|
|
13
|
+
inactive: 'badge-ghost',
|
|
14
|
+
error: 'badge-error',
|
|
15
|
+
draft: 'badge-neutral'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const displayLabel = $derived(label || status);
|
|
19
|
+
const colorClass = $derived(statusColors[status.toLowerCase()] || 'badge-neutral');
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<span class="badge {colorClass}">{displayLabel}</span>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"StatusBadge","category":"common","dependencies":{"packages":[],"components":[]},"props":{}}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* RecentActivity - Dashboard widget showing recent system activity
|
|
4
|
+
*/
|
|
5
|
+
interface Activity {
|
|
6
|
+
id: string;
|
|
7
|
+
user: string;
|
|
8
|
+
action: string;
|
|
9
|
+
entity: string;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
activities = [],
|
|
15
|
+
loading = false,
|
|
16
|
+
onLoadMore = null
|
|
17
|
+
}: {
|
|
18
|
+
activities: Activity[];
|
|
19
|
+
loading?: boolean;
|
|
20
|
+
onLoadMore?: (() => void) | null;
|
|
21
|
+
} = $props();
|
|
22
|
+
|
|
23
|
+
function formatTime(timestamp: string): string {
|
|
24
|
+
const date = new Date(timestamp);
|
|
25
|
+
const now = new Date();
|
|
26
|
+
const diff = now.getTime() - date.getTime();
|
|
27
|
+
const minutes = Math.floor(diff / 60000);
|
|
28
|
+
const hours = Math.floor(diff / 3600000);
|
|
29
|
+
const days = Math.floor(diff / 86400000);
|
|
30
|
+
|
|
31
|
+
if (minutes < 1) return 'Just now';
|
|
32
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
33
|
+
if (hours < 24) return `${hours}h ago`;
|
|
34
|
+
return `${days}d ago`;
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<div class="space-y-4">
|
|
39
|
+
<h3 class="font-semibold">Recent Activity</h3>
|
|
40
|
+
|
|
41
|
+
<div class="space-y-2">
|
|
42
|
+
{#if loading}
|
|
43
|
+
<div class="flex justify-center py-4"><span class="loading loading-spinner"></span></div>
|
|
44
|
+
{:else if activities.length === 0}
|
|
45
|
+
<div class="text-center py-4 text-sm opacity-50">No recent activity</div>
|
|
46
|
+
{:else}
|
|
47
|
+
{#each activities as activity}
|
|
48
|
+
<div class="flex items-start gap-3 p-3 bg-base-200 rounded-lg">
|
|
49
|
+
<div class="avatar placeholder">
|
|
50
|
+
<div class="w-8 rounded-full bg-neutral text-neutral-content text-xs">
|
|
51
|
+
{activity.user.substring(0, 2).toUpperCase()}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="flex-1 min-w-0">
|
|
55
|
+
<p class="text-sm">
|
|
56
|
+
<span class="font-medium">{activity.user}</span>
|
|
57
|
+
{' '}{activity.action}{' '}
|
|
58
|
+
<span class="font-medium">{activity.entity}</span>
|
|
59
|
+
</p>
|
|
60
|
+
<p class="text-xs opacity-60">{formatTime(activity.timestamp)}</p>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
{/each}
|
|
64
|
+
|
|
65
|
+
{#if onLoadMore}
|
|
66
|
+
<button class="btn btn-sm btn-ghost w-full" onclick={onLoadMore}>Load More</button>
|
|
67
|
+
{/if}
|
|
68
|
+
{/if}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"RecentActivity","category":"dashboard","dependencies":{"packages":[],"components":[]},"props":{}}
|