@marianmeres/stuic 3.7.0 → 3.8.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/dist/components/DataTable/DataTable.svelte +451 -0
- package/dist/components/DataTable/DataTable.svelte.d.ts +106 -0
- package/dist/components/DataTable/README.md +151 -0
- package/dist/components/DataTable/index.css +303 -0
- package/dist/components/DataTable/index.d.ts +1 -0
- package/dist/components/DataTable/index.js +1 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
import type { PagingCalcResult } from "@marianmeres/paging-store";
|
|
5
|
+
import type { THC } from "../Thc/index.js";
|
|
6
|
+
import type { TranslateFn } from "../../types.js";
|
|
7
|
+
import { isPlainObject } from "../../utils/is-plain-object.js";
|
|
8
|
+
import { replaceMap } from "../../utils/replace-map.js";
|
|
9
|
+
|
|
10
|
+
// i18n ready
|
|
11
|
+
function t_default(
|
|
12
|
+
k: string,
|
|
13
|
+
values: false | null | undefined | Record<string, string | number> = null,
|
|
14
|
+
fallback: string | boolean = "",
|
|
15
|
+
_i18nSpanWrap: boolean = true
|
|
16
|
+
) {
|
|
17
|
+
const m: Record<string, string> = {
|
|
18
|
+
previous_page: "Prev",
|
|
19
|
+
next_page: "Next",
|
|
20
|
+
page_x_of_y: "Page {page} of {pageCount}",
|
|
21
|
+
no_data: "No data",
|
|
22
|
+
select_all_rows: "Select all rows",
|
|
23
|
+
select_row: "Select row",
|
|
24
|
+
};
|
|
25
|
+
let out = m[k] ?? fallback ?? k;
|
|
26
|
+
return isPlainObject(values) ? replaceMap(out, values as any) : out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DataTableColumn<T = Record<string, any>> {
|
|
30
|
+
/** Property key to extract from row data (supports dot-notation: "data.name") */
|
|
31
|
+
key: string;
|
|
32
|
+
/** Column header label (string, HTML, component, or snippet) */
|
|
33
|
+
label?: THC;
|
|
34
|
+
/** CSS width value (e.g. "200px", "30%") */
|
|
35
|
+
width?: string;
|
|
36
|
+
/** Additional CSS class for body cells in this column */
|
|
37
|
+
class?: string;
|
|
38
|
+
/** Additional CSS class for the header cell */
|
|
39
|
+
classHeader?: string;
|
|
40
|
+
/** Text alignment */
|
|
41
|
+
align?: "left" | "center" | "right";
|
|
42
|
+
/** Hide this column in mobile card view */
|
|
43
|
+
hideOnMobile?: boolean;
|
|
44
|
+
/** Simple text formatter for cell values */
|
|
45
|
+
renderValue?: (value: any, row: T) => string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface Props<T = Record<string, any>> extends Omit<
|
|
49
|
+
HTMLAttributes<HTMLDivElement>,
|
|
50
|
+
"children"
|
|
51
|
+
> {
|
|
52
|
+
/** Column definitions */
|
|
53
|
+
columns: DataTableColumn<T>[];
|
|
54
|
+
/** Array of row data objects */
|
|
55
|
+
data: T[];
|
|
56
|
+
/** Function to extract a unique ID from a row. Defaults to index. */
|
|
57
|
+
getRowId?: (row: T, index: number) => string | number;
|
|
58
|
+
|
|
59
|
+
/** Paging calculation result from @marianmeres/paging-store */
|
|
60
|
+
paging?: PagingCalcResult;
|
|
61
|
+
/** Callback when the user navigates to a different page (receives new offset) */
|
|
62
|
+
onPageChange?: (offset: number) => void;
|
|
63
|
+
|
|
64
|
+
/** Enable row selection checkboxes */
|
|
65
|
+
selectable?: boolean;
|
|
66
|
+
/** Set of selected row IDs (bindable) */
|
|
67
|
+
selected?: Set<string | number>;
|
|
68
|
+
/** Toggle row selection when clicking anywhere on the row */
|
|
69
|
+
selectOnRowClick?: boolean;
|
|
70
|
+
|
|
71
|
+
/** Callback when a row is clicked */
|
|
72
|
+
onRowClick?: (row: T, index: number) => void;
|
|
73
|
+
|
|
74
|
+
/** Show loading state (spinner overlay + reduced opacity) */
|
|
75
|
+
loading?: boolean;
|
|
76
|
+
|
|
77
|
+
/** Custom cell renderer snippet */
|
|
78
|
+
cell?: Snippet<
|
|
79
|
+
[{ column: DataTableColumn<T>; row: T; value: any; rowIndex: number }]
|
|
80
|
+
>;
|
|
81
|
+
/** Batch actions bar snippet (shown when items are selected) */
|
|
82
|
+
batchActions?: Snippet<
|
|
83
|
+
[
|
|
84
|
+
{
|
|
85
|
+
selected: Set<string | number>;
|
|
86
|
+
selectedRows: T[];
|
|
87
|
+
clearSelection: () => void;
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
>;
|
|
91
|
+
/** Custom empty state snippet */
|
|
92
|
+
empty?: Snippet;
|
|
93
|
+
/** Custom mobile row card snippet */
|
|
94
|
+
mobileRow?: Snippet<[{ row: T; columns: DataTableColumn<T>[]; rowIndex: number }]>;
|
|
95
|
+
/** Default children snippet (not used directly) */
|
|
96
|
+
children?: Snippet;
|
|
97
|
+
|
|
98
|
+
/** Optional translate function */
|
|
99
|
+
t?: TranslateFn;
|
|
100
|
+
|
|
101
|
+
/** Force mobile/card layout regardless of breakpoint */
|
|
102
|
+
small?: boolean;
|
|
103
|
+
|
|
104
|
+
/** Skip all default styling */
|
|
105
|
+
unstyled?: boolean;
|
|
106
|
+
/** Additional CSS classes for the root container */
|
|
107
|
+
class?: string;
|
|
108
|
+
/** Bindable element reference */
|
|
109
|
+
el?: HTMLDivElement;
|
|
110
|
+
}
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<script lang="ts" generics="T extends Record<string, any> = Record<string, any>">
|
|
114
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
115
|
+
import { Breakpoint } from "../../utils/breakpoint.svelte.js";
|
|
116
|
+
import Spinner from "../Spinner/Spinner.svelte";
|
|
117
|
+
import Button from "../Button/Button.svelte";
|
|
118
|
+
import Thc, { isTHCNotEmpty, getTHCStringContent } from "../Thc/Thc.svelte";
|
|
119
|
+
|
|
120
|
+
let {
|
|
121
|
+
columns,
|
|
122
|
+
data,
|
|
123
|
+
getRowId = (_row: T, index: number) => index,
|
|
124
|
+
paging,
|
|
125
|
+
onPageChange,
|
|
126
|
+
selectable = false,
|
|
127
|
+
selected = $bindable(new Set()),
|
|
128
|
+
selectOnRowClick = false,
|
|
129
|
+
onRowClick,
|
|
130
|
+
loading = false,
|
|
131
|
+
cell,
|
|
132
|
+
batchActions,
|
|
133
|
+
empty,
|
|
134
|
+
mobileRow,
|
|
135
|
+
children,
|
|
136
|
+
t = t_default,
|
|
137
|
+
small = false,
|
|
138
|
+
unstyled = false,
|
|
139
|
+
class: classProp,
|
|
140
|
+
el = $bindable(),
|
|
141
|
+
...rest
|
|
142
|
+
}: Props<T> = $props();
|
|
143
|
+
|
|
144
|
+
// --- Responsive ---
|
|
145
|
+
const bp = Breakpoint.instance;
|
|
146
|
+
let isDesktop = $derived(small ? false : bp.md);
|
|
147
|
+
|
|
148
|
+
// --- Paging ---
|
|
149
|
+
let showPaging = $derived(paging != null && paging.pageCount > 1);
|
|
150
|
+
|
|
151
|
+
// --- Selection ---
|
|
152
|
+
let allRowIds = $derived(data.map((row, i) => getRowId(row, i)));
|
|
153
|
+
|
|
154
|
+
let allOnPageSelected = $derived.by(() => {
|
|
155
|
+
if (!selectable || allRowIds.length === 0) return false;
|
|
156
|
+
return allRowIds.every((id) => selected.has(id));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
let someOnPageSelected = $derived.by(() => {
|
|
160
|
+
if (!selectable || allRowIds.length === 0) return false;
|
|
161
|
+
return allRowIds.some((id) => selected.has(id)) && !allOnPageSelected;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
let selectedRows = $derived.by(() => {
|
|
165
|
+
if (!selectable || selected.size === 0) return [] as T[];
|
|
166
|
+
return data.filter((row, i) => selected.has(getRowId(row, i)));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
function toggleSelectAll() {
|
|
170
|
+
if (allOnPageSelected) {
|
|
171
|
+
const next = new Set(selected);
|
|
172
|
+
for (const id of allRowIds) next.delete(id);
|
|
173
|
+
selected = next;
|
|
174
|
+
} else {
|
|
175
|
+
const next = new Set(selected);
|
|
176
|
+
for (const id of allRowIds) next.add(id);
|
|
177
|
+
selected = next;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function toggleSelectRow(id: string | number) {
|
|
182
|
+
const next = new Set(selected);
|
|
183
|
+
if (next.has(id)) {
|
|
184
|
+
next.delete(id);
|
|
185
|
+
} else {
|
|
186
|
+
next.add(id);
|
|
187
|
+
}
|
|
188
|
+
selected = next;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function clearSelection() {
|
|
192
|
+
selected = new Set();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// --- Row click ---
|
|
196
|
+
function handleRowClick(row: T, index: number, e: MouseEvent) {
|
|
197
|
+
const target = e.target as HTMLElement;
|
|
198
|
+
if (
|
|
199
|
+
target.closest('input[type="checkbox"]') ||
|
|
200
|
+
target.closest("button") ||
|
|
201
|
+
target.closest("a")
|
|
202
|
+
) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (selectable && selectOnRowClick) {
|
|
206
|
+
toggleSelectRow(getRowId(row, index));
|
|
207
|
+
}
|
|
208
|
+
onRowClick?.(row, index);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --- Cell value helpers ---
|
|
212
|
+
function getCellValue(row: T, column: DataTableColumn<T>): any {
|
|
213
|
+
return column.key.split(".").reduce((obj: any, k) => obj?.[k], row);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getCellDisplay(row: T, column: DataTableColumn<T>): string {
|
|
217
|
+
const value = getCellValue(row, column);
|
|
218
|
+
if (column.renderValue) return column.renderValue(value, row);
|
|
219
|
+
return value == null ? "" : String(value);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// --- CSS ---
|
|
223
|
+
let rootClass = $derived(unstyled ? classProp : twMerge("stuic-data-table", classProp));
|
|
224
|
+
|
|
225
|
+
let mobileColumns = $derived(columns.filter((col) => !col.hideOnMobile));
|
|
226
|
+
</script>
|
|
227
|
+
|
|
228
|
+
<!-- Batch action bar -->
|
|
229
|
+
{#if selectable && selected.size > 0 && batchActions}
|
|
230
|
+
<div class={!unstyled ? "stuic-data-table-batch" : undefined}>
|
|
231
|
+
{@render batchActions({ selected, selectedRows, clearSelection })}
|
|
232
|
+
</div>
|
|
233
|
+
{/if}
|
|
234
|
+
|
|
235
|
+
<!-- Root container -->
|
|
236
|
+
<div bind:this={el} class={rootClass} {...rest}>
|
|
237
|
+
{#if isDesktop}
|
|
238
|
+
<!-- DESKTOP: TABLE -->
|
|
239
|
+
<div
|
|
240
|
+
class={!unstyled ? "stuic-data-table-wrapper" : undefined}
|
|
241
|
+
data-loading={!unstyled && loading ? "true" : undefined}
|
|
242
|
+
>
|
|
243
|
+
<table>
|
|
244
|
+
<thead>
|
|
245
|
+
<tr>
|
|
246
|
+
{#if selectable}
|
|
247
|
+
<th data-checkbox class="stuic-checkbox">
|
|
248
|
+
<input
|
|
249
|
+
type="checkbox"
|
|
250
|
+
checked={allOnPageSelected}
|
|
251
|
+
indeterminate={someOnPageSelected}
|
|
252
|
+
onchange={toggleSelectAll}
|
|
253
|
+
aria-label={t("select_all_rows")}
|
|
254
|
+
/>
|
|
255
|
+
</th>
|
|
256
|
+
{/if}
|
|
257
|
+
{#each columns as col (col.key)}
|
|
258
|
+
<th
|
|
259
|
+
class={col.classHeader}
|
|
260
|
+
data-align={!unstyled && col.align ? col.align : undefined}
|
|
261
|
+
style={col.width ? `width: ${col.width}` : undefined}
|
|
262
|
+
>
|
|
263
|
+
{#if isTHCNotEmpty(col.label)}
|
|
264
|
+
<Thc thc={col.label!} />
|
|
265
|
+
{:else}
|
|
266
|
+
{col.key}
|
|
267
|
+
{/if}
|
|
268
|
+
</th>
|
|
269
|
+
{/each}
|
|
270
|
+
</tr>
|
|
271
|
+
</thead>
|
|
272
|
+
<tbody>
|
|
273
|
+
{#each data as row, rowIndex (getRowId(row, rowIndex))}
|
|
274
|
+
{@const rowId = getRowId(row, rowIndex)}
|
|
275
|
+
{@const isSelected = selectable && selected.has(rowId)}
|
|
276
|
+
<tr
|
|
277
|
+
data-hoverable={!unstyled ? "true" : undefined}
|
|
278
|
+
data-clickable={!unstyled && (onRowClick || selectOnRowClick)
|
|
279
|
+
? "true"
|
|
280
|
+
: undefined}
|
|
281
|
+
data-selected={!unstyled && isSelected ? "true" : undefined}
|
|
282
|
+
onclick={(e) => handleRowClick(row, rowIndex, e)}
|
|
283
|
+
>
|
|
284
|
+
{#if selectable}
|
|
285
|
+
<td data-checkbox class="stuic-checkbox">
|
|
286
|
+
<input
|
|
287
|
+
type="checkbox"
|
|
288
|
+
checked={isSelected}
|
|
289
|
+
onchange={() => toggleSelectRow(rowId)}
|
|
290
|
+
aria-label={t("select_row")}
|
|
291
|
+
/>
|
|
292
|
+
</td>
|
|
293
|
+
{/if}
|
|
294
|
+
{#each columns as col (col.key)}
|
|
295
|
+
{@const value = getCellValue(row, col)}
|
|
296
|
+
<td
|
|
297
|
+
class={col.class}
|
|
298
|
+
data-align={!unstyled && col.align ? col.align : undefined}
|
|
299
|
+
>
|
|
300
|
+
{#if cell}
|
|
301
|
+
{@render cell({
|
|
302
|
+
column: col,
|
|
303
|
+
row,
|
|
304
|
+
value,
|
|
305
|
+
rowIndex,
|
|
306
|
+
})}
|
|
307
|
+
{:else}
|
|
308
|
+
{getCellDisplay(row, col)}
|
|
309
|
+
{/if}
|
|
310
|
+
</td>
|
|
311
|
+
{/each}
|
|
312
|
+
</tr>
|
|
313
|
+
{:else}
|
|
314
|
+
<tr>
|
|
315
|
+
<td
|
|
316
|
+
colspan={columns.length + (selectable ? 1 : 0)}
|
|
317
|
+
class={!unstyled ? "stuic-data-table-empty" : undefined}
|
|
318
|
+
>
|
|
319
|
+
{#if empty}
|
|
320
|
+
{@render empty()}
|
|
321
|
+
{:else}
|
|
322
|
+
{t("no_data")}
|
|
323
|
+
{/if}
|
|
324
|
+
</td>
|
|
325
|
+
</tr>
|
|
326
|
+
{/each}
|
|
327
|
+
</tbody>
|
|
328
|
+
</table>
|
|
329
|
+
</div>
|
|
330
|
+
{:else}
|
|
331
|
+
<!-- MOBILE: CARDS -->
|
|
332
|
+
<div
|
|
333
|
+
class={!unstyled ? "stuic-data-table-cards" : undefined}
|
|
334
|
+
data-loading={!unstyled && loading ? "true" : undefined}
|
|
335
|
+
>
|
|
336
|
+
{#each data as row, rowIndex (getRowId(row, rowIndex))}
|
|
337
|
+
{@const rowId = getRowId(row, rowIndex)}
|
|
338
|
+
{@const isSelected = selectable && selected.has(rowId)}
|
|
339
|
+
{#if mobileRow}
|
|
340
|
+
{@render mobileRow({
|
|
341
|
+
row,
|
|
342
|
+
columns: mobileColumns,
|
|
343
|
+
rowIndex,
|
|
344
|
+
})}
|
|
345
|
+
{:else}
|
|
346
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
347
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
348
|
+
<div
|
|
349
|
+
class={!unstyled ? "stuic-data-table-card" : undefined}
|
|
350
|
+
data-clickable={!unstyled && (onRowClick || selectOnRowClick)
|
|
351
|
+
? "true"
|
|
352
|
+
: undefined}
|
|
353
|
+
data-selected={!unstyled && isSelected ? "true" : undefined}
|
|
354
|
+
role={onRowClick || selectOnRowClick ? "button" : undefined}
|
|
355
|
+
tabindex={onRowClick || selectOnRowClick ? 0 : undefined}
|
|
356
|
+
onclick={(e) => handleRowClick(row, rowIndex, e)}
|
|
357
|
+
onkeydown={(e) => {
|
|
358
|
+
if (
|
|
359
|
+
(onRowClick || selectOnRowClick) &&
|
|
360
|
+
(e.key === "Enter" || e.key === " ")
|
|
361
|
+
) {
|
|
362
|
+
e.preventDefault();
|
|
363
|
+
if (selectable && selectOnRowClick) {
|
|
364
|
+
toggleSelectRow(rowId);
|
|
365
|
+
}
|
|
366
|
+
onRowClick?.(row, rowIndex);
|
|
367
|
+
}
|
|
368
|
+
}}
|
|
369
|
+
>
|
|
370
|
+
{#if selectable}
|
|
371
|
+
<div class="stuic-checkbox flex items-center gap-2 mb-1">
|
|
372
|
+
<input
|
|
373
|
+
type="checkbox"
|
|
374
|
+
checked={isSelected}
|
|
375
|
+
onchange={() => toggleSelectRow(rowId)}
|
|
376
|
+
aria-label={t("select_row")}
|
|
377
|
+
/>
|
|
378
|
+
</div>
|
|
379
|
+
{/if}
|
|
380
|
+
{#each mobileColumns as col (col.key)}
|
|
381
|
+
{@const value = getCellValue(row, col)}
|
|
382
|
+
<div class={!unstyled ? "stuic-data-table-card-row" : undefined}>
|
|
383
|
+
<span class={!unstyled ? "stuic-data-table-card-label" : undefined}>
|
|
384
|
+
{#if isTHCNotEmpty(col.label)}
|
|
385
|
+
{getTHCStringContent(col.label) || col.key}
|
|
386
|
+
{:else}
|
|
387
|
+
{col.key}
|
|
388
|
+
{/if}
|
|
389
|
+
</span>
|
|
390
|
+
<span class={!unstyled ? "stuic-data-table-card-value" : undefined}>
|
|
391
|
+
{#if cell}
|
|
392
|
+
{@render cell({
|
|
393
|
+
column: col,
|
|
394
|
+
row,
|
|
395
|
+
value,
|
|
396
|
+
rowIndex,
|
|
397
|
+
})}
|
|
398
|
+
{:else}
|
|
399
|
+
{getCellDisplay(row, col)}
|
|
400
|
+
{/if}
|
|
401
|
+
</span>
|
|
402
|
+
</div>
|
|
403
|
+
{/each}
|
|
404
|
+
</div>
|
|
405
|
+
{/if}
|
|
406
|
+
{:else}
|
|
407
|
+
<div class={!unstyled ? "stuic-data-table-empty" : undefined}>
|
|
408
|
+
{#if empty}
|
|
409
|
+
{@render empty()}
|
|
410
|
+
{:else}
|
|
411
|
+
No data
|
|
412
|
+
{/if}
|
|
413
|
+
</div>
|
|
414
|
+
{/each}
|
|
415
|
+
</div>
|
|
416
|
+
{/if}
|
|
417
|
+
|
|
418
|
+
<!-- Loading spinner overlay -->
|
|
419
|
+
{#if loading}
|
|
420
|
+
<div class={!unstyled ? "stuic-data-table-loading" : undefined}>
|
|
421
|
+
<Spinner />
|
|
422
|
+
</div>
|
|
423
|
+
{/if}
|
|
424
|
+
|
|
425
|
+
<!-- Paging -->
|
|
426
|
+
{#if showPaging && paging}
|
|
427
|
+
<div class={!unstyled ? "stuic-data-table-paging" : undefined}>
|
|
428
|
+
<Button
|
|
429
|
+
variant="ghost"
|
|
430
|
+
size="sm"
|
|
431
|
+
disabled={!paging.hasPrevious}
|
|
432
|
+
onclick={() => onPageChange?.(paging!.previousOffset)}
|
|
433
|
+
aria-label={t("previous_page")}
|
|
434
|
+
>
|
|
435
|
+
‹ {t("previous_page")}
|
|
436
|
+
</Button>
|
|
437
|
+
<span class={!unstyled ? "stuic-data-table-paging-info" : undefined}>
|
|
438
|
+
{t("page_x_of_y", { page: paging.currentPage, pageCount: paging.pageCount })}
|
|
439
|
+
</span>
|
|
440
|
+
<Button
|
|
441
|
+
variant="ghost"
|
|
442
|
+
size="sm"
|
|
443
|
+
disabled={!paging.hasNext}
|
|
444
|
+
onclick={() => onPageChange?.(paging!.nextOffset)}
|
|
445
|
+
aria-label={t("next_page")}
|
|
446
|
+
>
|
|
447
|
+
{t("next_page")} ›
|
|
448
|
+
</Button>
|
|
449
|
+
</div>
|
|
450
|
+
{/if}
|
|
451
|
+
</div>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import type { PagingCalcResult } from "@marianmeres/paging-store";
|
|
4
|
+
import type { THC } from "../Thc/index.js";
|
|
5
|
+
import type { TranslateFn } from "../../types.js";
|
|
6
|
+
export interface DataTableColumn<T = Record<string, any>> {
|
|
7
|
+
/** Property key to extract from row data (supports dot-notation: "data.name") */
|
|
8
|
+
key: string;
|
|
9
|
+
/** Column header label (string, HTML, component, or snippet) */
|
|
10
|
+
label?: THC;
|
|
11
|
+
/** CSS width value (e.g. "200px", "30%") */
|
|
12
|
+
width?: string;
|
|
13
|
+
/** Additional CSS class for body cells in this column */
|
|
14
|
+
class?: string;
|
|
15
|
+
/** Additional CSS class for the header cell */
|
|
16
|
+
classHeader?: string;
|
|
17
|
+
/** Text alignment */
|
|
18
|
+
align?: "left" | "center" | "right";
|
|
19
|
+
/** Hide this column in mobile card view */
|
|
20
|
+
hideOnMobile?: boolean;
|
|
21
|
+
/** Simple text formatter for cell values */
|
|
22
|
+
renderValue?: (value: any, row: T) => string;
|
|
23
|
+
}
|
|
24
|
+
export interface Props<T = Record<string, any>> extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
25
|
+
/** Column definitions */
|
|
26
|
+
columns: DataTableColumn<T>[];
|
|
27
|
+
/** Array of row data objects */
|
|
28
|
+
data: T[];
|
|
29
|
+
/** Function to extract a unique ID from a row. Defaults to index. */
|
|
30
|
+
getRowId?: (row: T, index: number) => string | number;
|
|
31
|
+
/** Paging calculation result from @marianmeres/paging-store */
|
|
32
|
+
paging?: PagingCalcResult;
|
|
33
|
+
/** Callback when the user navigates to a different page (receives new offset) */
|
|
34
|
+
onPageChange?: (offset: number) => void;
|
|
35
|
+
/** Enable row selection checkboxes */
|
|
36
|
+
selectable?: boolean;
|
|
37
|
+
/** Set of selected row IDs (bindable) */
|
|
38
|
+
selected?: Set<string | number>;
|
|
39
|
+
/** Toggle row selection when clicking anywhere on the row */
|
|
40
|
+
selectOnRowClick?: boolean;
|
|
41
|
+
/** Callback when a row is clicked */
|
|
42
|
+
onRowClick?: (row: T, index: number) => void;
|
|
43
|
+
/** Show loading state (spinner overlay + reduced opacity) */
|
|
44
|
+
loading?: boolean;
|
|
45
|
+
/** Custom cell renderer snippet */
|
|
46
|
+
cell?: Snippet<[
|
|
47
|
+
{
|
|
48
|
+
column: DataTableColumn<T>;
|
|
49
|
+
row: T;
|
|
50
|
+
value: any;
|
|
51
|
+
rowIndex: number;
|
|
52
|
+
}
|
|
53
|
+
]>;
|
|
54
|
+
/** Batch actions bar snippet (shown when items are selected) */
|
|
55
|
+
batchActions?: Snippet<[
|
|
56
|
+
{
|
|
57
|
+
selected: Set<string | number>;
|
|
58
|
+
selectedRows: T[];
|
|
59
|
+
clearSelection: () => void;
|
|
60
|
+
}
|
|
61
|
+
]>;
|
|
62
|
+
/** Custom empty state snippet */
|
|
63
|
+
empty?: Snippet;
|
|
64
|
+
/** Custom mobile row card snippet */
|
|
65
|
+
mobileRow?: Snippet<[{
|
|
66
|
+
row: T;
|
|
67
|
+
columns: DataTableColumn<T>[];
|
|
68
|
+
rowIndex: number;
|
|
69
|
+
}]>;
|
|
70
|
+
/** Default children snippet (not used directly) */
|
|
71
|
+
children?: Snippet;
|
|
72
|
+
/** Optional translate function */
|
|
73
|
+
t?: TranslateFn;
|
|
74
|
+
/** Force mobile/card layout regardless of breakpoint */
|
|
75
|
+
small?: boolean;
|
|
76
|
+
/** Skip all default styling */
|
|
77
|
+
unstyled?: boolean;
|
|
78
|
+
/** Additional CSS classes for the root container */
|
|
79
|
+
class?: string;
|
|
80
|
+
/** Bindable element reference */
|
|
81
|
+
el?: HTMLDivElement;
|
|
82
|
+
}
|
|
83
|
+
declare function $$render<T extends Record<string, any> = Record<string, any>>(): {
|
|
84
|
+
props: Props<T>;
|
|
85
|
+
exports: {};
|
|
86
|
+
bindings: "el" | "selected";
|
|
87
|
+
slots: {};
|
|
88
|
+
events: {};
|
|
89
|
+
};
|
|
90
|
+
declare class __sveltets_Render<T extends Record<string, any> = Record<string, any>> {
|
|
91
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
92
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
93
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
94
|
+
bindings(): "el" | "selected";
|
|
95
|
+
exports(): {};
|
|
96
|
+
}
|
|
97
|
+
interface $$IsomorphicComponent {
|
|
98
|
+
new <T extends Record<string, any> = Record<string, any>>(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']>> & {
|
|
99
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
100
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
101
|
+
<T extends Record<string, any> = Record<string, any>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
102
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
103
|
+
}
|
|
104
|
+
declare const DataTable: $$IsomorphicComponent;
|
|
105
|
+
type DataTable<T extends Record<string, any> = Record<string, any>> = InstanceType<typeof DataTable<T>>;
|
|
106
|
+
export default DataTable;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# DataTable
|
|
2
|
+
|
|
3
|
+
A responsive data table component with configurable columns, paging, batch selection,
|
|
4
|
+
loading state, and mobile card layout.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
|
|
8
|
+
### Basic
|
|
9
|
+
|
|
10
|
+
```svelte
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import { DataTable, type DataTableColumn } from "@marianmeres/stuic";
|
|
13
|
+
|
|
14
|
+
const columns: DataTableColumn[] = [
|
|
15
|
+
{ key: "id", label: "ID", width: "80px" },
|
|
16
|
+
{ key: "name", label: "Name" },
|
|
17
|
+
{ key: "email", label: "Email" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const data = [
|
|
21
|
+
{ id: 1, name: "Alice", email: "alice@example.com" },
|
|
22
|
+
{ id: 2, name: "Bob", email: "bob@example.com" },
|
|
23
|
+
];
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<DataTable {columns} {data} />
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### With Paging
|
|
30
|
+
|
|
31
|
+
```svelte
|
|
32
|
+
<DataTable
|
|
33
|
+
{columns}
|
|
34
|
+
{data}
|
|
35
|
+
page={1}
|
|
36
|
+
pageSize={20}
|
|
37
|
+
totalItems={100}
|
|
38
|
+
onPageChange={(p) => fetchPage(p)}
|
|
39
|
+
/>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### With Selection
|
|
43
|
+
|
|
44
|
+
```svelte
|
|
45
|
+
<script lang="ts">
|
|
46
|
+
let selected = $state(new Set<string | number>());
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<DataTable
|
|
50
|
+
{columns}
|
|
51
|
+
{data}
|
|
52
|
+
selectable
|
|
53
|
+
bind:selected
|
|
54
|
+
getRowId={(row) => row.id}
|
|
55
|
+
>
|
|
56
|
+
{#snippet batchActions({ selected, clearSelection })}
|
|
57
|
+
<span>{selected.size} selected</span>
|
|
58
|
+
<Button onclick={() => deleteSelected(selected)}>Delete</Button>
|
|
59
|
+
<Button variant="ghost" onclick={clearSelection}>Clear</Button>
|
|
60
|
+
{/snippet}
|
|
61
|
+
</DataTable>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Custom Cell Rendering
|
|
65
|
+
|
|
66
|
+
```svelte
|
|
67
|
+
<DataTable {columns} {data}>
|
|
68
|
+
{#snippet cell({ column, row, value })}
|
|
69
|
+
{#if column.key === "status"}
|
|
70
|
+
<span class="badge">{value}</span>
|
|
71
|
+
{:else}
|
|
72
|
+
{value}
|
|
73
|
+
{/if}
|
|
74
|
+
{/snippet}
|
|
75
|
+
</DataTable>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Custom Mobile Layout
|
|
79
|
+
|
|
80
|
+
```svelte
|
|
81
|
+
<DataTable {columns} {data}>
|
|
82
|
+
{#snippet mobileRow({ row })}
|
|
83
|
+
<div class="p-3 border rounded">
|
|
84
|
+
<strong>{row.name}</strong>
|
|
85
|
+
<p>{row.email}</p>
|
|
86
|
+
</div>
|
|
87
|
+
{/snippet}
|
|
88
|
+
</DataTable>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Props
|
|
92
|
+
|
|
93
|
+
| Prop | Type | Default | Description |
|
|
94
|
+
|------|------|---------|-------------|
|
|
95
|
+
| `columns` | `DataTableColumn<T>[]` | required | Column definitions |
|
|
96
|
+
| `data` | `T[]` | required | Array of row data |
|
|
97
|
+
| `getRowId` | `(row, index) => string \| number` | `(_, i) => i` | Row ID extractor |
|
|
98
|
+
| `page` | `number` | - | Current page (1-based) |
|
|
99
|
+
| `pageSize` | `number` | - | Rows per page |
|
|
100
|
+
| `totalItems` | `number` | - | Total items count |
|
|
101
|
+
| `onPageChange` | `(page: number) => void` | - | Page change callback |
|
|
102
|
+
| `selectable` | `boolean` | `false` | Enable selection checkboxes |
|
|
103
|
+
| `selected` | `Set<string \| number>` | `new Set()` | Selected row IDs (bindable) |
|
|
104
|
+
| `onRowClick` | `(row, index) => void` | - | Row click callback |
|
|
105
|
+
| `loading` | `boolean` | `false` | Show loading overlay |
|
|
106
|
+
| `cell` | `Snippet` | - | Custom cell renderer |
|
|
107
|
+
| `batchActions` | `Snippet` | - | Batch action bar content |
|
|
108
|
+
| `empty` | `Snippet` | - | Custom empty state |
|
|
109
|
+
| `mobileRow` | `Snippet` | - | Custom mobile row card |
|
|
110
|
+
| `unstyled` | `boolean` | `false` | Skip default styling |
|
|
111
|
+
| `class` | `string` | - | Additional CSS classes |
|
|
112
|
+
| `el` | `HTMLDivElement` | - | Bindable element ref |
|
|
113
|
+
|
|
114
|
+
## DataTableColumn
|
|
115
|
+
|
|
116
|
+
| Property | Type | Default | Description |
|
|
117
|
+
|----------|------|---------|-------------|
|
|
118
|
+
| `key` | `string` | required | Data property key (supports dot-notation) |
|
|
119
|
+
| `label` | `THC` | `key` | Column header content |
|
|
120
|
+
| `width` | `string` | - | CSS width value |
|
|
121
|
+
| `class` | `string` | - | Cell CSS class |
|
|
122
|
+
| `classHeader` | `string` | - | Header cell CSS class |
|
|
123
|
+
| `align` | `"left" \| "center" \| "right"` | `"left"` | Text alignment |
|
|
124
|
+
| `hideOnMobile` | `boolean` | `false` | Hide in mobile view |
|
|
125
|
+
| `renderValue` | `(value, row) => string` | - | Value formatter |
|
|
126
|
+
|
|
127
|
+
## CSS Variables
|
|
128
|
+
|
|
129
|
+
| Variable | Default | Description |
|
|
130
|
+
|----------|---------|-------------|
|
|
131
|
+
| `--stuic-data-table-radius` | `var(--radius-md)` | Border radius |
|
|
132
|
+
| `--stuic-data-table-border-color` | `var(--stuic-color-border)` | Border color |
|
|
133
|
+
| `--stuic-data-table-header-bg` | `var(--stuic-color-muted)` | Header background |
|
|
134
|
+
| `--stuic-data-table-header-color` | `var(--stuic-color-muted-foreground)` | Header text |
|
|
135
|
+
| `--stuic-data-table-header-font-size` | `0.875rem` | Header font size |
|
|
136
|
+
| `--stuic-data-table-header-font-weight` | `var(--font-weight-semibold)` | Header font weight |
|
|
137
|
+
| `--stuic-data-table-header-padding-x` | `0.75rem` | Header horizontal padding |
|
|
138
|
+
| `--stuic-data-table-header-padding-y` | `0.5rem` | Header vertical padding |
|
|
139
|
+
| `--stuic-data-table-row-bg` | `transparent` | Row background |
|
|
140
|
+
| `--stuic-data-table-row-bg-hover` | `var(--stuic-color-muted)` | Row hover background |
|
|
141
|
+
| `--stuic-data-table-row-bg-selected` | `color-mix(primary 10%)` | Selected row background |
|
|
142
|
+
| `--stuic-data-table-row-border-color` | `var(--stuic-color-border)` | Row border color |
|
|
143
|
+
| `--stuic-data-table-cell-padding-x` | `0.75rem` | Cell horizontal padding |
|
|
144
|
+
| `--stuic-data-table-cell-padding-y` | `0.75rem` | Cell vertical padding |
|
|
145
|
+
| `--stuic-data-table-cell-font-size` | `0.875rem` | Cell font size |
|
|
146
|
+
| `--stuic-data-table-loading-opacity` | `0.5` | Loading state opacity |
|
|
147
|
+
| `--stuic-data-table-card-bg` | `var(--stuic-color-background)` | Mobile card background |
|
|
148
|
+
| `--stuic-data-table-card-border-color` | `var(--stuic-color-border)` | Mobile card border |
|
|
149
|
+
| `--stuic-data-table-card-radius` | `var(--radius-md)` | Mobile card radius |
|
|
150
|
+
| `--stuic-data-table-card-padding` | `0.75rem` | Mobile card padding |
|
|
151
|
+
| `--stuic-data-table-card-gap` | `0.5rem` | Gap between mobile cards |
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
DATA TABLE COMPONENT TOKENS
|
|
3
|
+
Override globally: :root { --stuic-data-table-radius: 0; }
|
|
4
|
+
Override locally: <DataTable style="--stuic-data-table-radius: 0;">
|
|
5
|
+
============================================================================ */
|
|
6
|
+
|
|
7
|
+
/* prettier-ignore */
|
|
8
|
+
:root {
|
|
9
|
+
/* Structure */
|
|
10
|
+
--stuic-data-table-radius: var(--radius-md);
|
|
11
|
+
--stuic-data-table-border-width: 1px;
|
|
12
|
+
--stuic-data-table-border-color: var(--stuic-color-border);
|
|
13
|
+
--stuic-data-table-transition: 150ms;
|
|
14
|
+
|
|
15
|
+
/* Header */
|
|
16
|
+
--stuic-data-table-header-bg: var(--stuic-color-muted);
|
|
17
|
+
--stuic-data-table-header-color: var(--stuic-color-muted-foreground);
|
|
18
|
+
--stuic-data-table-header-font-size: 0.875rem;
|
|
19
|
+
--stuic-data-table-header-font-weight: var(--font-weight-semibold);
|
|
20
|
+
--stuic-data-table-header-padding-x: 0.75rem;
|
|
21
|
+
--stuic-data-table-header-padding-y: 0.5rem;
|
|
22
|
+
|
|
23
|
+
/* Row */
|
|
24
|
+
--stuic-data-table-row-bg: transparent;
|
|
25
|
+
--stuic-data-table-row-bg-hover: var(--stuic-color-muted);
|
|
26
|
+
--stuic-data-table-row-bg-selected: color-mix(in srgb, var(--stuic-color-primary) 10%, var(--stuic-color-background));
|
|
27
|
+
--stuic-data-table-row-border-color: var(--stuic-color-border);
|
|
28
|
+
|
|
29
|
+
/* Cell */
|
|
30
|
+
--stuic-data-table-cell-padding-x: 0.75rem;
|
|
31
|
+
--stuic-data-table-cell-padding-y: 0.75rem;
|
|
32
|
+
--stuic-data-table-cell-font-size: 0.875rem;
|
|
33
|
+
|
|
34
|
+
/* Paging */
|
|
35
|
+
--stuic-data-table-paging-gap: 0.5rem;
|
|
36
|
+
--stuic-data-table-paging-padding-y: 0.75rem;
|
|
37
|
+
|
|
38
|
+
/* Batch action bar */
|
|
39
|
+
--stuic-data-table-batch-bg: var(--stuic-color-muted);
|
|
40
|
+
--stuic-data-table-batch-padding-x: 0.75rem;
|
|
41
|
+
--stuic-data-table-batch-padding-y: 0.5rem;
|
|
42
|
+
|
|
43
|
+
/* Checkbox column */
|
|
44
|
+
--stuic-data-table-checkbox-width: 3rem;
|
|
45
|
+
|
|
46
|
+
/* Loading overlay */
|
|
47
|
+
--stuic-data-table-loading-opacity: 0.5;
|
|
48
|
+
|
|
49
|
+
/* Mobile card */
|
|
50
|
+
--stuic-data-table-card-bg: var(--stuic-color-background);
|
|
51
|
+
--stuic-data-table-card-border-color: var(--stuic-color-border);
|
|
52
|
+
--stuic-data-table-card-radius: var(--radius-md);
|
|
53
|
+
--stuic-data-table-card-padding: 0.75rem;
|
|
54
|
+
--stuic-data-table-card-gap: 0.5rem;
|
|
55
|
+
--stuic-data-table-card-bg-selected: color-mix(in srgb, var(--stuic-color-primary) 10%, var(--stuic-color-background));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@layer components {
|
|
59
|
+
/* ============================================================================
|
|
60
|
+
CONTAINER
|
|
61
|
+
============================================================================ */
|
|
62
|
+
|
|
63
|
+
.stuic-data-table {
|
|
64
|
+
position: relative;
|
|
65
|
+
width: 100%;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* ============================================================================
|
|
69
|
+
TABLE WRAPPER (desktop)
|
|
70
|
+
============================================================================ */
|
|
71
|
+
|
|
72
|
+
.stuic-data-table-wrapper {
|
|
73
|
+
overflow-x: auto;
|
|
74
|
+
border-radius: var(--stuic-data-table-radius);
|
|
75
|
+
border: var(--stuic-data-table-border-width) solid var(--stuic-data-table-border-color);
|
|
76
|
+
transition: opacity var(--stuic-data-table-transition);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.stuic-data-table-wrapper[data-loading="true"] {
|
|
80
|
+
opacity: var(--stuic-data-table-loading-opacity);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.stuic-data-table table {
|
|
84
|
+
width: 100%;
|
|
85
|
+
border-collapse: collapse;
|
|
86
|
+
table-layout: fixed;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ============================================================================
|
|
90
|
+
HEADER
|
|
91
|
+
============================================================================ */
|
|
92
|
+
|
|
93
|
+
.stuic-data-table thead {
|
|
94
|
+
background: var(--stuic-data-table-header-bg);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.stuic-data-table th {
|
|
98
|
+
padding: var(--stuic-data-table-header-padding-y) var(--stuic-data-table-header-padding-x);
|
|
99
|
+
font-size: var(--stuic-data-table-header-font-size);
|
|
100
|
+
font-weight: var(--stuic-data-table-header-font-weight);
|
|
101
|
+
color: var(--stuic-data-table-header-color);
|
|
102
|
+
text-align: left;
|
|
103
|
+
white-space: nowrap;
|
|
104
|
+
border-bottom: var(--stuic-data-table-border-width) solid
|
|
105
|
+
var(--stuic-data-table-border-color);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.stuic-data-table th[data-align="center"] {
|
|
109
|
+
text-align: center;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.stuic-data-table th[data-align="right"] {
|
|
113
|
+
text-align: right;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* ============================================================================
|
|
117
|
+
ROW
|
|
118
|
+
============================================================================ */
|
|
119
|
+
|
|
120
|
+
.stuic-data-table tbody tr {
|
|
121
|
+
background: var(--stuic-data-table-row-bg);
|
|
122
|
+
border-bottom: var(--stuic-data-table-border-width) solid
|
|
123
|
+
var(--stuic-data-table-row-border-color);
|
|
124
|
+
transition: background var(--stuic-data-table-transition);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.stuic-data-table tbody tr:last-child {
|
|
128
|
+
border-bottom: none;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.stuic-data-table tbody tr[data-hoverable="true"]:hover {
|
|
132
|
+
background: var(--stuic-data-table-row-bg-hover);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.stuic-data-table tbody tr[data-clickable="true"] {
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.stuic-data-table tbody tr[data-selected="true"] {
|
|
140
|
+
background: var(--stuic-data-table-row-bg-selected);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.stuic-data-table tbody tr[data-selected="true"][data-hoverable="true"]:hover {
|
|
144
|
+
background: color-mix(
|
|
145
|
+
in srgb,
|
|
146
|
+
var(--stuic-color-primary) 18%,
|
|
147
|
+
var(--stuic-color-background)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* ============================================================================
|
|
152
|
+
CELL
|
|
153
|
+
============================================================================ */
|
|
154
|
+
|
|
155
|
+
.stuic-data-table td {
|
|
156
|
+
padding: var(--stuic-data-table-cell-padding-y) var(--stuic-data-table-cell-padding-x);
|
|
157
|
+
font-size: var(--stuic-data-table-cell-font-size);
|
|
158
|
+
overflow: hidden;
|
|
159
|
+
text-overflow: ellipsis;
|
|
160
|
+
white-space: nowrap;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.stuic-data-table td[data-align="center"] {
|
|
164
|
+
text-align: center;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.stuic-data-table td[data-align="right"] {
|
|
168
|
+
text-align: right;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* Checkbox cell */
|
|
172
|
+
.stuic-data-table th[data-checkbox],
|
|
173
|
+
.stuic-data-table td[data-checkbox] {
|
|
174
|
+
width: var(--stuic-data-table-checkbox-width);
|
|
175
|
+
text-align: center;
|
|
176
|
+
padding-left: 0.5rem;
|
|
177
|
+
padding-right: 0.5rem;
|
|
178
|
+
line-height: 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* ============================================================================
|
|
182
|
+
PAGING
|
|
183
|
+
============================================================================ */
|
|
184
|
+
|
|
185
|
+
.stuic-data-table-paging {
|
|
186
|
+
display: flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
justify-content: center;
|
|
189
|
+
gap: var(--stuic-data-table-paging-gap);
|
|
190
|
+
padding-top: var(--stuic-data-table-paging-padding-y);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.stuic-data-table-paging-info {
|
|
194
|
+
font-size: var(--stuic-data-table-cell-font-size);
|
|
195
|
+
color: var(--stuic-data-table-header-color);
|
|
196
|
+
white-space: nowrap;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* ============================================================================
|
|
200
|
+
BATCH ACTION BAR
|
|
201
|
+
============================================================================ */
|
|
202
|
+
|
|
203
|
+
.stuic-data-table-batch {
|
|
204
|
+
display: flex;
|
|
205
|
+
align-items: center;
|
|
206
|
+
gap: var(--stuic-data-table-paging-gap);
|
|
207
|
+
padding: var(--stuic-data-table-batch-padding-y) var(--stuic-data-table-batch-padding-x);
|
|
208
|
+
background: var(--stuic-data-table-batch-bg);
|
|
209
|
+
border-radius: var(--stuic-data-table-radius);
|
|
210
|
+
margin-bottom: 0.5rem;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* ============================================================================
|
|
214
|
+
LOADING SPINNER OVERLAY
|
|
215
|
+
============================================================================ */
|
|
216
|
+
|
|
217
|
+
.stuic-data-table-loading {
|
|
218
|
+
position: absolute;
|
|
219
|
+
inset: 0;
|
|
220
|
+
display: flex;
|
|
221
|
+
align-items: center;
|
|
222
|
+
justify-content: center;
|
|
223
|
+
pointer-events: none;
|
|
224
|
+
z-index: 1;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* ============================================================================
|
|
228
|
+
EMPTY STATE
|
|
229
|
+
============================================================================ */
|
|
230
|
+
|
|
231
|
+
.stuic-data-table-empty {
|
|
232
|
+
padding: 2rem;
|
|
233
|
+
color: var(--stuic-data-table-header-color);
|
|
234
|
+
font-size: var(--stuic-data-table-cell-font-size);
|
|
235
|
+
text-align: center;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* When empty state is not inside a table cell (mobile cards) */
|
|
239
|
+
div.stuic-data-table-empty {
|
|
240
|
+
display: flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
justify-content: center;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* ============================================================================
|
|
246
|
+
MOBILE CARDS
|
|
247
|
+
============================================================================ */
|
|
248
|
+
|
|
249
|
+
.stuic-data-table-cards {
|
|
250
|
+
display: flex;
|
|
251
|
+
flex-direction: column;
|
|
252
|
+
gap: var(--stuic-data-table-card-gap);
|
|
253
|
+
transition: opacity var(--stuic-data-table-transition);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.stuic-data-table-cards[data-loading="true"] {
|
|
257
|
+
opacity: var(--stuic-data-table-loading-opacity);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.stuic-data-table-card {
|
|
261
|
+
background: var(--stuic-data-table-card-bg);
|
|
262
|
+
border: var(--stuic-data-table-border-width) solid var(--stuic-data-table-card-border-color);
|
|
263
|
+
border-radius: var(--stuic-data-table-card-radius);
|
|
264
|
+
padding: var(--stuic-data-table-card-padding);
|
|
265
|
+
transition: background var(--stuic-data-table-transition);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.stuic-data-table-card[data-clickable="true"] {
|
|
269
|
+
cursor: pointer;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.stuic-data-table-card[data-clickable="true"]:hover {
|
|
273
|
+
background: var(--stuic-data-table-row-bg-hover);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.stuic-data-table-card[data-selected="true"] {
|
|
277
|
+
background: var(--stuic-data-table-card-bg-selected);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.stuic-data-table-card-row {
|
|
281
|
+
display: flex;
|
|
282
|
+
justify-content: space-between;
|
|
283
|
+
align-items: baseline;
|
|
284
|
+
gap: 0.5rem;
|
|
285
|
+
padding: 0.25rem 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.stuic-data-table-card-label {
|
|
289
|
+
font-size: var(--stuic-data-table-header-font-size);
|
|
290
|
+
font-weight: var(--stuic-data-table-header-font-weight);
|
|
291
|
+
color: var(--stuic-data-table-header-color);
|
|
292
|
+
flex-shrink: 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.stuic-data-table-card-value {
|
|
296
|
+
font-size: var(--stuic-data-table-cell-font-size);
|
|
297
|
+
text-align: right;
|
|
298
|
+
overflow: hidden;
|
|
299
|
+
text-overflow: ellipsis;
|
|
300
|
+
white-space: nowrap;
|
|
301
|
+
min-width: 0;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as DataTable, type Props as DataTableProps, type DataTableColumn, } from "./DataTable.svelte";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as DataTable, } from "./DataTable.svelte";
|
package/dist/index.css
CHANGED
|
@@ -32,6 +32,7 @@ In practice:
|
|
|
32
32
|
@import "./components/Collapsible/index.css";
|
|
33
33
|
@import "./components/Carousel/index.css";
|
|
34
34
|
@import "./components/CommandMenu/index.css";
|
|
35
|
+
@import "./components/DataTable/index.css";
|
|
35
36
|
@import "./components/DismissibleMessage/index.css";
|
|
36
37
|
@import "./components/DropdownMenu/index.css";
|
|
37
38
|
@import "./components/Input/index.css";
|
package/dist/index.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export * from "./components/Carousel/index.js";
|
|
|
33
33
|
export * from "./components/Collapsible/index.js";
|
|
34
34
|
export * from "./components/ColorScheme/index.js";
|
|
35
35
|
export * from "./components/CommandMenu/index.js";
|
|
36
|
+
export * from "./components/DataTable/index.js";
|
|
36
37
|
export * from "./components/DismissibleMessage/index.js";
|
|
37
38
|
export * from "./components/Drawer/index.js";
|
|
38
39
|
export * from "./components/DropdownMenu/index.js";
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,7 @@ export * from "./components/Carousel/index.js";
|
|
|
34
34
|
export * from "./components/Collapsible/index.js";
|
|
35
35
|
export * from "./components/ColorScheme/index.js";
|
|
36
36
|
export * from "./components/CommandMenu/index.js";
|
|
37
|
+
export * from "./components/DataTable/index.js";
|
|
37
38
|
export * from "./components/DismissibleMessage/index.js";
|
|
38
39
|
export * from "./components/Drawer/index.js";
|
|
39
40
|
export * from "./components/DropdownMenu/index.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/stuic",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.0",
|
|
4
4
|
"files": [
|
|
5
5
|
"dist",
|
|
6
6
|
"!dist/**/*.test.*",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@marianmeres/clog": "^3.15.2",
|
|
67
67
|
"@marianmeres/item-collection": "^1.3.5",
|
|
68
|
+
"@marianmeres/paging-store": "^2.0.2",
|
|
68
69
|
"@marianmeres/parse-boolean": "^2.0.5",
|
|
69
70
|
"@marianmeres/ticker": "^1.16.5",
|
|
70
71
|
"esm-env": "^1.2.2",
|