@fivem-nui/svelte-components 0.0.0-stage → 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 (143) hide show
  1. package/README.md +101 -2
  2. package/dist/components/actions/NuiButton.svelte +292 -0
  3. package/dist/components/actions/NuiButton.svelte.d.ts +60 -0
  4. package/dist/components/data/NuiBadge.svelte +277 -0
  5. package/dist/components/data/NuiBadge.svelte.d.ts +74 -0
  6. package/dist/components/data/NuiCodeBlock.svelte +256 -0
  7. package/dist/components/data/NuiCodeBlock.svelte.d.ts +49 -0
  8. package/dist/components/data/NuiList.svelte +341 -0
  9. package/dist/components/data/NuiList.svelte.d.ts +52 -0
  10. package/dist/components/data/NuiProgressBar.svelte +264 -0
  11. package/dist/components/data/NuiProgressBar.svelte.d.ts +51 -0
  12. package/dist/components/data/NuiRichText.svelte +260 -0
  13. package/dist/components/data/NuiRichText.svelte.d.ts +33 -0
  14. package/dist/components/data/NuiTable.svelte +470 -0
  15. package/dist/components/data/NuiTable.svelte.d.ts +53 -0
  16. package/dist/components/data/chart/NuiChart.svelte +384 -0
  17. package/dist/components/data/chart/NuiChart.svelte.d.ts +49 -0
  18. package/dist/components/data/chart/color.d.ts +9 -0
  19. package/dist/components/data/chart/color.js +78 -0
  20. package/dist/components/data/chart/index.d.ts +10 -0
  21. package/dist/components/data/chart/index.js +10 -0
  22. package/dist/components/data/chart/registry.d.ts +14 -0
  23. package/dist/components/data/chart/registry.js +53 -0
  24. package/dist/components/data/chart/renderers/bar.d.ts +8 -0
  25. package/dist/components/data/chart/renderers/bar.js +79 -0
  26. package/dist/components/data/chart/renderers/base.d.ts +18 -0
  27. package/dist/components/data/chart/renderers/base.js +10 -0
  28. package/dist/components/data/chart/renderers/line.d.ts +8 -0
  29. package/dist/components/data/chart/renderers/line.js +89 -0
  30. package/dist/components/data/chart/renderers/pie.d.ts +10 -0
  31. package/dist/components/data/chart/renderers/pie.js +55 -0
  32. package/dist/components/data/chart/scale.d.ts +22 -0
  33. package/dist/components/data/chart/scale.js +101 -0
  34. package/dist/components/data/chart/types.d.ts +84 -0
  35. package/dist/components/data/chart/types.js +1 -0
  36. package/dist/components/data/chart/validator.d.ts +10 -0
  37. package/dist/components/data/chart/validator.js +82 -0
  38. package/dist/components/forms/NuiCheckbox.svelte +218 -0
  39. package/dist/components/forms/NuiCheckbox.svelte.d.ts +37 -0
  40. package/dist/components/forms/NuiDatepicker.svelte +755 -0
  41. package/dist/components/forms/NuiDatepicker.svelte.d.ts +34 -0
  42. package/dist/components/forms/NuiFileInput.svelte +1047 -0
  43. package/dist/components/forms/NuiFileInput.svelte.d.ts +93 -0
  44. package/dist/components/forms/NuiFileInput.types.d.ts +28 -0
  45. package/dist/components/forms/NuiFileInput.types.js +1 -0
  46. package/dist/components/forms/NuiForm.svelte +391 -0
  47. package/dist/components/forms/NuiForm.svelte.d.ts +69 -0
  48. package/dist/components/forms/NuiInput.svelte +296 -0
  49. package/dist/components/forms/NuiInput.svelte.d.ts +62 -0
  50. package/dist/components/forms/NuiRadioGroup.svelte +238 -0
  51. package/dist/components/forms/NuiRadioGroup.svelte.d.ts +47 -0
  52. package/dist/components/forms/NuiSelect.svelte +819 -0
  53. package/dist/components/forms/NuiSelect.svelte.d.ts +49 -0
  54. package/dist/components/layout/NuiCard.svelte +314 -0
  55. package/dist/components/layout/NuiCard.svelte.d.ts +48 -0
  56. package/dist/components/layout/NuiCardGrid.svelte +98 -0
  57. package/dist/components/layout/NuiCardGrid.svelte.d.ts +22 -0
  58. package/dist/components/layout/NuiFlex.svelte +121 -0
  59. package/dist/components/layout/NuiFlex.svelte.d.ts +23 -0
  60. package/dist/components/layout/NuiGrid.svelte +66 -0
  61. package/dist/components/layout/NuiGrid.svelte.d.ts +14 -0
  62. package/dist/components/layout/NuiStack.svelte +138 -0
  63. package/dist/components/layout/NuiStack.svelte.d.ts +26 -0
  64. package/dist/components/navigation/NuiAccordion.svelte +323 -0
  65. package/dist/components/navigation/NuiAccordion.svelte.d.ts +38 -0
  66. package/dist/components/navigation/NuiBreadcrumbs.svelte +133 -0
  67. package/dist/components/navigation/NuiBreadcrumbs.svelte.d.ts +19 -0
  68. package/dist/components/navigation/NuiMenu.svelte +596 -0
  69. package/dist/components/navigation/NuiMenu.svelte.d.ts +28 -0
  70. package/dist/components/navigation/NuiPagination.svelte +306 -0
  71. package/dist/components/navigation/NuiPagination.svelte.d.ts +40 -0
  72. package/dist/components/navigation/NuiSidebar.svelte +186 -0
  73. package/dist/components/navigation/NuiSidebar.svelte.d.ts +26 -0
  74. package/dist/components/navigation/NuiTabs.svelte +249 -0
  75. package/dist/components/navigation/NuiTabs.svelte.d.ts +31 -0
  76. package/dist/components/overlay/NuiDialog.svelte +118 -0
  77. package/dist/components/overlay/NuiDialog.svelte.d.ts +34 -0
  78. package/dist/components/overlay/NuiModal.svelte +992 -0
  79. package/dist/components/overlay/NuiModal.svelte.d.ts +72 -0
  80. package/dist/components/overlay/NuiTooltip.svelte +159 -0
  81. package/dist/components/overlay/NuiTooltip.svelte.d.ts +13 -0
  82. package/dist/composables/useClickOutside.d.ts +43 -0
  83. package/dist/composables/useClickOutside.js +73 -0
  84. package/dist/composables/useFormValidation.d.ts +23 -0
  85. package/dist/composables/useFormValidation.js +72 -0
  86. package/dist/composables/useIdle.svelte.d.ts +18 -0
  87. package/dist/composables/useIdle.svelte.js +144 -0
  88. package/dist/composables/useKeyboardShortcut.svelte.d.ts +114 -0
  89. package/dist/composables/useKeyboardShortcut.svelte.js +153 -0
  90. package/dist/composables/useLocalStorage.svelte.d.ts +22 -0
  91. package/dist/composables/useLocalStorage.svelte.js +105 -0
  92. package/dist/composables/useMediaQuery.svelte.d.ts +51 -0
  93. package/dist/composables/useMediaQuery.svelte.js +80 -0
  94. package/dist/index.d.ts +46 -0
  95. package/dist/index.js +42 -0
  96. package/package.json +58 -3
  97. package/src/components/actions/NuiButton.svelte +292 -0
  98. package/src/components/data/NuiBadge.svelte +277 -0
  99. package/src/components/data/NuiCodeBlock.svelte +256 -0
  100. package/src/components/data/NuiList.svelte +341 -0
  101. package/src/components/data/NuiProgressBar.svelte +264 -0
  102. package/src/components/data/NuiRichText.svelte +260 -0
  103. package/src/components/data/NuiTable.svelte +470 -0
  104. package/src/components/data/chart/NuiChart.svelte +384 -0
  105. package/src/components/data/chart/color.ts +83 -0
  106. package/src/components/data/chart/index.ts +10 -0
  107. package/src/components/data/chart/registry.ts +71 -0
  108. package/src/components/data/chart/renderers/bar.ts +90 -0
  109. package/src/components/data/chart/renderers/base.ts +32 -0
  110. package/src/components/data/chart/renderers/line.ts +99 -0
  111. package/src/components/data/chart/renderers/pie.ts +66 -0
  112. package/src/components/data/chart/scale.ts +139 -0
  113. package/src/components/data/chart/types.ts +98 -0
  114. package/src/components/data/chart/validator.ts +98 -0
  115. package/src/components/forms/NuiCheckbox.svelte +218 -0
  116. package/src/components/forms/NuiDatepicker.svelte +755 -0
  117. package/src/components/forms/NuiFileInput.svelte +1047 -0
  118. package/src/components/forms/NuiFileInput.types.ts +29 -0
  119. package/src/components/forms/NuiForm.svelte +391 -0
  120. package/src/components/forms/NuiInput.svelte +296 -0
  121. package/src/components/forms/NuiRadioGroup.svelte +238 -0
  122. package/src/components/forms/NuiSelect.svelte +819 -0
  123. package/src/components/layout/NuiCard.svelte +314 -0
  124. package/src/components/layout/NuiCardGrid.svelte +98 -0
  125. package/src/components/layout/NuiFlex.svelte +121 -0
  126. package/src/components/layout/NuiGrid.svelte +66 -0
  127. package/src/components/layout/NuiStack.svelte +138 -0
  128. package/src/components/navigation/NuiAccordion.svelte +323 -0
  129. package/src/components/navigation/NuiBreadcrumbs.svelte +133 -0
  130. package/src/components/navigation/NuiMenu.svelte +596 -0
  131. package/src/components/navigation/NuiPagination.svelte +306 -0
  132. package/src/components/navigation/NuiSidebar.svelte +186 -0
  133. package/src/components/navigation/NuiTabs.svelte +249 -0
  134. package/src/components/overlay/NuiDialog.svelte +118 -0
  135. package/src/components/overlay/NuiModal.svelte +992 -0
  136. package/src/components/overlay/NuiTooltip.svelte +159 -0
  137. package/src/composables/useClickOutside.ts +132 -0
  138. package/src/composables/useFormValidation.ts +105 -0
  139. package/src/composables/useIdle.svelte.ts +191 -0
  140. package/src/composables/useKeyboardShortcut.svelte.ts +278 -0
  141. package/src/composables/useLocalStorage.svelte.ts +137 -0
  142. package/src/composables/useMediaQuery.svelte.ts +102 -0
  143. package/src/index.ts +66 -0
@@ -0,0 +1,470 @@
1
+ <!--
2
+ @component NuiTable
3
+ Accessible, high-performance data table component supporting interactive sorting,
4
+ search filtering, windowed pagination, and Svelte 5 cell snippets.
5
+ -->
6
+ <script module lang="ts">
7
+ import type { Snippet } from 'svelte';
8
+
9
+ /**
10
+ * Column definition for {@link NuiTable}.
11
+ */
12
+ export interface Column<T = Record<string, unknown>> {
13
+ /** Unique property key corresponding to row data. */
14
+ key: string;
15
+ /** Column header label. */
16
+ header: string;
17
+ /** Whether the column can be sorted by clicking the header. */
18
+ sortable?: boolean;
19
+ /** Text alignment in header and body cells. */
20
+ align?: 'left' | 'center' | 'right';
21
+ /** Custom Svelte 5 snippet for rendering rich cell content. */
22
+ cell?: Snippet<[{ row: T; value: unknown; index: number }]>;
23
+ /** Text/primitive formatter function. */
24
+ format?: (value: unknown, row: T) => string | number;
25
+ /** Legacy row render function returning formatted text or number. */
26
+ render?: (row: T) => string | number;
27
+ }
28
+
29
+ export type NuiTableColumn<T = Record<string, unknown>> = Column<T>;
30
+ </script>
31
+
32
+ <script lang="ts">
33
+ interface Props<T = Record<string, unknown>> {
34
+ /** Column definitions. */
35
+ columns?: Column<T>[];
36
+ /** Array of row objects. */
37
+ data?: T[];
38
+ /** Sort expression ('key' for ascending, '-key' for descending). Supports two-way binding. */
39
+ sort?: string;
40
+ /** Number of rows per page. Defaults to 10. */
41
+ pageSize?: number;
42
+ /** Current 1-based page index. Supports two-way binding. */
43
+ page?: number;
44
+ /** Text query to filter rows across all columns. */
45
+ searchQuery?: string;
46
+ /** Whether to render pagination controls. Defaults to true. */
47
+ pagination?: boolean;
48
+ /** Message displayed when no rows are available. */
49
+ emptyText?: string;
50
+ /** Optional callback executed when sort state changes. */
51
+ onSortChange?: (sort: string | undefined) => void;
52
+ }
53
+
54
+ let {
55
+ columns = [],
56
+ data = [],
57
+ sort = $bindable(),
58
+ pageSize = 10,
59
+ page = $bindable(1),
60
+ searchQuery = '',
61
+ pagination = true,
62
+ emptyText = 'No data available.',
63
+ onSortChange
64
+ }: Props = $props();
65
+
66
+ const safePageSize = $derived(
67
+ typeof pageSize === 'number' && Number.isFinite(pageSize) && pageSize > 0
68
+ ? Math.floor(pageSize)
69
+ : 10
70
+ );
71
+
72
+ const safeData = $derived(data ?? []);
73
+ const safeColumns = $derived(columns ?? []);
74
+ const filteredData = $derived(filterBySearch(safeData, searchQuery));
75
+ const sortedData = $derived(sortRows(sort, filteredData));
76
+ const totalPages = $derived(Math.max(1, Math.ceil(filteredData.length / safePageSize)));
77
+
78
+ let lastSearchQuery = searchQuery;
79
+
80
+ // Reset to first page when search filter changes
81
+ $effect(() => {
82
+ if (searchQuery !== lastSearchQuery) {
83
+ lastSearchQuery = searchQuery;
84
+ page = 1;
85
+ }
86
+ });
87
+
88
+ // Clamp current page within valid bounds
89
+ $effect(() => {
90
+ if (page > totalPages) {
91
+ page = totalPages;
92
+ } else if (page < 1) {
93
+ page = 1;
94
+ }
95
+ });
96
+
97
+ const displayedData = $derived.by(() => {
98
+ if (sortedData.length === 0) return [];
99
+ const validPage = Math.min(Math.max(1, page), totalPages);
100
+ const start = (validPage - 1) * safePageSize;
101
+ return sortedData.slice(start, start + safePageSize);
102
+ });
103
+
104
+ function handleSort(key: string) {
105
+ let nextSort: string | undefined;
106
+
107
+ if (sort === key) {
108
+ nextSort = `-${key}`;
109
+ } else if (sort === `-${key}`) {
110
+ nextSort = undefined;
111
+ } else {
112
+ nextSort = key;
113
+ }
114
+
115
+ sort = nextSort;
116
+ onSortChange?.(nextSort);
117
+ }
118
+
119
+ function sortRows(sortBy: string | undefined, rows: Record<string, unknown>[]) {
120
+ if (!rows || rows.length === 0 || !sortBy) return rows ?? [];
121
+
122
+ const descending = sortBy.startsWith('-');
123
+ const key = descending ? sortBy.slice(1) : sortBy;
124
+ const direction = descending ? -1 : 1;
125
+
126
+ return [...rows].sort((a, b) => {
127
+ const aVal = a[key];
128
+ const bVal = b[key];
129
+
130
+ const aMissing = aVal === null || aVal === undefined || aVal === '';
131
+ const bMissing = bVal === null || bVal === undefined || bVal === '';
132
+
133
+ // Missing values consistently sink to the bottom regardless of sort direction
134
+ if (aMissing && bMissing) return 0;
135
+ if (aMissing) return 1;
136
+ if (bMissing) return -1;
137
+
138
+ // Numeric comparison
139
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
140
+ return (aVal - bVal) * direction;
141
+ }
142
+
143
+ // Numeric strings comparison
144
+ const aNum = Number(aVal);
145
+ const bNum = Number(bVal);
146
+ if (
147
+ typeof aVal !== 'boolean' &&
148
+ typeof bVal !== 'boolean' &&
149
+ !Number.isNaN(aNum) &&
150
+ !Number.isNaN(bNum)
151
+ ) {
152
+ return (aNum - bNum) * direction;
153
+ }
154
+
155
+ // Dates
156
+ if (aVal instanceof Date && bVal instanceof Date) {
157
+ return (aVal.getTime() - bVal.getTime()) * direction;
158
+ }
159
+
160
+ // Booleans
161
+ if (typeof aVal === 'boolean' && typeof bVal === 'boolean') {
162
+ return (aVal === bVal ? 0 : aVal ? 1 : -1) * direction;
163
+ }
164
+
165
+ // Natural language string comparison with numeric collation
166
+ return (
167
+ String(aVal).localeCompare(String(bVal), undefined, {
168
+ numeric: true,
169
+ sensitivity: 'base'
170
+ }) * direction
171
+ );
172
+ });
173
+ }
174
+
175
+ function filterBySearch(rows: Record<string, unknown>[], query: string) {
176
+ if (!rows || rows.length === 0) return [];
177
+ const trimmed = query.trim().toLowerCase();
178
+ if (!trimmed) return rows;
179
+
180
+ return rows.filter((row) =>
181
+ safeColumns.some((col) => {
182
+ const val = row[col.key];
183
+ return val !== null && val !== undefined && String(val).toLowerCase().includes(trimmed);
184
+ })
185
+ );
186
+ }
187
+
188
+ function goToPage(newPage: number) {
189
+ if (newPage < 1 || newPage > totalPages) return;
190
+ page = newPage;
191
+ }
192
+
193
+ type PageItem = number | 'ellipsis';
194
+
195
+ const paginationItems = $derived.by((): PageItem[] => {
196
+ if (totalPages <= 7) {
197
+ return Array.from({ length: totalPages }, (_, i) => i + 1);
198
+ }
199
+
200
+ const items: PageItem[] = [1];
201
+ const left = Math.max(2, page - 1);
202
+ const right = Math.min(totalPages - 1, page + 1);
203
+
204
+ if (left > 2) {
205
+ items.push('ellipsis');
206
+ }
207
+
208
+ for (let i = left; i <= right; i++) {
209
+ items.push(i);
210
+ }
211
+
212
+ if (right < totalPages - 1) {
213
+ items.push('ellipsis');
214
+ }
215
+
216
+ items.push(totalPages);
217
+ return items;
218
+ });
219
+ </script>
220
+
221
+ <div class="nui-table-container">
222
+ {#if searchQuery && searchQuery.trim()}
223
+ <div class="nui-table__search" role="status" aria-live="polite">
224
+ Found {filteredData.length} {filteredData.length === 1 ? 'result' : 'results'} for "{searchQuery.trim()}"
225
+ </div>
226
+ {/if}
227
+
228
+ <table class="nui-table">
229
+ <thead>
230
+ <tr>
231
+ {#each safeColumns as col (col.key)}
232
+ {@const isSortedAsc = sort === col.key}
233
+ {@const isSortedDesc = sort === `-${col.key}`}
234
+ {@const ariaSortValue = col.sortable
235
+ ? (isSortedAsc ? 'ascending' : isSortedDesc ? 'descending' : 'none')
236
+ : undefined}
237
+ <th
238
+ class="nui-table__th"
239
+ class:nui-table__th--sortable={col.sortable}
240
+ aria-sort={ariaSortValue}
241
+ style="text-align: {col.align ?? 'left'}"
242
+ >
243
+ {#if col.sortable}
244
+ <button
245
+ type="button"
246
+ class="nui-table__sort-btn"
247
+ onclick={() => handleSort(col.key)}
248
+ aria-label={`Sort by ${col.header}`}
249
+ >
250
+ <span>{col.header}</span>
251
+ <span class="nui-table__sort-icon" aria-hidden="true">
252
+ {#if isSortedAsc}
253
+ ▲
254
+ {:else if isSortedDesc}
255
+ ▼
256
+ {:else}
257
+ ↕
258
+ {/if}
259
+ </span>
260
+ </button>
261
+ {:else}
262
+ <span>{col.header}</span>
263
+ {/if}
264
+ </th>
265
+ {/each}
266
+ </tr>
267
+ </thead>
268
+ <tbody>
269
+ {#each displayedData as row, rowIndex ((row as Record<string, unknown>).id ?? (row as Record<string, unknown>).key ?? rowIndex)}
270
+ <tr>
271
+ {#each safeColumns as col (col.key)}
272
+ <td class="nui-table__td" style="text-align: {col.align ?? 'left'}">
273
+ {#if col.cell}
274
+ {@render col.cell({ row, value: row[col.key], index: rowIndex })}
275
+ {:else if col.format}
276
+ {col.format(row[col.key], row)}
277
+ {:else if col.render}
278
+ {col.render(row)}
279
+ {:else}
280
+ {row[col.key] ?? ''}
281
+ {/if}
282
+ </td>
283
+ {/each}
284
+ </tr>
285
+ {:else}
286
+ <tr>
287
+ <td
288
+ class="nui-table__td nui-table__empty"
289
+ colspan={safeColumns.length || 1}
290
+ role="status"
291
+ aria-live="polite"
292
+ >
293
+ {emptyText}
294
+ </td>
295
+ </tr>
296
+ {/each}
297
+ </tbody>
298
+ </table>
299
+
300
+ {#if pagination && totalPages > 1}
301
+ <nav class="nui-table__pagination" aria-label="Pagination">
302
+ <button
303
+ type="button"
304
+ class="nui-table__page-btn"
305
+ disabled={page === 1}
306
+ aria-label="Previous page"
307
+ onclick={() => goToPage(page - 1)}
308
+ >
309
+ ‹
310
+ </button>
311
+
312
+ {#each paginationItems as item, idx (`${item}-${idx}`)}
313
+ {#if item === 'ellipsis'}
314
+ <span class="nui-table__ellipsis" aria-hidden="true">…</span>
315
+ {:else}
316
+ <button
317
+ type="button"
318
+ class="nui-table__page-btn"
319
+ class:nui-table__page-btn--active={page === item}
320
+ aria-current={page === item ? 'page' : undefined}
321
+ aria-label={`Page ${item}`}
322
+ onclick={() => goToPage(item)}
323
+ >
324
+ {item}
325
+ </button>
326
+ {/if}
327
+ {/each}
328
+
329
+ <button
330
+ type="button"
331
+ class="nui-table__page-btn"
332
+ disabled={page === totalPages}
333
+ aria-label="Next page"
334
+ onclick={() => goToPage(page + 1)}
335
+ >
336
+ ›
337
+ </button>
338
+ </nav>
339
+ {/if}
340
+ </div>
341
+
342
+ <style>
343
+ .nui-table-container {
344
+ width: 100%;
345
+ overflow-x: auto;
346
+ font-family: inherit;
347
+ box-sizing: border-box;
348
+ }
349
+
350
+ .nui-table__search {
351
+ padding: 0.5rem;
352
+ font-size: 0.875rem;
353
+ color: var(--nui-fg-secondary, #8b8b99);
354
+ margin-bottom: 0.5rem;
355
+ }
356
+
357
+ .nui-table {
358
+ width: 100%;
359
+ border-collapse: collapse;
360
+ color: var(--nui-fg, #fafafa);
361
+ font-size: 0.875rem;
362
+ }
363
+
364
+ .nui-table__th,
365
+ .nui-table__td {
366
+ padding: 0.75rem 1rem;
367
+ border-bottom: 1px solid var(--nui-border, #27272a);
368
+ box-sizing: border-box;
369
+ }
370
+
371
+ .nui-table__th {
372
+ background: var(--nui-surface-alt, #101012);
373
+ font-weight: 600;
374
+ color: var(--nui-fg-secondary, #8b8b99);
375
+ position: sticky;
376
+ top: 0;
377
+ user-select: none;
378
+ }
379
+
380
+ .nui-table__sort-btn {
381
+ background: transparent;
382
+ border: none;
383
+ padding: 0;
384
+ font: inherit;
385
+ color: inherit;
386
+ font-weight: 600;
387
+ display: inline-flex;
388
+ align-items: center;
389
+ gap: 0.35rem;
390
+ cursor: pointer;
391
+ text-align: inherit;
392
+ width: 100%;
393
+ border-radius: var(--nui-radius, 0.25rem);
394
+ transition: color 0.15s ease;
395
+ }
396
+
397
+ .nui-table__sort-btn:hover {
398
+ color: var(--nui-fg, #fafafa);
399
+ }
400
+
401
+ .nui-table__sort-btn:focus-visible {
402
+ outline: 2px solid var(--nui-primary, #3b82f6);
403
+ outline-offset: 2px;
404
+ }
405
+
406
+ .nui-table__sort-icon {
407
+ font-size: 0.75rem;
408
+ opacity: 0.6;
409
+ user-select: none;
410
+ }
411
+
412
+ .nui-table__empty {
413
+ text-align: center;
414
+ padding: 2.5rem 1rem;
415
+ color: var(--nui-fg-secondary, #8b8b99);
416
+ }
417
+
418
+ .nui-table__pagination {
419
+ display: flex;
420
+ gap: 0.35rem;
421
+ justify-content: center;
422
+ align-items: center;
423
+ padding: 0.75rem;
424
+ margin-top: 0.5rem;
425
+ }
426
+
427
+ .nui-table__page-btn {
428
+ background: transparent;
429
+ border: 1px solid var(--nui-border, #3f3f46);
430
+ border-radius: var(--nui-radius, 0.25rem);
431
+ padding: 0.25rem 0.65rem;
432
+ font: inherit;
433
+ font-size: 0.8125rem;
434
+ color: inherit;
435
+ cursor: pointer;
436
+ transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
437
+ }
438
+
439
+ .nui-table__page-btn:hover:not(:disabled) {
440
+ background: var(--nui-hover, rgb(255 255 255 / 0.06));
441
+ border-color: var(--nui-border-hover, #52525b);
442
+ }
443
+
444
+ .nui-table__page-btn:focus-visible {
445
+ outline: 2px solid var(--nui-primary, #3b82f6);
446
+ outline-offset: 1px;
447
+ }
448
+
449
+ .nui-table__page-btn:disabled:not(.nui-table__page-btn--active) {
450
+ opacity: 0.35;
451
+ cursor: not-allowed;
452
+ }
453
+
454
+ .nui-table__page-btn--active {
455
+ background: var(--nui-primary, #3b82f6);
456
+ border-color: var(--nui-primary, #3b82f6);
457
+ color: #fff;
458
+ cursor: default;
459
+ }
460
+
461
+ .nui-table__ellipsis {
462
+ display: inline-flex;
463
+ align-items: center;
464
+ justify-content: center;
465
+ padding: 0.25rem 0.5rem;
466
+ font-size: 0.8125rem;
467
+ color: var(--nui-fg-secondary, #8b8b99);
468
+ user-select: none;
469
+ }
470
+ </style>
@@ -0,0 +1,53 @@
1
+ import type { Snippet } from 'svelte';
2
+ /**
3
+ * Column definition for {@link NuiTable}.
4
+ */
5
+ export interface Column<T = Record<string, unknown>> {
6
+ /** Unique property key corresponding to row data. */
7
+ key: string;
8
+ /** Column header label. */
9
+ header: string;
10
+ /** Whether the column can be sorted by clicking the header. */
11
+ sortable?: boolean;
12
+ /** Text alignment in header and body cells. */
13
+ align?: 'left' | 'center' | 'right';
14
+ /** Custom Svelte 5 snippet for rendering rich cell content. */
15
+ cell?: Snippet<[{
16
+ row: T;
17
+ value: unknown;
18
+ index: number;
19
+ }]>;
20
+ /** Text/primitive formatter function. */
21
+ format?: (value: unknown, row: T) => string | number;
22
+ /** Legacy row render function returning formatted text or number. */
23
+ render?: (row: T) => string | number;
24
+ }
25
+ export type NuiTableColumn<T = Record<string, unknown>> = Column<T>;
26
+ interface Props<T = Record<string, unknown>> {
27
+ /** Column definitions. */
28
+ columns?: Column<T>[];
29
+ /** Array of row objects. */
30
+ data?: T[];
31
+ /** Sort expression ('key' for ascending, '-key' for descending). Supports two-way binding. */
32
+ sort?: string;
33
+ /** Number of rows per page. Defaults to 10. */
34
+ pageSize?: number;
35
+ /** Current 1-based page index. Supports two-way binding. */
36
+ page?: number;
37
+ /** Text query to filter rows across all columns. */
38
+ searchQuery?: string;
39
+ /** Whether to render pagination controls. Defaults to true. */
40
+ pagination?: boolean;
41
+ /** Message displayed when no rows are available. */
42
+ emptyText?: string;
43
+ /** Optional callback executed when sort state changes. */
44
+ onSortChange?: (sort: string | undefined) => void;
45
+ }
46
+ /**
47
+ * NuiTable
48
+ * Accessible, high-performance data table component supporting interactive sorting,
49
+ * search filtering, windowed pagination, and Svelte 5 cell snippets.
50
+ */
51
+ declare const NuiTable: import("svelte").Component<Props<Record<string, unknown>>, {}, "page" | "sort">;
52
+ type NuiTable = ReturnType<typeof NuiTable>;
53
+ export default NuiTable;