nuxt-ui-tools 1.28.0 → 1.30.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 (45) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +5 -0
  3. package/dist/runtime/form/components/page/form-page-section.vue +4 -1
  4. package/dist/runtime/form/types/page.d.ts +6 -0
  5. package/dist/runtime/i18n/locales/en.js +5 -0
  6. package/dist/runtime/i18n/locales/fr.js +5 -0
  7. package/dist/runtime/i18n/types.d.ts +7 -0
  8. package/dist/runtime/shared/composables/use-sheet-navigation.d.ts +25 -0
  9. package/dist/runtime/shared/composables/use-sheet-navigation.js +41 -0
  10. package/dist/runtime/shared/index.d.ts +1 -0
  11. package/dist/runtime/shared/index.js +1 -0
  12. package/dist/runtime/table/components/grid/grid-renderer.vue +33 -8
  13. package/dist/runtime/table/components/group-header.d.vue.ts +30 -0
  14. package/dist/runtime/table/components/group-header.vue +44 -0
  15. package/dist/runtime/table/components/group-header.vue.d.ts +30 -0
  16. package/dist/runtime/table/components/table/table-group-header.d.vue.ts +9 -0
  17. package/dist/runtime/table/components/table/table-group-header.vue +56 -0
  18. package/dist/runtime/table/components/table/table-group-header.vue.d.ts +9 -0
  19. package/dist/runtime/table/components/table/table-renderer.vue +12 -7
  20. package/dist/runtime/table/components/table/table-row.d.vue.ts +4 -1
  21. package/dist/runtime/table/components/table/table-row.vue +16 -1
  22. package/dist/runtime/table/components/table/table-row.vue.d.ts +4 -1
  23. package/dist/runtime/table/components/table/table-selection-control.d.vue.ts +2 -0
  24. package/dist/runtime/table/components/table/table-selection-control.vue +3 -2
  25. package/dist/runtime/table/components/table/table-selection-control.vue.d.ts +2 -0
  26. package/dist/runtime/table/composables/use-table-grid.d.ts +7 -3
  27. package/dist/runtime/table/composables/use-table-grid.js +27 -2
  28. package/dist/runtime/table/composables/use-table-groups.d.ts +28 -0
  29. package/dist/runtime/table/composables/use-table-groups.js +133 -0
  30. package/dist/runtime/table/composables/use-table-internals.d.ts +42 -0
  31. package/dist/runtime/table/composables/use-table-internals.js +9 -0
  32. package/dist/runtime/table/composables/use-table-selection.d.ts +5 -0
  33. package/dist/runtime/table/composables/use-table-selection.js +13 -3
  34. package/dist/runtime/table/composables/use-table-tree.js +7 -5
  35. package/dist/runtime/table/constants/row-motion.d.ts +8 -0
  36. package/dist/runtime/table/constants/row-motion.js +5 -0
  37. package/dist/runtime/table/types/groups.d.ts +44 -0
  38. package/dist/runtime/table/types/groups.js +0 -0
  39. package/dist/runtime/table/types/index.d.ts +1 -0
  40. package/dist/runtime/table/types/layout.d.ts +5 -0
  41. package/dist/runtime/table/utils/groups.d.ts +14 -0
  42. package/dist/runtime/table/utils/groups.js +55 -0
  43. package/dist/runtime/table/utils/index.d.ts +1 -0
  44. package/dist/runtime/table/utils/index.js +1 -0
  45. 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.30.0",
9
9
  "builder": {
10
10
  "@nuxt/module-builder": "1.0.3",
11
11
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -47,6 +47,11 @@ function getPublicComponents(runtimeDir, options) {
47
47
  global: options.global,
48
48
  name: `${prefix}DataList${part}`
49
49
  })),
50
+ {
51
+ filePath: `${runtimeDir}/table/components/group-header.vue`,
52
+ global: options.global,
53
+ name: `${prefix}GroupHeader`
54
+ },
50
55
  // Spreadsheet
51
56
  {
52
57
  filePath: `${runtimeDir}/spreadsheet/components/spreadsheet-import.vue`,
@@ -177,7 +177,10 @@ onBeforeUnmount(() => unregister?.());
177
177
  </header>
178
178
  <div
179
179
  :class="
180
- mergeFormUiClass(`min-w-0 ${page.surfaceClasses.value.sectionBody}`, pageUi?.sectionBody)
180
+ mergeFormUiClass(
181
+ entry.section.flush ? 'min-w-0' : `min-w-0 ${page.surfaceClasses.value.sectionBody}`,
182
+ pageUi?.sectionBody
183
+ )
181
184
  "
182
185
  :style="grid.style.value"
183
186
  >
@@ -40,6 +40,12 @@ export interface FormPageSection<TContext = NonNullable<unknown>, TFields extend
40
40
  label: FormText;
41
41
  /** Supporting copy next to the title. */
42
42
  description?: FormText;
43
+ /**
44
+ * Lets the section's content reach the card's edges: the body under the heading has no padding.
45
+ * For a block that draws its own rows, such as a price grid. Only panel surfaces pad a section's
46
+ * body, so this changes nothing on a plain one.
47
+ */
48
+ flush?: boolean;
43
49
  /**
44
50
  * Iconify icon drawn before the title, so each section of a long page reads at a glance
45
51
  * (`'i-lucide-map-pin'` for an address). Style it with `ui.page.ui.sectionIcon`.
@@ -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;
@@ -0,0 +1,25 @@
1
+ import type { MaybeRefOrGetter } from 'vue';
2
+ /** The attribute a cell's input carries: its column, so up and down stay in it. */
3
+ export declare const SHEET_COLUMN_ATTRIBUTE = "data-sheet-col";
4
+ /**
5
+ * Where a key press moves in a grid of inputs, or `undefined` to leave it to the input. Up, down
6
+ * and Enter stay in the column; left and right follow the reading order, and only once the caret
7
+ * has nothing left to cross, so editing inside a value still works.
8
+ */
9
+ export declare function nextSheetInput<TInput>(params: {
10
+ inputs: readonly TInput[];
11
+ current: TInput;
12
+ key: string;
13
+ shiftKey?: boolean;
14
+ columnOf: (input: TInput) => string | null;
15
+ caret: {
16
+ atStart: boolean;
17
+ atEnd: boolean;
18
+ };
19
+ }): TInput | undefined;
20
+ /**
21
+ * Makes the inputs under `root` walk like a spreadsheet. Mark each one with
22
+ * `data-sheet-col="<column>"`: arrows and Enter move between them, and focusing one selects its
23
+ * value, so typing replaces it.
24
+ */
25
+ export declare function useSheetNavigation(root: MaybeRefOrGetter<HTMLElement | null | undefined>): void;
@@ -0,0 +1,41 @@
1
+ import { useEventListener } from "@vueuse/core";
2
+ export const SHEET_COLUMN_ATTRIBUTE = "data-sheet-col";
3
+ const SELECTOR = `input[${SHEET_COLUMN_ATTRIBUTE}]:not(:disabled)`;
4
+ export function nextSheetInput(params) {
5
+ const { inputs, current, key, caret } = params;
6
+ const across = inputs.indexOf(current);
7
+ const column = inputs.filter((input) => params.columnOf(input) === params.columnOf(current));
8
+ const along = column.indexOf(current);
9
+ if (key === "ArrowDown" || key === "Enter" && !params.shiftKey) return column[along + 1];
10
+ if (key === "ArrowUp" || key === "Enter" && params.shiftKey) return column[along - 1];
11
+ if (key === "ArrowRight" && caret.atEnd) return inputs[across + 1];
12
+ if (key === "ArrowLeft" && caret.atStart) return inputs[across - 1];
13
+ return void 0;
14
+ }
15
+ export function useSheetNavigation(root) {
16
+ useEventListener(root, "focusin", (event) => {
17
+ if (event.target instanceof HTMLInputElement && event.target.matches(SELECTOR)) {
18
+ event.target.select();
19
+ }
20
+ });
21
+ useEventListener(root, "keydown", (event) => {
22
+ const { currentTarget, target } = event;
23
+ if (!(target instanceof HTMLInputElement) || !(currentTarget instanceof HTMLElement)) return;
24
+ if (!target.matches(SELECTOR)) return;
25
+ const next = nextSheetInput({
26
+ caret: {
27
+ atEnd: target.selectionEnd === target.value.length,
28
+ atStart: target.selectionStart === 0
29
+ },
30
+ columnOf: (input) => input.getAttribute(SHEET_COLUMN_ATTRIBUTE),
31
+ current: target,
32
+ inputs: [...currentTarget.querySelectorAll(SELECTOR)],
33
+ key: event.key,
34
+ shiftKey: event.shiftKey
35
+ });
36
+ if (next) {
37
+ event.preventDefault();
38
+ next.focus();
39
+ }
40
+ });
41
+ }
@@ -1,5 +1,6 @@
1
1
  export * from './composables/use-responsive-value.js';
2
2
  export * from './composables/use-range-select.js';
3
+ export * from './composables/use-sheet-navigation.js';
3
4
  export { defineRemoteOptions } from './utils/define-remote-options.js';
4
5
  export * from './types/responsive.js';
5
6
  export * from './types/utils.js';
@@ -1,5 +1,6 @@
1
1
  export * from "./composables/use-responsive-value.js";
2
2
  export * from "./composables/use-range-select.js";
3
+ export * from "./composables/use-sheet-navigation.js";
3
4
  export { defineRemoteOptions } from "./utils/define-remote-options.js";
4
5
  export * from "./types/responsive.js";
5
6
  export * from "./types/utils.js";
@@ -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,30 @@
1
+ /**
2
+ * The fold header of a group of rows: chevron, label and count, with room for a checkbox before
3
+ * them. It holds no state, so a table's row groups and a hand-built grid read the same.
4
+ */
5
+ type __VLS_Props = {
6
+ collapsed?: boolean;
7
+ /** Names the chevron for assistive technology. */
8
+ toggleLabel: string;
9
+ };
10
+ type __VLS_Slots = {
11
+ /** Sits before the chevron, usually a checkbox. A click in it does not fold the group. */
12
+ leading?: () => unknown;
13
+ /** The group's name. */
14
+ default?: () => unknown;
15
+ /** Shown after the name, usually a count. */
16
+ summary?: () => unknown;
17
+ };
18
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
+ toggle: () => any;
20
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
21
+ onToggle?: (() => any) | undefined;
22
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
23
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
24
+ declare const _default: typeof __VLS_export;
25
+ export default _default;
26
+ type __VLS_WithSlots<T, S> = T & {
27
+ new (): {
28
+ $slots: S;
29
+ };
30
+ };
@@ -0,0 +1,44 @@
1
+ <script setup>
2
+ defineProps({
3
+ collapsed: { type: Boolean, required: false },
4
+ toggleLabel: { type: String, required: true }
5
+ });
6
+ defineSlots();
7
+ const emit = defineEmits(["toggle"]);
8
+ </script>
9
+
10
+ <template>
11
+ <div
12
+ class="nut-dl-group"
13
+ :data-collapsed="collapsed || void 0"
14
+ :data-selectable="$slots.leading ? true : void 0"
15
+ @click="emit('toggle')"
16
+ >
17
+ <span v-if="$slots.leading" class="nut-dl-group__check"><slot name="leading" /></span>
18
+ <button
19
+ type="button"
20
+ class="nut-dl-group__toggle"
21
+ :aria-expanded="!collapsed"
22
+ :aria-label="toggleLabel"
23
+ @click.stop="emit('toggle')"
24
+ >
25
+ <svg
26
+ viewBox="0 0 16 16"
27
+ fill="none"
28
+ stroke="currentColor"
29
+ stroke-width="1.75"
30
+ stroke-linecap="round"
31
+ stroke-linejoin="round"
32
+ aria-hidden="true"
33
+ >
34
+ <path d="m4 6 4 4 4-4" />
35
+ </svg>
36
+ </button>
37
+ <span class="nut-dl-group__label"><slot /></span>
38
+ <span v-if="$slots.summary" class="nut-dl-group__summary"><slot name="summary" /></span>
39
+ </div>
40
+ </template>
41
+
42
+ <style>
43
+ .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:var(--nut-group-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:var(--nut-group-toggle,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}@media (prefers-reduced-motion:reduce){.nut-dl-group__toggle svg{transition:none}}
44
+ </style>
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The fold header of a group of rows: chevron, label and count, with room for a checkbox before
3
+ * them. It holds no state, so a table's row groups and a hand-built grid read the same.
4
+ */
5
+ type __VLS_Props = {
6
+ collapsed?: boolean;
7
+ /** Names the chevron for assistive technology. */
8
+ toggleLabel: string;
9
+ };
10
+ type __VLS_Slots = {
11
+ /** Sits before the chevron, usually a checkbox. A click in it does not fold the group. */
12
+ leading?: () => unknown;
13
+ /** The group's name. */
14
+ default?: () => unknown;
15
+ /** Shown after the name, usually a count. */
16
+ summary?: () => unknown;
17
+ };
18
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
+ toggle: () => any;
20
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
21
+ onToggle?: (() => any) | undefined;
22
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
23
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
24
+ declare const _default: typeof __VLS_export;
25
+ export default _default;
26
+ type __VLS_WithSlots<T, S> = T & {
27
+ new (): {
28
+ $slots: S;
29
+ };
30
+ };
@@ -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,56 @@
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 GroupHeader from "../group-header.vue";
6
+ import TableCell from "./table-cell";
7
+ import TableSelectionControl from "./table-selection-control.vue";
8
+ const props = defineProps({
9
+ group: { type: Object, required: true },
10
+ selectable: { type: Boolean, required: false }
11
+ });
12
+ const { groups, selection } = useTableInternals();
13
+ const { t } = useUiToolsLocale();
14
+ const collapsed = computed(() => groups.isCollapsed(props.group));
15
+ const params = computed(() => ({ rows: props.group.rows, value: props.group.value }));
16
+ const checked = computed(() => selection.getRowsSelectionState({ rowIds: props.group.rowIds }));
17
+ const showCheckbox = computed(() => props.selectable && selection.selectionEnabled.value);
18
+ function renderLabel() {
19
+ const label = groups.config.value?.label;
20
+ if (label) return label(params.value);
21
+ return props.group.value === void 0 ? t("table.groups.empty") : String(props.group.value);
22
+ }
23
+ function renderSummary() {
24
+ const summary = groups.config.value?.summary;
25
+ return summary ? summary(params.value) : props.group.rows.length;
26
+ }
27
+ function toggleSelection() {
28
+ const { rowIds } = props.group;
29
+ if (checked.value === true) selection.unselectRows({ rowIds });
30
+ else selection.selectRows({ rowIds });
31
+ }
32
+ </script>
33
+
34
+ <template>
35
+ <GroupHeader
36
+ :collapsed="collapsed"
37
+ :toggle-label="t('table.groups.toggle')"
38
+ @toggle="groups.toggle(group)"
39
+ >
40
+ <template v-if="showCheckbox" #leading>
41
+ <TableSelectionControl
42
+ :model-value="checked"
43
+ :label="t('table.groups.select')"
44
+ @toggle="toggleSelection"
45
+ />
46
+ </template>
47
+ <TableCell :index="0" :render="renderLabel" :row="group" />
48
+ <template #summary>
49
+ <TableCell :index="0" :render="renderSummary" :row="group" />
50
+ </template>
51
+ </GroupHeader>
52
+ </template>
53
+
54
+ <style>
55
+ .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)}
56
+ </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
  };