nuxt-ui-tools 1.16.9 → 1.17.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 (43) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/i18n/locales/en.js +8 -0
  3. package/dist/runtime/i18n/locales/fr.js +8 -0
  4. package/dist/runtime/i18n/types.d.ts +8 -0
  5. package/dist/runtime/shared/types/query.d.ts +4 -3
  6. package/dist/runtime/table/components/table/table-layout.d.ts +2 -0
  7. package/dist/runtime/table/components/table/table-layout.js +1 -0
  8. package/dist/runtime/table/components/table/table-renderer.d.vue.ts +6 -6
  9. package/dist/runtime/table/components/table/table-renderer.vue +75 -11
  10. package/dist/runtime/table/components/table/table-renderer.vue.d.ts +6 -6
  11. package/dist/runtime/table/components/table/table-row.d.vue.ts +15 -2
  12. package/dist/runtime/table/components/table/table-row.vue +23 -2
  13. package/dist/runtime/table/components/table/table-row.vue.d.ts +15 -2
  14. package/dist/runtime/table/components/table/table-tree-control.d.vue.ts +19 -0
  15. package/dist/runtime/table/components/table/table-tree-control.vue +112 -0
  16. package/dist/runtime/table/components/table/table-tree-control.vue.d.ts +19 -0
  17. package/dist/runtime/table/components/table/table-tree-header.d.vue.ts +3 -0
  18. package/dist/runtime/table/components/table/table-tree-header.vue +66 -0
  19. package/dist/runtime/table/components/table/table-tree-header.vue.d.ts +3 -0
  20. package/dist/runtime/table/composables/use-table-api.d.ts +2 -0
  21. package/dist/runtime/table/composables/use-table-api.js +2 -0
  22. package/dist/runtime/table/composables/use-table-data.js +3 -2
  23. package/dist/runtime/table/composables/use-table-filter-options.js +1 -0
  24. package/dist/runtime/table/composables/use-table-internals.d.ts +48 -0
  25. package/dist/runtime/table/composables/use-table-internals.js +13 -4
  26. package/dist/runtime/table/composables/use-table-selection.d.ts +2 -0
  27. package/dist/runtime/table/composables/use-table-selection.js +15 -6
  28. package/dist/runtime/table/composables/use-table-tree.d.ts +36 -0
  29. package/dist/runtime/table/composables/use-table-tree.js +199 -0
  30. package/dist/runtime/table/types/index.d.ts +1 -0
  31. package/dist/runtime/table/types/layout.d.ts +3 -2
  32. package/dist/runtime/table/types/source.d.ts +2 -1
  33. package/dist/runtime/table/types/table-api.d.ts +6 -0
  34. package/dist/runtime/table/types/tree.d.ts +100 -0
  35. package/dist/runtime/table/types/tree.js +0 -0
  36. package/dist/runtime/table/utils/columns/defs.js +19 -2
  37. package/dist/runtime/table/utils/columns/types.d.ts +6 -1
  38. package/dist/runtime/table/utils/columns/types.js +7 -0
  39. package/dist/runtime/table/utils/index.d.ts +1 -0
  40. package/dist/runtime/table/utils/index.js +1 -0
  41. package/dist/runtime/table/utils/tree.d.ts +64 -0
  42. package/dist/runtime/table/utils/tree.js +112 -0
  43. package/package.json +1 -1
@@ -0,0 +1,66 @@
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 TableSelectionControl from "./table-selection-control.vue";
6
+ const { t } = useUiToolsLocale();
7
+ const { selection, tree } = useTableInternals();
8
+ const state = computed(() => tree.expansion.state.value);
9
+ const label = computed(
10
+ () => state.value.allExpanded ? t("table.tree.collapseAll") : t("table.tree.expandAll")
11
+ );
12
+ const checkState = computed(
13
+ () => selection.allSelected.value ? true : selection.partiallySelected.value ? "indeterminate" : false
14
+ );
15
+ function toggle() {
16
+ if (state.value.allExpanded) tree.expansion.collapseAll();
17
+ else tree.expansion.expandAll();
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <div class="nut-dl-tree-head">
23
+ <span class="nut-dl-tree__node">
24
+ <button
25
+ v-if="state.branchCount > 0"
26
+ type="button"
27
+ class="nut-dl-tree__toggle"
28
+ :aria-label="label"
29
+ :title="label"
30
+ @click="toggle"
31
+ >
32
+ <svg viewBox="0 0 24 24" width="12" height="12" fill="none" aria-hidden="true">
33
+ <path
34
+ v-if="state.allExpanded"
35
+ d="m7 20 5-5 5 5M7 4l5 5 5-5"
36
+ stroke="currentColor"
37
+ stroke-width="2.5"
38
+ stroke-linecap="round"
39
+ stroke-linejoin="round"
40
+ />
41
+ <path
42
+ v-else
43
+ d="m7 15 5 5 5-5M7 9l5-5 5 5"
44
+ stroke="currentColor"
45
+ stroke-width="2.5"
46
+ stroke-linecap="round"
47
+ stroke-linejoin="round"
48
+ />
49
+ </svg>
50
+ </button>
51
+ <span v-else class="nut-dl-tree__slot" aria-hidden="true" />
52
+ </span>
53
+ <span class="nut-dl-tree__check">
54
+ <TableSelectionControl
55
+ v-if="selection.selectionEnabled.value"
56
+ :model-value="checkState"
57
+ :ariaLabel="t('table.tree.selectAll')"
58
+ @toggle="selection.toggleAllRows({ selected: !selection.allSelected.value })"
59
+ />
60
+ </span>
61
+ </div>
62
+ </template>
63
+
64
+ <style>
65
+ .nut-dl-tree-head{align-items:center;display:flex;height:100%}
66
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -8,12 +8,14 @@ import type { useTableLayout } from './use-table-layout.js';
8
8
  import type { useTablePagination } from './use-table-pagination.js';
9
9
  import type { useTableSelection } from './use-table-selection.js';
10
10
  import type { useTableState } from './use-table-state.js';
11
+ import type { useTableTree } from './use-table-tree.js';
11
12
  export interface UseTableApiParams<TSchema = TableSchemaView> {
12
13
  runtimeSchema: ComputedRef<TableSchemaView>;
13
14
  publicSchema: ComputedRef<TSchema>;
14
15
  layout: ReturnType<typeof useTableLayout>;
15
16
  state: ReturnType<typeof useTableState>;
16
17
  selection: ReturnType<typeof useTableSelection>;
18
+ tree: ReturnType<typeof useTableTree>;
17
19
  controls: ReturnType<typeof useTableControls>;
18
20
  columns: ReturnType<typeof useTableColumns>;
19
21
  filters: ReturnType<typeof useTableFilters>;
@@ -85,6 +85,7 @@ export function useTableApi(params) {
85
85
  all() {
86
86
  reset.query();
87
87
  selection.clear();
88
+ params.tree.expansion.reset();
88
89
  },
89
90
  query() {
90
91
  const { defaultLayout } = params.runtimeSchema.value;
@@ -101,6 +102,7 @@ export function useTableApi(params) {
101
102
  };
102
103
  return {
103
104
  data,
105
+ expansion: params.tree.expansion,
104
106
  filters,
105
107
  layout: layoutApi,
106
108
  pagination,
@@ -289,7 +289,7 @@ export function useTableData(params) {
289
289
  ...result
290
290
  })),
291
291
  queries: () => {
292
- if (!isPageContextEnabled.value) {
292
+ if (!isActiveDataSuccess.value) {
293
293
  return [];
294
294
  }
295
295
  return pageContextItems.value.map(
@@ -298,7 +298,7 @@ export function useTableData(params) {
298
298
  context: contextData.value,
299
299
  rows: data.value.rows
300
300
  }),
301
- true,
301
+ isPageContextEnabled.value,
302
302
  {
303
303
  refetchOnWindowFocus: QUERY_DEFAULTS.refetchOnWindowFocus,
304
304
  staleTime: QUERY_DEFAULTS.staleTime.context
@@ -606,6 +606,7 @@ function createCursorQueryDefinition(options) {
606
606
  }
607
607
  return {
608
608
  enabled: isQueryDefinitionEnabled(firstPageDefinition),
609
+ meta: firstPageDefinition.meta,
609
610
  getNextPageParam(lastPage) {
610
611
  return isTableCursorPageResult(lastPage) ? lastPage.pageInfo.nextCursor ?? void 0 : void 0;
611
612
  },
@@ -233,6 +233,7 @@ export function useTableFilterOptions(options) {
233
233
  });
234
234
  return { result, values };
235
235
  },
236
+ meta: definition.meta,
236
237
  queryKey: ["table-filter-page-facets", ...definition.queryKey],
237
238
  refetchOnWindowFocus: QUERY_DEFAULTS.refetchOnWindowFocus,
238
239
  staleTime: QUERY_DEFAULTS.staleTime.filterOptions
@@ -593,6 +593,22 @@ declare function createTableInternals<TSchema>(options: {
593
593
  toggleSorting: (key: string) => void;
594
594
  visibleOrderedColumns: import("vue").ComputedRef<import("../utils/index.js").TableRuntimeColumn[]>;
595
595
  };
596
+ tree: {
597
+ enabled: import("vue").ComputedRef<boolean>;
598
+ expansion: import("../types/index.js").TableExpansionApi;
599
+ index: import("vue").ComputedRef<import("../types/index.js").TableTreeIndex>;
600
+ isExpanded: (rowId: string) => boolean;
601
+ isOnPath: (rowId: string) => boolean;
602
+ isSelectable: (node: import("../types/index.js").TableTreeNode) => boolean;
603
+ lightLineage: (rowId?: string) => void;
604
+ litUntil: (groupId: string) => number;
605
+ motionOf: (rowId: string) => import("../types/index.js").TableTreeMotion | undefined;
606
+ nodeOf: (rowId: string) => import("../types/index.js").TableTreeNode | undefined;
607
+ selectable: import("vue").ComputedRef<"all" | "roots">;
608
+ toggleRow: (rowId: string) => void;
609
+ visibleNodes: import("vue").ComputedRef<import("../types/index.js").TableTreeNode[]>;
610
+ visibleRows: import("vue").ComputedRef<object[]>;
611
+ };
596
612
  };
597
613
  declare function provideTableInternals(internals: TableInternals): void;
598
614
  declare function useProvideTableInternals<TSchema>(options: {
@@ -1189,6 +1205,22 @@ declare function useProvideTableInternals<TSchema>(options: {
1189
1205
  toggleSorting: (key: string) => void;
1190
1206
  visibleOrderedColumns: import("vue").ComputedRef<import("../utils/index.js").TableRuntimeColumn[]>;
1191
1207
  };
1208
+ tree: {
1209
+ enabled: import("vue").ComputedRef<boolean>;
1210
+ expansion: import("../types/index.js").TableExpansionApi;
1211
+ index: import("vue").ComputedRef<import("../types/index.js").TableTreeIndex>;
1212
+ isExpanded: (rowId: string) => boolean;
1213
+ isOnPath: (rowId: string) => boolean;
1214
+ isSelectable: (node: import("../types/index.js").TableTreeNode) => boolean;
1215
+ lightLineage: (rowId?: string) => void;
1216
+ litUntil: (groupId: string) => number;
1217
+ motionOf: (rowId: string) => import("../types/index.js").TableTreeMotion | undefined;
1218
+ nodeOf: (rowId: string) => import("../types/index.js").TableTreeNode | undefined;
1219
+ selectable: import("vue").ComputedRef<"all" | "roots">;
1220
+ toggleRow: (rowId: string) => void;
1221
+ visibleNodes: import("vue").ComputedRef<import("../types/index.js").TableTreeNode[]>;
1222
+ visibleRows: import("vue").ComputedRef<object[]>;
1223
+ };
1192
1224
  };
1193
1225
  declare function useTableInternals(): {
1194
1226
  actions: {
@@ -1782,6 +1814,22 @@ declare function useTableInternals(): {
1782
1814
  toggleSorting: (key: string) => void;
1783
1815
  visibleOrderedColumns: import("vue").ComputedRef<import("../utils/index.js").TableRuntimeColumn[]>;
1784
1816
  };
1817
+ tree: {
1818
+ enabled: import("vue").ComputedRef<boolean>;
1819
+ expansion: import("../types/index.js").TableExpansionApi;
1820
+ index: import("vue").ComputedRef<import("../types/index.js").TableTreeIndex>;
1821
+ isExpanded: (rowId: string) => boolean;
1822
+ isOnPath: (rowId: string) => boolean;
1823
+ isSelectable: (node: import("../types/index.js").TableTreeNode) => boolean;
1824
+ lightLineage: (rowId?: string) => void;
1825
+ litUntil: (groupId: string) => number;
1826
+ motionOf: (rowId: string) => import("../types/index.js").TableTreeMotion | undefined;
1827
+ nodeOf: (rowId: string) => import("../types/index.js").TableTreeNode | undefined;
1828
+ selectable: import("vue").ComputedRef<"all" | "roots">;
1829
+ toggleRow: (rowId: string) => void;
1830
+ visibleNodes: import("vue").ComputedRef<import("../types/index.js").TableTreeNode[]>;
1831
+ visibleRows: import("vue").ComputedRef<object[]>;
1832
+ };
1785
1833
  };
1786
1834
  export type TableInternals = ReturnType<typeof createTableInternals>;
1787
1835
  export { createTableInternals, provideTableInternals, useProvideTableInternals, useTableInternals };
@@ -14,6 +14,7 @@ import { useTableSelection } from "./use-table-selection.js";
14
14
  import { useTableStartup } from "./use-table-startup.js";
15
15
  import { useTableState } from "./use-table-state.js";
16
16
  import { useTableSummaries } from "./use-table-summaries.js";
17
+ import { useTableTree } from "./use-table-tree.js";
17
18
  function createTableInternals(options) {
18
19
  const publicSchema = computed(() => resolveSchemaSource({ schema: options.rawSchema }));
19
20
  const schema = computed(() => publicSchema.value);
@@ -29,10 +30,15 @@ function createTableInternals(options) {
29
30
  startup,
30
31
  state
31
32
  });
32
- const selection = useTableSelection({
33
+ const tree = useTableTree({
33
34
  queryContent,
34
35
  schema
35
36
  });
37
+ const selection = useTableSelection({
38
+ queryContent,
39
+ schema,
40
+ tree
41
+ });
36
42
  const controls = useTableControls({
37
43
  layout,
38
44
  schema
@@ -55,7 +61,8 @@ function createTableInternals(options) {
55
61
  selection,
56
62
  state,
57
63
  tableApi,
58
- tableLayout: controls.tableLayout
64
+ tableLayout: controls.tableLayout,
65
+ tree
59
66
  });
60
67
  const summaries = useTableSummaries({
61
68
  queryContent,
@@ -79,7 +86,8 @@ function createTableInternals(options) {
79
86
  queryContent,
80
87
  runtimeSchema: schema,
81
88
  selection,
82
- state
89
+ state,
90
+ tree
83
91
  });
84
92
  if (!tableApi.value) {
85
93
  throw new Error("Failed to initialize table API");
@@ -106,7 +114,8 @@ function createTableInternals(options) {
106
114
  startup,
107
115
  summaries,
108
116
  tableApi: tableApi.value,
109
- tableColumns
117
+ tableColumns,
118
+ tree
110
119
  };
111
120
  }
112
121
  const TABLE_INTERNALS_KEY = Symbol("nuxt-ui-tools.table.internals");
@@ -1,9 +1,11 @@
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 { useTableTree } from './use-table-tree.js';
4
5
  export interface UseTableSelectionParams {
5
6
  schema: ComputedRef<TableSchemaView>;
6
7
  queryContent: ReturnType<typeof useTableData>;
8
+ tree: ReturnType<typeof useTableTree>;
7
9
  }
8
10
  export declare function useTableSelection(options: UseTableSelectionParams): {
9
11
  allSelected: ComputedRef<boolean>;
@@ -1,10 +1,16 @@
1
1
  import { computed, ref, watch } from "vue";
2
- import { hasConfiguredTableActions, resolveTableRowId } from "../utils/index.js";
2
+ import { flattenTreeRows, hasConfiguredTableActions, resolveTableRowId } from "../utils/index.js";
3
3
  export function useTableSelection(options) {
4
4
  const selectedKeys = ref([]);
5
5
  const bulkScope = ref("selection");
6
6
  const lastTouchedRowId = ref(null);
7
- const pageRows = computed(() => options.queryContent.data.value.rows);
7
+ function withTreeRows(rows) {
8
+ const childrenPath = options.schema.value.table?.tree?.children;
9
+ return childrenPath && options.tree.selectable.value === "all" ? flattenTreeRows(rows, childrenPath) : rows;
10
+ }
11
+ const pageRows = computed(
12
+ () => withTreeRows(options.queryContent.data.value.rows)
13
+ );
8
14
  const selectionEnabled = computed(() => {
9
15
  const mode = options.schema.value.table?.selection ?? options.schema.value.selection?.mode ?? "auto";
10
16
  if (mode !== "auto") {
@@ -19,7 +25,7 @@ export function useTableSelection(options) {
19
25
  return options.schema.value.selection?.scope ?? "all";
20
26
  });
21
27
  const selectionRows = computed(
22
- () => selectionScope.value === "all" ? options.queryContent.selectableRows.value : pageRows.value
28
+ () => selectionScope.value === "all" ? withTreeRows(options.queryContent.selectableRows.value) : pageRows.value
23
29
  );
24
30
  const pageRowIds = computed(() => pageRows.value.map((row, index) => getRowId({ index, row })));
25
31
  const scopeRowIds = computed(
@@ -111,15 +117,18 @@ export function useTableSelection(options) {
111
117
  }
112
118
  lastTouchedRowId.value = params.rowId;
113
119
  }
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
+ );
114
123
  function getRangeRowIds(params) {
115
- const anchorIndex = scopeRowIds.value.indexOf(params.anchorRowId);
116
- const targetIndex = scopeRowIds.value.indexOf(params.targetRowId);
124
+ const anchorIndex = rangeRowIds.value.indexOf(params.anchorRowId);
125
+ const targetIndex = rangeRowIds.value.indexOf(params.targetRowId);
117
126
  if (anchorIndex === -1 || targetIndex === -1) {
118
127
  return [params.targetRowId];
119
128
  }
120
129
  const start = Math.min(anchorIndex, targetIndex);
121
130
  const end = Math.max(anchorIndex, targetIndex);
122
- return scopeRowIds.value.slice(start, end + 1);
131
+ return rangeRowIds.value.slice(start, end + 1);
123
132
  }
124
133
  watch(
125
134
  scopeRowIds,
@@ -0,0 +1,36 @@
1
+ import type { ComputedRef } from 'vue';
2
+ import type { TableExpansionApi, TableSchemaView, TableTreeIndex, TableTreeMotion, TableTreeNode } from '../types/index.js';
3
+ import type { UseTableDataReturn } from './use-table-data.js';
4
+ export interface UseTableTreeParams {
5
+ schema: ComputedRef<TableSchemaView>;
6
+ queryContent: Pick<UseTableDataReturn, 'data'>;
7
+ }
8
+ /**
9
+ * Owns tree rows: which rows are open, the flat list of rows that are on screen, and the motion
10
+ * of a branch opening or closing.
11
+ *
12
+ * Expansion is a baseline plus per-row overrides rather than a set of open ids. The baseline is
13
+ * the schema's `defaultExpanded` (or what `expandAll` / `collapseAll` last chose), so rows that
14
+ * load later, such as the next cursor page, follow it without being enumerated; an override only
15
+ * exists for a row someone actually toggled, and is dropped again if it matches the baseline.
16
+ *
17
+ * TanStack's expanded row model is deliberately not used: it materialises every descendant before
18
+ * the virtualizer sees a row. The nested source rows stay as they are, `visibleNodes` flattens
19
+ * only what is open, and that list is what the renderer virtualizes.
20
+ */
21
+ export declare function useTableTree(params: UseTableTreeParams): {
22
+ enabled: ComputedRef<boolean>;
23
+ expansion: TableExpansionApi;
24
+ index: ComputedRef<TableTreeIndex>;
25
+ isExpanded: (rowId: string) => boolean;
26
+ isOnPath: (rowId: string) => boolean;
27
+ isSelectable: (node: TableTreeNode) => boolean;
28
+ lightLineage: (rowId?: string) => void;
29
+ litUntil: (groupId: string) => number;
30
+ motionOf: (rowId: string) => TableTreeMotion | undefined;
31
+ nodeOf: (rowId: string) => TableTreeNode | undefined;
32
+ selectable: ComputedRef<"all" | "roots">;
33
+ toggleRow: (rowId: string) => void;
34
+ visibleNodes: ComputedRef<TableTreeNode[]>;
35
+ visibleRows: ComputedRef<object[]>;
36
+ };
@@ -0,0 +1,199 @@
1
+ import { usePreferredReducedMotion } from "@vueuse/core";
2
+ import { computed, onScopeDispose, reactive, ref, shallowRef } from "vue";
3
+ import { resolveTableRowId } from "../utils/rows.js";
4
+ import {
5
+ buildTableTree,
6
+ flattenVisibleTree,
7
+ getTreeLineage,
8
+ getVisibleDescendants,
9
+ isOpenByDefault
10
+ } 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
+ const EMPTY_INDEX = {
17
+ branches: [],
18
+ byId: /* @__PURE__ */ new Map(),
19
+ duplicateIds: [],
20
+ maxDepth: 0,
21
+ roots: []
22
+ };
23
+ export function useTableTree(params) {
24
+ const reducedMotion = usePreferredReducedMotion();
25
+ const config = computed(() => params.schema.value.table?.tree);
26
+ const enabled = computed(() => Boolean(config.value));
27
+ const defaultExpanded = computed(() => config.value?.defaultExpanded ?? false);
28
+ const selectable = computed(() => config.value?.selectable ?? "all");
29
+ const index = computed(() => {
30
+ const tree = config.value;
31
+ if (!tree) return EMPTY_INDEX;
32
+ const built = buildTableTree({
33
+ childrenPath: tree.children,
34
+ getRowId: (row, position) => String(resolveTableRowId({ index: position, row, rowKey: params.schema.value.rowKey })),
35
+ rows: params.queryContent.data.value.rows
36
+ });
37
+ if (built.duplicateIds.length && process.env.NODE_ENV !== "production")
38
+ console.warn(
39
+ `[nuxt-ui-tools] Tree rows must have unique ids across the whole tree; left out duplicates of: ${built.duplicateIds.slice(0, 5).join(", ")}`
40
+ );
41
+ return built;
42
+ });
43
+ const baseline = ref(null);
44
+ const overrides = reactive(/* @__PURE__ */ new Map());
45
+ const closing = reactive(/* @__PURE__ */ new Set());
46
+ function openByDefault(node) {
47
+ return isOpenByDefault(baseline.value ?? defaultExpanded.value, node.depth);
48
+ }
49
+ function isNodeOpen(node) {
50
+ return node.hasChildren && (overrides.get(node.id) ?? openByDefault(node));
51
+ }
52
+ function setNodeOpen(node, open) {
53
+ if (!node.hasChildren) return;
54
+ if (open === openByDefault(node)) overrides.delete(node.id);
55
+ else overrides.set(node.id, open);
56
+ }
57
+ function isExpanded(rowId) {
58
+ const node = index.value.byId.get(rowId);
59
+ return node !== void 0 && isNodeOpen(node) && !closing.has(rowId);
60
+ }
61
+ function nodeOf(rowId) {
62
+ return index.value.byId.get(rowId);
63
+ }
64
+ function isSelectable(node) {
65
+ return selectable.value === "all" || node.depth === 0;
66
+ }
67
+ function setExpanded(rowIds, open) {
68
+ for (const rowId of rowIds) {
69
+ const node = index.value.byId.get(rowId);
70
+ if (node) setNodeOpen(node, open);
71
+ }
72
+ }
73
+ function setAll(open) {
74
+ settle();
75
+ baseline.value = open;
76
+ overrides.clear();
77
+ }
78
+ const visibleNodes = computed(() => flattenVisibleTree(index.value.roots, isNodeOpen));
79
+ const visibleRows = computed(() => visibleNodes.value.map((node) => node.row));
80
+ const motion = shallowRef(null);
81
+ let timer;
82
+ let closingNode;
83
+ function motionOf(rowId) {
84
+ return motion.value?.get(rowId);
85
+ }
86
+ function playMotion(kind, branch) {
87
+ const rows = getVisibleDescendants(visibleNodes.value, branch, MOTION_ROW_LIMIT);
88
+ motion.value = new Map(
89
+ rows.map((node, order) => [
90
+ node.id,
91
+ { kind, order: Math.min(order, STAGGER_ROW_LIMIT) }
92
+ ])
93
+ );
94
+ return rows.length;
95
+ }
96
+ function settle() {
97
+ clearTimeout(timer);
98
+ if (closingNode) {
99
+ setNodeOpen(closingNode, false);
100
+ closing.delete(closingNode.id);
101
+ closingNode = void 0;
102
+ }
103
+ motion.value = null;
104
+ }
105
+ function toggleRow(rowId) {
106
+ const node = index.value.byId.get(rowId);
107
+ if (!node?.hasChildren) return;
108
+ if (closingNode === node) {
109
+ clearTimeout(timer);
110
+ closing.delete(rowId);
111
+ closingNode = void 0;
112
+ motion.value = null;
113
+ return;
114
+ }
115
+ settle();
116
+ const open = isNodeOpen(node);
117
+ if (reducedMotion.value === "reduce") {
118
+ setNodeOpen(node, !open);
119
+ return;
120
+ }
121
+ if (!open) {
122
+ setNodeOpen(node, true);
123
+ const revealed = playMotion("enter", node);
124
+ timer = setTimeout(
125
+ () => motion.value = null,
126
+ REVEAL_MS + Math.min(revealed, STAGGER_ROW_LIMIT) * STAGGER_MS
127
+ );
128
+ return;
129
+ }
130
+ if (!playMotion("leave", node)) {
131
+ setNodeOpen(node, false);
132
+ return;
133
+ }
134
+ closingNode = node;
135
+ closing.add(rowId);
136
+ timer = setTimeout(settle, CONCEAL_MS);
137
+ }
138
+ onScopeDispose(() => clearTimeout(timer));
139
+ const flatIndexById = computed(
140
+ () => new Map(visibleNodes.value.map((node, position) => [node.id, position]))
141
+ );
142
+ const litUntilByGroup = reactive(/* @__PURE__ */ new Map());
143
+ const onPath = reactive(/* @__PURE__ */ new Set());
144
+ function lightLineage(rowId) {
145
+ const node = rowId === void 0 ? void 0 : index.value.byId.get(rowId);
146
+ const lineage = node ? getTreeLineage(node).map((entry) => entry.id) : [];
147
+ const groups = lineage.slice(0, -1);
148
+ for (const group of litUntilByGroup.keys())
149
+ if (!groups.includes(group)) litUntilByGroup.delete(group);
150
+ for (const group of onPath) if (!groups.includes(group)) onPath.delete(group);
151
+ for (const [position, group] of groups.entries()) {
152
+ const child = lineage[position + 1];
153
+ litUntilByGroup.set(group, child === void 0 ? -1 : flatIndexById.value.get(child) ?? -1);
154
+ onPath.add(group);
155
+ }
156
+ }
157
+ function litUntil(groupId) {
158
+ return litUntilByGroup.get(groupId) ?? -1;
159
+ }
160
+ function isOnPath(rowId) {
161
+ return onPath.has(rowId);
162
+ }
163
+ const expansion = {
164
+ collapse: (rowIds) => setExpanded(rowIds, false),
165
+ collapseAll: () => setAll(false),
166
+ expand: (rowIds) => setExpanded(rowIds, true),
167
+ expandAll: () => setAll(true),
168
+ isExpanded,
169
+ reset: () => setAll(null),
170
+ state: computed(() => {
171
+ const { branches } = index.value;
172
+ const expandedKeys = branches.filter(isNodeOpen).map((node) => node.id);
173
+ return {
174
+ allExpanded: branches.length > 0 && expandedKeys.length === branches.length,
175
+ branchCount: branches.length,
176
+ enabled: enabled.value,
177
+ expandedCount: expandedKeys.length,
178
+ expandedKeys
179
+ };
180
+ }),
181
+ toggle: ({ expanded, rowId }) => setExpanded([rowId], expanded ?? !isExpanded(rowId))
182
+ };
183
+ return {
184
+ enabled,
185
+ expansion,
186
+ index,
187
+ isExpanded,
188
+ isOnPath,
189
+ isSelectable,
190
+ lightLineage,
191
+ litUntil,
192
+ motionOf,
193
+ nodeOf,
194
+ selectable,
195
+ toggleRow,
196
+ visibleNodes,
197
+ visibleRows
198
+ };
199
+ }
@@ -10,4 +10,5 @@ export type * from './schema';
10
10
  export type * from './source';
11
11
  export type * from './state';
12
12
  export type * from './table-api';
13
+ export type * from './tree';
13
14
  export type * from './utils';
@@ -1,4 +1,5 @@
1
1
  import type { TableColumnCollection, TableSummaryRequest, TableSummaryScope, TableSummaryValue } from './columns.js';
2
+ import type { TableTreeSchema } from './tree.js';
2
3
  import type { GenericObject, TableDefaultSort, TableGridSortOption, TableLayout, TableRowRenderParams, RenderableType, TableSortKey, TableTextValue } from './utils.js';
3
4
  export interface TablePersistenceOptions {
4
5
  state?: boolean;
@@ -53,8 +54,8 @@ export interface TableGridSchema<TRow extends GenericObject = GenericObject, TCo
53
54
  export interface TableTableSchema<TRow extends GenericObject = GenericObject, TContext extends GenericObject = GenericObject, TPageContext extends GenericObject = GenericObject, TSortKey extends string = TableSortKey<TRow>, TData = unknown> {
54
55
  enabled?: boolean | string | (() => boolean | string);
55
56
  columns?: TableColumnCollection<TRow, TContext, TPageContext, string, TSortKey, TData>;
56
- treeMode?: boolean;
57
- childrenKey?: TableSortKey<TRow>;
57
+ /** Lets rows hold child rows that open beneath them. See `TableTreeSchema`. */
58
+ tree?: TableTreeSchema<TRow>;
58
59
  defaultSorting?: TableDefaultSort<TSortKey>;
59
60
  selection?: boolean | 'auto';
60
61
  summaries?: TableSummariesSchema<TRow>;
@@ -1,4 +1,4 @@
1
- import type { InfiniteData, QueryFunction, QueryKey, UseQueryOptions } from '@tanstack/vue-query';
1
+ import type { InfiniteData, QueryFunction, QueryKey, QueryMeta, UseQueryOptions } from '@tanstack/vue-query';
2
2
  import type { QueryDefinition } from '../../shared/types/query.js';
3
3
  import type { TableResolvedFilterGroup } from './filters.js';
4
4
  import type { GenericObject, TableKnownFieldPath, TablePaginationState, TableSortKey, TableSortingRule, TableRowsFromSourceResult } from './utils.js';
@@ -19,6 +19,7 @@ export interface TableInfiniteQueryDefinition<TData = unknown> {
19
19
  initialPageParam: string | null;
20
20
  getNextPageParam: (lastPage: TData, allPages: TData[], lastPageParam: string | null, allPageParams: (string | null)[]) => string | null | undefined;
21
21
  enabled?: boolean;
22
+ meta?: QueryMeta;
22
23
  staleTime?: number;
23
24
  gcTime?: number;
24
25
  refetchOnWindowFocus?: boolean;
@@ -3,6 +3,7 @@ import type { useTableData } from '../composables/use-table-data.js';
3
3
  import type { useTablePagination } from '../composables/use-table-pagination.js';
4
4
  import type { TableFilterState } from './query-state.js';
5
5
  import type { TableSchemaView } from './schema.js';
6
+ import type { TableExpansionApi } from './tree.js';
6
7
  import type { ExtractTableContextData, ExtractTablePageContextData, ExtractTableRow, TableLayout, TablePaginationState, TableSortingDirection, TableSortingRule } from './utils.js';
7
8
  export interface PublicTableQueryState {
8
9
  layout: TableLayout;
@@ -143,6 +144,11 @@ export interface TableApi<TSchema = TableSchemaView> {
143
144
  }) => void;
144
145
  isSelected: (rowId: string) => boolean;
145
146
  };
147
+ /**
148
+ * Open and close tree rows. With no `table.tree` in the schema, `state.enabled` is `false` and
149
+ * nothing here has an effect.
150
+ */
151
+ expansion: TableExpansionApi;
146
152
  reset: {
147
153
  query: () => void;
148
154
  all: () => void;