@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,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,13 @@
1
+ {
2
+ "name": "Card",
3
+ "category": "common",
4
+ "description": "Card - Svelte 5 component",
5
+ "dependencies": {
6
+ "packages": [],
7
+ "components": []
8
+ },
9
+ "props": {
10
+ "title": "string",
11
+ "actions": "Snippet | null"
12
+ }
13
+ }
@@ -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,12 @@
1
+ {
2
+ "name": "Loading",
3
+ "category": "common",
4
+ "description": "Loading - Svelte 5 component",
5
+ "dependencies": {
6
+ "packages": [],
7
+ "components": []
8
+ },
9
+ "props": {
10
+ "message": "string"
11
+ }
12
+ }
@@ -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":{}}