@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.
- package/README.md +101 -2
- package/dist/components/actions/NuiButton.svelte +292 -0
- package/dist/components/actions/NuiButton.svelte.d.ts +60 -0
- package/dist/components/data/NuiBadge.svelte +277 -0
- package/dist/components/data/NuiBadge.svelte.d.ts +74 -0
- package/dist/components/data/NuiCodeBlock.svelte +256 -0
- package/dist/components/data/NuiCodeBlock.svelte.d.ts +49 -0
- package/dist/components/data/NuiList.svelte +341 -0
- package/dist/components/data/NuiList.svelte.d.ts +52 -0
- package/dist/components/data/NuiProgressBar.svelte +264 -0
- package/dist/components/data/NuiProgressBar.svelte.d.ts +51 -0
- package/dist/components/data/NuiRichText.svelte +260 -0
- package/dist/components/data/NuiRichText.svelte.d.ts +33 -0
- package/dist/components/data/NuiTable.svelte +470 -0
- package/dist/components/data/NuiTable.svelte.d.ts +53 -0
- package/dist/components/data/chart/NuiChart.svelte +384 -0
- package/dist/components/data/chart/NuiChart.svelte.d.ts +49 -0
- package/dist/components/data/chart/color.d.ts +9 -0
- package/dist/components/data/chart/color.js +78 -0
- package/dist/components/data/chart/index.d.ts +10 -0
- package/dist/components/data/chart/index.js +10 -0
- package/dist/components/data/chart/registry.d.ts +14 -0
- package/dist/components/data/chart/registry.js +53 -0
- package/dist/components/data/chart/renderers/bar.d.ts +8 -0
- package/dist/components/data/chart/renderers/bar.js +79 -0
- package/dist/components/data/chart/renderers/base.d.ts +18 -0
- package/dist/components/data/chart/renderers/base.js +10 -0
- package/dist/components/data/chart/renderers/line.d.ts +8 -0
- package/dist/components/data/chart/renderers/line.js +89 -0
- package/dist/components/data/chart/renderers/pie.d.ts +10 -0
- package/dist/components/data/chart/renderers/pie.js +55 -0
- package/dist/components/data/chart/scale.d.ts +22 -0
- package/dist/components/data/chart/scale.js +101 -0
- package/dist/components/data/chart/types.d.ts +84 -0
- package/dist/components/data/chart/types.js +1 -0
- package/dist/components/data/chart/validator.d.ts +10 -0
- package/dist/components/data/chart/validator.js +82 -0
- package/dist/components/forms/NuiCheckbox.svelte +218 -0
- package/dist/components/forms/NuiCheckbox.svelte.d.ts +37 -0
- package/dist/components/forms/NuiDatepicker.svelte +755 -0
- package/dist/components/forms/NuiDatepicker.svelte.d.ts +34 -0
- package/dist/components/forms/NuiFileInput.svelte +1047 -0
- package/dist/components/forms/NuiFileInput.svelte.d.ts +93 -0
- package/dist/components/forms/NuiFileInput.types.d.ts +28 -0
- package/dist/components/forms/NuiFileInput.types.js +1 -0
- package/dist/components/forms/NuiForm.svelte +391 -0
- package/dist/components/forms/NuiForm.svelte.d.ts +69 -0
- package/dist/components/forms/NuiInput.svelte +296 -0
- package/dist/components/forms/NuiInput.svelte.d.ts +62 -0
- package/dist/components/forms/NuiRadioGroup.svelte +238 -0
- package/dist/components/forms/NuiRadioGroup.svelte.d.ts +47 -0
- package/dist/components/forms/NuiSelect.svelte +819 -0
- package/dist/components/forms/NuiSelect.svelte.d.ts +49 -0
- package/dist/components/layout/NuiCard.svelte +314 -0
- package/dist/components/layout/NuiCard.svelte.d.ts +48 -0
- package/dist/components/layout/NuiCardGrid.svelte +98 -0
- package/dist/components/layout/NuiCardGrid.svelte.d.ts +22 -0
- package/dist/components/layout/NuiFlex.svelte +121 -0
- package/dist/components/layout/NuiFlex.svelte.d.ts +23 -0
- package/dist/components/layout/NuiGrid.svelte +66 -0
- package/dist/components/layout/NuiGrid.svelte.d.ts +14 -0
- package/dist/components/layout/NuiStack.svelte +138 -0
- package/dist/components/layout/NuiStack.svelte.d.ts +26 -0
- package/dist/components/navigation/NuiAccordion.svelte +323 -0
- package/dist/components/navigation/NuiAccordion.svelte.d.ts +38 -0
- package/dist/components/navigation/NuiBreadcrumbs.svelte +133 -0
- package/dist/components/navigation/NuiBreadcrumbs.svelte.d.ts +19 -0
- package/dist/components/navigation/NuiMenu.svelte +596 -0
- package/dist/components/navigation/NuiMenu.svelte.d.ts +28 -0
- package/dist/components/navigation/NuiPagination.svelte +306 -0
- package/dist/components/navigation/NuiPagination.svelte.d.ts +40 -0
- package/dist/components/navigation/NuiSidebar.svelte +186 -0
- package/dist/components/navigation/NuiSidebar.svelte.d.ts +26 -0
- package/dist/components/navigation/NuiTabs.svelte +249 -0
- package/dist/components/navigation/NuiTabs.svelte.d.ts +31 -0
- package/dist/components/overlay/NuiDialog.svelte +118 -0
- package/dist/components/overlay/NuiDialog.svelte.d.ts +34 -0
- package/dist/components/overlay/NuiModal.svelte +992 -0
- package/dist/components/overlay/NuiModal.svelte.d.ts +72 -0
- package/dist/components/overlay/NuiTooltip.svelte +159 -0
- package/dist/components/overlay/NuiTooltip.svelte.d.ts +13 -0
- package/dist/composables/useClickOutside.d.ts +43 -0
- package/dist/composables/useClickOutside.js +73 -0
- package/dist/composables/useFormValidation.d.ts +23 -0
- package/dist/composables/useFormValidation.js +72 -0
- package/dist/composables/useIdle.svelte.d.ts +18 -0
- package/dist/composables/useIdle.svelte.js +144 -0
- package/dist/composables/useKeyboardShortcut.svelte.d.ts +114 -0
- package/dist/composables/useKeyboardShortcut.svelte.js +153 -0
- package/dist/composables/useLocalStorage.svelte.d.ts +22 -0
- package/dist/composables/useLocalStorage.svelte.js +105 -0
- package/dist/composables/useMediaQuery.svelte.d.ts +51 -0
- package/dist/composables/useMediaQuery.svelte.js +80 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +42 -0
- package/package.json +58 -3
- package/src/components/actions/NuiButton.svelte +292 -0
- package/src/components/data/NuiBadge.svelte +277 -0
- package/src/components/data/NuiCodeBlock.svelte +256 -0
- package/src/components/data/NuiList.svelte +341 -0
- package/src/components/data/NuiProgressBar.svelte +264 -0
- package/src/components/data/NuiRichText.svelte +260 -0
- package/src/components/data/NuiTable.svelte +470 -0
- package/src/components/data/chart/NuiChart.svelte +384 -0
- package/src/components/data/chart/color.ts +83 -0
- package/src/components/data/chart/index.ts +10 -0
- package/src/components/data/chart/registry.ts +71 -0
- package/src/components/data/chart/renderers/bar.ts +90 -0
- package/src/components/data/chart/renderers/base.ts +32 -0
- package/src/components/data/chart/renderers/line.ts +99 -0
- package/src/components/data/chart/renderers/pie.ts +66 -0
- package/src/components/data/chart/scale.ts +139 -0
- package/src/components/data/chart/types.ts +98 -0
- package/src/components/data/chart/validator.ts +98 -0
- package/src/components/forms/NuiCheckbox.svelte +218 -0
- package/src/components/forms/NuiDatepicker.svelte +755 -0
- package/src/components/forms/NuiFileInput.svelte +1047 -0
- package/src/components/forms/NuiFileInput.types.ts +29 -0
- package/src/components/forms/NuiForm.svelte +391 -0
- package/src/components/forms/NuiInput.svelte +296 -0
- package/src/components/forms/NuiRadioGroup.svelte +238 -0
- package/src/components/forms/NuiSelect.svelte +819 -0
- package/src/components/layout/NuiCard.svelte +314 -0
- package/src/components/layout/NuiCardGrid.svelte +98 -0
- package/src/components/layout/NuiFlex.svelte +121 -0
- package/src/components/layout/NuiGrid.svelte +66 -0
- package/src/components/layout/NuiStack.svelte +138 -0
- package/src/components/navigation/NuiAccordion.svelte +323 -0
- package/src/components/navigation/NuiBreadcrumbs.svelte +133 -0
- package/src/components/navigation/NuiMenu.svelte +596 -0
- package/src/components/navigation/NuiPagination.svelte +306 -0
- package/src/components/navigation/NuiSidebar.svelte +186 -0
- package/src/components/navigation/NuiTabs.svelte +249 -0
- package/src/components/overlay/NuiDialog.svelte +118 -0
- package/src/components/overlay/NuiModal.svelte +992 -0
- package/src/components/overlay/NuiTooltip.svelte +159 -0
- package/src/composables/useClickOutside.ts +132 -0
- package/src/composables/useFormValidation.ts +105 -0
- package/src/composables/useIdle.svelte.ts +191 -0
- package/src/composables/useKeyboardShortcut.svelte.ts +278 -0
- package/src/composables/useLocalStorage.svelte.ts +137 -0
- package/src/composables/useMediaQuery.svelte.ts +102 -0
- 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;
|