@classic-homes/theme-svelte 0.1.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/README.md +305 -0
- package/dist/lib/components/Alert.svelte +51 -0
- package/dist/lib/components/Alert.svelte.d.ts +9 -0
- package/dist/lib/components/AlertDescription.svelte +16 -0
- package/dist/lib/components/AlertDescription.svelte.d.ts +9 -0
- package/dist/lib/components/AlertDialog.svelte +136 -0
- package/dist/lib/components/AlertDialog.svelte.d.ts +79 -0
- package/dist/lib/components/AlertTitle.svelte +16 -0
- package/dist/lib/components/AlertTitle.svelte.d.ts +9 -0
- package/dist/lib/components/Avatar.svelte +56 -0
- package/dist/lib/components/Avatar.svelte.d.ts +26 -0
- package/dist/lib/components/AvatarFallback.svelte +31 -0
- package/dist/lib/components/AvatarFallback.svelte.d.ts +17 -0
- package/dist/lib/components/AvatarImage.svelte +29 -0
- package/dist/lib/components/AvatarImage.svelte.d.ts +12 -0
- package/dist/lib/components/Badge.svelte +73 -0
- package/dist/lib/components/Badge.svelte.d.ts +11 -0
- package/dist/lib/components/Button.svelte +130 -0
- package/dist/lib/components/Button.svelte.d.ts +17 -0
- package/dist/lib/components/Card.svelte +58 -0
- package/dist/lib/components/Card.svelte.d.ts +26 -0
- package/dist/lib/components/CardContent.svelte +16 -0
- package/dist/lib/components/CardContent.svelte.d.ts +9 -0
- package/dist/lib/components/CardDescription.svelte +16 -0
- package/dist/lib/components/CardDescription.svelte.d.ts +9 -0
- package/dist/lib/components/CardFooter.svelte +16 -0
- package/dist/lib/components/CardFooter.svelte.d.ts +9 -0
- package/dist/lib/components/CardHeader.svelte +16 -0
- package/dist/lib/components/CardHeader.svelte.d.ts +9 -0
- package/dist/lib/components/CardTitle.svelte +16 -0
- package/dist/lib/components/CardTitle.svelte.d.ts +9 -0
- package/dist/lib/components/Checkbox.svelte +65 -0
- package/dist/lib/components/Checkbox.svelte.d.ts +14 -0
- package/dist/lib/components/DataTable.svelte +334 -0
- package/dist/lib/components/DataTable.svelte.d.ts +103 -0
- package/dist/lib/components/Dialog.svelte +111 -0
- package/dist/lib/components/Dialog.svelte.d.ts +22 -0
- package/dist/lib/components/DropdownMenu.svelte +135 -0
- package/dist/lib/components/DropdownMenu.svelte.d.ts +33 -0
- package/dist/lib/components/FileUpload.svelte +448 -0
- package/dist/lib/components/FileUpload.svelte.d.ts +42 -0
- package/dist/lib/components/FormField.svelte +134 -0
- package/dist/lib/components/FormField.svelte.d.ts +37 -0
- package/dist/lib/components/Input.svelte +61 -0
- package/dist/lib/components/Input.svelte.d.ts +19 -0
- package/dist/lib/components/Label.svelte +33 -0
- package/dist/lib/components/Label.svelte.d.ts +11 -0
- package/dist/lib/components/LoadingLogo.svelte +124 -0
- package/dist/lib/components/LoadingLogo.svelte.d.ts +16 -0
- package/dist/lib/components/LogoMain.svelte +237 -0
- package/dist/lib/components/LogoMain.svelte.d.ts +20 -0
- package/dist/lib/components/PageHeader.svelte +90 -0
- package/dist/lib/components/PageHeader.svelte.d.ts +28 -0
- package/dist/lib/components/Section.svelte +44 -0
- package/dist/lib/components/Section.svelte.d.ts +28 -0
- package/dist/lib/components/Select.svelte +174 -0
- package/dist/lib/components/Select.svelte.d.ts +32 -0
- package/dist/lib/components/Separator.svelte +29 -0
- package/dist/lib/components/Separator.svelte.d.ts +9 -0
- package/dist/lib/components/Skeleton.svelte +35 -0
- package/dist/lib/components/Skeleton.svelte.d.ts +7 -0
- package/dist/lib/components/Spinner.svelte +50 -0
- package/dist/lib/components/Spinner.svelte.d.ts +8 -0
- package/dist/lib/components/Switch.svelte +56 -0
- package/dist/lib/components/Switch.svelte.d.ts +14 -0
- package/dist/lib/components/TabPanel.svelte +44 -0
- package/dist/lib/components/TabPanel.svelte.d.ts +12 -0
- package/dist/lib/components/Tabs.svelte +125 -0
- package/dist/lib/components/Tabs.svelte.d.ts +19 -0
- package/dist/lib/components/Textarea.svelte +54 -0
- package/dist/lib/components/Textarea.svelte.d.ts +16 -0
- package/dist/lib/components/Toast.svelte +116 -0
- package/dist/lib/components/Toast.svelte.d.ts +12 -0
- package/dist/lib/components/ToastContainer.svelte +56 -0
- package/dist/lib/components/ToastContainer.svelte.d.ts +8 -0
- package/dist/lib/components/Tooltip.svelte +55 -0
- package/dist/lib/components/Tooltip.svelte.d.ts +18 -0
- package/dist/lib/components/layout/AppShell.svelte +82 -0
- package/dist/lib/components/layout/AppShell.svelte.d.ts +44 -0
- package/dist/lib/components/layout/DashboardLayout.svelte +248 -0
- package/dist/lib/components/layout/DashboardLayout.svelte.d.ts +62 -0
- package/dist/lib/components/layout/Footer.svelte +130 -0
- package/dist/lib/components/layout/Footer.svelte.d.ts +32 -0
- package/dist/lib/components/layout/FormPageLayout.svelte +92 -0
- package/dist/lib/components/layout/FormPageLayout.svelte.d.ts +33 -0
- package/dist/lib/components/layout/Header.svelte +94 -0
- package/dist/lib/components/layout/Header.svelte.d.ts +30 -0
- package/dist/lib/components/layout/PublicLayout.svelte +180 -0
- package/dist/lib/components/layout/PublicLayout.svelte.d.ts +39 -0
- package/dist/lib/components/layout/QuickLinks.svelte +112 -0
- package/dist/lib/components/layout/QuickLinks.svelte.d.ts +27 -0
- package/dist/lib/components/layout/Sidebar.svelte +243 -0
- package/dist/lib/components/layout/Sidebar.svelte.d.ts +48 -0
- package/dist/lib/composables/index.d.ts +8 -0
- package/dist/lib/composables/index.js +10 -0
- package/dist/lib/composables/useAsync.svelte.d.ts +102 -0
- package/dist/lib/composables/useAsync.svelte.js +210 -0
- package/dist/lib/composables/useForm.svelte.d.ts +123 -0
- package/dist/lib/composables/useForm.svelte.js +245 -0
- package/dist/lib/index.d.ts +65 -0
- package/dist/lib/index.js +83 -0
- package/dist/lib/performance.d.ts +79 -0
- package/dist/lib/performance.js +170 -0
- package/dist/lib/schemas/auth.d.ts +410 -0
- package/dist/lib/schemas/auth.js +216 -0
- package/dist/lib/schemas/common.d.ts +267 -0
- package/dist/lib/schemas/common.js +268 -0
- package/dist/lib/schemas/index.d.ts +24 -0
- package/dist/lib/schemas/index.js +32 -0
- package/dist/lib/stores/sidebar.svelte.d.ts +25 -0
- package/dist/lib/stores/sidebar.svelte.js +38 -0
- package/dist/lib/stores/theme.svelte.d.ts +72 -0
- package/dist/lib/stores/theme.svelte.js +150 -0
- package/dist/lib/stores/toast.svelte.d.ts +62 -0
- package/dist/lib/stores/toast.svelte.js +93 -0
- package/dist/lib/types/components.d.ts +85 -0
- package/dist/lib/types/components.js +7 -0
- package/dist/lib/types/layout.d.ts +258 -0
- package/dist/lib/types/layout.js +7 -0
- package/dist/lib/utils.d.ts +6 -0
- package/dist/lib/utils.js +9 -0
- package/dist/lib/validation.d.ts +101 -0
- package/dist/lib/validation.js +170 -0
- package/package.json +56 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends Record<string, unknown>">
|
|
2
|
+
/**
|
|
3
|
+
* DataTable - A sortable, accessible data table component
|
|
4
|
+
*
|
|
5
|
+
* @template T - The type of data objects in each row. Must be an object type.
|
|
6
|
+
*
|
|
7
|
+
* ## Features
|
|
8
|
+
* - Column definitions with headers and accessors
|
|
9
|
+
* - Client-side sorting (click column headers)
|
|
10
|
+
* - External sort control via onSort callback
|
|
11
|
+
* - Loading state with skeleton rows
|
|
12
|
+
* - Empty state message
|
|
13
|
+
* - Responsive horizontal scroll
|
|
14
|
+
* - Full accessibility support (ARIA attributes, keyboard navigation)
|
|
15
|
+
* - Custom row rendering via snippet
|
|
16
|
+
* - Memoized sorting for performance
|
|
17
|
+
*
|
|
18
|
+
* ## Basic Usage
|
|
19
|
+
* ```svelte
|
|
20
|
+
* <DataTable
|
|
21
|
+
* data={users}
|
|
22
|
+
* columns={[
|
|
23
|
+
* { id: 'name', header: 'Name', accessor: 'name', sortable: true },
|
|
24
|
+
* { id: 'email', header: 'Email', accessor: 'email' },
|
|
25
|
+
* ]}
|
|
26
|
+
* />
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* ## Custom Cell Formatting
|
|
30
|
+
* ```svelte
|
|
31
|
+
* columns={[
|
|
32
|
+
* {
|
|
33
|
+
* id: 'date',
|
|
34
|
+
* header: 'Created',
|
|
35
|
+
* accessor: 'createdAt',
|
|
36
|
+
* format: (value) => new Date(value).toLocaleDateString(),
|
|
37
|
+
* },
|
|
38
|
+
* ]}
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* ## Server-Side Sorting
|
|
42
|
+
* ```svelte
|
|
43
|
+
* <DataTable
|
|
44
|
+
* {data}
|
|
45
|
+
* {columns}
|
|
46
|
+
* sortColumn={serverSort.column}
|
|
47
|
+
* sortDirection={serverSort.direction}
|
|
48
|
+
* onSort={(col, dir) => fetchSorted(col, dir)}
|
|
49
|
+
* />
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @see DataTableColumn for column configuration options
|
|
53
|
+
*/
|
|
54
|
+
import type { DataTableColumn } from '../types/components.js';
|
|
55
|
+
import type { Snippet } from 'svelte';
|
|
56
|
+
import { cn } from '../utils.js';
|
|
57
|
+
import { validateNonEmptyArray } from '../validation.js';
|
|
58
|
+
import Skeleton from './Skeleton.svelte';
|
|
59
|
+
|
|
60
|
+
interface Props {
|
|
61
|
+
/** Array of data rows */
|
|
62
|
+
data: T[];
|
|
63
|
+
/** Column definitions */
|
|
64
|
+
columns: DataTableColumn<T>[];
|
|
65
|
+
/** Loading state */
|
|
66
|
+
loading?: boolean;
|
|
67
|
+
/** Number of skeleton rows to show when loading */
|
|
68
|
+
loadingRows?: number;
|
|
69
|
+
/** Message when no data */
|
|
70
|
+
emptyMessage?: string;
|
|
71
|
+
/** Current sort column ID */
|
|
72
|
+
sortColumn?: string;
|
|
73
|
+
/** Current sort direction */
|
|
74
|
+
sortDirection?: 'asc' | 'desc';
|
|
75
|
+
/** Callback when sort changes */
|
|
76
|
+
onSort?: (column: string, direction: 'asc' | 'desc') => void;
|
|
77
|
+
/** Callback when row is clicked */
|
|
78
|
+
onRowClick?: (row: T) => void;
|
|
79
|
+
/** Caption for accessibility */
|
|
80
|
+
caption?: string;
|
|
81
|
+
/** Custom row snippet for complete control */
|
|
82
|
+
row?: Snippet<[T, number]>;
|
|
83
|
+
/** Additional classes */
|
|
84
|
+
class?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let {
|
|
88
|
+
data,
|
|
89
|
+
columns,
|
|
90
|
+
loading = false,
|
|
91
|
+
loadingRows = 5,
|
|
92
|
+
emptyMessage = 'No data available',
|
|
93
|
+
sortColumn = $bindable(undefined),
|
|
94
|
+
sortDirection = $bindable('asc'),
|
|
95
|
+
onSort,
|
|
96
|
+
onRowClick,
|
|
97
|
+
caption,
|
|
98
|
+
row: rowSnippet,
|
|
99
|
+
class: className,
|
|
100
|
+
}: Props = $props();
|
|
101
|
+
|
|
102
|
+
// Validate props in development
|
|
103
|
+
$effect(() => {
|
|
104
|
+
validateNonEmptyArray(columns, 'columns', 'DataTable');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Get cell value from row
|
|
108
|
+
function getCellValue(row: T, column: DataTableColumn<T>): unknown {
|
|
109
|
+
if (typeof column.accessor === 'function') {
|
|
110
|
+
return column.accessor(row);
|
|
111
|
+
}
|
|
112
|
+
return row[column.accessor as keyof T];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Format cell value
|
|
116
|
+
function formatCellValue(row: T, column: DataTableColumn<T>): string {
|
|
117
|
+
const value = getCellValue(row, column);
|
|
118
|
+
if (column.format) {
|
|
119
|
+
return column.format(value, row);
|
|
120
|
+
}
|
|
121
|
+
if (value === null || value === undefined) {
|
|
122
|
+
return '';
|
|
123
|
+
}
|
|
124
|
+
return String(value);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Memoization cache for sorted data
|
|
129
|
+
* Tracks previous inputs to avoid unnecessary re-sorts
|
|
130
|
+
*/
|
|
131
|
+
let cachedData: T[] = [];
|
|
132
|
+
let cachedSortColumn: string | undefined;
|
|
133
|
+
let cachedSortDirection: 'asc' | 'desc' = 'asc';
|
|
134
|
+
let cachedResult: T[] = [];
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Sort data internally if no onSort callback
|
|
138
|
+
* Uses memoization to avoid re-sorting when inputs haven't changed
|
|
139
|
+
*
|
|
140
|
+
* Future enhancement path for virtualization:
|
|
141
|
+
* 1. Add `virtualize?: boolean` prop
|
|
142
|
+
* 2. Add `rowHeight?: number` prop (default: 48)
|
|
143
|
+
* 3. Add `overscan?: number` prop (default: 5)
|
|
144
|
+
* 4. When virtualize=true, render only visible rows + overscan
|
|
145
|
+
* 5. Use IntersectionObserver or scroll position for windowing
|
|
146
|
+
*/
|
|
147
|
+
const sortedData = $derived.by(() => {
|
|
148
|
+
// If external sort handler or no sort column, return data as-is
|
|
149
|
+
if (!sortColumn || onSort) {
|
|
150
|
+
return data;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check if we can use cached result (same inputs)
|
|
154
|
+
const inputsUnchanged =
|
|
155
|
+
data === cachedData &&
|
|
156
|
+
sortColumn === cachedSortColumn &&
|
|
157
|
+
sortDirection === cachedSortDirection;
|
|
158
|
+
|
|
159
|
+
if (inputsUnchanged && cachedResult.length > 0) {
|
|
160
|
+
return cachedResult;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Find the column definition
|
|
164
|
+
const column = columns.find((c) => c.id === sortColumn);
|
|
165
|
+
if (!column) {
|
|
166
|
+
return data;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Perform sort with improved comparison
|
|
170
|
+
const sorted = [...data].sort((a, b) => {
|
|
171
|
+
const aVal = getCellValue(a, column);
|
|
172
|
+
const bVal = getCellValue(b, column);
|
|
173
|
+
|
|
174
|
+
// Handle null/undefined - nulls sort to end
|
|
175
|
+
if (aVal == null && bVal == null) return 0;
|
|
176
|
+
if (aVal == null) return sortDirection === 'asc' ? 1 : -1;
|
|
177
|
+
if (bVal == null) return sortDirection === 'asc' ? -1 : 1;
|
|
178
|
+
|
|
179
|
+
// String comparison with locale support and numeric sorting
|
|
180
|
+
if (typeof aVal === 'string' && typeof bVal === 'string') {
|
|
181
|
+
const result = aVal.localeCompare(bVal, undefined, {
|
|
182
|
+
numeric: true,
|
|
183
|
+
sensitivity: 'base',
|
|
184
|
+
});
|
|
185
|
+
return sortDirection === 'asc' ? result : -result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Date comparison
|
|
189
|
+
if (aVal instanceof Date && bVal instanceof Date) {
|
|
190
|
+
const diff = aVal.getTime() - bVal.getTime();
|
|
191
|
+
return sortDirection === 'asc' ? diff : -diff;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Numeric/generic comparison
|
|
195
|
+
if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
|
|
196
|
+
if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
|
|
197
|
+
return 0;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Update cache
|
|
201
|
+
cachedData = data;
|
|
202
|
+
cachedSortColumn = sortColumn;
|
|
203
|
+
cachedSortDirection = sortDirection;
|
|
204
|
+
cachedResult = sorted;
|
|
205
|
+
|
|
206
|
+
return sorted;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Handle column header click for sorting
|
|
210
|
+
function handleSort(column: DataTableColumn<T>) {
|
|
211
|
+
if (!column.sortable) return;
|
|
212
|
+
|
|
213
|
+
const newDirection = sortColumn === column.id && sortDirection === 'asc' ? 'desc' : 'asc';
|
|
214
|
+
sortColumn = column.id;
|
|
215
|
+
sortDirection = newDirection;
|
|
216
|
+
|
|
217
|
+
if (onSort) {
|
|
218
|
+
onSort(column.id, newDirection);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Handle row click
|
|
223
|
+
function handleRowClick(row: T) {
|
|
224
|
+
if (onRowClick) {
|
|
225
|
+
onRowClick(row);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Get alignment class
|
|
230
|
+
function getAlignClass(align?: 'left' | 'center' | 'right'): string {
|
|
231
|
+
switch (align) {
|
|
232
|
+
case 'center':
|
|
233
|
+
return 'text-center';
|
|
234
|
+
case 'right':
|
|
235
|
+
return 'text-right';
|
|
236
|
+
default:
|
|
237
|
+
return 'text-left';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
</script>
|
|
241
|
+
|
|
242
|
+
<div class={cn('overflow-x-auto rounded-lg border', className)}>
|
|
243
|
+
<table class="w-full text-sm">
|
|
244
|
+
{#if caption}
|
|
245
|
+
<caption class="sr-only">{caption}</caption>
|
|
246
|
+
{/if}
|
|
247
|
+
|
|
248
|
+
<thead class="border-b bg-muted/50">
|
|
249
|
+
<tr>
|
|
250
|
+
{#each columns as column (column.id)}
|
|
251
|
+
<th
|
|
252
|
+
scope="col"
|
|
253
|
+
class={cn(
|
|
254
|
+
'px-4 py-3 font-medium text-muted-foreground',
|
|
255
|
+
getAlignClass(column.align),
|
|
256
|
+
column.sortable && 'cursor-pointer select-none hover:bg-muted/80'
|
|
257
|
+
)}
|
|
258
|
+
style={column.width ? `width: ${column.width}` : undefined}
|
|
259
|
+
aria-sort={sortColumn === column.id
|
|
260
|
+
? sortDirection === 'asc'
|
|
261
|
+
? 'ascending'
|
|
262
|
+
: 'descending'
|
|
263
|
+
: undefined}
|
|
264
|
+
>
|
|
265
|
+
{#if column.sortable}
|
|
266
|
+
<button
|
|
267
|
+
type="button"
|
|
268
|
+
class="inline-flex w-full items-center justify-between gap-2"
|
|
269
|
+
onclick={() => handleSort(column)}
|
|
270
|
+
>
|
|
271
|
+
<span>{column.header}</span>
|
|
272
|
+
<svg
|
|
273
|
+
class={cn(
|
|
274
|
+
'h-4 w-4 transition-transform',
|
|
275
|
+
sortColumn !== column.id && 'opacity-0',
|
|
276
|
+
sortColumn === column.id && sortDirection === 'desc' && 'rotate-180'
|
|
277
|
+
)}
|
|
278
|
+
fill="none"
|
|
279
|
+
viewBox="0 0 24 24"
|
|
280
|
+
stroke="currentColor"
|
|
281
|
+
stroke-width="2"
|
|
282
|
+
>
|
|
283
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7" />
|
|
284
|
+
</svg>
|
|
285
|
+
</button>
|
|
286
|
+
{:else}
|
|
287
|
+
{column.header}
|
|
288
|
+
{/if}
|
|
289
|
+
</th>
|
|
290
|
+
{/each}
|
|
291
|
+
</tr>
|
|
292
|
+
</thead>
|
|
293
|
+
|
|
294
|
+
<tbody class="divide-y">
|
|
295
|
+
{#if loading}
|
|
296
|
+
{#each Array(loadingRows) as _, i}
|
|
297
|
+
<tr>
|
|
298
|
+
{#each columns as column (column.id)}
|
|
299
|
+
<td class="px-4 py-3">
|
|
300
|
+
<Skeleton class="h-4 w-3/4" />
|
|
301
|
+
</td>
|
|
302
|
+
{/each}
|
|
303
|
+
</tr>
|
|
304
|
+
{/each}
|
|
305
|
+
{:else if sortedData.length === 0}
|
|
306
|
+
<tr>
|
|
307
|
+
<td colspan={columns.length} class="px-4 py-8 text-center text-muted-foreground">
|
|
308
|
+
{emptyMessage}
|
|
309
|
+
</td>
|
|
310
|
+
</tr>
|
|
311
|
+
{:else}
|
|
312
|
+
{#each sortedData as row, index (index)}
|
|
313
|
+
{#if rowSnippet}
|
|
314
|
+
{@render rowSnippet(row, index)}
|
|
315
|
+
{:else}
|
|
316
|
+
<tr
|
|
317
|
+
class={cn(
|
|
318
|
+
'bg-card transition-colors',
|
|
319
|
+
onRowClick && 'cursor-pointer hover:bg-muted/50'
|
|
320
|
+
)}
|
|
321
|
+
onclick={() => handleRowClick(row)}
|
|
322
|
+
>
|
|
323
|
+
{#each columns as column (column.id)}
|
|
324
|
+
<td class={cn('px-4 py-3', getAlignClass(column.align))}>
|
|
325
|
+
{formatCellValue(row, column)}
|
|
326
|
+
</td>
|
|
327
|
+
{/each}
|
|
328
|
+
</tr>
|
|
329
|
+
{/if}
|
|
330
|
+
{/each}
|
|
331
|
+
{/if}
|
|
332
|
+
</tbody>
|
|
333
|
+
</table>
|
|
334
|
+
</div>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataTable - A sortable, accessible data table component
|
|
3
|
+
*
|
|
4
|
+
* @template T - The type of data objects in each row. Must be an object type.
|
|
5
|
+
*
|
|
6
|
+
* ## Features
|
|
7
|
+
* - Column definitions with headers and accessors
|
|
8
|
+
* - Client-side sorting (click column headers)
|
|
9
|
+
* - External sort control via onSort callback
|
|
10
|
+
* - Loading state with skeleton rows
|
|
11
|
+
* - Empty state message
|
|
12
|
+
* - Responsive horizontal scroll
|
|
13
|
+
* - Full accessibility support (ARIA attributes, keyboard navigation)
|
|
14
|
+
* - Custom row rendering via snippet
|
|
15
|
+
* - Memoized sorting for performance
|
|
16
|
+
*
|
|
17
|
+
* ## Basic Usage
|
|
18
|
+
* ```svelte
|
|
19
|
+
* <DataTable
|
|
20
|
+
* data={users}
|
|
21
|
+
* columns={[
|
|
22
|
+
* { id: 'name', header: 'Name', accessor: 'name', sortable: true },
|
|
23
|
+
* { id: 'email', header: 'Email', accessor: 'email' },
|
|
24
|
+
* ]}
|
|
25
|
+
* />
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* ## Custom Cell Formatting
|
|
29
|
+
* ```svelte
|
|
30
|
+
* columns={[
|
|
31
|
+
* {
|
|
32
|
+
* id: 'date',
|
|
33
|
+
* header: 'Created',
|
|
34
|
+
* accessor: 'createdAt',
|
|
35
|
+
* format: (value) => new Date(value).toLocaleDateString(),
|
|
36
|
+
* },
|
|
37
|
+
* ]}
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* ## Server-Side Sorting
|
|
41
|
+
* ```svelte
|
|
42
|
+
* <DataTable
|
|
43
|
+
* {data}
|
|
44
|
+
* {columns}
|
|
45
|
+
* sortColumn={serverSort.column}
|
|
46
|
+
* sortDirection={serverSort.direction}
|
|
47
|
+
* onSort={(col, dir) => fetchSorted(col, dir)}
|
|
48
|
+
* />
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @see DataTableColumn for column configuration options
|
|
52
|
+
*/
|
|
53
|
+
import type { DataTableColumn } from '../types/components.js';
|
|
54
|
+
import type { Snippet } from 'svelte';
|
|
55
|
+
declare function $$render<T extends Record<string, unknown>>(): {
|
|
56
|
+
props: {
|
|
57
|
+
/** Array of data rows */
|
|
58
|
+
data: T[];
|
|
59
|
+
/** Column definitions */
|
|
60
|
+
columns: DataTableColumn<T>[];
|
|
61
|
+
/** Loading state */
|
|
62
|
+
loading?: boolean;
|
|
63
|
+
/** Number of skeleton rows to show when loading */
|
|
64
|
+
loadingRows?: number;
|
|
65
|
+
/** Message when no data */
|
|
66
|
+
emptyMessage?: string;
|
|
67
|
+
/** Current sort column ID */
|
|
68
|
+
sortColumn?: string;
|
|
69
|
+
/** Current sort direction */
|
|
70
|
+
sortDirection?: "asc" | "desc";
|
|
71
|
+
/** Callback when sort changes */
|
|
72
|
+
onSort?: (column: string, direction: "asc" | "desc") => void;
|
|
73
|
+
/** Callback when row is clicked */
|
|
74
|
+
onRowClick?: (row: T) => void;
|
|
75
|
+
/** Caption for accessibility */
|
|
76
|
+
caption?: string;
|
|
77
|
+
/** Custom row snippet for complete control */
|
|
78
|
+
row?: Snippet<[T, number]>;
|
|
79
|
+
/** Additional classes */
|
|
80
|
+
class?: string;
|
|
81
|
+
};
|
|
82
|
+
exports: {};
|
|
83
|
+
bindings: "sortColumn" | "sortDirection";
|
|
84
|
+
slots: {};
|
|
85
|
+
events: {};
|
|
86
|
+
};
|
|
87
|
+
declare class __sveltets_Render<T extends Record<string, unknown>> {
|
|
88
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
89
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
90
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
91
|
+
bindings(): "sortColumn" | "sortDirection";
|
|
92
|
+
exports(): {};
|
|
93
|
+
}
|
|
94
|
+
interface $$IsomorphicComponent {
|
|
95
|
+
new <T extends Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
96
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
97
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
98
|
+
<T extends Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
99
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
100
|
+
}
|
|
101
|
+
declare const DataTable: $$IsomorphicComponent;
|
|
102
|
+
type DataTable<T extends Record<string, unknown>> = InstanceType<typeof DataTable<T>>;
|
|
103
|
+
export default DataTable;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Dialog as DialogPrimitive } from 'bits-ui';
|
|
3
|
+
import { cn } from '../utils.js';
|
|
4
|
+
import type { Snippet } from 'svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
/** Whether the dialog is open */
|
|
8
|
+
open?: boolean;
|
|
9
|
+
/** Callback when open state changes */
|
|
10
|
+
onOpenChange?: (open: boolean) => void;
|
|
11
|
+
/** Dialog title */
|
|
12
|
+
title?: string;
|
|
13
|
+
/** Dialog description */
|
|
14
|
+
description?: string;
|
|
15
|
+
/** Additional class for dialog content */
|
|
16
|
+
class?: string;
|
|
17
|
+
/** Content inside the dialog */
|
|
18
|
+
children: Snippet;
|
|
19
|
+
/** Optional trigger element */
|
|
20
|
+
trigger?: Snippet;
|
|
21
|
+
/** Optional footer element */
|
|
22
|
+
footer?: Snippet;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
open = $bindable(false),
|
|
27
|
+
onOpenChange,
|
|
28
|
+
title,
|
|
29
|
+
description,
|
|
30
|
+
class: className,
|
|
31
|
+
children,
|
|
32
|
+
trigger,
|
|
33
|
+
footer,
|
|
34
|
+
}: Props = $props();
|
|
35
|
+
|
|
36
|
+
function handleOpenChange(newOpen: boolean) {
|
|
37
|
+
open = newOpen;
|
|
38
|
+
onOpenChange?.(newOpen);
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<DialogPrimitive.Root bind:open onOpenChange={handleOpenChange}>
|
|
43
|
+
{#if trigger}
|
|
44
|
+
<DialogPrimitive.Trigger asChild>
|
|
45
|
+
{#snippet child({ props })}
|
|
46
|
+
<span {...props}>
|
|
47
|
+
{@render trigger()}
|
|
48
|
+
</span>
|
|
49
|
+
{/snippet}
|
|
50
|
+
</DialogPrimitive.Trigger>
|
|
51
|
+
{/if}
|
|
52
|
+
|
|
53
|
+
<DialogPrimitive.Portal>
|
|
54
|
+
<DialogPrimitive.Overlay
|
|
55
|
+
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
|
56
|
+
/>
|
|
57
|
+
<DialogPrimitive.Content
|
|
58
|
+
class={cn(
|
|
59
|
+
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
|
60
|
+
className
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
{#if title || description}
|
|
64
|
+
<div class="flex flex-col space-y-1.5 text-center sm:text-left">
|
|
65
|
+
{#if title}
|
|
66
|
+
<DialogPrimitive.Title class="text-lg font-semibold leading-none tracking-tight">
|
|
67
|
+
{title}
|
|
68
|
+
</DialogPrimitive.Title>
|
|
69
|
+
{/if}
|
|
70
|
+
{#if description}
|
|
71
|
+
<DialogPrimitive.Description class="text-sm text-muted-foreground">
|
|
72
|
+
{description}
|
|
73
|
+
</DialogPrimitive.Description>
|
|
74
|
+
{/if}
|
|
75
|
+
</div>
|
|
76
|
+
{/if}
|
|
77
|
+
|
|
78
|
+
<div class="dialog-content">
|
|
79
|
+
{@render children()}
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{#if footer}
|
|
83
|
+
<div class="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2">
|
|
84
|
+
{@render footer()}
|
|
85
|
+
</div>
|
|
86
|
+
{/if}
|
|
87
|
+
|
|
88
|
+
<DialogPrimitive.Close
|
|
89
|
+
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
|
90
|
+
aria-label="Close dialog"
|
|
91
|
+
>
|
|
92
|
+
<svg
|
|
93
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
94
|
+
width="24"
|
|
95
|
+
height="24"
|
|
96
|
+
viewBox="0 0 24 24"
|
|
97
|
+
fill="none"
|
|
98
|
+
stroke="currentColor"
|
|
99
|
+
stroke-width="2"
|
|
100
|
+
stroke-linecap="round"
|
|
101
|
+
stroke-linejoin="round"
|
|
102
|
+
class="h-4 w-4"
|
|
103
|
+
>
|
|
104
|
+
<path d="M18 6 6 18" />
|
|
105
|
+
<path d="m6 6 12 12" />
|
|
106
|
+
</svg>
|
|
107
|
+
<span class="sr-only">Close</span>
|
|
108
|
+
</DialogPrimitive.Close>
|
|
109
|
+
</DialogPrimitive.Content>
|
|
110
|
+
</DialogPrimitive.Portal>
|
|
111
|
+
</DialogPrimitive.Root>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Whether the dialog is open */
|
|
4
|
+
open?: boolean;
|
|
5
|
+
/** Callback when open state changes */
|
|
6
|
+
onOpenChange?: (open: boolean) => void;
|
|
7
|
+
/** Dialog title */
|
|
8
|
+
title?: string;
|
|
9
|
+
/** Dialog description */
|
|
10
|
+
description?: string;
|
|
11
|
+
/** Additional class for dialog content */
|
|
12
|
+
class?: string;
|
|
13
|
+
/** Content inside the dialog */
|
|
14
|
+
children: Snippet;
|
|
15
|
+
/** Optional trigger element */
|
|
16
|
+
trigger?: Snippet;
|
|
17
|
+
/** Optional footer element */
|
|
18
|
+
footer?: Snippet;
|
|
19
|
+
}
|
|
20
|
+
declare const Dialog: import("svelte").Component<Props, {}, "open">;
|
|
21
|
+
type Dialog = ReturnType<typeof Dialog>;
|
|
22
|
+
export default Dialog;
|