@synthaxai/ui 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 (185) hide show
  1. package/README.md +262 -0
  2. package/dist/app.css +2 -0
  3. package/dist/app.html +12 -0
  4. package/dist/data-display/DataTable/DataTable.svelte +773 -0
  5. package/dist/data-display/DataTable/DataTable.svelte.d.ts +120 -0
  6. package/dist/data-display/DataTable/DataTable.svelte.d.ts.map +1 -0
  7. package/dist/data-display/DataTable/index.d.ts +2 -0
  8. package/dist/data-display/DataTable/index.d.ts.map +1 -0
  9. package/dist/data-display/DataTable/index.js +1 -0
  10. package/dist/data-display/StatCard/StatCard.svelte +409 -0
  11. package/dist/data-display/StatCard/StatCard.svelte.d.ts +63 -0
  12. package/dist/data-display/StatCard/StatCard.svelte.d.ts.map +1 -0
  13. package/dist/data-display/StatCard/index.d.ts +2 -0
  14. package/dist/data-display/StatCard/index.d.ts.map +1 -0
  15. package/dist/data-display/StatCard/index.js +1 -0
  16. package/dist/data-display/index.d.ts +8 -0
  17. package/dist/data-display/index.d.ts.map +1 -0
  18. package/dist/data-display/index.js +7 -0
  19. package/dist/dialogs/ConfirmDialog/ConfirmDialog.svelte +693 -0
  20. package/dist/dialogs/ConfirmDialog/ConfirmDialog.svelte.d.ts +66 -0
  21. package/dist/dialogs/ConfirmDialog/ConfirmDialog.svelte.d.ts.map +1 -0
  22. package/dist/dialogs/ConfirmDialog/index.d.ts +2 -0
  23. package/dist/dialogs/ConfirmDialog/index.d.ts.map +1 -0
  24. package/dist/dialogs/ConfirmDialog/index.js +1 -0
  25. package/dist/dialogs/Modal/Modal.svelte +441 -0
  26. package/dist/dialogs/Modal/Modal.svelte.d.ts +69 -0
  27. package/dist/dialogs/Modal/Modal.svelte.d.ts.map +1 -0
  28. package/dist/dialogs/Modal/index.d.ts +2 -0
  29. package/dist/dialogs/Modal/index.d.ts.map +1 -0
  30. package/dist/dialogs/Modal/index.js +1 -0
  31. package/dist/dialogs/index.d.ts +8 -0
  32. package/dist/dialogs/index.d.ts.map +1 -0
  33. package/dist/dialogs/index.js +7 -0
  34. package/dist/feedback/Alert/Alert.svelte +565 -0
  35. package/dist/feedback/Alert/Alert.svelte.d.ts +60 -0
  36. package/dist/feedback/Alert/Alert.svelte.d.ts.map +1 -0
  37. package/dist/feedback/Alert/index.d.ts +2 -0
  38. package/dist/feedback/Alert/index.d.ts.map +1 -0
  39. package/dist/feedback/Alert/index.js +1 -0
  40. package/dist/feedback/EmptyState/EmptyState.svelte +377 -0
  41. package/dist/feedback/EmptyState/EmptyState.svelte.d.ts +63 -0
  42. package/dist/feedback/EmptyState/EmptyState.svelte.d.ts.map +1 -0
  43. package/dist/feedback/EmptyState/index.d.ts +2 -0
  44. package/dist/feedback/EmptyState/index.d.ts.map +1 -0
  45. package/dist/feedback/EmptyState/index.js +1 -0
  46. package/dist/feedback/ProgressBar/ProgressBar.svelte +585 -0
  47. package/dist/feedback/ProgressBar/ProgressBar.svelte.d.ts +68 -0
  48. package/dist/feedback/ProgressBar/ProgressBar.svelte.d.ts.map +1 -0
  49. package/dist/feedback/ProgressBar/index.d.ts +2 -0
  50. package/dist/feedback/ProgressBar/index.d.ts.map +1 -0
  51. package/dist/feedback/ProgressBar/index.js +1 -0
  52. package/dist/feedback/Skeleton/Skeleton.svelte +568 -0
  53. package/dist/feedback/Skeleton/Skeleton.svelte.d.ts +54 -0
  54. package/dist/feedback/Skeleton/Skeleton.svelte.d.ts.map +1 -0
  55. package/dist/feedback/Skeleton/index.d.ts +2 -0
  56. package/dist/feedback/Skeleton/index.d.ts.map +1 -0
  57. package/dist/feedback/Skeleton/index.js +1 -0
  58. package/dist/feedback/Spinner/Spinner.svelte +434 -0
  59. package/dist/feedback/Spinner/Spinner.svelte.d.ts +49 -0
  60. package/dist/feedback/Spinner/Spinner.svelte.d.ts.map +1 -0
  61. package/dist/feedback/Spinner/index.d.ts +2 -0
  62. package/dist/feedback/Spinner/index.d.ts.map +1 -0
  63. package/dist/feedback/Spinner/index.js +1 -0
  64. package/dist/feedback/Toast/Toast.svelte +587 -0
  65. package/dist/feedback/Toast/Toast.svelte.d.ts +55 -0
  66. package/dist/feedback/Toast/Toast.svelte.d.ts.map +1 -0
  67. package/dist/feedback/Toast/ToastContainer.svelte +168 -0
  68. package/dist/feedback/Toast/ToastContainer.svelte.d.ts +28 -0
  69. package/dist/feedback/Toast/ToastContainer.svelte.d.ts.map +1 -0
  70. package/dist/feedback/Toast/index.d.ts +4 -0
  71. package/dist/feedback/Toast/index.d.ts.map +1 -0
  72. package/dist/feedback/Toast/index.js +3 -0
  73. package/dist/feedback/Toast/toast-store.d.ts +72 -0
  74. package/dist/feedback/Toast/toast-store.d.ts.map +1 -0
  75. package/dist/feedback/Toast/toast-store.js +157 -0
  76. package/dist/feedback/index.d.ts +13 -0
  77. package/dist/feedback/index.d.ts.map +1 -0
  78. package/dist/feedback/index.js +12 -0
  79. package/dist/forms/Checkbox/Checkbox.svelte +404 -0
  80. package/dist/forms/Checkbox/Checkbox.svelte.d.ts +62 -0
  81. package/dist/forms/Checkbox/Checkbox.svelte.d.ts.map +1 -0
  82. package/dist/forms/Checkbox/index.d.ts +2 -0
  83. package/dist/forms/Checkbox/index.d.ts.map +1 -0
  84. package/dist/forms/Checkbox/index.js +1 -0
  85. package/dist/forms/FormField/FormField.svelte +299 -0
  86. package/dist/forms/FormField/FormField.svelte.d.ts +43 -0
  87. package/dist/forms/FormField/FormField.svelte.d.ts.map +1 -0
  88. package/dist/forms/FormField/index.d.ts +2 -0
  89. package/dist/forms/FormField/index.d.ts.map +1 -0
  90. package/dist/forms/FormField/index.js +1 -0
  91. package/dist/forms/RadioGroup/RadioGroup.svelte +418 -0
  92. package/dist/forms/RadioGroup/RadioGroup.svelte.d.ts +70 -0
  93. package/dist/forms/RadioGroup/RadioGroup.svelte.d.ts.map +1 -0
  94. package/dist/forms/RadioGroup/index.d.ts +2 -0
  95. package/dist/forms/RadioGroup/index.d.ts.map +1 -0
  96. package/dist/forms/RadioGroup/index.js +1 -0
  97. package/dist/forms/Select/Select.svelte +548 -0
  98. package/dist/forms/Select/Select.svelte.d.ts +74 -0
  99. package/dist/forms/Select/Select.svelte.d.ts.map +1 -0
  100. package/dist/forms/Select/index.d.ts +2 -0
  101. package/dist/forms/Select/index.d.ts.map +1 -0
  102. package/dist/forms/Select/index.js +1 -0
  103. package/dist/forms/TextInput/TextInput.svelte +628 -0
  104. package/dist/forms/TextInput/TextInput.svelte.d.ts +97 -0
  105. package/dist/forms/TextInput/TextInput.svelte.d.ts.map +1 -0
  106. package/dist/forms/TextInput/index.d.ts +2 -0
  107. package/dist/forms/TextInput/index.d.ts.map +1 -0
  108. package/dist/forms/TextInput/index.js +1 -0
  109. package/dist/forms/Textarea/Textarea.svelte +587 -0
  110. package/dist/forms/Textarea/Textarea.svelte.d.ts +71 -0
  111. package/dist/forms/Textarea/Textarea.svelte.d.ts.map +1 -0
  112. package/dist/forms/Textarea/index.d.ts +2 -0
  113. package/dist/forms/Textarea/index.d.ts.map +1 -0
  114. package/dist/forms/Textarea/index.js +1 -0
  115. package/dist/forms/index.d.ts +13 -0
  116. package/dist/forms/index.d.ts.map +1 -0
  117. package/dist/forms/index.js +12 -0
  118. package/dist/index.d.ts +37 -0
  119. package/dist/index.d.ts.map +1 -0
  120. package/dist/index.js +65 -0
  121. package/dist/layout/Card/Card.svelte +316 -0
  122. package/dist/layout/Card/Card.svelte.d.ts +63 -0
  123. package/dist/layout/Card/Card.svelte.d.ts.map +1 -0
  124. package/dist/layout/Card/index.d.ts +2 -0
  125. package/dist/layout/Card/index.d.ts.map +1 -0
  126. package/dist/layout/Card/index.js +1 -0
  127. package/dist/layout/Container/Container.svelte +252 -0
  128. package/dist/layout/Container/Container.svelte.d.ts +50 -0
  129. package/dist/layout/Container/Container.svelte.d.ts.map +1 -0
  130. package/dist/layout/Container/index.d.ts +2 -0
  131. package/dist/layout/Container/index.d.ts.map +1 -0
  132. package/dist/layout/Container/index.js +1 -0
  133. package/dist/layout/index.d.ts +8 -0
  134. package/dist/layout/index.d.ts.map +1 -0
  135. package/dist/layout/index.js +7 -0
  136. package/dist/navigation/StepIndicator/StepIndicator.svelte +601 -0
  137. package/dist/navigation/StepIndicator/StepIndicator.svelte.d.ts +70 -0
  138. package/dist/navigation/StepIndicator/StepIndicator.svelte.d.ts.map +1 -0
  139. package/dist/navigation/StepIndicator/index.d.ts +2 -0
  140. package/dist/navigation/StepIndicator/index.d.ts.map +1 -0
  141. package/dist/navigation/StepIndicator/index.js +1 -0
  142. package/dist/navigation/index.d.ts +7 -0
  143. package/dist/navigation/index.d.ts.map +1 -0
  144. package/dist/navigation/index.js +6 -0
  145. package/dist/primitives/Badge/Badge.svelte +365 -0
  146. package/dist/primitives/Badge/Badge.svelte.d.ts +39 -0
  147. package/dist/primitives/Badge/Badge.svelte.d.ts.map +1 -0
  148. package/dist/primitives/Badge/index.d.ts +2 -0
  149. package/dist/primitives/Badge/index.d.ts.map +1 -0
  150. package/dist/primitives/Badge/index.js +1 -0
  151. package/dist/primitives/Button/Button.svelte +430 -0
  152. package/dist/primitives/Button/Button.svelte.d.ts +50 -0
  153. package/dist/primitives/Button/Button.svelte.d.ts.map +1 -0
  154. package/dist/primitives/Button/index.d.ts +2 -0
  155. package/dist/primitives/Button/index.d.ts.map +1 -0
  156. package/dist/primitives/Button/index.js +1 -0
  157. package/dist/primitives/index.d.ts +9 -0
  158. package/dist/primitives/index.d.ts.map +1 -0
  159. package/dist/primitives/index.js +8 -0
  160. package/dist/routes/+layout.svelte +12 -0
  161. package/dist/routes/+layout.svelte.d.ts +12 -0
  162. package/dist/routes/+layout.svelte.d.ts.map +1 -0
  163. package/dist/routes/+page.svelte +53 -0
  164. package/dist/routes/+page.svelte.d.ts +27 -0
  165. package/dist/routes/+page.svelte.d.ts.map +1 -0
  166. package/dist/styles/tokens.css +399 -0
  167. package/dist/types/index.d.ts +175 -0
  168. package/dist/types/index.d.ts.map +1 -0
  169. package/dist/types/index.js +7 -0
  170. package/dist/utils/accessibility.d.ts +103 -0
  171. package/dist/utils/accessibility.d.ts.map +1 -0
  172. package/dist/utils/accessibility.js +202 -0
  173. package/dist/utils/cn.d.ts +71 -0
  174. package/dist/utils/cn.d.ts.map +1 -0
  175. package/dist/utils/cn.js +61 -0
  176. package/dist/utils/form-styles.d.ts +76 -0
  177. package/dist/utils/form-styles.d.ts.map +1 -0
  178. package/dist/utils/form-styles.js +95 -0
  179. package/dist/utils/index.d.ts +10 -0
  180. package/dist/utils/index.d.ts.map +1 -0
  181. package/dist/utils/index.js +13 -0
  182. package/dist/utils/keyboard.d.ts +94 -0
  183. package/dist/utils/keyboard.d.ts.map +1 -0
  184. package/dist/utils/keyboard.js +179 -0
  185. package/package.json +119 -0
@@ -0,0 +1,773 @@
1
+ <!--
2
+ @component DataTable
3
+
4
+ A world-class responsive data table component designed for healthcare applications.
5
+ Features sorting, row selection, sticky headers, pagination, and full accessibility.
6
+
7
+ Features:
8
+ - Full WCAG 2.1 AA compliance with proper semantic markup
9
+ - Sortable columns with keyboard support
10
+ - Row selection with checkbox support
11
+ - Sticky header option for long tables
12
+ - Size variants (sm, md, lg) for different data densities
13
+ - Loading state with skeleton placeholders
14
+ - Empty state with customizable message
15
+ - Pagination support for large datasets
16
+ - Mobile-responsive with column hiding
17
+
18
+ @example
19
+ <DataTable
20
+ columns={[
21
+ { key: 'name', header: 'Patient Name', sortable: true },
22
+ { key: 'status', header: 'Status' },
23
+ { key: 'date', header: 'Date', hideOnMobile: true }
24
+ ]}
25
+ data={patients}
26
+ caption="Patient list for Dr. Smith"
27
+ selectable
28
+ stickyHeader
29
+ />
30
+ -->
31
+ <script lang="ts" generics="T extends Record<string, unknown>">
32
+ import type { Snippet } from 'svelte';
33
+ import { ArrowUp, ArrowDown, ArrowUpDown, ChevronLeft, ChevronRight } from 'lucide-svelte';
34
+ import { Skeleton } from '../../feedback/Skeleton/index.js';
35
+ import { EmptyState } from '../../feedback/EmptyState/index.js';
36
+ import { Checkbox } from '../../forms/Checkbox/index.js';
37
+ import type { TableColumn, SortState, SortDirection } from '../../types/index.js';
38
+
39
+ type TableSize = 'sm' | 'md' | 'lg';
40
+
41
+ interface Props {
42
+ /** Column definitions */
43
+ columns: TableColumn<T>[];
44
+ /** Data rows */
45
+ data: T[];
46
+ /** Table caption for accessibility - describes the table's purpose */
47
+ caption?: string;
48
+ /** Whether to visually hide the caption (still available to screen readers) */
49
+ captionHidden?: boolean;
50
+ /** Size variant */
51
+ size?: TableSize;
52
+ /** Initial sort state */
53
+ sortState?: SortState;
54
+ /** Whether rows are hoverable */
55
+ hoverable?: boolean;
56
+ /** Whether rows are striped */
57
+ striped?: boolean;
58
+ /** Whether the header should stick when scrolling */
59
+ stickyHeader?: boolean;
60
+ /** Maximum height for scrollable table (enables sticky header behavior) */
61
+ maxHeight?: string;
62
+ /** Whether rows are selectable */
63
+ selectable?: boolean;
64
+ /** Currently selected row keys */
65
+ selectedKeys?: Set<string>;
66
+ /** Whether the table is loading */
67
+ loading?: boolean;
68
+ /** Number of skeleton rows to show when loading */
69
+ loadingRows?: number;
70
+ /** Empty state message */
71
+ emptyMessage?: string;
72
+ /** Empty state description */
73
+ emptyDescription?: string;
74
+ /** Row key function */
75
+ getRowKey?: (row: T, index: number) => string;
76
+ /** Custom cell renderer */
77
+ cellRenderer?: Snippet<[{ value: unknown; row: T; column: TableColumn<T> }]>;
78
+ /** Custom empty state snippet */
79
+ emptyState?: Snippet;
80
+ /** Row click handler */
81
+ onRowClick?: (row: T) => void;
82
+ /** Sort change handler */
83
+ onSort?: (state: SortState) => void;
84
+ /** Selection change handler */
85
+ onSelectionChange?: (selectedKeys: Set<string>) => void;
86
+ /** Pagination - current page (1-indexed) */
87
+ currentPage?: number;
88
+ /** Pagination - items per page */
89
+ pageSize?: number;
90
+ /** Pagination - total items (for server-side pagination) */
91
+ totalItems?: number;
92
+ /** Pagination - page change handler */
93
+ onPageChange?: (page: number) => void;
94
+ /** Row highlight function - return variant name for highlighting */
95
+ getRowHighlight?: (row: T) => 'error' | 'warning' | 'success' | 'info' | null;
96
+ /** Additional CSS classes */
97
+ class?: string;
98
+ /** ID for the table */
99
+ id?: string;
100
+ }
101
+
102
+ let {
103
+ columns,
104
+ data,
105
+ caption = '',
106
+ captionHidden = false,
107
+ size = 'md',
108
+ sortState = { column: null, direction: null },
109
+ hoverable = true,
110
+ striped = false,
111
+ stickyHeader = false,
112
+ maxHeight = '',
113
+ selectable = false,
114
+ selectedKeys = new Set<string>(),
115
+ loading = false,
116
+ loadingRows = 5,
117
+ emptyMessage = 'No data available',
118
+ emptyDescription = '',
119
+ getRowKey = (_row: T, index: number) => String(index),
120
+ cellRenderer,
121
+ emptyState,
122
+ onRowClick,
123
+ onSort,
124
+ onSelectionChange,
125
+ currentPage,
126
+ pageSize,
127
+ totalItems,
128
+ onPageChange,
129
+ getRowHighlight,
130
+ class: className = '',
131
+ id
132
+ }: Props = $props();
133
+
134
+ // Size configurations
135
+ const sizeConfig: Record<TableSize, { cellPadding: string; fontSize: string; headerPadding: string }> = {
136
+ sm: { cellPadding: '0.5rem 0.75rem', fontSize: '0.8125rem', headerPadding: '0.5rem 0.75rem' },
137
+ md: { cellPadding: '0.75rem 1rem', fontSize: '0.875rem', headerPadding: '0.75rem 1rem' },
138
+ lg: { cellPadding: '1rem 1.25rem', fontSize: '0.9375rem', headerPadding: '1rem 1.25rem' }
139
+ };
140
+
141
+ const config = $derived(sizeConfig[size]);
142
+
143
+ // Pagination calculations
144
+ const isPaginated = $derived(currentPage !== undefined && pageSize !== undefined);
145
+ const totalPages = $derived(
146
+ isPaginated
147
+ ? Math.ceil((totalItems ?? data.length) / (pageSize ?? 10))
148
+ : 1
149
+ );
150
+ const paginatedData = $derived(
151
+ isPaginated && !totalItems
152
+ ? data.slice(((currentPage ?? 1) - 1) * (pageSize ?? 10), (currentPage ?? 1) * (pageSize ?? 10))
153
+ : data
154
+ );
155
+
156
+ // Selection state
157
+ const allSelected = $derived(
158
+ selectable && data.length > 0 && data.every((row, i) => selectedKeys.has(getRowKey(row, i)))
159
+ );
160
+ const someSelected = $derived(
161
+ selectable && data.some((row, i) => selectedKeys.has(getRowKey(row, i))) && !allSelected
162
+ );
163
+
164
+ // Aria live region for announcing sort changes
165
+ let sortAnnouncement = $state('');
166
+
167
+ function handleSort(columnKey: string) {
168
+ if (!onSort) return;
169
+
170
+ const column = columns.find((c) => c.key === columnKey);
171
+ if (!column?.sortable) return;
172
+
173
+ let newDirection: SortDirection = 'asc';
174
+ if (sortState.column === columnKey) {
175
+ if (sortState.direction === 'asc') newDirection = 'desc';
176
+ else if (sortState.direction === 'desc') newDirection = null;
177
+ }
178
+
179
+ // Announce sort change to screen readers
180
+ if (newDirection) {
181
+ sortAnnouncement = `Table sorted by ${column.header} ${newDirection === 'asc' ? 'ascending' : 'descending'}`;
182
+ } else {
183
+ sortAnnouncement = `Sort cleared`;
184
+ }
185
+
186
+ onSort({
187
+ column: newDirection ? columnKey : null,
188
+ direction: newDirection
189
+ });
190
+ }
191
+
192
+ function handleRowKeyDown(e: KeyboardEvent, row: T) {
193
+ if (onRowClick && (e.key === 'Enter' || e.key === ' ')) {
194
+ e.preventDefault();
195
+ onRowClick(row);
196
+ }
197
+ }
198
+
199
+ function handleHeaderKeyDown(e: KeyboardEvent, columnKey: string) {
200
+ if (e.key === 'Enter' || e.key === ' ') {
201
+ e.preventDefault();
202
+ handleSort(columnKey);
203
+ }
204
+ }
205
+
206
+ function handleSelectAll() {
207
+ if (!onSelectionChange) return;
208
+
209
+ if (allSelected) {
210
+ onSelectionChange(new Set());
211
+ } else {
212
+ const allKeys = new Set(data.map((row, i) => getRowKey(row, i)));
213
+ onSelectionChange(allKeys);
214
+ }
215
+ }
216
+
217
+ function handleSelectRow(row: T, index: number) {
218
+ if (!onSelectionChange) return;
219
+
220
+ const key = getRowKey(row, index);
221
+ const newSelection = new Set(selectedKeys);
222
+
223
+ if (newSelection.has(key)) {
224
+ newSelection.delete(key);
225
+ } else {
226
+ newSelection.add(key);
227
+ }
228
+
229
+ onSelectionChange(newSelection);
230
+ }
231
+
232
+ function getCellValue(row: T, column: TableColumn<T>): unknown {
233
+ const value = row[column.key as keyof T];
234
+ if (column.render) {
235
+ return column.render(value, row);
236
+ }
237
+ return value;
238
+ }
239
+
240
+ function getRowHighlightClass(row: T): string {
241
+ if (!getRowHighlight) return '';
242
+ const highlight = getRowHighlight(row);
243
+ if (!highlight) return '';
244
+ return `data-table-row-${highlight}`;
245
+ }
246
+
247
+ // Generate array for skeleton rows
248
+ const skeletonRows = $derived(Array.from({ length: loadingRows }, (_, i) => i));
249
+ </script>
250
+
251
+ <!-- Screen reader announcements for sort changes -->
252
+ <div class="sr-only" role="status" aria-live="polite" aria-atomic="true">
253
+ {sortAnnouncement}
254
+ </div>
255
+
256
+ <div
257
+ {id}
258
+ class="data-table-wrapper data-table-{size} {className}"
259
+ class:data-table-scrollable={maxHeight}
260
+ style={maxHeight ? `max-height: ${maxHeight};` : undefined}
261
+ >
262
+ <div class="data-table-scroll-container">
263
+ <table class="data-table" role="grid" aria-busy={loading}>
264
+ {#if caption}
265
+ <caption class:sr-only={captionHidden} class="data-table-caption">
266
+ {caption}
267
+ </caption>
268
+ {/if}
269
+
270
+ <thead class:data-table-sticky-header={stickyHeader}>
271
+ <tr>
272
+ {#if selectable}
273
+ <th
274
+ class="data-table-header data-table-checkbox-cell"
275
+ scope="col"
276
+ style="padding: {config.headerPadding};"
277
+ >
278
+ <Checkbox
279
+ checked={allSelected}
280
+ indeterminate={someSelected}
281
+ onchange={handleSelectAll}
282
+ label="Select all rows"
283
+ hideLabel
284
+ size="sm"
285
+ />
286
+ </th>
287
+ {/if}
288
+ {#each columns as column (column.key)}
289
+ <th
290
+ class="data-table-header"
291
+ class:data-table-header-sortable={column.sortable}
292
+ class:data-table-header-sorted={sortState.column === column.key}
293
+ class:data-table-hide-mobile={column.hideOnMobile}
294
+ class:data-table-align-center={column.align === 'center'}
295
+ class:data-table-align-right={column.align === 'right'}
296
+ scope="col"
297
+ style="padding: {config.headerPadding}; font-size: {config.fontSize}; {column.width ? `width: ${column.width};` : ''}"
298
+ onclick={() => column.sortable && handleSort(column.key)}
299
+ onkeydown={(e) => column.sortable && handleHeaderKeyDown(e, column.key)}
300
+ tabindex={column.sortable ? 0 : undefined}
301
+ role={column.sortable ? 'columnheader' : 'columnheader'}
302
+ aria-sort={
303
+ sortState.column === column.key
304
+ ? sortState.direction === 'asc'
305
+ ? 'ascending'
306
+ : 'descending'
307
+ : undefined
308
+ }
309
+ >
310
+ <span class="data-table-header-content">
311
+ <span class="data-table-header-text">{column.header}</span>
312
+ {#if column.sortable}
313
+ <span class="data-table-sort-icon" aria-hidden="true">
314
+ {#if sortState.column === column.key && sortState.direction === 'asc'}
315
+ <ArrowUp size={14} />
316
+ {:else if sortState.column === column.key && sortState.direction === 'desc'}
317
+ <ArrowDown size={14} />
318
+ {:else}
319
+ <ArrowUpDown size={14} />
320
+ {/if}
321
+ </span>
322
+ {/if}
323
+ </span>
324
+ </th>
325
+ {/each}
326
+ </tr>
327
+ </thead>
328
+
329
+ <tbody>
330
+ {#if loading}
331
+ {#each skeletonRows as _row, index (index)}
332
+ <tr class="data-table-row">
333
+ {#if selectable}
334
+ <td class="data-table-cell data-table-checkbox-cell" style="padding: {config.cellPadding};">
335
+ <Skeleton variant="circle" width="1rem" height="1rem" animation="wave" />
336
+ </td>
337
+ {/if}
338
+ {#each columns as column, colIndex (column.key)}
339
+ <td
340
+ class="data-table-cell"
341
+ class:data-table-hide-mobile={column.hideOnMobile}
342
+ class:data-table-align-center={column.align === 'center'}
343
+ class:data-table-align-right={column.align === 'right'}
344
+ style="padding: {config.cellPadding};"
345
+ >
346
+ <Skeleton
347
+ variant="text"
348
+ size="sm"
349
+ width={colIndex === 0 ? '80%' : colIndex === columns.length - 1 ? '60%' : '70%'}
350
+ animation="wave"
351
+ />
352
+ </td>
353
+ {/each}
354
+ </tr>
355
+ {/each}
356
+ {:else if paginatedData.length === 0}
357
+ <tr>
358
+ <td
359
+ colspan={columns.length + (selectable ? 1 : 0)}
360
+ class="data-table-empty-cell"
361
+ >
362
+ {#if emptyState}
363
+ {@render emptyState()}
364
+ {:else}
365
+ <EmptyState
366
+ variant="no-results"
367
+ title={emptyMessage}
368
+ description={emptyDescription}
369
+ size="sm"
370
+ compact
371
+ />
372
+ {/if}
373
+ </td>
374
+ </tr>
375
+ {:else}
376
+ {#each paginatedData as row, index (getRowKey(row, index))}
377
+ {@const rowKey = getRowKey(row, index)}
378
+ {@const isSelected = selectedKeys.has(rowKey)}
379
+ <tr
380
+ class="data-table-row {getRowHighlightClass(row)}"
381
+ class:data-table-row-hoverable={hoverable}
382
+ class:data-table-row-striped={striped && index % 2 === 1}
383
+ class:data-table-row-clickable={onRowClick}
384
+ class:data-table-row-selected={isSelected}
385
+ onclick={() => onRowClick?.(row)}
386
+ onkeydown={(e) => handleRowKeyDown(e, row)}
387
+ tabindex={onRowClick ? 0 : undefined}
388
+ role={onRowClick ? 'row' : 'row'}
389
+ aria-selected={selectable ? isSelected : undefined}
390
+ >
391
+ {#if selectable}
392
+ <td
393
+ class="data-table-cell data-table-checkbox-cell"
394
+ style="padding: {config.cellPadding};"
395
+ onclick={(e) => e.stopPropagation()}
396
+ onkeydown={(e) => e.stopPropagation()}
397
+ >
398
+ <Checkbox
399
+ checked={isSelected}
400
+ onchange={() => handleSelectRow(row, index)}
401
+ label="Select row"
402
+ hideLabel
403
+ size="sm"
404
+ />
405
+ </td>
406
+ {/if}
407
+ {#each columns as column (column.key)}
408
+ <td
409
+ class="data-table-cell"
410
+ class:data-table-hide-mobile={column.hideOnMobile}
411
+ class:data-table-align-center={column.align === 'center'}
412
+ class:data-table-align-right={column.align === 'right'}
413
+ style="padding: {config.cellPadding}; font-size: {config.fontSize};"
414
+ >
415
+ {#if cellRenderer}
416
+ {@render cellRenderer({ value: getCellValue(row, column), row, column })}
417
+ {:else}
418
+ {getCellValue(row, column)}
419
+ {/if}
420
+ </td>
421
+ {/each}
422
+ </tr>
423
+ {/each}
424
+ {/if}
425
+ </tbody>
426
+ </table>
427
+ </div>
428
+
429
+ <!-- Pagination -->
430
+ {#if isPaginated && totalPages > 1 && !loading}
431
+ <div class="data-table-pagination">
432
+ <span class="data-table-pagination-info">
433
+ {#if totalItems}
434
+ Showing {((currentPage ?? 1) - 1) * (pageSize ?? 10) + 1} - {Math.min((currentPage ?? 1) * (pageSize ?? 10), totalItems)} of {totalItems}
435
+ {:else}
436
+ Page {currentPage} of {totalPages}
437
+ {/if}
438
+ </span>
439
+ <div class="data-table-pagination-controls">
440
+ <button
441
+ type="button"
442
+ class="data-table-pagination-btn"
443
+ disabled={(currentPage ?? 1) <= 1}
444
+ onclick={() => onPageChange?.((currentPage ?? 1) - 1)}
445
+ aria-label="Previous page"
446
+ >
447
+ <ChevronLeft size={16} />
448
+ </button>
449
+ <span class="data-table-pagination-pages">
450
+ {currentPage} / {totalPages}
451
+ </span>
452
+ <button
453
+ type="button"
454
+ class="data-table-pagination-btn"
455
+ disabled={(currentPage ?? 1) >= totalPages}
456
+ onclick={() => onPageChange?.((currentPage ?? 1) + 1)}
457
+ aria-label="Next page"
458
+ >
459
+ <ChevronRight size={16} />
460
+ </button>
461
+ </div>
462
+ </div>
463
+ {/if}
464
+ </div>
465
+
466
+ <style>
467
+ /* ========================================
468
+ SCREEN READER ONLY
469
+ ======================================== */
470
+ .sr-only {
471
+ position: absolute;
472
+ width: 1px;
473
+ height: 1px;
474
+ padding: 0;
475
+ margin: -1px;
476
+ overflow: hidden;
477
+ clip: rect(0, 0, 0, 0);
478
+ white-space: nowrap;
479
+ border: 0;
480
+ }
481
+
482
+ /* ========================================
483
+ WRAPPER STYLES
484
+ ======================================== */
485
+ .data-table-wrapper {
486
+ overflow: hidden;
487
+ border-radius: 0.75rem;
488
+ border: 1px solid var(--ui-border-default);
489
+ background-color: var(--ui-bg-primary);
490
+ }
491
+
492
+ .data-table-scrollable {
493
+ overflow: hidden;
494
+ }
495
+
496
+ .data-table-scroll-container {
497
+ overflow-x: auto;
498
+ overflow-y: auto;
499
+ }
500
+
501
+ .data-table-scrollable .data-table-scroll-container {
502
+ max-height: inherit;
503
+ }
504
+
505
+ /* ========================================
506
+ TABLE BASE STYLES
507
+ ======================================== */
508
+ .data-table {
509
+ width: 100%;
510
+ border-collapse: collapse;
511
+ text-align: left;
512
+ }
513
+
514
+ /* ========================================
515
+ CAPTION
516
+ ======================================== */
517
+ .data-table-caption {
518
+ padding: 0.75rem 1rem;
519
+ font-weight: 600;
520
+ text-align: left;
521
+ color: var(--ui-text-primary);
522
+ background-color: var(--ui-bg-secondary);
523
+ border-bottom: 1px solid var(--ui-border-default);
524
+ }
525
+
526
+ /* ========================================
527
+ HEADER STYLES
528
+ ======================================== */
529
+ .data-table thead {
530
+ background-color: var(--ui-bg-tertiary);
531
+ }
532
+
533
+ .data-table-sticky-header {
534
+ position: sticky;
535
+ top: 0;
536
+ z-index: 10;
537
+ background-color: var(--ui-bg-tertiary);
538
+ }
539
+
540
+ .data-table-header {
541
+ font-weight: 600;
542
+ color: var(--ui-text-primary);
543
+ text-align: left;
544
+ border-bottom: 1px solid var(--ui-border-default);
545
+ white-space: nowrap;
546
+ }
547
+
548
+ .data-table-header-sortable {
549
+ cursor: pointer;
550
+ user-select: none;
551
+ transition: background-color 0.15s ease;
552
+ }
553
+
554
+ .data-table-header-sortable:hover {
555
+ background-color: var(--ui-bg-secondary);
556
+ }
557
+
558
+ .data-table-header-sortable:focus-visible {
559
+ outline: none;
560
+ box-shadow: inset 0 0 0 2px rgb(var(--ui-color-primary) / 0.4);
561
+ }
562
+
563
+ .data-table-header-sorted {
564
+ background-color: rgb(var(--ui-color-primary) / 0.05);
565
+ }
566
+
567
+ .data-table-header-content {
568
+ display: inline-flex;
569
+ align-items: center;
570
+ gap: 0.375rem;
571
+ }
572
+
573
+ .data-table-header-text {
574
+ flex: 1;
575
+ }
576
+
577
+ .data-table-sort-icon {
578
+ display: flex;
579
+ align-items: center;
580
+ color: var(--ui-text-tertiary);
581
+ transition: color 0.15s ease;
582
+ }
583
+
584
+ .data-table-header-sorted .data-table-sort-icon {
585
+ color: rgb(var(--ui-color-primary));
586
+ }
587
+
588
+ /* ========================================
589
+ CELL STYLES
590
+ ======================================== */
591
+ .data-table-cell {
592
+ color: var(--ui-text-secondary);
593
+ border-bottom: 1px solid var(--ui-border-default);
594
+ }
595
+
596
+ .data-table-row:last-child .data-table-cell {
597
+ border-bottom: none;
598
+ }
599
+
600
+ .data-table-checkbox-cell {
601
+ width: 3rem;
602
+ }
603
+
604
+ /* ========================================
605
+ ROW STYLES
606
+ ======================================== */
607
+ .data-table-row {
608
+ transition: background-color 0.15s ease;
609
+ }
610
+
611
+ .data-table-row-hoverable:hover {
612
+ background-color: var(--ui-bg-secondary);
613
+ }
614
+
615
+ .data-table-row-striped {
616
+ background-color: var(--ui-bg-secondary);
617
+ }
618
+
619
+ .data-table-row-clickable {
620
+ cursor: pointer;
621
+ }
622
+
623
+ .data-table-row-clickable:focus-visible {
624
+ outline: none;
625
+ box-shadow: inset 0 0 0 2px rgb(var(--ui-color-primary) / 0.4);
626
+ }
627
+
628
+ .data-table-row-selected {
629
+ background-color: rgb(var(--ui-color-primary) / 0.08);
630
+ }
631
+
632
+ .data-table-row-selected:hover {
633
+ background-color: rgb(var(--ui-color-primary) / 0.12);
634
+ }
635
+
636
+ /* Row highlighting for status */
637
+ .data-table-row-error {
638
+ background-color: rgb(var(--ui-color-error) / 0.06);
639
+ }
640
+
641
+ .data-table-row-error:hover {
642
+ background-color: rgb(var(--ui-color-error) / 0.1);
643
+ }
644
+
645
+ .data-table-row-warning {
646
+ background-color: rgb(var(--ui-color-warning) / 0.06);
647
+ }
648
+
649
+ .data-table-row-warning:hover {
650
+ background-color: rgb(var(--ui-color-warning) / 0.1);
651
+ }
652
+
653
+ .data-table-row-success {
654
+ background-color: rgb(var(--ui-color-success) / 0.06);
655
+ }
656
+
657
+ .data-table-row-success:hover {
658
+ background-color: rgb(var(--ui-color-success) / 0.1);
659
+ }
660
+
661
+ .data-table-row-info {
662
+ background-color: rgb(var(--ui-color-info) / 0.06);
663
+ }
664
+
665
+ .data-table-row-info:hover {
666
+ background-color: rgb(var(--ui-color-info) / 0.1);
667
+ }
668
+
669
+ /* ========================================
670
+ ALIGNMENT
671
+ ======================================== */
672
+ .data-table-align-center {
673
+ text-align: center;
674
+ }
675
+
676
+ .data-table-align-right {
677
+ text-align: right;
678
+ }
679
+
680
+ /* ========================================
681
+ RESPONSIVE
682
+ ======================================== */
683
+ .data-table-hide-mobile {
684
+ display: none;
685
+ }
686
+
687
+ @media (min-width: 768px) {
688
+ .data-table-hide-mobile {
689
+ display: table-cell;
690
+ }
691
+ }
692
+
693
+ /* ========================================
694
+ EMPTY STATE
695
+ ======================================== */
696
+ .data-table-empty-cell {
697
+ padding: 2rem 1rem;
698
+ text-align: center;
699
+ }
700
+
701
+ /* ========================================
702
+ PAGINATION
703
+ ======================================== */
704
+ .data-table-pagination {
705
+ display: flex;
706
+ align-items: center;
707
+ justify-content: space-between;
708
+ padding: 0.75rem 1rem;
709
+ border-top: 1px solid var(--ui-border-default);
710
+ background-color: var(--ui-bg-secondary);
711
+ gap: 1rem;
712
+ flex-wrap: wrap;
713
+ }
714
+
715
+ .data-table-pagination-info {
716
+ font-size: 0.8125rem;
717
+ color: var(--ui-text-secondary);
718
+ }
719
+
720
+ .data-table-pagination-controls {
721
+ display: flex;
722
+ align-items: center;
723
+ gap: 0.5rem;
724
+ }
725
+
726
+ .data-table-pagination-btn {
727
+ display: flex;
728
+ align-items: center;
729
+ justify-content: center;
730
+ width: 2rem;
731
+ height: 2rem;
732
+ border: 1px solid var(--ui-border-default);
733
+ border-radius: 0.375rem;
734
+ background-color: var(--ui-bg-primary);
735
+ color: var(--ui-text-primary);
736
+ cursor: pointer;
737
+ transition: all 0.15s ease;
738
+ }
739
+
740
+ .data-table-pagination-btn:hover:not(:disabled) {
741
+ background-color: var(--ui-bg-secondary);
742
+ border-color: var(--ui-border-hover);
743
+ }
744
+
745
+ .data-table-pagination-btn:focus-visible {
746
+ outline: none;
747
+ box-shadow: 0 0 0 2px rgb(var(--ui-color-primary) / 0.4);
748
+ }
749
+
750
+ .data-table-pagination-btn:disabled {
751
+ opacity: 0.5;
752
+ cursor: not-allowed;
753
+ }
754
+
755
+ .data-table-pagination-pages {
756
+ font-size: 0.8125rem;
757
+ font-weight: 500;
758
+ color: var(--ui-text-primary);
759
+ min-width: 4rem;
760
+ text-align: center;
761
+ }
762
+
763
+ /* ========================================
764
+ REDUCED MOTION
765
+ ======================================== */
766
+ @media (prefers-reduced-motion: reduce) {
767
+ .data-table-row,
768
+ .data-table-header-sortable,
769
+ .data-table-pagination-btn {
770
+ transition: none;
771
+ }
772
+ }
773
+ </style>