nuxt-ui-tools 1.28.0 → 1.29.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 (35) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/i18n/locales/en.js +5 -0
  3. package/dist/runtime/i18n/locales/fr.js +5 -0
  4. package/dist/runtime/i18n/types.d.ts +7 -0
  5. package/dist/runtime/table/components/grid/grid-renderer.vue +33 -8
  6. package/dist/runtime/table/components/table/table-group-header.d.vue.ts +9 -0
  7. package/dist/runtime/table/components/table/table-group-header.vue +77 -0
  8. package/dist/runtime/table/components/table/table-group-header.vue.d.ts +9 -0
  9. package/dist/runtime/table/components/table/table-renderer.vue +12 -7
  10. package/dist/runtime/table/components/table/table-row.d.vue.ts +4 -1
  11. package/dist/runtime/table/components/table/table-row.vue +16 -1
  12. package/dist/runtime/table/components/table/table-row.vue.d.ts +4 -1
  13. package/dist/runtime/table/components/table/table-selection-control.d.vue.ts +2 -0
  14. package/dist/runtime/table/components/table/table-selection-control.vue +3 -2
  15. package/dist/runtime/table/components/table/table-selection-control.vue.d.ts +2 -0
  16. package/dist/runtime/table/composables/use-table-grid.d.ts +7 -3
  17. package/dist/runtime/table/composables/use-table-grid.js +27 -2
  18. package/dist/runtime/table/composables/use-table-groups.d.ts +28 -0
  19. package/dist/runtime/table/composables/use-table-groups.js +133 -0
  20. package/dist/runtime/table/composables/use-table-internals.d.ts +42 -0
  21. package/dist/runtime/table/composables/use-table-internals.js +9 -0
  22. package/dist/runtime/table/composables/use-table-selection.d.ts +5 -0
  23. package/dist/runtime/table/composables/use-table-selection.js +13 -3
  24. package/dist/runtime/table/composables/use-table-tree.js +7 -5
  25. package/dist/runtime/table/constants/row-motion.d.ts +8 -0
  26. package/dist/runtime/table/constants/row-motion.js +5 -0
  27. package/dist/runtime/table/types/groups.d.ts +44 -0
  28. package/dist/runtime/table/types/groups.js +0 -0
  29. package/dist/runtime/table/types/index.d.ts +1 -0
  30. package/dist/runtime/table/types/layout.d.ts +5 -0
  31. package/dist/runtime/table/utils/groups.d.ts +14 -0
  32. package/dist/runtime/table/utils/groups.js +55 -0
  33. package/dist/runtime/table/utils/index.d.ts +1 -0
  34. package/dist/runtime/table/utils/index.js +1 -0
  35. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "configKey": "nuxtUiTools",
6
6
  "docs": "https://ui.nuxt.com/docs/getting-started/installation/nuxt",
7
7
  "name": "nuxt-ui-tools",
8
- "version": "1.28.0",
8
+ "version": "1.29.0",
9
9
  "builder": {
10
10
  "@nuxt/module-builder": "1.0.3",
11
11
  "unbuild": "unknown"
@@ -748,6 +748,11 @@ export default defineUiToolsLocale({
748
748
  label: "Actions for {label}",
749
749
  labelAt: "Actions for row {position}"
750
750
  },
751
+ groups: {
752
+ empty: "Other",
753
+ select: "Select the whole group",
754
+ toggle: "Fold or unfold the group"
755
+ },
751
756
  selection: {
752
757
  selectAll: "Select all rows",
753
758
  selectRow: "Select {label}",
@@ -748,6 +748,11 @@ export default defineUiToolsLocale({
748
748
  label: "Actions pour {label}",
749
749
  labelAt: "Actions pour la ligne {position}"
750
750
  },
751
+ groups: {
752
+ empty: "Autres",
753
+ select: "S\xE9lectionner tout le groupe",
754
+ toggle: "Replier ou d\xE9plier le groupe"
755
+ },
751
756
  selection: {
752
757
  selectAll: "S\xE9lectionner toutes les lignes",
753
758
  selectRow: "S\xE9lectionner {label}",
@@ -23,6 +23,13 @@ export interface UiToolsTableMessages {
23
23
  firstPage: string;
24
24
  lastPage: string;
25
25
  };
26
+ /** A row group's header. */
27
+ groups: {
28
+ /** The label of the group holding rows with no value. */
29
+ empty: string;
30
+ select: string;
31
+ toggle: string;
32
+ };
26
33
  /** Accessible names of the selection checkboxes. */
27
34
  selection: {
28
35
  selectAll: string;
@@ -11,6 +11,7 @@ import { mergeDataListUiClass, resolveTableRowId } from "../../utils";
11
11
  import DataListErrorState from "../data-list/data-list-error-state.vue";
12
12
  import ProgressLine from "../layout/progress-line.vue";
13
13
  import TableEmptyState from "../table/table-empty-state.vue";
14
+ import TableGroupHeader from "../table/table-group-header.vue";
14
15
  import OverlayInsetSpacer from "../utils/overlay-inset-spacer.vue";
15
16
  import GridCard from "./grid-card.vue";
16
17
  import GridSkeleton from "./grid-skeleton.vue";
@@ -75,12 +76,21 @@ watch(
75
76
  );
76
77
  function getVirtualRowKey(index) {
77
78
  const chunk = rowChunks.value[index];
79
+ if (chunk?.group) return `grid-group:${chunk.group.key}`;
78
80
  const firstRow = chunk?.rows[0];
79
81
  return firstRow ? `grid-row:${resolveTableRowId({ index: chunk.start, row: firstRow, rowKey: internals.schema.value.rowKey })}` : `grid-row:${index}`;
80
82
  }
81
83
  function rowId(row, index) {
82
84
  return resolveTableRowId({ index, row, rowKey: internals.schema.value.rowKey });
83
85
  }
86
+ const flowItems = computed(
87
+ () => rowChunks.value.flatMap(
88
+ (chunk) => chunk.group ? [{ group: chunk.group, index: chunk.start, key: `grid-group:${chunk.group.key}` }] : chunk.rows.map((row, offset) => ({
89
+ index: chunk.start + offset,
90
+ key: rowId(row, chunk.start + offset)
91
+ }))
92
+ )
93
+ );
84
94
  function scrollToTop() {
85
95
  viewportRef.value?.scrollTo({ behavior: "auto", left: 0, top: 0 });
86
96
  }
@@ -258,6 +268,13 @@ function refreshData() {
258
268
  }"
259
269
  :data-index="virtualRow.index"
260
270
  >
271
+ <div
272
+ v-if="rowChunks[virtualRow.index]?.group"
273
+ class="nut-dl-grid__group min-w-0"
274
+ :style="{ gridColumn: '1 / -1' }"
275
+ >
276
+ <TableGroupHeader :group="rowChunks[virtualRow.index].group" />
277
+ </div>
261
278
  <div
262
279
  v-for="(row, offset) in rowChunks[virtualRow.index]?.rows ?? []"
263
280
  :key="rowId(row, (rowChunks[virtualRow.index]?.start ?? 0) + offset)"
@@ -275,14 +292,22 @@ function refreshData() {
275
292
  :class="mergeDataListUiClass('nut-dl-grid__flow grid', void 0, ui?.flow)"
276
293
  :style="{ gridTemplateColumns, gap: `${gap}px` }"
277
294
  >
278
- <div
279
- v-for="(row, index) in tableRows"
280
- :key="rowId(row, index)"
281
- :class="mergeDataListUiClass('nut-dl-grid__item min-w-0', void 0, ui?.item)"
282
- :style="{ gridColumn }"
283
- >
284
- <GridCard :row-index="index" />
285
- </div>
295
+ <template v-for="item in flowItems" :key="item.key">
296
+ <div
297
+ v-if="item.group"
298
+ class="nut-dl-grid__group min-w-0"
299
+ :style="{ gridColumn: '1 / -1' }"
300
+ >
301
+ <TableGroupHeader :group="item.group" />
302
+ </div>
303
+ <div
304
+ v-else
305
+ :class="mergeDataListUiClass('nut-dl-grid__item min-w-0', void 0, ui?.item)"
306
+ :style="{ gridColumn }"
307
+ >
308
+ <GridCard :row-index="item.index" />
309
+ </div>
310
+ </template>
286
311
  </div>
287
312
  <OverlayInsetSpacer />
288
313
  </div>
@@ -0,0 +1,9 @@
1
+ import type { TableGroup } from '../../types/index.js';
2
+ type __VLS_Props = {
3
+ group: TableGroup;
4
+ /** Whether the rows beneath it carry checkboxes. */
5
+ selectable?: boolean;
6
+ };
7
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
+ declare const _default: typeof __VLS_export;
9
+ export default _default;
@@ -0,0 +1,77 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { useUiToolsLocale } from "#ui-tools/i18n";
4
+ import { useTableInternals } from "../../composables/use-table-internals";
5
+ import TableCell from "./table-cell";
6
+ import TableSelectionControl from "./table-selection-control.vue";
7
+ const props = defineProps({
8
+ group: { type: Object, required: true },
9
+ selectable: { type: Boolean, required: false }
10
+ });
11
+ const { groups, selection } = useTableInternals();
12
+ const { t } = useUiToolsLocale();
13
+ const collapsed = computed(() => groups.isCollapsed(props.group));
14
+ const params = computed(() => ({ rows: props.group.rows, value: props.group.value }));
15
+ const checked = computed(() => selection.getRowsSelectionState({ rowIds: props.group.rowIds }));
16
+ const showCheckbox = computed(() => props.selectable && selection.selectionEnabled.value);
17
+ function renderLabel() {
18
+ const label = groups.config.value?.label;
19
+ if (label) return label(params.value);
20
+ return props.group.value === void 0 ? t("table.groups.empty") : String(props.group.value);
21
+ }
22
+ function renderSummary() {
23
+ const summary = groups.config.value?.summary;
24
+ return summary ? summary(params.value) : props.group.rows.length;
25
+ }
26
+ function toggleSelection() {
27
+ const { rowIds } = props.group;
28
+ if (checked.value === true) selection.unselectRows({ rowIds });
29
+ else selection.selectRows({ rowIds });
30
+ }
31
+ </script>
32
+
33
+ <template>
34
+ <div
35
+ class="nut-dl-group"
36
+ :data-collapsed="collapsed || void 0"
37
+ :data-selectable="showCheckbox || void 0"
38
+ @click="groups.toggle(group)"
39
+ >
40
+ <span v-if="showCheckbox" class="nut-dl-group__check">
41
+ <TableSelectionControl
42
+ :model-value="checked"
43
+ :label="t('table.groups.select')"
44
+ @toggle="toggleSelection"
45
+ />
46
+ </span>
47
+ <button
48
+ type="button"
49
+ class="nut-dl-group__toggle"
50
+ :aria-expanded="!collapsed"
51
+ :aria-label="t('table.groups.toggle')"
52
+ @click.stop="groups.toggle(group)"
53
+ >
54
+ <svg
55
+ viewBox="0 0 16 16"
56
+ fill="none"
57
+ stroke="currentColor"
58
+ stroke-width="1.75"
59
+ stroke-linecap="round"
60
+ stroke-linejoin="round"
61
+ aria-hidden="true"
62
+ >
63
+ <path d="m4 6 4 4 4-4" />
64
+ </svg>
65
+ </button>
66
+ <span class="nut-dl-group__label">
67
+ <TableCell :index="0" :render="renderLabel" :row="group" />
68
+ </span>
69
+ <span class="nut-dl-group__summary">
70
+ <TableCell :index="0" :render="renderSummary" :row="group" />
71
+ </span>
72
+ </div>
73
+ </template>
74
+
75
+ <style>
76
+ .nut-dl-group{--nut-dl-group-gutter:var(--nut-dl-gutter,0px);box-sizing:border-box;color:var(--ui-text-highlighted);cursor:pointer;font-size:13px;font-weight:600;gap:6px;height:100%;left:0;max-width:100%;min-height:32px;padding-inline:var(--nut-dl-group-gutter);position:sticky;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.nut-dl-group,.nut-dl-group__check{align-items:center;display:inline-flex}.nut-dl-group__check{flex:none;margin-right:-6px;width:calc(var(--nut-dl-select-w, 44px) - var(--nut-dl-group-gutter) + var(--nut-dl-cell-x, 0px))}.nut-dl-group__toggle{background:transparent;border:0;border-radius:calc(var(--ui-radius)*1.5);color:var(--ui-text-muted);cursor:pointer;display:grid;flex:none;height:22px;outline:none;padding:0;place-items:center;width:22px}.nut-dl-group__toggle:hover{background:var(--nut-dl-toggle-hover,var(--ui-bg-accented))}.nut-dl-group__toggle:focus-visible{box-shadow:0 0 0 2px var(--ui-primary)}.nut-dl-group__toggle svg{height:16px;transition:transform .16s ease;width:16px}.nut-dl-group[data-collapsed] .nut-dl-group__toggle svg{transform:rotate(-90deg)}.nut-dl-group__label{min-width:0;overflow:hidden;text-overflow:ellipsis}.nut-dl-group__summary{color:var(--ui-text-muted);flex:none;font-size:12px;font-variant-numeric:tabular-nums;font-weight:500}.nut-dl-row.nut-dl-row--group{background:var(--nut-dl-head-bg)}.nut-dl-row.nut-dl-row--group:hover{background:color-mix(in srgb,var(--nut-dl-head-fg) 7%,var(--nut-dl-head-bg))}.nut-dl-group-cell{background:inherit;border-bottom:1px solid var(--nut-dl-line);height:var(--nut-dl-head-h);padding:0}.nut-dl-grid__group{border-bottom:1px solid var(--ui-border)}@media (prefers-reduced-motion:reduce){.nut-dl-group__toggle svg{transition:none}}
77
+ </style>
@@ -0,0 +1,9 @@
1
+ import type { TableGroup } from '../../types/index.js';
2
+ type __VLS_Props = {
3
+ group: TableGroup;
4
+ /** Whether the rows beneath it carry checkboxes. */
5
+ selectable?: boolean;
6
+ };
7
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
+ declare const _default: typeof __VLS_export;
9
+ export default _default;
@@ -6,7 +6,7 @@ import { isFunction, isNumber, isObject } from "../../../shared/utils/predicate"
6
6
  import { useDataListUi } from "../../composables/use-data-list-ui";
7
7
  import { useTableInternals } from "../../composables/use-table-internals";
8
8
  import { useTanstackTable } from "../../composables/use-tanstack-table";
9
- import { mergeDataListUiClass } from "../../utils";
9
+ import { mergeDataListUiClass, tableGroupOf } from "../../utils";
10
10
  import {
11
11
  ROW_ACTIONS_COLUMN_ID,
12
12
  SELECT_COLUMN_ID,
@@ -54,7 +54,7 @@ const props = defineProps({
54
54
  ui: { type: Object, required: false }
55
55
  });
56
56
  const internals = useTableInternals();
57
- const { tree } = internals;
57
+ const { groups, tree } = internals;
58
58
  const dataListUi = useDataListUi();
59
59
  const { locale, t } = useUiToolsLocale();
60
60
  const rows = computed(
@@ -89,6 +89,7 @@ const tokenStyle = computed(() => ({
89
89
  "--nut-dl-gutter": `${gutter.value}px`,
90
90
  "--nut-dl-head-h": `${tokens.value.head}px`,
91
91
  "--nut-dl-row-h": `${tokens.value.row}px`,
92
+ "--nut-dl-select-w": `${SELECT_COLUMN_WIDTH + gutterExtra.value}px`,
92
93
  "--nut-dl-tree-gutter": `${TREE_GUTTER_WIDTH}px`,
93
94
  "--nut-dl-tree-toggle": `${TREE_TOGGLE_WIDTH}px`,
94
95
  "--nut-dl-tree-gap": `${TREE_GAP}px`,
@@ -141,11 +142,13 @@ const pinned = computed(() => {
141
142
  };
142
143
  });
143
144
  function getRowId(row, index) {
144
- return internals.selection.getRowId({ index, row });
145
+ return tableGroupOf(row)?.key ?? internals.selection.getRowId({ index, row });
145
146
  }
146
- const sourceRows = computed(
147
- () => tree.enabled.value && rows.value.length > 0 ? tree.visibleRows.value : rows.value
148
- );
147
+ const sourceRows = computed(() => {
148
+ if (rows.value.length === 0) return rows.value;
149
+ if (tree.enabled.value) return tree.visibleRows.value;
150
+ return groups.enabled.value ? groups.displayRows.value : rows.value;
151
+ });
149
152
  const renderRows = computed(() => rowsMounted.value ? sourceRows.value : []);
150
153
  const virtualized = computed(
151
154
  () => renderRows.value.length >= VIRTUALIZE_ROW_THRESHOLD || renderRows.value.length * columnDefs.value.length >= VIRTUALIZE_CELL_THRESHOLD
@@ -745,7 +748,9 @@ defineExpose({ resetColumnSizing });
745
748
  :node="tree.nodeOf(row.id)"
746
749
  :expanded="tree.isExpanded(row.id)"
747
750
  :checkbox="hasCheckbox(row.id)"
748
- :motion="tree.motionOf(row.id)"
751
+ :motion="tree.motionOf(row.id) ?? groups.motionOf(row.id)"
752
+ :group="tableGroupOf(row.original)"
753
+ :col-span="columnCount"
749
754
  @toggle="tree.toggleRow(row.id)"
750
755
  @select="onSelectRow(row.id, $event)"
751
756
  />
@@ -3,7 +3,7 @@
3
3
  * so rows that stay on screen while the virtual window slides keep their cells untouched.
4
4
  */
5
5
  import type { HTMLAttributes } from 'vue';
6
- import type { GenericObject, TableTreeMotion, TableTreeNode } from '../../types/index.js';
6
+ import type { GenericObject, TableGroup, TableTreeMotion, TableTreeNode } from '../../types/index.js';
7
7
  import type { tableRowCells } from './table-layout.js';
8
8
  type __VLS_Props = {
9
9
  rowId: string;
@@ -20,6 +20,9 @@ type __VLS_Props = {
20
20
  checkbox?: boolean;
21
21
  /** The row is being revealed or concealed by a branch opening or closing. */
22
22
  motion?: TableTreeMotion;
23
+ /** Set on a group's header row, which spans the columns instead of filling them. */
24
+ group?: TableGroup;
25
+ colSpan?: number;
23
26
  };
24
27
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
25
28
  select: (event: MouseEvent) => any;
@@ -1,5 +1,6 @@
1
1
  <script setup>
2
2
  import TableCell from "./table-cell";
3
+ import TableGroupHeader from "./table-group-header.vue";
3
4
  import TableTreeControl from "./table-tree-control.vue";
4
5
  defineProps({
5
6
  rowId: { type: String, required: true },
@@ -12,13 +13,27 @@ defineProps({
12
13
  node: { type: Object, required: false },
13
14
  expanded: { type: Boolean, required: false },
14
15
  checkbox: { type: Boolean, required: false },
15
- motion: { type: Object, required: false }
16
+ motion: { type: Object, required: false },
17
+ group: { type: Object, required: false },
18
+ colSpan: { type: Number, required: false }
16
19
  });
17
20
  const emit = defineEmits(["toggle", "select"]);
18
21
  </script>
19
22
 
20
23
  <template>
21
24
  <tr
25
+ v-if="group"
26
+ class="nut-dl-row nut-dl-row--group"
27
+ :class="rowClass"
28
+ :data-index="index"
29
+ :data-row-id="rowId"
30
+ >
31
+ <td :colspan="colSpan" class="nut-dl-group-cell">
32
+ <TableGroupHeader :group="group" selectable />
33
+ </td>
34
+ </tr>
35
+ <tr
36
+ v-else
22
37
  class="nut-dl-row group/row"
23
38
  :class="[
24
39
  selected ? 'nut-dl-row--selected' : '',
@@ -3,7 +3,7 @@
3
3
  * so rows that stay on screen while the virtual window slides keep their cells untouched.
4
4
  */
5
5
  import type { HTMLAttributes } from 'vue';
6
- import type { GenericObject, TableTreeMotion, TableTreeNode } from '../../types/index.js';
6
+ import type { GenericObject, TableGroup, TableTreeMotion, TableTreeNode } from '../../types/index.js';
7
7
  import type { tableRowCells } from './table-layout.js';
8
8
  type __VLS_Props = {
9
9
  rowId: string;
@@ -20,6 +20,9 @@ type __VLS_Props = {
20
20
  checkbox?: boolean;
21
21
  /** The row is being revealed or concealed by a branch opening or closing. */
22
22
  motion?: TableTreeMotion;
23
+ /** Set on a group's header row, which spans the columns instead of filling them. */
24
+ group?: TableGroup;
25
+ colSpan?: number;
23
26
  };
24
27
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
25
28
  select: (event: MouseEvent) => any;
@@ -5,6 +5,8 @@ type __VLS_Props = {
5
5
  row?: GenericObject;
6
6
  /** Where the record sits in the rendered rows: it names the record when the schema has no `rowLabel`. */
7
7
  index?: number;
8
+ /** Names what it selects when that is neither one record nor every row. */
9
+ label?: string;
8
10
  };
9
11
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
12
  toggle: (event: MouseEvent) => any;
@@ -7,7 +7,8 @@ import { mergeDataListProps, mergeDataListUiClass } from "../../utils";
7
7
  const props = defineProps({
8
8
  modelValue: { type: [Boolean, String], required: true },
9
9
  row: { type: Object, required: false },
10
- index: { type: Number, required: false }
10
+ index: { type: Number, required: false },
11
+ label: { type: String, required: false }
11
12
  });
12
13
  const emit = defineEmits(["toggle"]);
13
14
  const dataListUi = useDataListUi();
@@ -20,7 +21,7 @@ const areaClass = computed(
20
21
  )
21
22
  );
22
23
  const ariaLabel = computed(
23
- () => props.row ? labels.selectRow({ index: props.index ?? 0, row: props.row }) : labels.selectAll()
24
+ () => props.label ?? (props.row ? labels.selectRow({ index: props.index ?? 0, row: props.row }) : labels.selectAll())
24
25
  );
25
26
  const checkboxProps = computed(
26
27
  () => mergeDataListProps(
@@ -5,6 +5,8 @@ type __VLS_Props = {
5
5
  row?: GenericObject;
6
6
  /** Where the record sits in the rendered rows: it names the record when the schema has no `rowLabel`. */
7
7
  index?: number;
8
+ /** Names what it selects when that is neither one record nor every row. */
9
+ label?: string;
8
10
  };
9
11
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
12
  toggle: (event: MouseEvent) => any;
@@ -1,20 +1,24 @@
1
1
  import type { ComputedRef } from 'vue';
2
- import type { GenericObject, TableExternalState, TableGridMode, TableSchemaView } from '../types/index.js';
2
+ import type { GenericObject, TableExternalState, TableGridMode, TableGroup, TableSchemaView } from '../types/index.js';
3
+ import type { useTableGroups } from './use-table-groups.js';
3
4
  export interface GridRowChunk<TRow = unknown> {
4
5
  index: number;
5
6
  start: number;
6
7
  end: number;
7
8
  rows: TRow[];
9
+ /** The group this chunk is the header of; a header holds no rows. */
10
+ group?: TableGroup;
8
11
  }
9
12
  export interface UseTableGridParams<TRow extends GenericObject = GenericObject> {
10
13
  schema: ComputedRef<TableSchemaView>;
11
14
  data: ComputedRef<TableExternalState<TRow>>;
15
+ groups: ReturnType<typeof useTableGroups>;
12
16
  }
13
17
  export declare function useTableGrid<TRow extends GenericObject>(options: UseTableGridParams<TRow>): {
14
18
  cardsPerRow: ComputedRef<number>;
15
19
  columnCount: ComputedRef<number>;
16
20
  itemColumnSpan: ComputedRef<number>;
17
21
  mode: ComputedRef<TableGridMode>;
18
- rowChunks: ComputedRef<GridRowChunk<TRow>[]>;
19
- rows: ComputedRef<TRow[]>;
22
+ rowChunks: ComputedRef<GridRowChunk<object>[]>;
23
+ rows: ComputedRef<object[]>;
20
24
  };
@@ -19,9 +19,15 @@ export function useTableGrid(options) {
19
19
  const cardsPerRow = computed(
20
20
  () => Math.max(1, Math.floor(columnCount.value / itemColumnSpan.value))
21
21
  );
22
- const rows = computed(() => options.data.value.rows);
22
+ const rows = computed(
23
+ () => options.groups.enabled.value ? options.groups.orderedRows.value : options.data.value.rows
24
+ );
23
25
  const rowChunks = computed(
24
- () => chunkRows({
26
+ () => options.groups.enabled.value ? chunkGroups({
27
+ cardsPerRow: cardsPerRow.value,
28
+ groups: options.groups.groups.value,
29
+ isCollapsed: options.groups.isCollapsed
30
+ }) : chunkRows({
25
31
  cardsPerRow: cardsPerRow.value,
26
32
  rows: rows.value
27
33
  })
@@ -48,6 +54,25 @@ function resolveResponsiveGridNumber(value) {
48
54
  function isResponsiveGridValueResolver(value) {
49
55
  return isFunction(value);
50
56
  }
57
+ function chunkGroups(options) {
58
+ const chunks = [];
59
+ let offset = 0;
60
+ for (const group of options.groups) {
61
+ chunks.push({ end: offset, group, index: chunks.length, rows: [], start: offset });
62
+ if (!options.isCollapsed(group)) {
63
+ for (const chunk of chunkRows({ cardsPerRow: options.cardsPerRow, rows: group.rows })) {
64
+ chunks.push({
65
+ ...chunk,
66
+ end: offset + chunk.end,
67
+ index: chunks.length,
68
+ start: offset + chunk.start
69
+ });
70
+ }
71
+ }
72
+ offset += group.rows.length;
73
+ }
74
+ return chunks;
75
+ }
51
76
  function chunkRows(options) {
52
77
  return Array.from(
53
78
  { length: Math.ceil(options.rows.length / options.cardsPerRow) },
@@ -0,0 +1,28 @@
1
+ import type { ComputedRef } from 'vue';
2
+ import type { TableGroup, TableSchemaView, TableTreeMotion } from '../types/index.js';
3
+ import type { UseTableDataReturn } from './use-table-data.js';
4
+ import type { useTableLayout } from './use-table-layout.js';
5
+ export interface UseTableGroupsParams {
6
+ schema: ComputedRef<TableSchemaView>;
7
+ layout: ReturnType<typeof useTableLayout>;
8
+ queryContent: Pick<UseTableDataReturn, 'data'>;
9
+ }
10
+ /**
11
+ * Owns row groups: which groups the loaded rows form, which are folded, and the motion of folding
12
+ * one. The data stays flat; `displayRows` adds the header rows to what the table renders.
13
+ *
14
+ * Fold state is the schema's `collapsed` plus the groups the reader toggled, kept per table so a
15
+ * trip to a record and back finds the list as it was left. A table with `table.tree` has its own
16
+ * hierarchy and is not grouped.
17
+ */
18
+ export declare function useTableGroups(params: UseTableGroupsParams): {
19
+ config: ComputedRef<import("../types/index.js").TableGroupBySchema<object> | undefined>;
20
+ displayRows: ComputedRef<object[]>;
21
+ enabled: ComputedRef<boolean>;
22
+ groups: ComputedRef<TableGroup<object>[]>;
23
+ isCollapsed: (group: TableGroup) => boolean;
24
+ motionOf: (rowId: string) => TableTreeMotion | undefined;
25
+ orderedRows: ComputedRef<object[]>;
26
+ toggle: (group: TableGroup) => void;
27
+ visibleRowIds: ComputedRef<string[]>;
28
+ };
@@ -0,0 +1,133 @@
1
+ import { usePreferredReducedMotion } from "@vueuse/core";
2
+ import { computed, getCurrentInstance, onMounted, onScopeDispose, ref, shallowRef } from "vue";
3
+ import {
4
+ ROW_CONCEAL_MS,
5
+ ROW_MOTION_LIMIT,
6
+ ROW_REVEAL_MS,
7
+ ROW_STAGGER_LIMIT,
8
+ ROW_STAGGER_MS
9
+ } from "../constants/row-motion.js";
10
+ import {
11
+ buildTableGroups,
12
+ parseStoredGroupKeys,
13
+ tableGroupRow,
14
+ tableGroupsStorageKey
15
+ } from "../utils/groups.js";
16
+ import { resolveTableRowId } from "../utils/rows.js";
17
+ export function useTableGroups(params) {
18
+ const reducedMotion = usePreferredReducedMotion();
19
+ const config = computed(() => {
20
+ const { grid, table } = params.schema.value;
21
+ if (params.layout.activeLayout.value === "grid") return grid?.groupBy;
22
+ return table?.tree ? void 0 : table?.groupBy;
23
+ });
24
+ const enabled = computed(() => Boolean(config.value));
25
+ const groups = computed(() => {
26
+ const groupBy = config.value;
27
+ if (!groupBy) return [];
28
+ return buildTableGroups({
29
+ getRowId: (row, index) => String(resolveTableRowId({ index, row, rowKey: params.schema.value.rowKey })),
30
+ groupBy,
31
+ rows: params.queryContent.data.value.rows
32
+ });
33
+ });
34
+ const toggled = ref(/* @__PURE__ */ new Set());
35
+ const persists = computed(() => params.schema.value.persistence?.preferences !== false);
36
+ function storage() {
37
+ try {
38
+ return typeof window === "undefined" || !persists.value ? null : window.localStorage;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+ function restore() {
44
+ try {
45
+ const raw = storage()?.getItem(tableGroupsStorageKey(params.schema.value.tableKey)) ?? null;
46
+ toggled.value = new Set(parseStoredGroupKeys(raw));
47
+ } catch {
48
+ toggled.value = /* @__PURE__ */ new Set();
49
+ }
50
+ }
51
+ function remember() {
52
+ const key = tableGroupsStorageKey(params.schema.value.tableKey);
53
+ try {
54
+ if (toggled.value.size) storage()?.setItem(key, JSON.stringify([...toggled.value]));
55
+ else storage()?.removeItem(key);
56
+ } catch {
57
+ return;
58
+ }
59
+ }
60
+ if (getCurrentInstance()) onMounted(restore);
61
+ else restore();
62
+ function startsCollapsed(group) {
63
+ const collapsed = config.value?.collapsed;
64
+ return typeof collapsed === "function" ? collapsed({ rows: group.rows, value: group.value }) : Boolean(collapsed);
65
+ }
66
+ function isCollapsed(group) {
67
+ return startsCollapsed(group) !== toggled.value.has(group.key);
68
+ }
69
+ function flip(group) {
70
+ const next = new Set(toggled.value);
71
+ if (!next.delete(group.key)) next.add(group.key);
72
+ toggled.value = next;
73
+ remember();
74
+ }
75
+ const motion = shallowRef(null);
76
+ const closing = ref(null);
77
+ let timer;
78
+ function settle() {
79
+ clearTimeout(timer);
80
+ closing.value = null;
81
+ motion.value = null;
82
+ }
83
+ function toggle(group) {
84
+ settle();
85
+ const opening = isCollapsed(group);
86
+ flip(group);
87
+ if (reducedMotion.value === "reduce" || params.layout.activeLayout.value === "grid") return;
88
+ const moving = group.rowIds.slice(0, ROW_MOTION_LIMIT);
89
+ motion.value = new Map(
90
+ moving.map((rowId, order) => [
91
+ rowId,
92
+ { kind: opening ? "enter" : "leave", order: Math.min(order, ROW_STAGGER_LIMIT) }
93
+ ])
94
+ );
95
+ if (opening) {
96
+ timer = setTimeout(
97
+ settle,
98
+ ROW_REVEAL_MS + Math.min(moving.length, ROW_STAGGER_LIMIT) * ROW_STAGGER_MS
99
+ );
100
+ return;
101
+ }
102
+ closing.value = group.key;
103
+ timer = setTimeout(settle, ROW_CONCEAL_MS);
104
+ }
105
+ onScopeDispose(() => clearTimeout(timer));
106
+ function motionOf(rowId) {
107
+ return motion.value?.get(rowId);
108
+ }
109
+ function rowsListed(group) {
110
+ return !isCollapsed(group) || closing.value === group.key;
111
+ }
112
+ const displayRows = computed(
113
+ () => groups.value.flatMap((group) => [
114
+ tableGroupRow(group),
115
+ ...rowsListed(group) ? group.rows : []
116
+ ])
117
+ );
118
+ const orderedRows = computed(() => groups.value.flatMap((group) => group.rows));
119
+ const visibleRowIds = computed(
120
+ () => groups.value.flatMap((group) => isCollapsed(group) ? [] : group.rowIds)
121
+ );
122
+ return {
123
+ config,
124
+ displayRows,
125
+ enabled,
126
+ groups,
127
+ isCollapsed,
128
+ motionOf,
129
+ orderedRows,
130
+ toggle,
131
+ visibleRowIds
132
+ };
133
+ }
@@ -228,6 +228,17 @@ declare function createTableInternals<TSchema>(options: {
228
228
  rowChunks: import("vue").ComputedRef<import("./use-table-grid.js").GridRowChunk<object>[]>;
229
229
  rows: import("vue").ComputedRef<object[]>;
230
230
  };
231
+ groups: {
232
+ config: import("vue").ComputedRef<import("../types/index.js").TableGroupBySchema<object> | undefined>;
233
+ displayRows: import("vue").ComputedRef<object[]>;
234
+ enabled: import("vue").ComputedRef<boolean>;
235
+ groups: import("vue").ComputedRef<import("../types/index.js").TableGroup<object>[]>;
236
+ isCollapsed: (group: import("../types/index.js").TableGroup) => boolean;
237
+ motionOf: (rowId: string) => import("../types/index.js").TableTreeMotion | undefined;
238
+ orderedRows: import("vue").ComputedRef<object[]>;
239
+ toggle: (group: import("../types/index.js").TableGroup) => void;
240
+ visibleRowIds: import("vue").ComputedRef<string[]>;
241
+ };
231
242
  layout: {
232
243
  activeLayout: import("vue").WritableComputedRef<import("../types/index.js").TableLayout, import("../types/index.js").TableLayout>;
233
244
  gridEnabled: import("vue").ComputedRef<boolean>;
@@ -345,6 +356,9 @@ declare function createTableInternals<TSchema>(options: {
345
356
  row: import("../types/index.js").GenericObject;
346
357
  index?: number;
347
358
  }) => string;
359
+ getRowsSelectionState: (params: {
360
+ rowIds: string[];
361
+ }) => boolean | "indeterminate";
348
362
  isRowSelected: (params: {
349
363
  rowId: string;
350
364
  }) => boolean;
@@ -851,6 +865,17 @@ declare function useProvideTableInternals<TSchema>(options: {
851
865
  rowChunks: import("vue").ComputedRef<import("./use-table-grid.js").GridRowChunk<object>[]>;
852
866
  rows: import("vue").ComputedRef<object[]>;
853
867
  };
868
+ groups: {
869
+ config: import("vue").ComputedRef<import("../types/index.js").TableGroupBySchema<object> | undefined>;
870
+ displayRows: import("vue").ComputedRef<object[]>;
871
+ enabled: import("vue").ComputedRef<boolean>;
872
+ groups: import("vue").ComputedRef<import("../types/index.js").TableGroup<object>[]>;
873
+ isCollapsed: (group: import("../types/index.js").TableGroup) => boolean;
874
+ motionOf: (rowId: string) => import("../types/index.js").TableTreeMotion | undefined;
875
+ orderedRows: import("vue").ComputedRef<object[]>;
876
+ toggle: (group: import("../types/index.js").TableGroup) => void;
877
+ visibleRowIds: import("vue").ComputedRef<string[]>;
878
+ };
854
879
  layout: {
855
880
  activeLayout: import("vue").WritableComputedRef<import("../types/index.js").TableLayout, import("../types/index.js").TableLayout>;
856
881
  gridEnabled: import("vue").ComputedRef<boolean>;
@@ -968,6 +993,9 @@ declare function useProvideTableInternals<TSchema>(options: {
968
993
  row: import("../types/index.js").GenericObject;
969
994
  index?: number;
970
995
  }) => string;
996
+ getRowsSelectionState: (params: {
997
+ rowIds: string[];
998
+ }) => boolean | "indeterminate";
971
999
  isRowSelected: (params: {
972
1000
  rowId: string;
973
1001
  }) => boolean;
@@ -1471,6 +1499,17 @@ declare function useTableInternals(): {
1471
1499
  rowChunks: import("vue").ComputedRef<import("./use-table-grid.js").GridRowChunk<object>[]>;
1472
1500
  rows: import("vue").ComputedRef<object[]>;
1473
1501
  };
1502
+ groups: {
1503
+ config: import("vue").ComputedRef<import("../types/index.js").TableGroupBySchema<object> | undefined>;
1504
+ displayRows: import("vue").ComputedRef<object[]>;
1505
+ enabled: import("vue").ComputedRef<boolean>;
1506
+ groups: import("vue").ComputedRef<import("../types/index.js").TableGroup<object>[]>;
1507
+ isCollapsed: (group: import("../types/index.js").TableGroup) => boolean;
1508
+ motionOf: (rowId: string) => import("../types/index.js").TableTreeMotion | undefined;
1509
+ orderedRows: import("vue").ComputedRef<object[]>;
1510
+ toggle: (group: import("../types/index.js").TableGroup) => void;
1511
+ visibleRowIds: import("vue").ComputedRef<string[]>;
1512
+ };
1474
1513
  layout: {
1475
1514
  activeLayout: import("vue").WritableComputedRef<import("../types/index.js").TableLayout, import("../types/index.js").TableLayout>;
1476
1515
  gridEnabled: import("vue").ComputedRef<boolean>;
@@ -1588,6 +1627,9 @@ declare function useTableInternals(): {
1588
1627
  row: import("../types/index.js").GenericObject;
1589
1628
  index?: number;
1590
1629
  }) => string;
1630
+ getRowsSelectionState: (params: {
1631
+ rowIds: string[];
1632
+ }) => boolean | "indeterminate";
1591
1633
  isRowSelected: (params: {
1592
1634
  rowId: string;
1593
1635
  }) => boolean;
@@ -8,6 +8,7 @@ import { useTableData } from "./use-table-data.js";
8
8
  import { useTableFilterPresentation } from "./use-table-filter-presentation.js";
9
9
  import { useTableFilters } from "./use-table-filters.js";
10
10
  import { useTableGrid } from "./use-table-grid.js";
11
+ import { useTableGroups } from "./use-table-groups.js";
11
12
  import { useTableLayout } from "./use-table-layout.js";
12
13
  import { useTablePagination } from "./use-table-pagination.js";
13
14
  import { useTableRowSummary } from "./use-table-row-summary.js";
@@ -35,7 +36,13 @@ function createTableInternals(options) {
35
36
  queryContent,
36
37
  schema
37
38
  });
39
+ const groups = useTableGroups({
40
+ layout,
41
+ queryContent,
42
+ schema
43
+ });
38
44
  const selection = useTableSelection({
45
+ groups,
39
46
  queryContent,
40
47
  schema,
41
48
  tree
@@ -60,6 +67,7 @@ function createTableInternals(options) {
60
67
  });
61
68
  const grid = useTableGrid({
62
69
  data: queryContent.data,
70
+ groups,
63
71
  schema
64
72
  });
65
73
  const tableColumns = useTableColumns({
@@ -112,6 +120,7 @@ function createTableInternals(options) {
112
120
  filterPresentation,
113
121
  filters,
114
122
  grid,
123
+ groups,
115
124
  layout,
116
125
  pagination,
117
126
  queryContent,
@@ -1,11 +1,13 @@
1
1
  import type { ComputedRef } from 'vue';
2
2
  import type { GenericObject, TableSchemaView } from '../types/index.js';
3
3
  import type { useTableData } from './use-table-data.js';
4
+ import type { useTableGroups } from './use-table-groups.js';
4
5
  import type { useTableTree } from './use-table-tree.js';
5
6
  export interface UseTableSelectionParams {
6
7
  schema: ComputedRef<TableSchemaView>;
7
8
  queryContent: ReturnType<typeof useTableData>;
8
9
  tree: ReturnType<typeof useTableTree>;
10
+ groups: ReturnType<typeof useTableGroups>;
9
11
  }
10
12
  export declare function useTableSelection(options: UseTableSelectionParams): {
11
13
  allSelected: ComputedRef<boolean>;
@@ -15,6 +17,9 @@ export declare function useTableSelection(options: UseTableSelectionParams): {
15
17
  row: GenericObject;
16
18
  index?: number;
17
19
  }) => string;
20
+ getRowsSelectionState: (params: {
21
+ rowIds: string[];
22
+ }) => boolean | "indeterminate";
18
23
  isRowSelected: (params: {
19
24
  rowId: string;
20
25
  }) => boolean;
@@ -61,6 +61,12 @@ export function useTableSelection(options) {
61
61
  function isRowSelected(params) {
62
62
  return selectedKeys.value.includes(params.rowId);
63
63
  }
64
+ function getRowsSelectionState(params) {
65
+ const selected = new Set(selectedKeys.value);
66
+ const count = params.rowIds.filter((rowId) => selected.has(rowId)).length;
67
+ if (count === 0) return false;
68
+ return count === params.rowIds.length ? true : "indeterminate";
69
+ }
64
70
  function selectRows(params) {
65
71
  if (!selectionEnabled.value) {
66
72
  return;
@@ -117,9 +123,12 @@ export function useTableSelection(options) {
117
123
  }
118
124
  lastTouchedRowId.value = params.rowId;
119
125
  }
120
- const rangeRowIds = computed(
121
- () => options.tree.enabled.value ? options.tree.visibleNodes.value.filter((node) => options.tree.isSelectable(node)).map((node) => node.id) : scopeRowIds.value
122
- );
126
+ const rangeRowIds = computed(() => {
127
+ if (options.tree.enabled.value) {
128
+ return options.tree.visibleNodes.value.filter((node) => options.tree.isSelectable(node)).map((node) => node.id);
129
+ }
130
+ return options.groups.enabled.value ? options.groups.visibleRowIds.value : scopeRowIds.value;
131
+ });
123
132
  function getRangeRowIds(params) {
124
133
  const anchorIndex = rangeRowIds.value.indexOf(params.anchorRowId);
125
134
  const targetIndex = rangeRowIds.value.indexOf(params.targetRowId);
@@ -157,6 +166,7 @@ export function useTableSelection(options) {
157
166
  bulkScope,
158
167
  clearSelection,
159
168
  getRowId,
169
+ getRowsSelectionState,
160
170
  isRowSelected,
161
171
  lastTouchedRowId,
162
172
  partiallySelected,
@@ -1,5 +1,12 @@
1
1
  import { usePreferredReducedMotion } from "@vueuse/core";
2
2
  import { computed, onScopeDispose, reactive, ref, shallowRef } from "vue";
3
+ import {
4
+ ROW_CONCEAL_MS as CONCEAL_MS,
5
+ ROW_MOTION_LIMIT as MOTION_ROW_LIMIT,
6
+ ROW_REVEAL_MS as REVEAL_MS,
7
+ ROW_STAGGER_LIMIT as STAGGER_ROW_LIMIT,
8
+ ROW_STAGGER_MS as STAGGER_MS
9
+ } from "../constants/row-motion.js";
3
10
  import { resolveTableRowId } from "../utils/rows.js";
4
11
  import {
5
12
  buildTableTree,
@@ -8,11 +15,6 @@ import {
8
15
  getVisibleDescendants,
9
16
  isOpenByDefault
10
17
  } from "../utils/tree.js";
11
- const CONCEAL_MS = 120;
12
- const REVEAL_MS = 190;
13
- const STAGGER_MS = 24;
14
- const MOTION_ROW_LIMIT = 24;
15
- const STAGGER_ROW_LIMIT = 8;
16
18
  const EMPTY_INDEX = {
17
19
  branches: [],
18
20
  byId: /* @__PURE__ */ new Map(),
@@ -0,0 +1,8 @@
1
+ /** How long a closing branch or group plays out before its rows leave the list. */
2
+ export declare const ROW_CONCEAL_MS = 120;
3
+ /** How long the last revealed row takes to settle; the row's CSS owns the actual timing. */
4
+ export declare const ROW_REVEAL_MS = 190;
5
+ export declare const ROW_STAGGER_MS = 24;
6
+ /** Rows beyond these start or stop without a transition: they are mostly off screen anyway. */
7
+ export declare const ROW_MOTION_LIMIT = 24;
8
+ export declare const ROW_STAGGER_LIMIT = 8;
@@ -0,0 +1,5 @@
1
+ export const ROW_CONCEAL_MS = 120;
2
+ export const ROW_REVEAL_MS = 190;
3
+ export const ROW_STAGGER_MS = 24;
4
+ export const ROW_MOTION_LIMIT = 24;
5
+ export const ROW_STAGGER_LIMIT = 8;
@@ -0,0 +1,44 @@
1
+ import type { GenericObject, RenderableType, TableKnownFieldPath } from './utils.js';
2
+ /** What a group's `label`, `summary` and `collapsed` receive. */
3
+ export interface TableGroupParams<TRow extends GenericObject = GenericObject> {
4
+ /** The value its rows share; `undefined` for rows that have none. */
5
+ value: unknown;
6
+ /** Its rows among those loaded, in display order. */
7
+ rows: TRow[];
8
+ }
9
+ /**
10
+ * Gathers rows that share a value under a header row that folds them.
11
+ *
12
+ * Groups exist only on screen: the data stays flat, so filters, pagination and bulk actions keep
13
+ * working on rows. They cover the rows that are loaded, so a remote source needs a page large
14
+ * enough to hold a whole group. A column sort applies inside each group.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * table: {
19
+ * groupBy: {
20
+ * key: 'country',
21
+ * label: ({ value }) => countryName(value),
22
+ * },
23
+ * }
24
+ * ```
25
+ */
26
+ export interface TableGroupBySchema<TRow extends GenericObject = GenericObject> {
27
+ /** The row field to group on, or a function for a value the row does not hold as a field. */
28
+ key: TableKnownFieldPath<TRow> | ((row: TRow) => unknown);
29
+ /** The header's text. Defaults to the value, or the locale's "Other" for rows with none. */
30
+ label?: (params: TableGroupParams<TRow>) => RenderableType;
31
+ /** Shown next to the label. Defaults to the number of rows. */
32
+ summary?: (params: TableGroupParams<TRow>) => RenderableType;
33
+ /** Values listed here come first, in this order. The rest follow by value, empty ones last. */
34
+ order?: readonly unknown[];
35
+ /** Whether a group starts folded. A group the reader has toggled keeps that state. */
36
+ collapsed?: boolean | ((params: TableGroupParams<TRow>) => boolean);
37
+ }
38
+ /** One group of the loaded rows, resolved once per data change. */
39
+ export interface TableGroup<TRow extends GenericObject = GenericObject> {
40
+ readonly key: string;
41
+ readonly value: unknown;
42
+ readonly rows: TRow[];
43
+ readonly rowIds: string[];
44
+ }
File without changes
@@ -3,6 +3,7 @@ export type * from './columns';
3
3
  export type * from './context';
4
4
  export type * from './data-list';
5
5
  export type * from './filters';
6
+ export type * from './groups';
6
7
  export type * from './layout';
7
8
  export type * from './query-state';
8
9
  export type * from './remote-options';
@@ -1,6 +1,7 @@
1
1
  import type { ButtonProps } from '@nuxt/ui/components/Button.vue';
2
2
  import type { LinkProps } from '@nuxt/ui/components/Link.vue';
3
3
  import type { TableColumnCollection, TableSummaryRequest, TableSummaryScope, TableSummaryValue } from './columns.js';
4
+ import type { TableGroupBySchema } from './groups.js';
4
5
  import type { TableTreeSchema } from './tree.js';
5
6
  import type { GenericObject, TableDefaultSort, TableGridSortOption, TableLayout, TableRowRenderParams, RenderableType, TableSortKey, TableTextValue } from './utils.js';
6
7
  export interface TablePersistenceOptions {
@@ -75,12 +76,16 @@ export interface TableGridSchema<TRow extends GenericObject = GenericObject, TCo
75
76
  itemSize?: number | string | (() => number | string);
76
77
  sortOptions?: TableGridSortOption<TSortKey>[];
77
78
  defaultSorting?: TableDefaultSort<TSortKey>;
79
+ /** Gathers the cards that share a value under a header that folds them. See `TableGroupBySchema`. */
80
+ groupBy?: TableGroupBySchema<TRow>;
78
81
  }
79
82
  export interface TableTableSchema<TRow extends GenericObject = GenericObject, TContext extends GenericObject = GenericObject, TPageContext extends GenericObject = GenericObject, TSortKey extends string = TableSortKey<TRow>, TData = unknown> {
80
83
  enabled?: boolean | string | (() => boolean | string);
81
84
  columns?: TableColumnCollection<TRow, TContext, TPageContext, string, TSortKey, TData>;
82
85
  /** Lets rows hold child rows that open beneath them. See `TableTreeSchema`. */
83
86
  tree?: TableTreeSchema<TRow>;
87
+ /** Gathers rows that share a value under a header row that folds them. Ignored with `tree`. See `TableGroupBySchema`. */
88
+ groupBy?: TableGroupBySchema<TRow>;
84
89
  defaultSorting?: TableDefaultSort<TSortKey>;
85
90
  selection?: boolean | 'auto';
86
91
  summaries?: TableSummariesSchema<TRow>;
@@ -0,0 +1,14 @@
1
+ import type { GenericObject, TableGroup, TableGroupBySchema } from '../types/index.js';
2
+ /**
3
+ * Brings the rows sharing a value together. Rows keep their order inside a group, so the sort the
4
+ * source applied survives in each one.
5
+ */
6
+ export declare function buildTableGroups<TRow extends GenericObject>(options: {
7
+ rows: TRow[];
8
+ groupBy: TableGroupBySchema<TRow>;
9
+ getRowId: (row: TRow, index: number) => string;
10
+ }): TableGroup<TRow>[];
11
+ export declare function tableGroupRow(group: TableGroup): GenericObject;
12
+ export declare function tableGroupOf(row: GenericObject): TableGroup | undefined;
13
+ export declare function tableGroupsStorageKey(tableKey: string): string;
14
+ export declare function parseStoredGroupKeys(raw: string | null): string[];
@@ -0,0 +1,55 @@
1
+ import { getTableRowValue } from "./rows.js";
2
+ const GROUP_ROW_KEY = "__nutTableGroup";
3
+ function groupValueOf(row, groupBy) {
4
+ const { key } = groupBy;
5
+ const value = typeof key === "function" ? key(row) : getTableRowValue({ path: String(key), row });
6
+ return value === null || value === void 0 || value === "" ? void 0 : value;
7
+ }
8
+ function compareGroupValues(order, left, right) {
9
+ const leftRank = order.indexOf(left);
10
+ const rightRank = order.indexOf(right);
11
+ if (leftRank !== -1 && rightRank !== -1) return leftRank - rightRank;
12
+ if (leftRank !== -1) return -1;
13
+ if (rightRank !== -1) return 1;
14
+ if (left === void 0) return 1;
15
+ if (right === void 0) return -1;
16
+ return String(left).localeCompare(String(right), void 0, { numeric: true });
17
+ }
18
+ export function buildTableGroups(options) {
19
+ const groups = /* @__PURE__ */ new Map();
20
+ for (const [index, row] of options.rows.entries()) {
21
+ const value = groupValueOf(row, options.groupBy);
22
+ const group = groups.get(value) ?? { rowIds: [], rows: [] };
23
+ group.rows.push(row);
24
+ group.rowIds.push(options.getRowId(row, index));
25
+ groups.set(value, group);
26
+ }
27
+ const order = options.groupBy.order ?? [];
28
+ return [...groups.entries()].sort(([left], [right]) => compareGroupValues(order, left, right)).map(([value, group]) => ({
29
+ key: `group:${JSON.stringify(value ?? null)}`,
30
+ rowIds: group.rowIds,
31
+ rows: group.rows,
32
+ value
33
+ }));
34
+ }
35
+ export function tableGroupRow(group) {
36
+ return { [GROUP_ROW_KEY]: group };
37
+ }
38
+ function isTableGroupRow(row) {
39
+ return GROUP_ROW_KEY in row;
40
+ }
41
+ export function tableGroupOf(row) {
42
+ return isTableGroupRow(row) ? row[GROUP_ROW_KEY] : void 0;
43
+ }
44
+ export function tableGroupsStorageKey(tableKey) {
45
+ return `nuxt-ui-tools:table-groups:${tableKey}`;
46
+ }
47
+ export function parseStoredGroupKeys(raw) {
48
+ if (!raw) return [];
49
+ try {
50
+ const parsed = JSON.parse(raw);
51
+ return Array.isArray(parsed) ? parsed.filter((key) => typeof key === "string") : [];
52
+ } catch {
53
+ return [];
54
+ }
55
+ }
@@ -6,6 +6,7 @@ export * from './data-list-ui.js';
6
6
  export * from './facets.js';
7
7
  export * from './filter-persistence.js';
8
8
  export * from './filters/index.js';
9
+ export * from './groups.js';
9
10
  export * from './pagination.js';
10
11
  export * from './prefetch.js';
11
12
  export * from './query-state.js';
@@ -6,6 +6,7 @@ export * from "./data-list-ui.js";
6
6
  export * from "./facets.js";
7
7
  export * from "./filter-persistence.js";
8
8
  export * from "./filters/index.js";
9
+ export * from "./groups.js";
9
10
  export * from "./pagination.js";
10
11
  export * from "./prefetch.js";
11
12
  export * from "./query-state.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-ui-tools",
3
- "version": "1.28.0",
3
+ "version": "1.29.0",
4
4
  "description": "Nuxt module and runtime toolkit for UI-heavy Nuxt applications",
5
5
  "homepage": "https://github.com/ChronicStone/nuxt-ui-tools#readme",
6
6
  "bugs": "https://github.com/ChronicStone/nuxt-ui-tools/issues",