@vuecs/table 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 (58) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +59 -0
  3. package/dist/components/Table.vue.d.ts +511 -0
  4. package/dist/components/Table.vue.d.ts.map +1 -0
  5. package/dist/components/TableBody.vue.d.ts +43 -0
  6. package/dist/components/TableBody.vue.d.ts.map +1 -0
  7. package/dist/components/TableCell.vue.d.ts +182 -0
  8. package/dist/components/TableCell.vue.d.ts.map +1 -0
  9. package/dist/components/TableEmpty.vue.d.ts +104 -0
  10. package/dist/components/TableEmpty.vue.d.ts.map +1 -0
  11. package/dist/components/TableFooter.vue.d.ts +40 -0
  12. package/dist/components/TableFooter.vue.d.ts.map +1 -0
  13. package/dist/components/TableHeadCell.vue.d.ts +267 -0
  14. package/dist/components/TableHeadCell.vue.d.ts.map +1 -0
  15. package/dist/components/TableHeader.vue.d.ts +40 -0
  16. package/dist/components/TableHeader.vue.d.ts.map +1 -0
  17. package/dist/components/TableLite.vue.d.ts +250 -0
  18. package/dist/components/TableLite.vue.d.ts.map +1 -0
  19. package/dist/components/TableLoading.vue.d.ts +88 -0
  20. package/dist/components/TableLoading.vue.d.ts.map +1 -0
  21. package/dist/components/TableRow.vue.d.ts +116 -0
  22. package/dist/components/TableRow.vue.d.ts.map +1 -0
  23. package/dist/components/TableSortIndicators.vue.d.ts +335 -0
  24. package/dist/components/TableSortIndicators.vue.d.ts.map +1 -0
  25. package/dist/components/index.d.ts +23 -0
  26. package/dist/components/index.d.ts.map +1 -0
  27. package/dist/composables/context.d.ts +104 -0
  28. package/dist/composables/context.d.ts.map +1 -0
  29. package/dist/composables/define-table.d.ts +48 -0
  30. package/dist/composables/define-table.d.ts.map +1 -0
  31. package/dist/composables/index.d.ts +5 -0
  32. package/dist/composables/index.d.ts.map +1 -0
  33. package/dist/composables/selection.d.ts +10 -0
  34. package/dist/composables/selection.d.ts.map +1 -0
  35. package/dist/composables/sort.d.ts +61 -0
  36. package/dist/composables/sort.d.ts.map +1 -0
  37. package/dist/defaults.d.ts +41 -0
  38. package/dist/defaults.d.ts.map +1 -0
  39. package/dist/index.d.ts +18 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.mjs +2081 -0
  42. package/dist/index.mjs.map +1 -0
  43. package/dist/style.css +145 -0
  44. package/dist/theme.d.ts +13 -0
  45. package/dist/theme.d.ts.map +1 -0
  46. package/dist/types.d.ts +248 -0
  47. package/dist/types.d.ts.map +1 -0
  48. package/dist/utils/auto-render.d.ts +31 -0
  49. package/dist/utils/auto-render.d.ts.map +1 -0
  50. package/dist/utils/index.d.ts +3 -0
  51. package/dist/utils/index.d.ts.map +1 -0
  52. package/dist/utils/render-utils.d.ts +49 -0
  53. package/dist/utils/render-utils.d.ts.map +1 -0
  54. package/dist/utils/sort-rows.d.ts +29 -0
  55. package/dist/utils/sort-rows.d.ts.map +1 -0
  56. package/dist/vue.d.ts +27 -0
  57. package/dist/vue.d.ts.map +1 -0
  58. package/package.json +62 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,2081 @@
1
+ import { installDefaultsManager, installThemeManager, isObject, themableProps, useComponentDefaults, useComponentTheme, useSelectionMachine as useRowSelectionMachine, useThemeProps } from "@vuecs/core";
2
+ import { Comment, Fragment, Teleport, Text, computed, defineComponent, h, inject, mergeProps, nextTick, onBeforeUnmount, onMounted, provide, ref, shallowRef, toRef, triggerRef, watch } from "vue";
3
+ //#region src/composables/context.ts
4
+ const TABLE_CONTEXT_KEY = Symbol("vcTableContext");
5
+ function provideTableContext(ctx) {
6
+ provide(TABLE_CONTEXT_KEY, ctx);
7
+ }
8
+ function useTable() {
9
+ return inject(TABLE_CONTEXT_KEY, null);
10
+ }
11
+ const TABLE_ROW_CONTEXT_KEY = Symbol("vcTableRowContext");
12
+ function provideTableRowContext(ctx) {
13
+ provide(TABLE_ROW_CONTEXT_KEY, ctx);
14
+ }
15
+ function useTableRow() {
16
+ return inject(TABLE_ROW_CONTEXT_KEY, null);
17
+ }
18
+ const TABLE_HEAD_CELL_COUNT_CONTEXT_KEY = Symbol("vcTableHeadCellCountContext");
19
+ function provideHeadCellCountContext(ctx) {
20
+ provide(TABLE_HEAD_CELL_COUNT_CONTEXT_KEY, ctx);
21
+ }
22
+ function useHeadCellCountContext() {
23
+ return inject(TABLE_HEAD_CELL_COUNT_CONTEXT_KEY, null);
24
+ }
25
+ //#endregion
26
+ //#region src/composables/sort.ts
27
+ /**
28
+ * Controlled sort machine. The consumer owns the sort state via
29
+ * `v-model:sort` — the table emits intent only. When `<VCTable
30
+ * :client-sort>` is set, the table additionally sorts data internally
31
+ * via `sortRows()` from `utils/sort-rows.ts`; the controlled v-model
32
+ * still emits, so consumers stay observable.
33
+ *
34
+ * State shape is `SortDescriptor[]` from v1.x-B onward — single-column
35
+ * sort is an array of length 0–1. Multi-key cycling lives entirely
36
+ * here; `<VCTableHeadCell>` reads `directionFor` / `positionFor` to
37
+ * paint the indicator + numeric badge.
38
+ */
39
+ function useSortMachine(options) {
40
+ const { source, columns, mustSort, maxSortKeys, emit } = options;
41
+ const local = ref(normalize(source.value));
42
+ watch(source, (next) => {
43
+ local.value = normalize(next);
44
+ });
45
+ const state = computed(() => local.value);
46
+ function findInitialDirection(key) {
47
+ return columns.value.find((c) => c.key === key)?.initialSortDirection ?? "asc";
48
+ }
49
+ function setSort(key, opts = {}) {
50
+ const current = local.value;
51
+ const idx = current.findIndex((s) => s.key === key);
52
+ const initial = findInitialDirection(key);
53
+ if (opts.direction !== void 0) {
54
+ const next = opts.append ? upsertAppend(current, key, opts.direction, maxSortKeys.value) : [{
55
+ key,
56
+ direction: opts.direction
57
+ }];
58
+ local.value = next;
59
+ emit(next);
60
+ return;
61
+ }
62
+ if (!opts.append) {
63
+ const existing = idx >= 0 ? current[idx] : null;
64
+ let next;
65
+ if (!existing) next = [{
66
+ key,
67
+ direction: initial
68
+ }];
69
+ else if (existing.direction === initial) next = [{
70
+ key,
71
+ direction: initial === "asc" ? "desc" : "asc"
72
+ }];
73
+ else next = mustSort.value ? [{
74
+ key,
75
+ direction: initial
76
+ }] : [];
77
+ local.value = next;
78
+ emit(next);
79
+ return;
80
+ }
81
+ if (idx < 0) {
82
+ const next = appendCapped(current, {
83
+ key,
84
+ direction: initial
85
+ }, maxSortKeys.value);
86
+ local.value = next;
87
+ emit(next);
88
+ return;
89
+ }
90
+ const existing = current[idx];
91
+ let next;
92
+ if (existing.direction === initial) next = current.map((s, i) => i === idx ? {
93
+ key,
94
+ direction: initial === "asc" ? "desc" : "asc"
95
+ } : s);
96
+ else next = current.filter((_, i) => i !== idx);
97
+ local.value = next;
98
+ emit(next);
99
+ }
100
+ function directionFor(key) {
101
+ const cur = local.value.find((s) => s.key === key);
102
+ return cur ? cur.direction : null;
103
+ }
104
+ function positionFor(key) {
105
+ const i = local.value.findIndex((s) => s.key === key);
106
+ return i < 0 ? null : i + 1;
107
+ }
108
+ /**
109
+ * Replace the entire sort state. Bypasses cycle logic + multi-sort
110
+ * gating — for callers that know exactly what they want (e.g. the
111
+ * sort-indicator chip row removes / reorders / clears descriptors
112
+ * directly without going through Shift-click semantics).
113
+ */
114
+ function setState(next) {
115
+ local.value = next;
116
+ emit(next);
117
+ }
118
+ return {
119
+ state,
120
+ setSort,
121
+ setState,
122
+ directionFor,
123
+ positionFor
124
+ };
125
+ }
126
+ /**
127
+ * Coerce v-model input into the canonical array shape. `undefined` /
128
+ * `null` (which Vue may bind through during the v1.x-B migration
129
+ * window when consumers still hold `ref<...>(null)`) become `[]`.
130
+ */
131
+ function normalize(v) {
132
+ if (v == null) return [];
133
+ return v;
134
+ }
135
+ /**
136
+ * Append a new descriptor, evicting the oldest when over the cap.
137
+ * Cap of `<= 0` is treated as "unlimited" so consumers can opt out.
138
+ */
139
+ function appendCapped(state, descriptor, max) {
140
+ const next = [...state, descriptor];
141
+ if (max > 0 && next.length > max) return next.slice(next.length - max);
142
+ return next;
143
+ }
144
+ /**
145
+ * In-place semantic: if `key` is already in the state, update its
146
+ * direction; else append (capped). Used by the explicit-direction
147
+ * path when `append` is set.
148
+ */
149
+ function upsertAppend(state, key, direction, max) {
150
+ const idx = state.findIndex((s) => s.key === key);
151
+ if (idx >= 0) return state.map((s, i) => i === idx ? {
152
+ key,
153
+ direction
154
+ } : s);
155
+ return appendCapped(state, {
156
+ key,
157
+ direction
158
+ }, max);
159
+ }
160
+ //#endregion
161
+ //#region src/components/TableBody.vue?vue&type=script&lang.ts
162
+ const tableBodyThemeDefaults$1 = { classes: { root: "vc-table-body" } };
163
+ //#endregion
164
+ //#region src/components/TableBody.vue
165
+ var TableBody_default = defineComponent({
166
+ name: "VCTableBody",
167
+ inheritAttrs: false,
168
+ props: { ...themableProps() },
169
+ slots: Object,
170
+ setup(props, { attrs, slots }) {
171
+ const theme = useComponentTheme("tableBody", useThemeProps(props), tableBodyThemeDefaults$1);
172
+ const ctx = useTable();
173
+ return () => {
174
+ const rowSlot = slots.row;
175
+ const data = ctx?.data.value ?? [];
176
+ let children;
177
+ if (rowSlot) {
178
+ if (data.length === 0) return null;
179
+ children = data.map((row, index) => rowSlot({
180
+ row,
181
+ index
182
+ }));
183
+ } else children = [slots.default?.() ?? null];
184
+ return h("tbody", mergeProps(attrs, { class: theme.value.root || void 0 }), children);
185
+ };
186
+ }
187
+ });
188
+ //#endregion
189
+ //#region src/utils/render-utils.ts
190
+ /**
191
+ * Convert a key like `'firstName'` / `'first_name'` / `'first-name'` to
192
+ * `'First Name'`. Used by the bare-string column shorthand to derive
193
+ * a label when only the key is given.
194
+ */
195
+ function startCase(input) {
196
+ if (!input) return "";
197
+ return input.replace(/[_-]+/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/\s+/g, " ").trim().split(" ").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
198
+ }
199
+ /**
200
+ * Normalize the `:columns` prop into a uniform `TableColumn[]`.
201
+ *
202
+ * - **Explicit array** (including empty `[]`): pass through, fill `label`
203
+ * from `startCase(key)`. Treating `[]` as authoritative lets consumers
204
+ * deliberately render a header-less table during loading without the
205
+ * auto-derive kicking in the moment data arrives.
206
+ * - **Bare-string shorthand**: `['id', 'name']` → `[{ key, label }, ...]`.
207
+ * - **Auto-derive**: only when `columns` is `undefined` AND `data[0]` is an
208
+ * object, derive the columns from `Object.keys(data[0])`, skipping
209
+ * underscore-prefixed row-meta keys (`_rowVariant` / `_cellVariants`).
210
+ */
211
+ function normalizeColumns(columns, data) {
212
+ const fromRaw = (col) => {
213
+ if (typeof col === "string") return {
214
+ key: col,
215
+ label: startCase(col)
216
+ };
217
+ return {
218
+ ...col,
219
+ label: col.label ?? startCase(col.key)
220
+ };
221
+ };
222
+ if (columns !== void 0) return columns.map(fromRaw);
223
+ if (data.length === 0 || !isObject(data[0])) return [];
224
+ const first = data[0];
225
+ return Object.keys(first).filter((key) => !key.startsWith("_")).map((key) => ({
226
+ key,
227
+ label: startCase(key)
228
+ }));
229
+ }
230
+ /**
231
+ * Resolve a column's value against a row.
232
+ *
233
+ * - String accessor: dot-path lookup (e.g. `'profile.email'` → `row.profile.email`)
234
+ * - Function accessor: invoked with the row
235
+ * - Falls back to `row[column.key]`
236
+ */
237
+ function resolveCellValue(column, row) {
238
+ const { accessor } = column;
239
+ if (typeof accessor === "function") return accessor(row);
240
+ if (typeof accessor === "string") return resolveDotPath(row, accessor);
241
+ if (isObject(row)) return row[column.key];
242
+ }
243
+ function resolveDotPath(obj, path) {
244
+ if (!isObject(obj)) return void 0;
245
+ let cur = obj;
246
+ for (const part of path.split(".")) {
247
+ if (!isObject(cur)) return void 0;
248
+ cur = cur[part];
249
+ }
250
+ return cur;
251
+ }
252
+ /**
253
+ * Resolve attribute objects, supporting both static-object and function forms
254
+ * used by `cellAttrs` and `headerAttrs`.
255
+ */
256
+ function resolveAttrs(attrs, ctx) {
257
+ if (!attrs) return void 0;
258
+ return typeof attrs === "function" ? attrs(ctx) : attrs;
259
+ }
260
+ /**
261
+ * Selectors that mark an element (or an ancestor of it) as interactive —
262
+ * row-click handlers should NOT fire when the click originates inside one
263
+ * of these. Plan 028 D5 ships this list; the closest-ancestor matching
264
+ * (vs direct-match) is what catches clicks on text INSIDE a button or
265
+ * anchor.
266
+ */
267
+ const INTERACTIVE_SELECTOR = [
268
+ "a",
269
+ "button",
270
+ "input",
271
+ "select",
272
+ "textarea",
273
+ "label[for]",
274
+ "[role=\"button\"]",
275
+ "[role=\"link\"]",
276
+ "[contenteditable]:not([contenteditable=\"false\"])",
277
+ "[tabindex]:not([tabindex=\"-1\"])",
278
+ ".vc-overlay-portal-content"
279
+ ].join(",");
280
+ /**
281
+ * Returns `true` when a row-level click should be suppressed because the
282
+ * event originated inside an interactive descendant — a button, anchor,
283
+ * form input, role=button/link, an explicitly-focusable element, or a
284
+ * portal'd overlay rendered into the document body (e.g. a
285
+ * `<VCDropdownMenu>` action menu inside the row).
286
+ *
287
+ * Pass the `<tr>` element as `rowEl` so the helper can disambiguate
288
+ * legitimate row clicks (where the `closest()` ancestor walk reaches the
289
+ * `<tr>` itself, which carries `tabindex="0"` when the row is clickable)
290
+ * from clicks on a genuine interactive descendant.
291
+ *
292
+ * Use at the row click handler: `if (filterRowClickEvent(event, tr)) return;`.
293
+ */
294
+ function filterRowClickEvent(event, rowEl) {
295
+ const { target } = event;
296
+ if (!(target instanceof Element)) return false;
297
+ const hit = target.closest(INTERACTIVE_SELECTOR);
298
+ if (hit === null) return false;
299
+ if (rowEl && hit === rowEl) return false;
300
+ return true;
301
+ }
302
+ //#endregion
303
+ //#region src/components/TableCell.vue?vue&type=script&lang.ts
304
+ const tableCellThemeDefaults$1 = { classes: { root: "vc-table-cell" } };
305
+ const tableCellProps = {
306
+ /** Render as `<th scope="row">` instead of `<td>` (mirror of `column.isRowHeader`). */
307
+ isRowHeader: {
308
+ type: Boolean,
309
+ default: false
310
+ },
311
+ /**
312
+ * Column key this cell corresponds to — used to resolve
313
+ * `_cellVariants[key]` from row meta AND to look up the column's
314
+ * `accessor` / `formatter` for the default-render path.
315
+ */
316
+ columnKey: {
317
+ type: String,
318
+ default: void 0
319
+ },
320
+ /** Forward-compat for stacked-mode CSS — emitted as `data-label`. */
321
+ dataLabel: {
322
+ type: String,
323
+ default: void 0
324
+ },
325
+ /** Alignment helper — selects the `align.<value>` theme variant. */
326
+ align: {
327
+ type: String,
328
+ default: void 0
329
+ },
330
+ /** `position: sticky` on this cell. Forwarded as `themeVariant.stickyColumn`. */
331
+ stickyColumn: {
332
+ type: Boolean,
333
+ default: void 0
334
+ },
335
+ /**
336
+ * Renders a selection checkbox/radio for this row. Pairs with
337
+ * `<VCTableHeadCell isSelector>` to build a selection column.
338
+ * State mirrors `selection.isSelected(rowKey)`; clicking toggles
339
+ * that row independently of any row-click handler. Falls back
340
+ * to the default slot when selection is off.
341
+ */
342
+ isSelector: {
343
+ type: Boolean,
344
+ default: false
345
+ },
346
+ /** `aria-label` for the per-row checkbox (defaults to `'Select row'`). */
347
+ selectorAriaLabel: {
348
+ type: String,
349
+ default: "Select row"
350
+ },
351
+ ...themableProps()
352
+ };
353
+ /**
354
+ * Detect whether the consumer-passed default slot's rendered vnodes
355
+ * contain any meaningful content (vs being an empty / whitespace-only
356
+ * / comment-only render).
357
+ *
358
+ * Vue passes `slots.default` as a render fn that's always present when
359
+ * a `<template>` slot is declared (even if empty), so a naive
360
+ * `slots.default?.()` truthy check would always pick the slot path
361
+ * and skip the auto-render. Walk the rendered vnodes and recurse into
362
+ * `Fragment` children — a `<template v-if>` / `<template v-for>` /
363
+ * multi-child template renders as a Fragment whose `children` is the
364
+ * actual content. Without recursion those would be falsely classified
365
+ * as empty.
366
+ *
367
+ * Takes the pre-rendered vnodes (not the slot fn) so the caller can
368
+ * invoke the slot at most once per render — slot fns can be
369
+ * non-trivial and should not be called twice.
370
+ */
371
+ function hasMeaningfulSlotContent(nodes) {
372
+ if (nodes == null || nodes === false) return false;
373
+ if (typeof nodes === "string") return nodes.trim().length > 0;
374
+ if (typeof nodes === "number") return true;
375
+ if (Array.isArray(nodes)) {
376
+ for (const child of nodes) if (hasMeaningfulSlotContent(child)) return true;
377
+ return false;
378
+ }
379
+ if (typeof nodes !== "object") return false;
380
+ const v = nodes;
381
+ if (v.type === Comment) return false;
382
+ if (v.type === Text) return typeof v.children === "string" && v.children.trim().length > 0;
383
+ if (v.type === Fragment) return hasMeaningfulSlotContent(v.children);
384
+ return true;
385
+ }
386
+ //#endregion
387
+ //#region src/components/TableCell.vue
388
+ var TableCell_default = defineComponent({
389
+ name: "VCTableCell",
390
+ inheritAttrs: false,
391
+ props: tableCellProps,
392
+ setup(props, { attrs, slots }) {
393
+ const tableCtx = useTable();
394
+ const rowCtx = useTableRow();
395
+ const themeProps = useThemeProps(props, "align", "stickyColumn");
396
+ const theme = useComponentTheme("tableCell", {
397
+ get themeClass() {
398
+ return themeProps.themeClass;
399
+ },
400
+ get themeVariant() {
401
+ const cellVariant = props.columnKey ? rowCtx?.cellVariants.value[props.columnKey] ?? void 0 : void 0;
402
+ return {
403
+ ...themeProps.themeVariant ?? {},
404
+ ...cellVariant ? { cellVariant } : {}
405
+ };
406
+ }
407
+ }, tableCellThemeDefaults$1);
408
+ return () => {
409
+ if (props.isSelector && tableCtx?.selection.mode.value !== void 0 && rowCtx) {
410
+ const rowKey = rowCtx.selectionKey.value;
411
+ const mode = tableCtx.selection.mode.value;
412
+ const checked = tableCtx.selection.isSelected(rowKey);
413
+ return h(props.isRowHeader ? "th" : "td", mergeProps(attrs, {
414
+ class: theme.value.root || void 0,
415
+ "data-label": props.dataLabel || void 0,
416
+ "data-sticky-column": props.stickyColumn ? "" : void 0,
417
+ scope: props.isRowHeader ? "row" : void 0,
418
+ role: props.isRowHeader ? "rowheader" : "gridcell"
419
+ }), [h("input", {
420
+ type: mode === "single" ? "radio" : "checkbox",
421
+ class: "vc-table-selector-checkbox",
422
+ "aria-label": props.selectorAriaLabel,
423
+ checked,
424
+ onClick: (e) => {
425
+ e.stopPropagation();
426
+ if (rowKey === void 0) return;
427
+ tableCtx.selection.toggle(rowKey);
428
+ }
429
+ })]);
430
+ }
431
+ const slotVNodes = slots.default?.();
432
+ let content = slotVNodes;
433
+ if (props.columnKey && tableCtx && rowCtx && !hasMeaningfulSlotContent(slotVNodes)) {
434
+ const column = tableCtx.columns.value.find((c) => c.key === props.columnKey);
435
+ if (column) {
436
+ const value = resolveCellValue(column, rowCtx.row.value);
437
+ if (column.formatter) content = column.formatter({
438
+ value,
439
+ key: column.key,
440
+ row: rowCtx.row.value
441
+ });
442
+ else if (value === void 0 || value === null) content = "";
443
+ else content = String(value);
444
+ }
445
+ }
446
+ const inGrid = tableCtx?.selection.mode.value !== void 0;
447
+ let cellRole;
448
+ if (inGrid) cellRole = props.isRowHeader ? "rowheader" : "gridcell";
449
+ const ariaAttrs = inGrid ? { role: cellRole } : {};
450
+ return h(props.isRowHeader ? "th" : "td", mergeProps(attrs, {
451
+ class: theme.value.root || void 0,
452
+ "data-label": props.dataLabel || void 0,
453
+ "data-sticky-column": props.stickyColumn ? "" : void 0,
454
+ scope: props.isRowHeader ? "row" : void 0,
455
+ ...ariaAttrs
456
+ }), content);
457
+ };
458
+ }
459
+ });
460
+ //#endregion
461
+ //#region src/components/TableFooter.vue?vue&type=script&lang.ts
462
+ const tableFooterThemeDefaults$1 = { classes: { root: "vc-table-footer" } };
463
+ //#endregion
464
+ //#region src/components/TableFooter.vue
465
+ var TableFooter_default = defineComponent({
466
+ name: "VCTableFooter",
467
+ inheritAttrs: false,
468
+ props: { ...themableProps() },
469
+ setup(props, { attrs, slots }) {
470
+ const theme = useComponentTheme("tableFooter", useThemeProps(props), tableFooterThemeDefaults$1);
471
+ provideHeadCellCountContext({
472
+ register: () => {},
473
+ unregister: () => {}
474
+ });
475
+ return () => h("tfoot", mergeProps(attrs, { class: theme.value.root || void 0 }), slots.default?.());
476
+ }
477
+ });
478
+ //#endregion
479
+ //#region src/components/TableHeadCell.vue?vue&type=script&lang.ts
480
+ const tableHeadCellThemeDefaults$1 = { classes: {
481
+ root: "vc-table-head-cell",
482
+ sortIcon: "vc-table-head-cell-sort-icon"
483
+ } };
484
+ function renderHeadContent(args) {
485
+ if (!args.isSelector) return args.defaultSlot?.();
486
+ if (args.selectionMode === "single") return null;
487
+ if (args.selectionMode === void 0) return args.defaultSlot?.();
488
+ return h("input", {
489
+ type: "checkbox",
490
+ class: "vc-table-selector-checkbox",
491
+ "aria-label": args.selectorAriaLabel,
492
+ checked: args.selectAllState === "all",
493
+ indeterminate: args.selectAllState === "some",
494
+ onClick: (e) => {
495
+ e.stopPropagation();
496
+ args.onSelectorClick();
497
+ }
498
+ });
499
+ }
500
+ //#endregion
501
+ //#region src/components/TableHeadCell.vue
502
+ var TableHeadCell_default = defineComponent({
503
+ name: "VCTableHeadCell",
504
+ inheritAttrs: false,
505
+ props: {
506
+ /** Column key — required for sort wiring (omit for purely presentational heads). */
507
+ columnKey: {
508
+ type: String,
509
+ default: void 0
510
+ },
511
+ /** Mark this header sortable. When set, click + Enter/Space cycle sort state. */
512
+ sortable: {
513
+ type: Boolean,
514
+ default: false
515
+ },
516
+ /** Override scope ("col" / "colgroup" / "row" / "rowgroup"). Smart default: colspan→colgroup, rowspan→rowgroup, else col. */
517
+ scope: {
518
+ type: String,
519
+ default: void 0
520
+ },
521
+ /** Native `<th colspan>` (forwarded; also drives the smart scope default). */
522
+ colspan: {
523
+ type: Number,
524
+ default: void 0
525
+ },
526
+ /** Native `<th rowspan>` (forwarded; also drives the smart scope default). */
527
+ rowspan: {
528
+ type: Number,
529
+ default: void 0
530
+ },
531
+ /** Native `<th title>`. */
532
+ title: {
533
+ type: String,
534
+ default: void 0
535
+ },
536
+ /** Native `<th abbr>`. */
537
+ abbr: {
538
+ type: String,
539
+ default: void 0
540
+ },
541
+ /** Alignment helper — selects the `align.<value>` theme variant. */
542
+ align: {
543
+ type: String,
544
+ default: void 0
545
+ },
546
+ /** `position: sticky` on this header cell. Forwarded as `themeVariant.stickyColumn`. */
547
+ stickyColumn: {
548
+ type: Boolean,
549
+ default: void 0
550
+ },
551
+ /**
552
+ * Renders a select-all checkbox when the parent table has
553
+ * `:selection-mode="multi"`. State is derived from the current
554
+ * selection against the visible data set: checked = all visible
555
+ * rows selected; indeterminate = some visible rows selected;
556
+ * unchecked = none.
557
+ *
558
+ * Click semantics — Gmail / GitHub style: when state is `none`
559
+ * or `some`, ADDS every visible row's key to the selection
560
+ * (preserving any off-screen / paginated selection); when state
561
+ * is `all`, REMOVES every visible row's key (also preserving
562
+ * off-screen selection). Cross-page selection persists across
563
+ * pagination flips.
564
+ *
565
+ * In `single` mode the header renders empty (only one row can
566
+ * be selected); when selection is off, the slot's default
567
+ * content renders so the column collapses gracefully.
568
+ */
569
+ isSelector: {
570
+ type: Boolean,
571
+ default: false
572
+ },
573
+ /** `aria-label` for the select-all checkbox (defaults to `'Select all rows'`). */
574
+ selectorAriaLabel: {
575
+ type: String,
576
+ default: "Select all rows"
577
+ },
578
+ ...themableProps()
579
+ },
580
+ setup(props, { attrs, slots }) {
581
+ const ctx = useTable();
582
+ const countCtx = useHeadCellCountContext();
583
+ onMounted(() => countCtx?.register());
584
+ onBeforeUnmount(() => countCtx?.unregister());
585
+ const themeProps = useThemeProps(props, "align", "stickyColumn");
586
+ const sortDirection = computed(() => {
587
+ if (!props.sortable || !props.columnKey || !ctx) return null;
588
+ const found = ctx.sort.value.find((s) => s.key === props.columnKey);
589
+ return found ? found.direction : null;
590
+ });
591
+ const sortIndex = computed(() => {
592
+ if (!props.sortable || !props.columnKey || !ctx) return null;
593
+ const i = ctx.sort.value.findIndex((s) => s.key === props.columnKey);
594
+ return i < 0 ? null : i + 1;
595
+ });
596
+ const theme = useComponentTheme("tableHeadCell", {
597
+ get themeClass() {
598
+ return themeProps.themeClass;
599
+ },
600
+ get themeVariant() {
601
+ return {
602
+ ...themeProps.themeVariant ?? {},
603
+ sorted: sortDirection.value ?? "none"
604
+ };
605
+ }
606
+ }, tableHeadCellThemeDefaults$1);
607
+ const resolvedScope = computed(() => {
608
+ if (props.scope) return props.scope;
609
+ if (props.colspan && props.colspan > 1) return "colgroup";
610
+ if (props.rowspan && props.rowspan > 1) return "rowgroup";
611
+ return "col";
612
+ });
613
+ const ariaSort = computed(() => {
614
+ if (!props.sortable) return void 0;
615
+ const d = sortDirection.value;
616
+ if (d === "asc") return "ascending";
617
+ if (d === "desc") return "descending";
618
+ return "none";
619
+ });
620
+ function onClick(event) {
621
+ if (!props.sortable || !props.columnKey) return;
622
+ event.preventDefault();
623
+ ctx?.setSort(props.columnKey, { append: event.shiftKey });
624
+ }
625
+ const allRowKeys = computed(() => {
626
+ if (!ctx) return [];
627
+ return ctx.data.value.map((row, i) => ctx.getRowKey(row, i) ?? i);
628
+ });
629
+ const selectAllState = computed(() => {
630
+ if (!ctx || ctx.selection.mode.value !== "multi") return "none";
631
+ const keys = allRowKeys.value;
632
+ if (keys.length === 0) return "none";
633
+ const sel = ctx.selection.value.value;
634
+ const selectedSet = new Set(Array.isArray(sel) ? sel : []);
635
+ if (selectedSet.size === 0) return "none";
636
+ let selectedCount = 0;
637
+ for (const k of keys) if (selectedSet.has(k)) selectedCount += 1;
638
+ if (selectedCount === 0) return "none";
639
+ if (selectedCount === keys.length) return "all";
640
+ return "some";
641
+ });
642
+ function onSelectorClick() {
643
+ if (!ctx || ctx.selection.mode.value !== "multi") return;
644
+ const current = ctx.selection.value.value;
645
+ const existing = new Set(Array.isArray(current) ? current : []);
646
+ if (selectAllState.value === "all") for (const k of allRowKeys.value) existing.delete(k);
647
+ else for (const k of allRowKeys.value) existing.add(k);
648
+ ctx.selection.setValue(Array.from(existing));
649
+ }
650
+ function onKeydown(event) {
651
+ if (!props.sortable || !props.columnKey) return;
652
+ if (event.key === "Enter" || event.key === " ") {
653
+ event.preventDefault();
654
+ ctx?.setSort(props.columnKey, { append: event.shiftKey });
655
+ }
656
+ }
657
+ return () => h("th", mergeProps(attrs, {
658
+ class: theme.value.root || void 0,
659
+ scope: resolvedScope.value,
660
+ "data-sticky-column": props.stickyColumn ? "" : void 0,
661
+ colspan: props.colspan,
662
+ rowspan: props.rowspan,
663
+ title: props.title,
664
+ abbr: props.abbr,
665
+ "aria-sort": ariaSort.value,
666
+ "data-sort-index": sortIndex.value !== null && sortIndex.value > 1 ? String(sortIndex.value) : void 0,
667
+ tabindex: props.sortable ? 0 : void 0,
668
+ role: props.sortable || ctx?.selection.mode.value !== void 0 ? "columnheader" : void 0,
669
+ onClick: props.sortable ? onClick : void 0,
670
+ onKeydown: props.sortable ? onKeydown : void 0
671
+ }), [renderHeadContent({
672
+ isSelector: props.isSelector,
673
+ selectionMode: ctx?.selection.mode.value,
674
+ selectAllState: selectAllState.value,
675
+ selectorAriaLabel: props.selectorAriaLabel,
676
+ onSelectorClick,
677
+ defaultSlot: slots.default
678
+ }), props.sortable && sortDirection.value ? h("span", {
679
+ class: theme.value.sortIcon || void 0,
680
+ "aria-hidden": "true",
681
+ "data-sort": sortDirection.value
682
+ }, sortDirection.value === "asc" ? "↑" : "↓") : null]);
683
+ }
684
+ });
685
+ //#endregion
686
+ //#region src/components/TableHeader.vue?vue&type=script&lang.ts
687
+ const tableHeaderThemeDefaults$1 = { classes: { root: "vc-table-header" } };
688
+ //#endregion
689
+ //#region src/components/TableHeader.vue
690
+ var TableHeader_default = defineComponent({
691
+ name: "VCTableHeader",
692
+ inheritAttrs: false,
693
+ props: { ...themableProps() },
694
+ setup(props, { attrs, slots }) {
695
+ const theme = useComponentTheme("tableHeader", useThemeProps(props), tableHeaderThemeDefaults$1);
696
+ return () => h("thead", mergeProps(attrs, { class: theme.value.root || void 0 }), slots.default?.());
697
+ }
698
+ });
699
+ //#endregion
700
+ //#region src/components/TableRow.vue?vue&type=script&lang.ts
701
+ const tableRowThemeDefaults$1 = { classes: { root: "vc-table-row" } };
702
+ //#endregion
703
+ //#region src/components/TableRow.vue
704
+ var TableRow_default = defineComponent({
705
+ name: "VCTableRow",
706
+ inheritAttrs: false,
707
+ props: {
708
+ /** Current row data — used to resolve `_rowVariant` / `_cellVariants` and to emit `@row-click`. */
709
+ row: {
710
+ type: null,
711
+ default: void 0
712
+ },
713
+ /** Row index — used by row keyboard navigation + focused-row tracking. */
714
+ index: {
715
+ type: Number,
716
+ default: void 0
717
+ },
718
+ /** Mark the row disabled — forwarded to `themeVariant.disabled`. */
719
+ disabled: {
720
+ type: Boolean,
721
+ default: void 0
722
+ },
723
+ /**
724
+ * Manual override for the row's selected state. When set, wins over
725
+ * the auto-resolved selection from `useTable().selection`. Leave
726
+ * undefined to let `<VCTable :selection>` drive selection state.
727
+ */
728
+ selected: {
729
+ type: Boolean,
730
+ default: void 0
731
+ },
732
+ ...themableProps()
733
+ },
734
+ setup(props, { attrs, slots }) {
735
+ const ctx = useTable();
736
+ const rowVariant = computed(() => {
737
+ if (!isObject(props.row)) return null;
738
+ const v = props.row._rowVariant;
739
+ return typeof v === "string" ? v : null;
740
+ });
741
+ const cellVariants = computed(() => {
742
+ if (!isObject(props.row)) return {};
743
+ const v = props.row._cellVariants;
744
+ return isObject(v) ? v : {};
745
+ });
746
+ const focused = computed(() => {
747
+ if (props.index === void 0) return false;
748
+ return ctx?.focusedRow.value === props.index;
749
+ });
750
+ const selectionKey = computed(() => {
751
+ if (props.index === void 0) return -1;
752
+ return ctx?.getRowKey(props.row, props.index) ?? props.index;
753
+ });
754
+ const selectionMode = computed(() => ctx?.selection.mode.value);
755
+ const autoSelected = computed(() => {
756
+ if (props.selected !== void 0) return props.selected;
757
+ if (props.index === void 0) return false;
758
+ return ctx?.selection.isSelected(selectionKey.value) ?? false;
759
+ });
760
+ const themeProps = useThemeProps(props, "disabled");
761
+ const theme = useComponentTheme("tableRow", {
762
+ get themeClass() {
763
+ return themeProps.themeClass;
764
+ },
765
+ get themeVariant() {
766
+ return {
767
+ ...themeProps.themeVariant ?? {},
768
+ ...rowVariant.value ? { rowVariant: rowVariant.value } : {},
769
+ focused: focused.value,
770
+ selected: autoSelected.value
771
+ };
772
+ }
773
+ }, tableRowThemeDefaults$1);
774
+ if (props.index !== void 0) provideTableRowContext({
775
+ row: toRef(props, "row"),
776
+ index: toRef(props, "index"),
777
+ rowVariant,
778
+ cellVariants,
779
+ focused,
780
+ selectionKey,
781
+ selected: autoSelected
782
+ });
783
+ const selectionActive = computed(() => selectionMode.value !== void 0);
784
+ const isInteractive = computed(() => (ctx?.rowClickable.value || selectionActive.value) && props.index !== void 0 && !props.disabled);
785
+ watch([isInteractive, () => props.index], ([active, idx], [, prevIdx]) => {
786
+ if (!ctx) return;
787
+ if (prevIdx !== void 0 && prevIdx !== idx) ctx.unregisterInteractiveRow(prevIdx);
788
+ if (active && idx !== void 0) ctx.registerInteractiveRow(idx);
789
+ else if (idx !== void 0) ctx.unregisterInteractiveRow(idx);
790
+ }, { immediate: true });
791
+ onBeforeUnmount(() => {
792
+ if (ctx && props.index !== void 0) ctx.unregisterInteractiveRow(props.index);
793
+ });
794
+ const tabindex = computed(() => {
795
+ if (!isInteractive.value) return void 0;
796
+ if (!selectionActive.value) return 0;
797
+ const focusIdx = ctx?.focusedRow.value;
798
+ const interactives = ctx?.interactiveRows.value;
799
+ if (focusIdx !== null && focusIdx !== void 0 && interactives !== void 0 && interactives.has(focusIdx)) return props.index === focusIdx ? 0 : -1;
800
+ if (!interactives || interactives.size === 0) return -1;
801
+ let firstInteractive = null;
802
+ for (const idx of interactives) if (firstInteractive === null || idx < firstInteractive) firstInteractive = idx;
803
+ return props.index === firstInteractive ? 0 : -1;
804
+ });
805
+ function activateSelection(event) {
806
+ if (!selectionActive.value || props.index === void 0) return;
807
+ ctx?.selection.toggle(selectionKey.value, {
808
+ range: event.shiftKey,
809
+ toggle: event.metaKey || event.ctrlKey
810
+ });
811
+ ctx?.setFocusedRow(props.index);
812
+ }
813
+ function onClick(event) {
814
+ if (!isInteractive.value) return;
815
+ const rowEl = event.currentTarget;
816
+ if (filterRowClickEvent(event, rowEl)) return;
817
+ if (props.index === void 0) return;
818
+ ctx?.setFocusedRow(props.index);
819
+ activateSelection(event);
820
+ if (ctx?.rowClickable.value) ctx?.emitRowClick(props.row, props.index, event);
821
+ }
822
+ function onKeydown(event) {
823
+ if (!isInteractive.value || props.index === void 0) return;
824
+ const i = props.index;
825
+ const interactives = ctx?.interactiveRows.value;
826
+ const sorted = interactives && interactives.size > 0 ? Array.from(interactives).sort((a, b) => a - b) : [];
827
+ const posInSorted = sorted.indexOf(i);
828
+ const moveTo = (target) => {
829
+ event.preventDefault();
830
+ if (sorted.length === 0) return;
831
+ const nextIdx = sorted[Math.max(0, Math.min(sorted.length - 1, target))];
832
+ ctx?.setFocusedRow(nextIdx);
833
+ const tr = event.currentTarget;
834
+ if (!tr) return;
835
+ nextTick(() => {
836
+ const direction = nextIdx > i ? "nextElementSibling" : "previousElementSibling";
837
+ let sibling = tr[direction];
838
+ while (sibling) {
839
+ if (sibling instanceof globalThis.HTMLElement && sibling.tagName === "TR" && sibling.getAttribute("tabindex") === "0") {
840
+ sibling.focus();
841
+ break;
842
+ }
843
+ sibling = sibling[direction];
844
+ }
845
+ });
846
+ if (event.shiftKey && selectionActive.value && selectionMode.value === "multi" && ctx) {
847
+ if (ctx.selection.rangeAnchor.value === null) {
848
+ const currentRow = ctx.data.value[i];
849
+ if (currentRow !== void 0) ctx.selection.rangeAnchor.value = ctx.getRowKey(currentRow, i);
850
+ }
851
+ const targetRow = ctx.data.value[nextIdx];
852
+ if (targetRow !== void 0) {
853
+ const targetKey = ctx.getRowKey(targetRow, nextIdx);
854
+ ctx.selection.toggle(targetKey, { range: true });
855
+ }
856
+ }
857
+ };
858
+ if (event.key === "ArrowDown") moveTo(posInSorted + 1);
859
+ else if (event.key === "ArrowUp") moveTo(posInSorted - 1);
860
+ else if (event.key === "Home") moveTo(0);
861
+ else if (event.key === "End") moveTo(sorted.length - 1);
862
+ else if (event.key === "Enter" || event.key === " ") {
863
+ event.preventDefault();
864
+ activateSelection(event);
865
+ if (ctx?.rowClickable.value) ctx?.emitRowClick(props.row, i, event);
866
+ }
867
+ }
868
+ function onFocus() {
869
+ if (!isInteractive.value || props.index === void 0) return;
870
+ ctx?.setFocusedRow(props.index);
871
+ }
872
+ return () => {
873
+ const selectionAttrs = selectionActive.value ? {
874
+ role: "row",
875
+ "aria-selected": autoSelected.value ? "true" : "false"
876
+ } : {};
877
+ return h("tr", mergeProps(attrs, {
878
+ class: theme.value.root || void 0,
879
+ tabindex: tabindex.value,
880
+ "data-row-variant": rowVariant.value || void 0,
881
+ ...selectionAttrs,
882
+ onClick: isInteractive.value ? onClick : void 0,
883
+ onKeydown: isInteractive.value ? onKeydown : void 0,
884
+ onFocus: isInteractive.value ? onFocus : void 0
885
+ }), slots.default?.());
886
+ };
887
+ }
888
+ });
889
+ //#endregion
890
+ //#region src/utils/auto-render.ts
891
+ /**
892
+ * Recursively check whether `nodes` (a slot return) contains a vnode
893
+ * whose `type` equals `target`. Recurses into Vue `Fragment` vnodes so
894
+ * `<template v-if>` / `<template v-for>` wrapping around a part
895
+ * doesn't hide it from the auto-render check.
896
+ *
897
+ * Used by `<VCTable>` and `<VCTableLite>` to detect whether the
898
+ * consumer wrote a manual `<VCTableHeader>` / `<VCTableBody>` in the
899
+ * default slot — if not, and `:columns` is set, the table renders the
900
+ * missing band itself.
901
+ */
902
+ function containsComponent(nodes, target) {
903
+ if (nodes == null || nodes === false) return false;
904
+ if (Array.isArray(nodes)) {
905
+ for (const child of nodes) if (containsComponent(child, target)) return true;
906
+ return false;
907
+ }
908
+ if (typeof nodes !== "object") return false;
909
+ const v = nodes;
910
+ if (v.type === target) return true;
911
+ if (v.type === Fragment) return containsComponent(v.children, target);
912
+ return false;
913
+ }
914
+ /**
915
+ * Flatten a slot return into a single-level array, unwrapping
916
+ * `Fragment` vnodes so partition logic operates on a flat sequence.
917
+ * Fragments only serve as grouping markers and lose no DOM semantics
918
+ * when flattened.
919
+ */
920
+ function flattenSlot(nodes) {
921
+ if (nodes == null || nodes === false) return [];
922
+ if (Array.isArray(nodes)) return nodes.flatMap(flattenSlot);
923
+ if (typeof nodes !== "object") return [nodes];
924
+ const v = nodes;
925
+ if (v.type === Fragment) return flattenSlot(v.children);
926
+ return [nodes];
927
+ }
928
+ /**
929
+ * Partition slot children into vnodes that should render BEFORE the
930
+ * auto-body and vnodes that should render AFTER it. Body precedence
931
+ * rules:
932
+ *
933
+ * <thead> ← always first
934
+ * <tbody> ← auto-rendered band
935
+ * <VCTableEmpty> ← own <tbody>, gated on empty data
936
+ * <VCTableLoading> ← own <tbody>, gated on busy
937
+ * <tfoot> ← must come last (HTML5 says SHOULD; matches
938
+ * browser visual placement so the source order
939
+ * lines up with the rendered order)
940
+ *
941
+ * Only `<VCTableFooter>` is partitioned to "after"; everything else
942
+ * keeps source order in "before".
943
+ */
944
+ function partitionBeforeAfterBody(nodes) {
945
+ const flat = flattenSlot(nodes);
946
+ const before = [];
947
+ const after = [];
948
+ for (const n of flat) if (n && typeof n === "object" && n.type === TableFooter_default) after.push(n);
949
+ else before.push(n);
950
+ return {
951
+ before,
952
+ after
953
+ };
954
+ }
955
+ /**
956
+ * Compose the inner children of a `<table>` for the driver auto-render
957
+ * path (plan 033 v0.2-B). Returns the array of vnodes in the order
958
+ * the table should render them:
959
+ *
960
+ * caption? · colgroup? · autoHeader? · slotChildren · autoBody?
961
+ *
962
+ * Header / body auto-render fires when `columns.length > 0` AND the
963
+ * matching part isn't already present in `slotChildren` (Fragment-
964
+ * aware via `containsComponent`).
965
+ */
966
+ function composeTableInner(opts) {
967
+ const { cols, slotChildren, captionSlot, colgroupSlot } = opts;
968
+ const inner = [];
969
+ if (captionSlot) inner.push(h("caption", null, captionSlot()));
970
+ if (colgroupSlot) inner.push(h("colgroup", null, colgroupSlot()));
971
+ const autoRender = cols.length > 0;
972
+ const hasHeader = autoRender && containsComponent(slotChildren, TableHeader_default);
973
+ const hasBody = autoRender && containsComponent(slotChildren, TableBody_default);
974
+ if (autoRender && !hasHeader) inner.push(h(TableHeader_default, null, () => h(TableRow_default, null, () => cols.map((col) => h(TableHeadCell_default, {
975
+ key: col.key,
976
+ columnKey: col.key,
977
+ sortable: col.sortable,
978
+ stickyColumn: col.stickyColumn,
979
+ title: col.headerTitle,
980
+ abbr: col.headerAbbr,
981
+ class: [col.class, col.headerClass]
982
+ }, () => col.label)))));
983
+ const { before, after } = partitionBeforeAfterBody(slotChildren);
984
+ if (before.length > 0) inner.push(before);
985
+ if (autoRender && !hasBody) inner.push(h(TableBody_default, null, { row: ({ row, index }) => h(TableRow_default, {
986
+ row,
987
+ index
988
+ }, () => cols.map((col) => h(TableCell_default, {
989
+ key: col.key,
990
+ columnKey: col.key,
991
+ isRowHeader: col.isRowHeader,
992
+ stickyColumn: col.stickyColumn,
993
+ dataLabel: col.label,
994
+ class: [col.class, col.cellClass]
995
+ }))) }));
996
+ if (after.length > 0) inner.push(after);
997
+ return inner;
998
+ }
999
+ //#endregion
1000
+ //#region src/utils/sort-rows.ts
1001
+ /**
1002
+ * Client-side row sort (plan 033 v1.x-B).
1003
+ *
1004
+ * Returns a new array — the input is untouched. Honors:
1005
+ *
1006
+ * - **Sort key**: `column.sortByFormatted` → use `formatter` output;
1007
+ * else → use `accessor` resolved value (default, matches v0.1
1008
+ * `resolveCellValue`).
1009
+ * - **Comparator**: `column.sortFn(a, b) => number` when present;
1010
+ * else a built-in compare that handles numbers, Dates, booleans,
1011
+ * and uses `localeCompare` (with `numeric: true`) for everything
1012
+ * else — so `'item 2'` sorts before `'item 10'`.
1013
+ * - **Null handling**: `null` / `undefined` sort LAST regardless of
1014
+ * direction. Per-column `nullsFirst: true` floats them to the top.
1015
+ * - **Multi-key tie-break**: descriptors are applied in order. The
1016
+ * first non-zero comparison wins.
1017
+ * - **Stability**: relies on `Array.prototype.sort`'s stable
1018
+ * guarantee (ES2019+, every supported runtime).
1019
+ *
1020
+ * Empty `sorts` returns a shallow copy — never the input reference
1021
+ * itself, so callers can safely mutate the result.
1022
+ */
1023
+ function sortRows(rows, options) {
1024
+ if (options.sorts.length === 0) return rows.slice();
1025
+ const columnByKey = /* @__PURE__ */ new Map();
1026
+ for (const col of options.columns) columnByKey.set(col.key, col);
1027
+ return rows.slice().sort((a, b) => {
1028
+ for (const sort of options.sorts) {
1029
+ const column = columnByKey.get(sort.key);
1030
+ if (!column) continue;
1031
+ const cmp = compareWithColumn(a, b, column, sort.direction);
1032
+ if (cmp !== 0) return cmp;
1033
+ }
1034
+ return 0;
1035
+ });
1036
+ }
1037
+ function compareWithColumn(a, b, column, direction) {
1038
+ const av = resolveSortValue(column, a);
1039
+ const bv = resolveSortValue(column, b);
1040
+ const aMissing = av === null || av === void 0;
1041
+ const bMissing = bv === null || bv === void 0;
1042
+ if (aMissing && bMissing) return 0;
1043
+ if (aMissing) return column.nullsFirst ? -1 : 1;
1044
+ if (bMissing) return column.nullsFirst ? 1 : -1;
1045
+ const cmp = column.sortFn ? column.sortFn(av, bv) : defaultCompare(av, bv);
1046
+ return direction === "asc" ? cmp : -cmp;
1047
+ }
1048
+ function resolveSortValue(column, row) {
1049
+ const value = resolveCellValue(column, row);
1050
+ if (!column.sortByFormatted || !column.formatter) return value;
1051
+ return column.formatter({
1052
+ value,
1053
+ key: column.key,
1054
+ row
1055
+ });
1056
+ }
1057
+ /**
1058
+ * Default comparator. Locale-aware for strings, numeric for numbers,
1059
+ * chronological for Dates. Falls back to coerced-string compare so
1060
+ * mixed-type columns don't throw.
1061
+ */
1062
+ function defaultCompare(a, b) {
1063
+ if (typeof a === "number" && typeof b === "number") return a - b;
1064
+ if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
1065
+ if (typeof a === "boolean" && typeof b === "boolean") {
1066
+ if (a === b) return 0;
1067
+ return a ? 1 : -1;
1068
+ }
1069
+ const as = typeof a === "string" ? a : String(a);
1070
+ const bs = typeof b === "string" ? b : String(b);
1071
+ return as.localeCompare(bs, void 0, { numeric: true });
1072
+ }
1073
+ //#endregion
1074
+ //#region src/components/Table.vue?vue&type=script&lang.ts
1075
+ const tableThemeDefaults$1 = { classes: {
1076
+ root: "vc-table",
1077
+ scrollContainer: "vc-table-scroll-container"
1078
+ } };
1079
+ //#endregion
1080
+ //#region src/components/Table.vue
1081
+ var Table_default = defineComponent({
1082
+ name: "VCTable",
1083
+ inheritAttrs: false,
1084
+ props: {
1085
+ /** Row data array. */
1086
+ data: {
1087
+ type: Array,
1088
+ default: () => []
1089
+ },
1090
+ /** Column definitions (TableColumn or bare-string shorthand). When omitted, columns are derived from `Object.keys(data[0])`. */
1091
+ columns: {
1092
+ type: Array,
1093
+ default: void 0
1094
+ },
1095
+ /** Busy flag — drives `aria-busy` on the `<table>` and gates the loading-band render. */
1096
+ busy: {
1097
+ type: Boolean,
1098
+ default: false
1099
+ },
1100
+ /**
1101
+ * Controlled sort state as `SortDescriptor[]`. Use `v-model:sort`.
1102
+ * Empty array means "no sort". Since v1.x-B this is always an
1103
+ * array (BREAKING change from v0.1's `{ key, direction } | null`).
1104
+ * Single-column sort is just an array of length 0 or 1.
1105
+ */
1106
+ sort: {
1107
+ type: Array,
1108
+ default: () => []
1109
+ },
1110
+ /** When `true`, the cycle skips the `null` step: `null → asc → desc → asc`. */
1111
+ mustSort: {
1112
+ type: Boolean,
1113
+ default: false
1114
+ },
1115
+ /**
1116
+ * Enable multi-column sort. When `true`, Shift-click on a
1117
+ * sortable header adds it as a secondary descriptor (or cycles
1118
+ * its direction if already present). Plain click without Shift
1119
+ * replaces multi-sort with single-sort of the clicked column.
1120
+ * Default `false` — Shift-click behaves identically to plain
1121
+ * click and the sort array stays length 0–1.
1122
+ */
1123
+ multiSort: {
1124
+ type: Boolean,
1125
+ default: false
1126
+ },
1127
+ /**
1128
+ * Maximum number of sort keys retained when `:multi-sort` is on.
1129
+ * Adding a key past the cap drops the oldest descriptor. `0`
1130
+ * means unlimited. Default `3` (matches Excel / bvnext / TanStack
1131
+ * convention).
1132
+ */
1133
+ maxSortKeys: {
1134
+ type: Number,
1135
+ default: 3
1136
+ },
1137
+ /**
1138
+ * When `true`, the table reorders `:data` internally using
1139
+ * `accessor` (or `formatter` output if `column.sortByFormatted`),
1140
+ * honoring `column.sortFn` / `nullsFirst` if set. `v-model:sort`
1141
+ * still emits intent so consumers stay observable. Default
1142
+ * `false` — v0.1 controlled-sort behaviour preserved.
1143
+ */
1144
+ clientSort: {
1145
+ type: Boolean,
1146
+ default: false
1147
+ },
1148
+ /** Wrap the `<table>` in an overflow scroll container. */
1149
+ scrollable: {
1150
+ type: Boolean,
1151
+ default: false
1152
+ },
1153
+ /** When `:scrollable`, sticks the `<thead>` to the top of the scroll container. */
1154
+ stickyHeader: {
1155
+ type: Boolean,
1156
+ default: false
1157
+ },
1158
+ /** When `:scrollable`, applied as `max-height` on the scroll container (any CSS length, e.g. `'24rem'`). */
1159
+ maxHeight: {
1160
+ type: String,
1161
+ default: void 0
1162
+ },
1163
+ /** Opt-in row-click affordance — adds `tabindex` + cursor-pointer on every row and emits `@row-click`. */
1164
+ rowClickable: {
1165
+ type: Boolean,
1166
+ default: false
1167
+ },
1168
+ /**
1169
+ * Row selection mode (plan 033 v1.x-A). When set, the table flips
1170
+ * to ARIA `role="grid"` (+ `aria-multiselectable` for multi) and
1171
+ * rows render with `aria-selected`. `undefined` keeps the v0.1
1172
+ * plain-table semantics.
1173
+ */
1174
+ selectionMode: {
1175
+ type: String,
1176
+ default: void 0
1177
+ },
1178
+ /**
1179
+ * Controlled selection state. Use `v-model:selection`. Type is
1180
+ * `string|number` for single mode, `(string|number)[]` for multi.
1181
+ * `null` clears the selection.
1182
+ */
1183
+ selection: {
1184
+ type: [
1185
+ String,
1186
+ Number,
1187
+ Array,
1188
+ null
1189
+ ],
1190
+ default: null
1191
+ },
1192
+ /**
1193
+ * Resolve a row's selection key. Defaults to `row.id` (falling
1194
+ * back to the row index when absent). Pass a function for richer
1195
+ * mappings (e.g. `(row) => row.uuid`).
1196
+ */
1197
+ getRowKey: {
1198
+ type: Function,
1199
+ default: void 0
1200
+ },
1201
+ /**
1202
+ * Stacked responsive mode (v0.2-D). When `true`, sets
1203
+ * `data-responsive="true"` on the `<table>` so the structural CSS
1204
+ * (and any theme-specific overrides) can collapse the table into
1205
+ * per-row cards at narrow viewports. Uses each cell's `data-label`
1206
+ * as the per-row column label.
1207
+ */
1208
+ responsive: {
1209
+ type: Boolean,
1210
+ default: false
1211
+ },
1212
+ /** Density shorthand for `themeVariant.density`. */
1213
+ density: {
1214
+ type: String,
1215
+ default: void 0
1216
+ },
1217
+ /** Alternating row backgrounds — shorthand for `themeVariant.striped`. */
1218
+ striped: {
1219
+ type: Boolean,
1220
+ default: void 0
1221
+ },
1222
+ /** Cell borders — shorthand for `themeVariant.bordered`. */
1223
+ bordered: {
1224
+ type: Boolean,
1225
+ default: void 0
1226
+ },
1227
+ /** Row hover highlight — shorthand for `themeVariant.hover`. */
1228
+ hover: {
1229
+ type: Boolean,
1230
+ default: void 0
1231
+ },
1232
+ /** HTML tag to render. */
1233
+ tag: {
1234
+ type: String,
1235
+ default: "table"
1236
+ },
1237
+ ...themableProps()
1238
+ },
1239
+ emits: [
1240
+ "update:sort",
1241
+ "update:selection",
1242
+ "row-click"
1243
+ ],
1244
+ slots: Object,
1245
+ setup(props, { attrs, slots, emit }) {
1246
+ const theme = useComponentTheme("table", useThemeProps(props, "density", "striped", "bordered", "hover", "stickyHeader"), tableThemeDefaults$1);
1247
+ const dataRef = toRef(props, "data");
1248
+ const rawColumns = toRef(props, "columns");
1249
+ const columns = computed(() => normalizeColumns(rawColumns.value, dataRef.value));
1250
+ const sortSource = toRef(props, "sort");
1251
+ const mustSortRef = toRef(props, "mustSort");
1252
+ const maxSortKeysRef = toRef(props, "maxSortKeys");
1253
+ const sortMachine = useSortMachine({
1254
+ source: sortSource,
1255
+ columns,
1256
+ mustSort: mustSortRef,
1257
+ maxSortKeys: maxSortKeysRef,
1258
+ emit: (next) => emit("update:sort", next)
1259
+ });
1260
+ const visibleData = computed(() => {
1261
+ if (!props.clientSort || sortMachine.state.value.length === 0) return dataRef.value;
1262
+ return sortRows(dataRef.value, {
1263
+ columns: columns.value,
1264
+ sorts: sortMachine.state.value
1265
+ });
1266
+ });
1267
+ const childCellCount = ref(0);
1268
+ provideHeadCellCountContext({
1269
+ register: () => {
1270
+ childCellCount.value += 1;
1271
+ },
1272
+ unregister: () => {
1273
+ childCellCount.value = Math.max(0, childCellCount.value - 1);
1274
+ }
1275
+ });
1276
+ const colspan = computed(() => {
1277
+ if (columns.value.length > 0) return columns.value.length;
1278
+ return Math.max(1, childCellCount.value);
1279
+ });
1280
+ watch(() => columns.value.length > 0, (hasColumns) => {
1281
+ if (hasColumns) childCellCount.value = 0;
1282
+ });
1283
+ const focusedRow = ref(null);
1284
+ const setFocusedRow = (index) => {
1285
+ focusedRow.value = index;
1286
+ };
1287
+ const emitRowClick = (row, index, event) => {
1288
+ emit("row-click", row, index, event);
1289
+ };
1290
+ const wrapperEl = ref(null);
1291
+ const interactiveRows = shallowRef(/* @__PURE__ */ new Set());
1292
+ const registerInteractiveRow = (index) => {
1293
+ if (interactiveRows.value.has(index)) return;
1294
+ interactiveRows.value.add(index);
1295
+ triggerRef(interactiveRows);
1296
+ };
1297
+ const unregisterInteractiveRow = (index) => {
1298
+ if (!interactiveRows.value.has(index)) return;
1299
+ interactiveRows.value.delete(index);
1300
+ triggerRef(interactiveRows);
1301
+ };
1302
+ const selectionMode = computed(() => props.selectionMode);
1303
+ const selectionValue = computed(() => props.selection);
1304
+ const getRowKey = (row, index) => {
1305
+ const custom = props.getRowKey;
1306
+ if (typeof custom === "function") return custom(row, index);
1307
+ if (row && typeof row === "object") {
1308
+ const { id } = row;
1309
+ if (typeof id === "string" || typeof id === "number") return id;
1310
+ }
1311
+ return index;
1312
+ };
1313
+ const selection = useRowSelectionMachine({
1314
+ mode: selectionMode,
1315
+ value: selectionValue,
1316
+ emit: (next) => emit("update:selection", next),
1317
+ keyAt: (index) => {
1318
+ const view = visibleData.value;
1319
+ if (index < 0 || index >= view.length) return void 0;
1320
+ return getRowKey(view[index], index);
1321
+ }
1322
+ });
1323
+ provideTableContext({
1324
+ data: visibleData,
1325
+ busy: toRef(props, "busy"),
1326
+ columns,
1327
+ sort: sortMachine.state,
1328
+ setSort: (key, opts) => sortMachine.setSort(key, {
1329
+ ...opts,
1330
+ append: props.multiSort ? opts?.append : false
1331
+ }),
1332
+ setSortState: sortMachine.setState,
1333
+ maxSortKeys: maxSortKeysRef,
1334
+ supportsSortMutation: true,
1335
+ rowClickable: toRef(props, "rowClickable"),
1336
+ focusedRow,
1337
+ setFocusedRow,
1338
+ selection,
1339
+ getRowKey,
1340
+ interactiveRows,
1341
+ registerInteractiveRow,
1342
+ unregisterInteractiveRow,
1343
+ colspan,
1344
+ emitRowClick,
1345
+ wrapperEl
1346
+ });
1347
+ const slotProps = computed(() => ({
1348
+ data: visibleData.value,
1349
+ busy: props.busy,
1350
+ columns: columns.value,
1351
+ sort: sortMachine.state.value,
1352
+ setSort: sortMachine.setSort
1353
+ }));
1354
+ const setWrapperRef = (el) => {
1355
+ wrapperEl.value = el ?? null;
1356
+ };
1357
+ return () => {
1358
+ const inner = composeTableInner({
1359
+ cols: columns.value,
1360
+ slotChildren: slots.default?.(slotProps.value),
1361
+ captionSlot: slots.caption,
1362
+ colgroupSlot: slots.colgroup
1363
+ });
1364
+ const mode = selectionMode.value;
1365
+ const selectionAttrs = mode === void 0 ? {} : {
1366
+ role: "grid",
1367
+ ...mode === "multi" ? { "aria-multiselectable": "true" } : {}
1368
+ };
1369
+ const tableNode = h(props.tag, mergeProps(attrs, {
1370
+ class: theme.value.root || void 0,
1371
+ "aria-busy": props.busy ? "true" : void 0,
1372
+ "data-responsive": props.responsive ? "true" : void 0,
1373
+ ...selectionAttrs
1374
+ }), inner);
1375
+ const wrapper = h("div", {
1376
+ ref: setWrapperRef,
1377
+ class: "vc-table-wrapper",
1378
+ style: { position: "relative" }
1379
+ }, [tableNode]);
1380
+ if (!props.scrollable) return wrapper;
1381
+ return h("div", {
1382
+ class: theme.value.scrollContainer || void 0,
1383
+ style: props.maxHeight ? {
1384
+ maxHeight: props.maxHeight,
1385
+ overflow: "auto"
1386
+ } : { overflow: "auto" }
1387
+ }, [wrapper]);
1388
+ };
1389
+ }
1390
+ });
1391
+ //#endregion
1392
+ //#region src/components/TableLite.vue?vue&type=script&lang.ts
1393
+ const tableLiteThemeDefaults = { classes: {
1394
+ root: "vc-table",
1395
+ scrollContainer: "vc-table-scroll-container"
1396
+ } };
1397
+ /**
1398
+ * Slim sibling of `<VCTable>` — same columns driver + theme system +
1399
+ * auto-render, but with the sort + row-click + keyboard-nav machinery
1400
+ * stripped out (plan 033 v0.2-C). Consumers who want their own state
1401
+ * plumbing (e.g. tanstack-table on top) import this instead of the
1402
+ * full `<VCTable>`. Lite-only consumers tree-shake `useSortMachine`
1403
+ * out of their bundle.
1404
+ *
1405
+ * Provides the same `TableContext` shape so child components
1406
+ * (`<VCTableRow>`, `<VCTableHeadCell>`, …) work identically — the
1407
+ * sort / row-click hooks are wired to no-ops, so sortable headers and
1408
+ * `:row-clickable` rows visually behave as if the consumer hadn't
1409
+ * opted in.
1410
+ */
1411
+ const tableLiteProps = {
1412
+ /** Row data array. */
1413
+ data: {
1414
+ type: Array,
1415
+ default: () => []
1416
+ },
1417
+ /** Column definitions (TableColumn or bare-string shorthand). When omitted, columns are derived from `Object.keys(data[0])`. */
1418
+ columns: {
1419
+ type: Array,
1420
+ default: void 0
1421
+ },
1422
+ /** Busy flag — drives `aria-busy` on the `<table>` and gates the loading-band render. */
1423
+ busy: {
1424
+ type: Boolean,
1425
+ default: false
1426
+ },
1427
+ /** Wrap the `<table>` in an overflow scroll container. */
1428
+ scrollable: {
1429
+ type: Boolean,
1430
+ default: false
1431
+ },
1432
+ /** When `:scrollable`, sticks the `<thead>` to the top of the scroll container. */
1433
+ stickyHeader: {
1434
+ type: Boolean,
1435
+ default: false
1436
+ },
1437
+ /** When `:scrollable`, applied as `max-height` on the scroll container. */
1438
+ maxHeight: {
1439
+ type: String,
1440
+ default: void 0
1441
+ },
1442
+ /** Stacked responsive mode opt-in. Same shape as `<VCTable :responsive>`. */
1443
+ responsive: {
1444
+ type: Boolean,
1445
+ default: false
1446
+ },
1447
+ /** Density shorthand for `themeVariant.density`. */
1448
+ density: {
1449
+ type: String,
1450
+ default: void 0
1451
+ },
1452
+ /** Alternating row backgrounds — shorthand for `themeVariant.striped`. */
1453
+ striped: {
1454
+ type: Boolean,
1455
+ default: void 0
1456
+ },
1457
+ /** Cell borders — shorthand for `themeVariant.bordered`. */
1458
+ bordered: {
1459
+ type: Boolean,
1460
+ default: void 0
1461
+ },
1462
+ /** Row hover highlight — shorthand for `themeVariant.hover`. */
1463
+ hover: {
1464
+ type: Boolean,
1465
+ default: void 0
1466
+ },
1467
+ /** HTML tag to render. */
1468
+ tag: {
1469
+ type: String,
1470
+ default: "table"
1471
+ },
1472
+ ...themableProps()
1473
+ };
1474
+ const NOOP_SORT_STATE = ref([]);
1475
+ const NOOP_SELECTION_MODE = computed(() => void 0);
1476
+ const NOOP_SELECTION_VALUE = computed(() => null);
1477
+ //#endregion
1478
+ //#region src/components/TableLite.vue
1479
+ var TableLite_default = defineComponent({
1480
+ name: "VCTableLite",
1481
+ inheritAttrs: false,
1482
+ props: tableLiteProps,
1483
+ slots: Object,
1484
+ setup(props, { attrs, slots }) {
1485
+ const theme = useComponentTheme("table", useThemeProps(props, "density", "striped", "bordered", "hover", "stickyHeader"), tableLiteThemeDefaults);
1486
+ const dataRef = toRef(props, "data");
1487
+ const rawColumns = toRef(props, "columns");
1488
+ const columns = computed(() => normalizeColumns(rawColumns.value, dataRef.value));
1489
+ const childCellCount = ref(0);
1490
+ provideHeadCellCountContext({
1491
+ register: () => {
1492
+ childCellCount.value += 1;
1493
+ },
1494
+ unregister: () => {
1495
+ childCellCount.value = Math.max(0, childCellCount.value - 1);
1496
+ }
1497
+ });
1498
+ const colspan = computed(() => {
1499
+ if (columns.value.length > 0) return columns.value.length;
1500
+ return Math.max(1, childCellCount.value);
1501
+ });
1502
+ watch(() => columns.value.length > 0, (hasColumns) => {
1503
+ if (hasColumns) childCellCount.value = 0;
1504
+ });
1505
+ const wrapperEl = ref(null);
1506
+ const liteSelection = useRowSelectionMachine({
1507
+ mode: NOOP_SELECTION_MODE,
1508
+ value: NOOP_SELECTION_VALUE,
1509
+ emit: () => {},
1510
+ keyAt: () => void 0
1511
+ });
1512
+ const liteGetRowKey = (row, index) => {
1513
+ if (row && typeof row === "object") {
1514
+ const { id } = row;
1515
+ if (typeof id === "string" || typeof id === "number") return id;
1516
+ }
1517
+ return index;
1518
+ };
1519
+ provideTableContext({
1520
+ data: dataRef,
1521
+ busy: toRef(props, "busy"),
1522
+ columns,
1523
+ sort: NOOP_SORT_STATE,
1524
+ setSort: () => {},
1525
+ setSortState: () => {},
1526
+ maxSortKeys: ref(0),
1527
+ supportsSortMutation: false,
1528
+ rowClickable: computed(() => false),
1529
+ focusedRow: ref(null),
1530
+ setFocusedRow: () => {},
1531
+ colspan,
1532
+ emitRowClick: () => {},
1533
+ wrapperEl,
1534
+ selection: liteSelection,
1535
+ getRowKey: liteGetRowKey,
1536
+ interactiveRows: ref(/* @__PURE__ */ new Set()),
1537
+ registerInteractiveRow: () => {},
1538
+ unregisterInteractiveRow: () => {}
1539
+ });
1540
+ const slotProps = computed(() => ({
1541
+ data: dataRef.value,
1542
+ busy: props.busy,
1543
+ columns: columns.value,
1544
+ sort: NOOP_SORT_STATE.value,
1545
+ setSort: () => {}
1546
+ }));
1547
+ const setWrapperRef = (el) => {
1548
+ wrapperEl.value = el ?? null;
1549
+ };
1550
+ return () => {
1551
+ const inner = composeTableInner({
1552
+ cols: columns.value,
1553
+ slotChildren: slots.default?.(slotProps.value),
1554
+ captionSlot: slots.caption,
1555
+ colgroupSlot: slots.colgroup
1556
+ });
1557
+ const tableNode = h(props.tag, mergeProps(attrs, {
1558
+ class: theme.value.root || void 0,
1559
+ "aria-busy": props.busy ? "true" : void 0,
1560
+ "data-responsive": props.responsive ? "true" : void 0
1561
+ }), inner);
1562
+ const wrapper = h("div", {
1563
+ ref: setWrapperRef,
1564
+ class: "vc-table-wrapper",
1565
+ style: { position: "relative" }
1566
+ }, [tableNode]);
1567
+ if (!props.scrollable) return wrapper;
1568
+ return h("div", {
1569
+ class: theme.value.scrollContainer || void 0,
1570
+ style: props.maxHeight ? {
1571
+ maxHeight: props.maxHeight,
1572
+ overflow: "auto"
1573
+ } : { overflow: "auto" }
1574
+ }, [wrapper]);
1575
+ };
1576
+ }
1577
+ });
1578
+ //#endregion
1579
+ //#region src/components/TableEmpty.vue?vue&type=script&lang.ts
1580
+ const tableEmptyThemeDefaults$1 = { classes: { root: "vc-table-empty" } };
1581
+ const behavioralDefaults$2 = {
1582
+ content: "No results.",
1583
+ filteredContent: "No matches."
1584
+ };
1585
+ //#endregion
1586
+ //#region src/components/TableEmpty.vue
1587
+ var TableEmpty_default = defineComponent({
1588
+ name: "VCTableEmpty",
1589
+ inheritAttrs: false,
1590
+ props: {
1591
+ /** Override the auto-derived `colspan`. When omitted, uses the table's resolved colspan. */
1592
+ colspan: {
1593
+ type: Number,
1594
+ default: void 0
1595
+ },
1596
+ /** Mark this as the empty-after-filter case (distinct copy / icon vs empty-no-data). */
1597
+ filtered: {
1598
+ type: Boolean,
1599
+ default: false
1600
+ },
1601
+ /** Override the rendered text. Falls back to global defaults / hardcoded. */
1602
+ content: {
1603
+ type: String,
1604
+ default: void 0
1605
+ },
1606
+ /** Override the rendered text for the filtered case. */
1607
+ filteredContent: {
1608
+ type: String,
1609
+ default: void 0
1610
+ },
1611
+ ...themableProps()
1612
+ },
1613
+ setup(props, { attrs, slots }) {
1614
+ const ctx = useTable();
1615
+ const defaults = useComponentDefaults("tableEmpty", props, behavioralDefaults$2);
1616
+ const themeProps = useThemeProps(props);
1617
+ const theme = useComponentTheme("tableEmpty", {
1618
+ get themeClass() {
1619
+ return themeProps.themeClass;
1620
+ },
1621
+ get themeVariant() {
1622
+ return {
1623
+ ...themeProps.themeVariant ?? {},
1624
+ filtered: props.filtered
1625
+ };
1626
+ }
1627
+ }, tableEmptyThemeDefaults$1);
1628
+ const shouldRender = computed(() => {
1629
+ if (!ctx) return true;
1630
+ return ctx.data.value.length === 0 && !ctx.busy.value;
1631
+ });
1632
+ const resolvedColspan = computed(() => props.colspan ?? ctx?.colspan.value ?? 1);
1633
+ return () => {
1634
+ if (!shouldRender.value) return null;
1635
+ const cellContent = slots.default?.() ?? (props.filtered ? defaults.value.filteredContent : defaults.value.content);
1636
+ return h("tbody", mergeProps(attrs, { class: theme.value.root || void 0 }), [h("tr", null, [h("td", { colspan: resolvedColspan.value }, [h("div", {
1637
+ role: "status",
1638
+ "aria-live": "polite"
1639
+ }, cellContent)])])]);
1640
+ };
1641
+ }
1642
+ });
1643
+ //#endregion
1644
+ //#region src/components/TableLoading.vue?vue&type=script&lang.ts
1645
+ const tableLoadingThemeDefaults$1 = { classes: {
1646
+ root: "vc-table-loading",
1647
+ overlay: "vc-table-loading-overlay"
1648
+ } };
1649
+ const behavioralDefaults$1 = { content: "Loading…" };
1650
+ //#endregion
1651
+ //#region src/components/TableLoading.vue
1652
+ var TableLoading_default = defineComponent({
1653
+ name: "VCTableLoading",
1654
+ inheritAttrs: false,
1655
+ props: {
1656
+ /** Override the auto-derived `colspan` (default mode only — ignored in overlay mode). */
1657
+ colspan: {
1658
+ type: Number,
1659
+ default: void 0
1660
+ },
1661
+ /** Render as a positioned overlay on top of the existing `<tbody>` (refresh feedback). Default mode is a centered tbody/tr/td placeholder. */
1662
+ overlay: {
1663
+ type: Boolean,
1664
+ default: false
1665
+ },
1666
+ /** Override the rendered text. Falls back to global defaults. */
1667
+ content: {
1668
+ type: String,
1669
+ default: void 0
1670
+ },
1671
+ ...themableProps()
1672
+ },
1673
+ setup(props, { attrs, slots }) {
1674
+ const ctx = useTable();
1675
+ const defaults = useComponentDefaults("tableLoading", props, behavioralDefaults$1);
1676
+ const themeProps = useThemeProps(props);
1677
+ const theme = useComponentTheme("tableLoading", {
1678
+ get themeClass() {
1679
+ return themeProps.themeClass;
1680
+ },
1681
+ get themeVariant() {
1682
+ return {
1683
+ ...themeProps.themeVariant ?? {},
1684
+ overlay: props.overlay
1685
+ };
1686
+ }
1687
+ }, tableLoadingThemeDefaults$1);
1688
+ const shouldRender = computed(() => {
1689
+ if (!ctx) return true;
1690
+ const busy = ctx.busy.value;
1691
+ const hasData = ctx.data.value.length > 0;
1692
+ return props.overlay ? busy : busy && !hasData;
1693
+ });
1694
+ const resolvedColspan = computed(() => props.colspan ?? ctx?.colspan.value ?? 1);
1695
+ return () => {
1696
+ if (!shouldRender.value) return null;
1697
+ const cellContent = slots.default?.() ?? defaults.value.content;
1698
+ if (props.overlay) {
1699
+ const overlayNode = h("div", mergeProps(attrs, {
1700
+ class: [theme.value.root || void 0, theme.value.overlay || void 0],
1701
+ role: "status",
1702
+ "aria-live": "polite",
1703
+ "aria-busy": "true"
1704
+ }), cellContent);
1705
+ if (ctx?.wrapperEl.value) return h(Teleport, { to: ctx.wrapperEl.value }, [overlayNode]);
1706
+ return overlayNode;
1707
+ }
1708
+ return h("tbody", mergeProps(attrs, { class: theme.value.root || void 0 }), [h("tr", null, [h("td", { colspan: resolvedColspan.value }, [h("div", {
1709
+ role: "status",
1710
+ "aria-live": "polite",
1711
+ "aria-busy": "true"
1712
+ }, cellContent)])])]);
1713
+ };
1714
+ }
1715
+ });
1716
+ //#endregion
1717
+ //#region src/components/TableSortIndicators.vue?vue&type=script&lang.ts
1718
+ const tableSortIndicatorsThemeDefaults$1 = { classes: {
1719
+ root: "vc-table-sort-indicators",
1720
+ label: "vc-table-sort-indicators-label",
1721
+ empty: "vc-table-sort-indicators-empty",
1722
+ chip: "vc-table-sort-indicators-chip",
1723
+ chipToggle: "vc-table-sort-indicators-chip-toggle",
1724
+ chipPosition: "vc-table-sort-indicators-chip-position",
1725
+ chipLabel: "vc-table-sort-indicators-chip-label",
1726
+ chipArrow: "vc-table-sort-indicators-chip-arrow",
1727
+ chipRemove: "vc-table-sort-indicators-chip-remove",
1728
+ addWrapper: "",
1729
+ add: "vc-table-sort-indicators-add",
1730
+ clear: "vc-table-sort-indicators-clear"
1731
+ } };
1732
+ const behavioralDefaults = {
1733
+ label: "Sort:",
1734
+ emptyContent: "no columns sorted yet",
1735
+ addLabel: "+ Add column",
1736
+ clearLabel: "Clear all",
1737
+ removeAriaLabel: "Remove sort key",
1738
+ toggleAscTitle: "Click to toggle descending",
1739
+ toggleDescTitle: "Click to toggle ascending",
1740
+ arrowAsc: "↑",
1741
+ arrowDesc: "↓",
1742
+ removeGlyph: "×"
1743
+ };
1744
+ //#endregion
1745
+ //#region src/components/TableSortIndicators.vue
1746
+ var TableSortIndicators_default = defineComponent({
1747
+ name: "VCTableSortIndicators",
1748
+ inheritAttrs: false,
1749
+ props: {
1750
+ /**
1751
+ * Sort state to render. Use `v-model:sort` for two-way binding.
1752
+ * `null` is treated as "empty array" so consumers carrying
1753
+ * migration-era `ref<TableSortState | null>(null)` don't crash.
1754
+ * When BOTH `:sort` and `:columns` are omitted, the component
1755
+ * falls back to `useTable()` context — note that with the
1756
+ * default `<table>` rendering, slot children render INSIDE the
1757
+ * `<table>`, so the v-model path is the recommended placement
1758
+ * for chip rows above / below the table.
1759
+ */
1760
+ sort: {
1761
+ type: Array,
1762
+ default: void 0
1763
+ },
1764
+ /**
1765
+ * Columns the sort can reference. Required when using v-model
1766
+ * mode (to look up labels + filter the add-column dropdown).
1767
+ * Falls back to `useTable()` context when omitted.
1768
+ */
1769
+ columns: {
1770
+ type: Array,
1771
+ default: void 0
1772
+ },
1773
+ /**
1774
+ * Cap on the sort-state array length (mirrors `<VCTable
1775
+ * :max-sort-keys>`). `0` = unlimited. When the cap is hit,
1776
+ * adding via this component evicts the oldest descriptor.
1777
+ * Falls back to `useTable().maxSortKeys` when omitted; defaults
1778
+ * to `0` (unlimited) in stand-alone v-model mode without a
1779
+ * context.
1780
+ */
1781
+ maxSortKeys: {
1782
+ type: Number,
1783
+ default: void 0
1784
+ },
1785
+ /** Override the leading label text. Falls back to global defaults. */
1786
+ label: {
1787
+ type: String,
1788
+ default: void 0
1789
+ },
1790
+ /** Override the empty-state copy. Falls back to global defaults. */
1791
+ emptyContent: {
1792
+ type: String,
1793
+ default: void 0
1794
+ },
1795
+ /** Override the add-column trigger text. Falls back to global defaults. */
1796
+ addLabel: {
1797
+ type: String,
1798
+ default: void 0
1799
+ },
1800
+ /** Override the clear-all trigger text. Falls back to global defaults. */
1801
+ clearLabel: {
1802
+ type: String,
1803
+ default: void 0
1804
+ },
1805
+ /** Override the aria-label applied to per-chip remove buttons. */
1806
+ removeAriaLabel: {
1807
+ type: String,
1808
+ default: void 0
1809
+ },
1810
+ /** Hide the add-column dropdown (consumer brings their own affordance). */
1811
+ hideAdd: {
1812
+ type: Boolean,
1813
+ default: false
1814
+ },
1815
+ /** Hide the clear-all button. */
1816
+ hideClear: {
1817
+ type: Boolean,
1818
+ default: false
1819
+ },
1820
+ ...themableProps()
1821
+ },
1822
+ emits: ["update:sort"],
1823
+ slots: Object,
1824
+ setup(props, { attrs, emit, slots }) {
1825
+ const ctx = useTable();
1826
+ const defaults = useComponentDefaults("tableSortIndicators", props, behavioralDefaults);
1827
+ const theme = useComponentTheme("tableSortIndicators", useThemeProps(props), tableSortIndicatorsThemeDefaults$1);
1828
+ const sortPropProvided = computed(() => props.sort !== void 0);
1829
+ const resolvedSort = computed(() => {
1830
+ if (sortPropProvided.value) return props.sort ?? [];
1831
+ return ctx?.sort.value ?? [];
1832
+ });
1833
+ const resolvedColumns = computed(() => {
1834
+ if (props.columns !== void 0) return props.columns;
1835
+ return ctx?.columns.value ?? [];
1836
+ });
1837
+ function commitSort(next) {
1838
+ if (sortPropProvided.value) {
1839
+ emit("update:sort", next);
1840
+ return;
1841
+ }
1842
+ if (ctx) {
1843
+ if (globalThis.process?.env?.NODE_ENV !== "production" && !ctx.supportsSortMutation) console.warn("[VCTableSortIndicators] mounted inside a table context that does not support sort mutation (likely <VCTableLite>). Clicks will be swallowed. Either bind :sort + :columns directly for v-model mode, or use <VCTable> instead of Lite.");
1844
+ ctx.setSortState(next);
1845
+ }
1846
+ }
1847
+ const columnByKey = computed(() => {
1848
+ const map = /* @__PURE__ */ new Map();
1849
+ for (const col of resolvedColumns.value) map.set(col.key, col);
1850
+ return map;
1851
+ });
1852
+ const unsortedSortable = computed(() => {
1853
+ const inSort = new Set(resolvedSort.value.map((s) => s.key));
1854
+ return resolvedColumns.value.filter((c) => c.sortable && !inSort.has(c.key));
1855
+ });
1856
+ function toggleDirection(key) {
1857
+ commitSort(resolvedSort.value.map((s) => s.key === key ? {
1858
+ ...s,
1859
+ direction: s.direction === "asc" ? "desc" : "asc"
1860
+ } : s));
1861
+ }
1862
+ function removeKey(key) {
1863
+ commitSort(resolvedSort.value.filter((s) => s.key !== key));
1864
+ }
1865
+ const resolvedMaxSortKeys = computed(() => {
1866
+ if (props.maxSortKeys !== void 0) return props.maxSortKeys;
1867
+ return ctx?.maxSortKeys.value ?? 0;
1868
+ });
1869
+ function appendKey(key) {
1870
+ if (!key) return;
1871
+ const col = columnByKey.value.get(key);
1872
+ if (!col || !col.sortable) return;
1873
+ if (resolvedSort.value.some((s) => s.key === key)) return;
1874
+ const next = [...resolvedSort.value, {
1875
+ key,
1876
+ direction: "asc"
1877
+ }];
1878
+ const cap = resolvedMaxSortKeys.value;
1879
+ commitSort(cap > 0 && next.length > cap ? next.slice(next.length - cap) : next);
1880
+ }
1881
+ function clearAll() {
1882
+ commitSort([]);
1883
+ }
1884
+ const chipSlotProps = computed(() => resolvedSort.value.map((descriptor, index) => ({
1885
+ descriptor,
1886
+ index,
1887
+ position: index + 1,
1888
+ column: columnByKey.value.get(descriptor.key),
1889
+ toggle: () => toggleDirection(descriptor.key),
1890
+ remove: () => removeKey(descriptor.key)
1891
+ })));
1892
+ return () => {
1893
+ if (!sortPropProvided.value && !ctx) return null;
1894
+ const sortState = resolvedSort.value;
1895
+ const t = theme.value;
1896
+ const d = defaults.value;
1897
+ const addSlotProps = {
1898
+ options: unsortedSortable.value,
1899
+ add: appendKey
1900
+ };
1901
+ const clearSlotProps = { clear: clearAll };
1902
+ const renderChip = (chip) => {
1903
+ if (slots.chip) return slots.chip(chip);
1904
+ const isAsc = chip.descriptor.direction === "asc";
1905
+ return h("div", {
1906
+ key: chip.descriptor.key,
1907
+ class: t.chip || void 0,
1908
+ "data-sort-key": chip.descriptor.key,
1909
+ "data-direction": chip.descriptor.direction
1910
+ }, [h("button", {
1911
+ type: "button",
1912
+ class: t.chipToggle || void 0,
1913
+ title: isAsc ? d.toggleAscTitle : d.toggleDescTitle,
1914
+ onClick: chip.toggle
1915
+ }, [
1916
+ h("span", { class: t.chipPosition || void 0 }, `${chip.position}.`),
1917
+ h("span", { class: t.chipLabel || void 0 }, chip.column?.label ?? chip.descriptor.key),
1918
+ h("span", { class: t.chipArrow || void 0 }, isAsc ? d.arrowAsc : d.arrowDesc)
1919
+ ]), h("button", {
1920
+ type: "button",
1921
+ class: t.chipRemove || void 0,
1922
+ "aria-label": d.removeAriaLabel,
1923
+ onClick: chip.remove
1924
+ }, d.removeGlyph)]);
1925
+ };
1926
+ const renderLabel = () => slots.label ? slots.label() : h("span", { class: t.label || void 0 }, d.label);
1927
+ const renderEmpty = () => slots.empty ? slots.empty() : h("span", { class: t.empty || void 0 }, d.emptyContent);
1928
+ const renderAdd = () => {
1929
+ if (props.hideAdd || addSlotProps.options.length === 0) return null;
1930
+ if (slots.add) return slots.add(addSlotProps);
1931
+ const selectNode = h("select", {
1932
+ class: t.add || void 0,
1933
+ "aria-label": d.addLabel,
1934
+ onChange: (e) => {
1935
+ const target = e.target;
1936
+ appendKey(target.value);
1937
+ target.value = "";
1938
+ }
1939
+ }, [h("option", { value: "" }, d.addLabel), ...addSlotProps.options.map((col) => h("option", {
1940
+ key: col.key,
1941
+ value: col.key
1942
+ }, col.label ?? col.key))]);
1943
+ if (!t.addWrapper) return selectNode;
1944
+ return h("div", { class: t.addWrapper }, [selectNode]);
1945
+ };
1946
+ const renderClear = () => {
1947
+ if (props.hideClear || sortState.length === 0) return null;
1948
+ if (slots.clear) return slots.clear(clearSlotProps);
1949
+ return h("button", {
1950
+ type: "button",
1951
+ class: t.clear || void 0,
1952
+ onClick: clearAll
1953
+ }, d.clearLabel);
1954
+ };
1955
+ const rootAttrs = {
1956
+ class: t.root || void 0,
1957
+ role: "toolbar",
1958
+ "aria-label": "Active sort columns"
1959
+ };
1960
+ if (slots.default) return h("div", mergeProps(attrs, rootAttrs), slots.default({
1961
+ sort: sortState,
1962
+ chips: chipSlotProps.value,
1963
+ add: addSlotProps,
1964
+ clear: clearSlotProps
1965
+ }));
1966
+ const chipsContent = sortState.length === 0 ? [renderEmpty()] : chipSlotProps.value.map(renderChip);
1967
+ return h("div", mergeProps(attrs, rootAttrs), [
1968
+ renderLabel(),
1969
+ ...chipsContent,
1970
+ renderAdd(),
1971
+ renderClear()
1972
+ ]);
1973
+ };
1974
+ }
1975
+ });
1976
+ //#endregion
1977
+ //#region src/composables/define-table.ts
1978
+ /**
1979
+ * Pinia-style factory mirroring `defineList()`. Returns reactive state
1980
+ * containers + a `setSort` mutator. Useful when a consumer wants to share
1981
+ * a table's state across multiple components without prop drilling, or
1982
+ * when wiring a controlled table outside the SFC.
1983
+ *
1984
+ * const usersTable = defineTable<User>({
1985
+ * columns: ['id', 'name', 'email'],
1986
+ * data: [],
1987
+ * busy: false,
1988
+ * });
1989
+ *
1990
+ * <VCTable :data="usersTable.data" :columns="usersTable.columns" ... />
1991
+ *
1992
+ * The sort cycle composes `useSortMachine` internally — same semantics
1993
+ * as the `<VCTable>` SFC path (honors per-column `initialSortDirection`
1994
+ * + `mustSort`).
1995
+ */
1996
+ function defineTable(options = {}) {
1997
+ const data = ref(options.data ?? []);
1998
+ const rawColumns = ref(options.columns ?? []);
1999
+ const busy = ref(options.busy ?? false);
2000
+ const mustSort = ref(options.mustSort ?? false);
2001
+ const sort = ref(options.sort ?? []);
2002
+ const maxSortKeys = ref(options.maxSortKeys ?? 3);
2003
+ const columns = computed(() => normalizeColumns(rawColumns.value, data.value));
2004
+ return {
2005
+ data,
2006
+ columns,
2007
+ rawColumns,
2008
+ busy,
2009
+ mustSort,
2010
+ sort,
2011
+ setSort: useSortMachine({
2012
+ source: sort,
2013
+ columns,
2014
+ mustSort,
2015
+ maxSortKeys,
2016
+ emit: (next) => {
2017
+ sort.value = next;
2018
+ }
2019
+ }).setSort
2020
+ };
2021
+ }
2022
+ //#endregion
2023
+ //#region src/theme.ts
2024
+ const tableThemeDefaults = { classes: {
2025
+ root: "vc-table",
2026
+ scrollContainer: "vc-table-scroll-container"
2027
+ } };
2028
+ const tableHeaderThemeDefaults = { classes: { root: "vc-table-header" } };
2029
+ const tableBodyThemeDefaults = { classes: { root: "vc-table-body" } };
2030
+ const tableFooterThemeDefaults = { classes: { root: "vc-table-footer" } };
2031
+ const tableRowThemeDefaults = { classes: { root: "vc-table-row" } };
2032
+ const tableCellThemeDefaults = { classes: { root: "vc-table-cell" } };
2033
+ const tableHeadCellThemeDefaults = { classes: {
2034
+ root: "vc-table-head-cell",
2035
+ sortIcon: "vc-table-head-cell-sort-icon"
2036
+ } };
2037
+ const tableEmptyThemeDefaults = { classes: { root: "vc-table-empty" } };
2038
+ const tableLoadingThemeDefaults = { classes: {
2039
+ root: "vc-table-loading",
2040
+ overlay: "vc-table-loading-overlay"
2041
+ } };
2042
+ const tableSortIndicatorsThemeDefaults = { classes: {
2043
+ root: "vc-table-sort-indicators",
2044
+ label: "vc-table-sort-indicators-label",
2045
+ empty: "vc-table-sort-indicators-empty",
2046
+ chip: "vc-table-sort-indicators-chip",
2047
+ chipToggle: "vc-table-sort-indicators-chip-toggle",
2048
+ chipPosition: "vc-table-sort-indicators-chip-position",
2049
+ chipLabel: "vc-table-sort-indicators-chip-label",
2050
+ chipArrow: "vc-table-sort-indicators-chip-arrow",
2051
+ chipRemove: "vc-table-sort-indicators-chip-remove",
2052
+ addWrapper: "",
2053
+ add: "vc-table-sort-indicators-add",
2054
+ clear: "vc-table-sort-indicators-clear"
2055
+ } };
2056
+ //#endregion
2057
+ //#region src/index.ts
2058
+ function install(app, options = {}) {
2059
+ installThemeManager(app, options);
2060
+ installDefaultsManager(app, options);
2061
+ Object.entries({
2062
+ VCTable: Table_default,
2063
+ VCTableLite: TableLite_default,
2064
+ VCTableHeader: TableHeader_default,
2065
+ VCTableBody: TableBody_default,
2066
+ VCTableFooter: TableFooter_default,
2067
+ VCTableRow: TableRow_default,
2068
+ VCTableCell: TableCell_default,
2069
+ VCTableHeadCell: TableHeadCell_default,
2070
+ VCTableEmpty: TableEmpty_default,
2071
+ VCTableLoading: TableLoading_default,
2072
+ VCTableSortIndicators: TableSortIndicators_default
2073
+ }).forEach(([name, component]) => {
2074
+ app.component(name, component);
2075
+ });
2076
+ }
2077
+ var src_default = { install };
2078
+ //#endregion
2079
+ export { Table_default as VCTable, TableBody_default as VCTableBody, TableCell_default as VCTableCell, TableEmpty_default as VCTableEmpty, TableFooter_default as VCTableFooter, TableHeadCell_default as VCTableHeadCell, TableHeader_default as VCTableHeader, TableLite_default as VCTableLite, TableLoading_default as VCTableLoading, TableRow_default as VCTableRow, TableSortIndicators_default as VCTableSortIndicators, src_default as default, defineTable, filterRowClickEvent, install, normalizeColumns, provideHeadCellCountContext, provideTableContext, provideTableRowContext, resolveAttrs, resolveCellValue, sortRows, startCase, tableBodyThemeDefaults, tableCellThemeDefaults, tableEmptyThemeDefaults, tableFooterThemeDefaults, tableHeadCellThemeDefaults, tableHeaderThemeDefaults, tableLoadingThemeDefaults, tableRowThemeDefaults, tableSortIndicatorsThemeDefaults, tableThemeDefaults, useHeadCellCountContext, useRowSelectionMachine, useSortMachine, useTable, useTableRow };
2080
+
2081
+ //# sourceMappingURL=index.mjs.map