@cosmicdrift/kumiko-renderer 0.388.0 → 0.389.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 (161) hide show
  1. package/dist/app/base-path-context.d.ts +9 -0
  2. package/dist/app/base-path-context.js +13 -0
  3. package/dist/app/base-path.d.ts +3 -0
  4. package/dist/app/base-path.js +27 -0
  5. package/dist/app/config-edit-body.d.ts +1 -1
  6. package/dist/app/config-edit-body.js +15 -6
  7. package/dist/app/config-edit-without-secret-fields.d.ts +2 -0
  8. package/dist/app/config-edit-without-secret-fields.js +28 -0
  9. package/dist/app/entity-list-facet-specs.d.ts +3 -0
  10. package/dist/app/entity-list-facet-specs.js +65 -0
  11. package/dist/app/entity-list-screen.js +74 -206
  12. package/dist/app/kumiko-screen.js +6 -14
  13. package/dist/app/list-bulk-actions.d.ts +18 -0
  14. package/dist/app/list-bulk-actions.js +54 -0
  15. package/dist/app/locked-screen-header.d.ts +9 -0
  16. package/dist/app/locked-screen-header.js +33 -0
  17. package/dist/app/projection-list-body.js +51 -46
  18. package/dist/app/row-actions.d.ts +3 -0
  19. package/dist/app/row-actions.js +10 -5
  20. package/dist/app/screen-shared.js +9 -2
  21. package/dist/app/use-entity-range-facets.d.ts +24 -0
  22. package/dist/app/use-entity-range-facets.js +64 -0
  23. package/dist/app/use-projection-date-range.d.ts +12 -0
  24. package/dist/app/use-projection-date-range.js +21 -0
  25. package/dist/app/use-screen-list-rows.d.ts +37 -0
  26. package/dist/app/use-screen-list-rows.js +104 -0
  27. package/dist/app/use-screen-list-table.d.ts +24 -0
  28. package/dist/app/use-screen-list-table.js +46 -0
  29. package/dist/components/list-group-page-bridges.d.ts +3 -0
  30. package/dist/components/list-group-page-bridges.js +31 -0
  31. package/dist/components/list-state-store.d.ts +16 -0
  32. package/dist/components/list-state-store.js +31 -0
  33. package/dist/components/related-list-section.js +37 -89
  34. package/dist/components/render-field.js +6 -5
  35. package/dist/components/render-list-toolbar-action.d.ts +27 -0
  36. package/dist/components/render-list-toolbar-action.js +40 -0
  37. package/dist/components/render-list.d.ts +13 -29
  38. package/dist/components/render-list.js +42 -50
  39. package/dist/current-user.d.ts +2 -0
  40. package/dist/data-table-aggregate-types.d.ts +47 -0
  41. package/dist/data-table-aggregate-types.js +1 -0
  42. package/dist/data-table-bulk-types.d.ts +27 -0
  43. package/dist/data-table-bulk-types.js +1 -0
  44. package/dist/data-table-facet-types.d.ts +36 -0
  45. package/dist/data-table-facet-types.js +1 -0
  46. package/dist/data-table-view-types.d.ts +73 -0
  47. package/dist/data-table-view-types.js +1 -0
  48. package/dist/headless/view-model/edit.js +3 -1
  49. package/dist/headless/view-model/types.d.ts +4 -2
  50. package/dist/hooks/list-column-layout.d.ts +19 -0
  51. package/dist/hooks/list-column-layout.js +36 -0
  52. package/dist/hooks/list-group-pages.d.ts +26 -0
  53. package/dist/hooks/list-group-pages.js +37 -0
  54. package/dist/hooks/list-reset.d.ts +8 -0
  55. package/dist/hooks/list-reset.js +15 -0
  56. package/dist/hooks/list-server-groups.d.ts +39 -0
  57. package/dist/hooks/list-server-groups.js +84 -0
  58. package/dist/hooks/list-state-schema.d.ts +28 -0
  59. package/dist/hooks/list-state-schema.js +76 -0
  60. package/dist/hooks/use-bulk-run.d.ts +41 -0
  61. package/dist/hooks/use-bulk-run.js +65 -0
  62. package/dist/hooks/use-group-load-state.d.ts +11 -0
  63. package/dist/hooks/use-group-load-state.js +31 -0
  64. package/dist/hooks/use-list-aggregates.d.ts +38 -0
  65. package/dist/hooks/use-list-aggregates.js +107 -0
  66. package/dist/hooks/use-list-table.d.ts +38 -0
  67. package/dist/hooks/use-list-table.js +91 -0
  68. package/dist/hooks/use-list-view-state.d.ts +43 -0
  69. package/dist/hooks/use-list-view-state.js +161 -0
  70. package/dist/hooks/use-related-list-rows.d.ts +27 -0
  71. package/dist/hooks/use-related-list-rows.js +81 -0
  72. package/dist/hooks/use-related-list-state.d.ts +15 -0
  73. package/dist/hooks/use-related-list-state.js +31 -0
  74. package/dist/hooks/use-remembered-list-params.d.ts +13 -0
  75. package/dist/hooks/use-remembered-list-params.js +53 -0
  76. package/dist/hooks/use-remembered-local-list-state.d.ts +26 -0
  77. package/dist/hooks/use-remembered-local-list-state.js +63 -0
  78. package/dist/i18n-defaults.js +53 -0
  79. package/dist/index.d.ts +3 -1
  80. package/dist/index.js +2 -0
  81. package/dist/primitives.d.ts +11 -47
  82. package/dist/web/app/create-app.d.ts +5 -0
  83. package/dist/web/app/create-app.js +7 -4
  84. package/dist/web/app/history-leave-guard.d.ts +6 -0
  85. package/dist/web/app/history-leave-guard.js +115 -0
  86. package/dist/web/app/list-state-store-boot.d.ts +6 -0
  87. package/dist/web/app/list-state-store-boot.js +15 -0
  88. package/dist/web/app/nav.js +7 -54
  89. package/dist/web/app/session-list-state-store.d.ts +15 -0
  90. package/dist/web/app/session-list-state-store.js +117 -0
  91. package/dist/web/index.d.ts +1 -0
  92. package/dist/web/index.js +1 -0
  93. package/dist/web/lib/export-xlsx-request.d.ts +11 -0
  94. package/dist/web/lib/export-xlsx-request.js +46 -0
  95. package/dist/web/primitives/data-table/actions-column.d.ts +16 -0
  96. package/dist/web/primitives/data-table/actions-column.js +34 -0
  97. package/dist/web/primitives/data-table/bulk-selection-bar.d.ts +20 -0
  98. package/dist/web/primitives/data-table/bulk-selection-bar.js +37 -0
  99. package/dist/web/primitives/data-table/card-sort-select.d.ts +14 -0
  100. package/dist/web/primitives/data-table/card-sort-select.js +40 -0
  101. package/dist/web/primitives/data-table/column-layout-mode.d.ts +5 -0
  102. package/dist/web/primitives/data-table/column-layout-mode.js +10 -0
  103. package/dist/web/primitives/data-table/column-picker-section.d.ts +7 -0
  104. package/dist/web/primitives/data-table/column-picker-section.js +35 -0
  105. package/dist/web/primitives/data-table/column-resize-handle.d.ts +14 -0
  106. package/dist/web/primitives/data-table/column-resize-handle.js +46 -0
  107. package/dist/web/primitives/data-table/density.d.ts +14 -0
  108. package/dist/web/primitives/data-table/density.js +20 -0
  109. package/dist/web/primitives/data-table/export-button.d.ts +5 -0
  110. package/dist/web/primitives/data-table/export-button.js +37 -0
  111. package/dist/web/primitives/data-table/facet-cluster.d.ts +8 -0
  112. package/dist/web/primitives/data-table/facet-cluster.js +37 -0
  113. package/dist/web/primitives/data-table/group-band.d.ts +28 -0
  114. package/dist/web/primitives/data-table/group-band.js +53 -0
  115. package/dist/web/primitives/data-table/measure-value.d.ts +10 -0
  116. package/dist/web/primitives/data-table/measure-value.js +28 -0
  117. package/dist/web/primitives/data-table/pin-section.d.ts +8 -0
  118. package/dist/web/primitives/data-table/pin-section.js +20 -0
  119. package/dist/web/primitives/data-table/reset-all-button.d.ts +4 -0
  120. package/dist/web/primitives/data-table/reset-all-button.js +8 -0
  121. package/dist/web/primitives/data-table/server-group-header.d.ts +11 -0
  122. package/dist/web/primitives/data-table/server-group-header.js +51 -0
  123. package/dist/web/primitives/data-table/sortable-header.d.ts +17 -0
  124. package/dist/web/primitives/data-table/sortable-header.js +41 -0
  125. package/dist/web/primitives/data-table/table-chrome.d.ts +13 -0
  126. package/dist/web/primitives/data-table/table-chrome.js +30 -0
  127. package/dist/web/primitives/data-table/table-footer.d.ts +7 -0
  128. package/dist/web/primitives/data-table/table-footer.js +20 -0
  129. package/dist/web/primitives/data-table/toolbar-end-cluster.d.ts +10 -0
  130. package/dist/web/primitives/data-table/toolbar-end-cluster.js +13 -0
  131. package/dist/web/primitives/data-table/totals-footer.d.ts +24 -0
  132. package/dist/web/primitives/data-table/totals-footer.js +48 -0
  133. package/dist/web/primitives/data-table/use-bulk-selection.d.ts +21 -0
  134. package/dist/web/primitives/data-table/use-bulk-selection.js +66 -0
  135. package/dist/web/primitives/data-table/use-column-pins.d.ts +32 -0
  136. package/dist/web/primitives/data-table/use-column-pins.js +117 -0
  137. package/dist/web/primitives/data-table/use-column-sizing.d.ts +22 -0
  138. package/dist/web/primitives/data-table/use-column-sizing.js +44 -0
  139. package/dist/web/primitives/data-table/use-measured-column-widths.d.ts +15 -0
  140. package/dist/web/primitives/data-table/use-measured-column-widths.js +83 -0
  141. package/dist/web/primitives/data-table/use-row-keyboard-navigation.d.ts +22 -0
  142. package/dist/web/primitives/data-table/use-row-keyboard-navigation.js +65 -0
  143. package/dist/web/primitives/data-table/use-row-selection.d.ts +14 -0
  144. package/dist/web/primitives/data-table/use-row-selection.js +48 -0
  145. package/dist/web/primitives/data-table/use-view-columns.d.ts +6 -0
  146. package/dist/web/primitives/data-table/use-view-columns.js +17 -0
  147. package/dist/web/primitives/data-table/view-menu.d.ts +8 -0
  148. package/dist/web/primitives/data-table/view-menu.js +45 -0
  149. package/dist/web/primitives/data-table-cell.d.ts +0 -22
  150. package/dist/web/primitives/data-table-cell.js +1 -58
  151. package/dist/web/primitives/data-table.d.ts +1 -1
  152. package/dist/web/primitives/data-table.js +122 -144
  153. package/dist/web/primitives/file-value-list.d.ts +1 -1
  154. package/dist/web/primitives/file-value-list.js +5 -5
  155. package/dist/web/primitives/number-range-facet.d.ts +7 -0
  156. package/dist/web/primitives/number-range-facet.js +33 -0
  157. package/dist/web/widgets/bounded-load-cache.d.ts +5 -1
  158. package/dist/web/widgets/bounded-load-cache.js +48 -26
  159. package/dist/web/widgets/pdf-preview.js +9 -4
  160. package/package.json +5 -5
  161. package/src/changes.json +109 -0
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ChevronDown, ChevronRight } from "lucide-react";
3
+ import { useEffect, useRef } from "react";
4
+ import { useOptionalLocale, useOptionalTranslation } from "../../../index.js";
5
+ import { DataTableCell } from "../data-table-cell.js";
6
+ import { MeasureValue, measureLabel } from "./measure-value.js";
7
+ const NO_ROW = {};
8
+ // Reports the header once it scrolls near the viewport; without an observer
9
+ // (tests, old engines) every header counts as visible right away.
10
+ function useReportWhenVisible(onVisible) {
11
+ const ref = useRef(null);
12
+ const onVisibleRef = useRef(onVisible);
13
+ onVisibleRef.current = onVisible;
14
+ useEffect(() => {
15
+ const node = ref.current;
16
+ if (node === null)
17
+ return;
18
+ if (typeof IntersectionObserver === "undefined") {
19
+ onVisibleRef.current();
20
+ return;
21
+ }
22
+ const observer = new IntersectionObserver((entries) => {
23
+ if (!entries.some((entry) => entry.isIntersecting))
24
+ return;
25
+ observer.disconnect();
26
+ onVisibleRef.current();
27
+ }, { rootMargin: "200px" });
28
+ observer.observe(node);
29
+ return () => observer.disconnect();
30
+ }, []);
31
+ return ref;
32
+ }
33
+ function GroupValueLabel({ group, column, }) {
34
+ const translate = useOptionalTranslation();
35
+ const locale = useOptionalLocale();
36
+ if (group.label !== undefined)
37
+ return group.label;
38
+ if (group.value === null || group.value === "") {
39
+ return translate?.("kumiko.list.group.empty") ?? "(empty)";
40
+ }
41
+ return (_jsx(DataTableCell, { value: group.value, row: NO_ROW, field: column?.field ?? "", type: column?.type ?? "text", translate: translate, locale: locale, ...(column?.optionLabels !== undefined && { optionLabels: column.optionLabels }), ...(column?.optionTones !== undefined && { optionTones: column.optionTones }) }));
42
+ }
43
+ /** Header of a server group: toggle, value, server row count and the group's measures. */
44
+ export function ServerGroupHeader({ group, groupColumn, columnOf, onToggle, onVisible, }) {
45
+ const translate = useOptionalTranslation();
46
+ const reportRef = useReportWhenVisible(() => onVisible(group.key));
47
+ return (_jsxs("div", { ref: reportRef, className: "flex flex-wrap items-center gap-x-4 gap-y-1 pe-4 text-xs text-foreground-secondary", children: [_jsxs("button", { type: "button", "aria-expanded": !group.collapsed, "data-testid": `row-group-${group.key}-toggle`, onClick: () => onToggle(group.key), className: "flex items-center gap-2 py-2 ps-4 text-left font-medium", children: [group.collapsed ? (_jsx(ChevronRight, { className: "size-3.5", "aria-hidden": "true" })) : (_jsx(ChevronDown, { className: "size-3.5", "aria-hidden": "true" })), _jsx(GroupValueLabel, { group: group, column: groupColumn }), _jsx("span", { "data-testid": `row-group-${group.key}-count`, className: "rounded-full bg-muted px-2 py-0.5 tabular-nums text-muted-foreground", children: group.count.toLocaleString() })] }), group.measures.map((measure) => {
48
+ const column = columnOf(measure.field);
49
+ return (_jsxs("span", { "data-testid": `row-group-${group.key}-${measure.kind}-${measure.field}`, className: "inline-flex items-center gap-1.5 tabular-nums", children: [_jsx("span", { className: "text-muted-foreground", children: measureLabel(measure, column, translate) }), _jsx(MeasureValue, { measure: measure, column: column })] }, `${measure.kind}-${measure.field}`));
50
+ })] }));
51
+ }
@@ -0,0 +1,17 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import type { DataTableSort } from "../../../index.ts";
3
+ export declare function SortableHeader({ field, label, sortable, highlighted, numeric, fitContent, className, sort, onSortChange, style, columnField, trailing, }: {
4
+ readonly field: string;
5
+ readonly label: string;
6
+ readonly sortable: boolean;
7
+ readonly highlighted?: boolean;
8
+ readonly numeric?: boolean;
9
+ readonly fitContent?: boolean;
10
+ readonly className?: string;
11
+ readonly sort?: DataTableSort;
12
+ readonly onSortChange?: (next: DataTableSort | null) => void;
13
+ readonly style?: CSSProperties;
14
+ readonly columnField?: string;
15
+ /** Rendered inside the th after the label, e.g. a resize handle. */
16
+ readonly trailing?: ReactNode;
17
+ }): ReactNode;
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react";
3
+ import { cn } from "../../lib/cn.js";
4
+ import { TableHead } from "../../ui/table.js";
5
+ import { FIT_CONTENT_CELL_CLASS } from "../data-table-cell.js";
6
+ // SortableHeader: renders the th header per column, with or without
7
+ // click-sort. Three paths:
8
+ // (a) sortable=false OR no onSortChange -> plain label, no cursor
9
+ // interaction (DataTable purely as a view without sort wiring).
10
+ // (b) sortable=true + onSortChange -> header is a button, click
11
+ // cycles asc -> desc -> null. aria-sort mirrors the state.
12
+ // (c) sortable=true in the schema but no onSortChange prop -> still
13
+ // label-only, but data-sortable=true stays so tests that know the
14
+ // schema view keep working.
15
+ export function SortableHeader({ field, label, sortable, highlighted, numeric, fitContent, className, sort, onSortChange, style, columnField, trailing, }) {
16
+ const active = sort?.field === field ? sort : undefined;
17
+ const ariaSort = active?.dir === "asc" ? "ascending" : active?.dir === "desc" ? "descending" : "none";
18
+ const cellExtras = {
19
+ ...(style !== undefined && { style }),
20
+ ...(columnField !== undefined && { "data-column-field": columnField }),
21
+ };
22
+ if (!sortable || onSortChange === undefined) {
23
+ return (_jsxs(TableHead, { "data-testid": `column-${field}`, "data-sortable": sortable === true ? true : undefined, "data-highlighted": highlighted === true ? "true" : undefined, className: cn("font-medium whitespace-normal text-muted-foreground", numeric === true && "text-right", fitContent === true && FIT_CONTENT_CELL_CLASS, highlighted === true && "bg-accent/40", trailing !== undefined && "group/header relative", className), ...cellExtras, children: [label, trailing] }));
24
+ }
25
+ const Icon = active?.dir === "asc" ? ArrowUp : active?.dir === "desc" ? ArrowDown : ArrowUpDown;
26
+ const next = nextSortState(active?.dir, field);
27
+ return (_jsxs(TableHead, { "data-testid": `column-${field}`, "data-sortable": "true", "data-highlighted": highlighted === true ? "true" : undefined, "aria-sort": ariaSort, className: cn(active !== undefined
28
+ ? "font-semibold text-foreground"
29
+ : "font-medium text-muted-foreground", "whitespace-normal", numeric === true && "text-right", fitContent === true && FIT_CONTENT_CELL_CLASS, highlighted === true && "bg-accent/40", trailing !== undefined && "group/header relative", className), ...cellExtras, children: [_jsxs("button", { type: "button", onClick: () => onSortChange(next), className: cn("group/sort inline-flex h-8 items-center gap-1.5 rounded-sm px-2 -mx-2 text-sm [font-weight:inherit]", "hover:bg-accent hover:text-accent-foreground", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", active !== undefined && "text-foreground"), children: [_jsx("span", { children: label }), _jsx(Icon, { "data-sort-icon": active === undefined ? "idle" : "active", className: cn("size-3.5", active === undefined &&
30
+ "opacity-0 group-hover/sort:opacity-40 group-focus-visible/sort:opacity-40"), "aria-hidden": "true" })] }), trailing] }));
31
+ }
32
+ // 3-state toggle: no sort -> asc -> desc -> no sort. Idiomatic for
33
+ // power-user lists where "I want the server default order back" is a
34
+ // real action (instead of toggling asc<->desc forever).
35
+ function nextSortState(current, field) {
36
+ if (current === undefined)
37
+ return { field, dir: "asc" };
38
+ if (current === "asc")
39
+ return { field, dir: "desc" };
40
+ return null;
41
+ }
@@ -0,0 +1,13 @@
1
+ import type { ReactNode, TableHTMLAttributes } from "react";
2
+ export declare function FlushTable({ className, ...props }: TableHTMLAttributes<HTMLTableElement>): ReactNode;
3
+ export declare const FILL_TABLE_CLASS: string;
4
+ export declare const EXPAND_TOGGLE_CELL_CLASS = "w-10 px-1! py-0!";
5
+ export declare const EXPANSION_CELL_CLASS = "whitespace-normal bg-muted p-0! align-top md:[&_td:last-child]:bg-muted";
6
+ export declare const EXPANSION_CONTENT_CLASS = "pt-1 pr-4 pb-3.5 pl-[52px]";
7
+ export declare function RowExpandToggle({ expanded, controlsId, label, onToggle, testId, }: {
8
+ readonly expanded: boolean;
9
+ readonly controlsId: string;
10
+ readonly label: string;
11
+ readonly onToggle: () => void;
12
+ readonly testId: string;
13
+ }): ReactNode;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { ChevronRight } from "lucide-react";
3
+ import { cn } from "../../lib/cn.js";
4
+ import { Button as UiButton } from "../../ui/button.js";
5
+ // The vendored Table wraps <table> in an `overflow-x-auto` container, which
6
+ // would become the scrollport of a sticky header. A scrollBody table scrolls
7
+ // in its parent instead, so it renders the bare <table>.
8
+ export function FlushTable({ className, ...props }) {
9
+ return (_jsx("table", { "data-slot": "table", className: cn("w-full caption-bottom text-sm", className), ...props }));
10
+ }
11
+ // Header cells stick to the scroll surface; the bottom rule is an inset
12
+ // shadow because a border on a sticky cell scrolls away under border-collapse.
13
+ // Column weight (medium vs. semibold for the sorted one) lives on the th itself
14
+ // in SortableHeader: a `[&_th]:font-*` rule here would outrank it.
15
+ // Child selectors only: an expanded row nests a whole table inside a cell, and
16
+ // descendant selectors would make its header sticky and re-pad its cells.
17
+ export const FILL_TABLE_CLASS = cn("[&>thead>tr>th]:sticky [&>thead>tr>th]:top-0 [&>thead>tr>th]:z-10 [&>thead>tr>th]:h-9 [&>thead>tr>th]:bg-muted [&>thead>tr>th]:px-2 [&>thead>tr>th]:text-[13px]", "[&>thead>tr>th]:shadow-[inset_0_-1px_0_var(--color-border)] [&>thead>tr>th_button]:text-[13px] [&>tbody>tr>td]:px-2 [&>tbody>tr>td]:py-0", "[&>*>tr>:first-child]:pl-6 [&>*>tr>:last-child]:pr-6");
18
+ // `!`: beats the first/last-child padding of FILL_TABLE_CLASS, which would
19
+ // otherwise widen the narrow toggle column / pad the full-width expansion cell.
20
+ // The nested table's sticky actions cell (last td) gets the muted surface
21
+ // instead of its own md:bg-background, so it blends into the area.
22
+ export const EXPAND_TOGGLE_CELL_CLASS = "w-10 px-1! py-0!";
23
+ export const EXPANSION_CELL_CLASS = "whitespace-normal bg-muted p-0! align-top md:[&_td:last-child]:bg-muted";
24
+ export const EXPANSION_CONTENT_CLASS = "pt-1 pr-4 pb-3.5 pl-[52px]";
25
+ export function RowExpandToggle({ expanded, controlsId, label, onToggle, testId, }) {
26
+ return (_jsx(UiButton, { type: "button", variant: "ghost", size: "icon", className: "size-8 shrink-0 max-md:size-11", "aria-expanded": expanded, ...(expanded && { "aria-controls": controlsId }), "aria-label": label, "data-testid": testId, onClick: (e) => {
27
+ e.stopPropagation();
28
+ onToggle();
29
+ }, onKeyDown: (e) => e.stopPropagation(), children: _jsx(ChevronRight, { className: cn("size-4 transition-transform", expanded && "rotate-90"), "aria-hidden": "true" }) }));
30
+ }
@@ -0,0 +1,7 @@
1
+ import type { PointerEvent, ReactNode } from "react";
2
+ export declare function DataTableFooter({ count, itemNoun, testId, }: {
3
+ readonly count: number;
4
+ readonly itemNoun?: (count: number) => string;
5
+ readonly testId: string;
6
+ }): ReactNode;
7
+ export declare function showFullTextOnHover(event: PointerEvent<HTMLElement>): void;
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useOptionalTranslation } from "../../../index.js";
3
+ import { TABLE_FOOTER_BAR_CLASS } from "../pager.js";
4
+ export function DataTableFooter({ count, itemNoun, testId, }) {
5
+ const t = useOptionalTranslation();
6
+ const noun = itemNoun?.(count);
7
+ const label = noun !== undefined
8
+ ? (t?.("kumiko.list.count.noun", { count: count.toLocaleString(), noun }) ??
9
+ `${count.toLocaleString()} ${noun}`)
10
+ : count === 1
11
+ ? (t?.("kumiko.list.count.one") ?? "1 entry")
12
+ : (t?.("kumiko.list.count.other", { count: count.toLocaleString() }) ??
13
+ `${count.toLocaleString()} entries`);
14
+ return (_jsx("div", { "data-testid": testId, className: TABLE_FOOTER_BAR_CLASS, children: _jsx("span", { "data-testid": `${testId}-count`, children: label }) }));
15
+ }
16
+ // Cell content is a ReactNode, so the tooltip text is read from the rendered DOM.
17
+ export function showFullTextOnHover(event) {
18
+ const element = event.currentTarget;
19
+ element.title = element.scrollWidth > element.clientWidth ? (element.textContent ?? "") : "";
20
+ }
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from "react";
2
+ import type { DataTableExport, DataTableViewOptions } from "../../../data-table-view-types.ts";
3
+ export declare function toolbarEndHasContent(view: DataTableViewOptions | undefined, exportList: DataTableExport | undefined, toolbarEnd: ReactNode): boolean;
4
+ /** Right-hand toolbar group: view menu, export button, then the caller's own end content. */
5
+ export declare function ToolbarEndCluster({ view, columnLabels, exportList, toolbarEnd, }: {
6
+ readonly view: DataTableViewOptions | undefined;
7
+ readonly columnLabels: ReadonlyMap<string, string>;
8
+ readonly exportList: DataTableExport | undefined;
9
+ readonly toolbarEnd: ReactNode;
10
+ }): ReactNode;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ExportButton } from "./export-button.js";
3
+ import { ViewMenu, viewHasMenu } from "./view-menu.js";
4
+ export function toolbarEndHasContent(view, exportList, toolbarEnd) {
5
+ return toolbarEnd !== undefined || viewHasMenu(view) || exportList !== undefined;
6
+ }
7
+ /** Right-hand toolbar group: view menu, export button, then the caller's own end content. */
8
+ export function ToolbarEndCluster({ view, columnLabels, exportList, toolbarEnd, }) {
9
+ if (!toolbarEndHasContent(view, exportList, toolbarEnd))
10
+ return null;
11
+ const showsViewMenu = viewHasMenu(view);
12
+ return (_jsxs("div", { className: "flex flex-wrap items-center gap-2 ml-auto", children: [showsViewMenu && _jsx(ViewMenu, { view: view, columnLabels: columnLabels }), exportList !== undefined && _jsx(ExportButton, { source: exportList }), toolbarEnd] }));
13
+ }
@@ -0,0 +1,24 @@
1
+ import type { ReactNode } from "react";
2
+ import type { DataTableTotals } from "../../../data-table-aggregate-types.ts";
3
+ import type { ListColumnViewModel } from "../../../headless/index.ts";
4
+ import { type PinnedCellProps } from "./use-column-pins.ts";
5
+ /** Footer row under the table body, one cell per visible column. */
6
+ export declare function TotalsFooterRow({ totals, columns, cellPin, leadingCells, actionsCell, cellClassName, }: {
7
+ readonly totals: DataTableTotals;
8
+ readonly columns: readonly ListColumnViewModel[];
9
+ readonly cellPin: (field: string) => PinnedCellProps;
10
+ readonly leadingCells: {
11
+ readonly select: boolean;
12
+ readonly expand: boolean;
13
+ };
14
+ /** Present when the table has an actions column; the cell then sticks like that column. */
15
+ readonly actionsCell: {
16
+ readonly fillsHeight: boolean;
17
+ } | undefined;
18
+ readonly cellClassName?: string;
19
+ }): ReactNode;
20
+ /** Card layout: the totals as a labelled list under the cards. */
21
+ export declare function TotalsCard({ totals, columnOf, }: {
22
+ readonly totals: DataTableTotals;
23
+ readonly columnOf: (field: string) => ListColumnViewModel | undefined;
24
+ }): ReactNode;
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useOptionalTranslation } from "../../../index.js";
3
+ import { cn } from "../../lib/cn.js";
4
+ import { TableCell, TableFooter, TableRow } from "../../ui/table.js";
5
+ import { NUMERIC_COLUMN_TYPES } from "../data-table-cell.js";
6
+ import { stickyActionsCellClass } from "./actions-column.js";
7
+ import { MeasureValue, measureKindLabel, measureLabel } from "./measure-value.js";
8
+ import { EXPAND_COLUMN_KEY, SELECT_COLUMN_KEY } from "./use-column-pins.js";
9
+ function measuresOf(totals, field) {
10
+ return totals.measures.filter((measure) => measure.field === field);
11
+ }
12
+ // Same order as the body row: select before expand.
13
+ function leadingCellKeys(leading) {
14
+ return [
15
+ ...(leading.select ? [SELECT_COLUMN_KEY] : []),
16
+ ...(leading.expand ? [EXPAND_COLUMN_KEY] : []),
17
+ ];
18
+ }
19
+ function totalsLabel(translate) {
20
+ return translate?.("kumiko.list.totals.label") ?? "Total";
21
+ }
22
+ function FooterMeasures({ measures, column, }) {
23
+ const translate = useOptionalTranslation();
24
+ if (measures.length === 1 && measures[0]?.kind === "sum") {
25
+ return _jsx(MeasureValue, { measure: measures[0], column: column });
26
+ }
27
+ return (_jsx("span", { className: "inline-flex flex-col gap-0.5", children: measures.map((measure) => (_jsxs("span", { className: "inline-flex items-baseline justify-end gap-1.5", children: [_jsx("span", { className: "text-xs font-normal text-muted-foreground", children: measureKindLabel(measure.kind, translate) }), _jsx(MeasureValue, { measure: measure, column: column })] }, measure.kind))) }));
28
+ }
29
+ /** Footer row under the table body, one cell per visible column. */
30
+ export function TotalsFooterRow({ totals, columns, cellPin, leadingCells, actionsCell, cellClassName, }) {
31
+ const translate = useOptionalTranslation();
32
+ return (_jsx(TableFooter, { "data-testid": "list-totals", children: _jsxs(TableRow, { className: "hover:bg-transparent", children: [leadingCellKeys(leadingCells).map((key) => {
33
+ const pin = cellPin(key);
34
+ return (_jsx(TableCell, { "data-testid": `list-totals-${key}`, className: pin.className, style: pin.style }, key));
35
+ }), columns.map((column, index) => {
36
+ const measures = measuresOf(totals, column.field);
37
+ const pin = cellPin(column.field);
38
+ return (_jsx(TableCell, { "data-testid": `list-totals-${column.field}`, className: cn("tabular-nums", NUMERIC_COLUMN_TYPES.has(column.type) && "text-right", cellClassName, pin.className), style: pin.style, children: measures.length > 0 ? (_jsx(FooterMeasures, { measures: measures, column: column })) : index === 0 ? (totalsLabel(translate)) : null }, column.field));
39
+ }), actionsCell !== undefined && (_jsx(TableCell, { className: stickyActionsCellClass(actionsCell.fillsHeight) }))] }) }));
40
+ }
41
+ /** Card layout: the totals as a labelled list under the cards. */
42
+ export function TotalsCard({ totals, columnOf, }) {
43
+ const translate = useOptionalTranslation();
44
+ return (_jsx("dl", { "data-testid": "list-totals", className: "m-0 flex flex-col gap-1 border-b border-border-row bg-muted/50 px-4 py-3 text-sm", children: totals.measures.map((measure) => {
45
+ const column = columnOf(measure.field);
46
+ return (_jsxs("div", { "data-testid": `list-totals-${measure.field}`, className: "flex items-baseline justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: measureLabel(measure, column, translate) }), _jsx("dd", { className: "m-0 font-medium tabular-nums", children: _jsx(MeasureValue, { measure: measure, column: column }) })] }, `${measure.kind}-${measure.field}`));
47
+ }) }));
48
+ }
@@ -0,0 +1,21 @@
1
+ import { type ReactNode } from "react";
2
+ import type { DataTableBulk } from "../../../data-table-bulk-types.ts";
3
+ import type { ListRowViewModel } from "../../../headless/index.ts";
4
+ import type { PinnedCellProps } from "./use-column-pins.ts";
5
+ export type BulkSelection = {
6
+ readonly active: boolean;
7
+ readonly bar: ReactNode;
8
+ readonly onKeyboardSelect: ((rowId: string) => void) | undefined;
9
+ readonly headerCell: (pin: PinnedCellProps) => ReactNode;
10
+ readonly bodyCell: (row: ListRowViewModel, pin: PinnedCellProps) => ReactNode;
11
+ readonly cardCheckbox: (row: ListRowViewModel) => ReactNode;
12
+ };
13
+ /** Row checkboxes, the selection bar and the run flow of a table with bulk actions. */
14
+ export declare function useBulkSelection({ bulk, rows, titleField, rowTestId, flush, }: {
15
+ readonly bulk: DataTableBulk | undefined;
16
+ readonly rows: readonly ListRowViewModel[];
17
+ readonly titleField: string | undefined;
18
+ readonly rowTestId: (row: ListRowViewModel) => string;
19
+ /** The table has no frame (scrollBody): the bar spans its full width. */
20
+ readonly flush: boolean;
21
+ }): BulkSelection;
@@ -0,0 +1,66 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { useBulkRun } from "../../../hooks/use-bulk-run.js";
4
+ import { useOptionalTranslation } from "../../../i18n.js";
5
+ import { cn } from "../../lib/cn.js";
6
+ import { Checkbox } from "../../ui/checkbox.js";
7
+ import { TableCell, TableHead } from "../../ui/table.js";
8
+ import { cellTitle } from "../data-table-cell.js";
9
+ import { BulkSelectionBar } from "./bulk-selection-bar.js";
10
+ import { EXPAND_TOGGLE_CELL_CLASS } from "./table-chrome.js";
11
+ import { useRowSelection } from "./use-row-selection.js";
12
+ const INACTIVE = {
13
+ active: false,
14
+ bar: null,
15
+ onKeyboardSelect: undefined,
16
+ headerCell: () => null,
17
+ bodyCell: () => null,
18
+ cardCheckbox: () => null,
19
+ };
20
+ const keyAsText = (key) => key;
21
+ // The checkbox sits inside a clickable row; its click must not open the row.
22
+ function stopRowClick(event) {
23
+ event.stopPropagation();
24
+ }
25
+ /** Row checkboxes, the selection bar and the run flow of a table with bulk actions. */
26
+ export function useBulkSelection({ bulk, rows, titleField, rowTestId, flush, }) {
27
+ const t = useOptionalTranslation() ?? keyAsText;
28
+ const selection = useRowSelection(rows);
29
+ const run = useBulkRun();
30
+ const [pendingAction, setPendingAction] = useState(undefined);
31
+ const [resultActionLabel, setResultActionLabel] = useState("");
32
+ if (bulk === undefined)
33
+ return INACTIVE;
34
+ const isRunning = run.state.phase === "running";
35
+ const reload = bulk.reload;
36
+ async function execute(action) {
37
+ setPendingAction(undefined);
38
+ setResultActionLabel(action.label);
39
+ const result = await run.start(action, selection.selectedRows);
40
+ // Failed rows stay selected for a retry; so do rows a cancel kept from running.
41
+ selection.retainOnly(new Set([...result.failedIds, ...result.notStartedIds]));
42
+ await reload();
43
+ }
44
+ function rowCheckbox(row) {
45
+ const checked = selection.isSelected(row.id);
46
+ const title = (titleField !== undefined ? cellTitle(row.values[titleField]) : undefined) ?? "";
47
+ return (_jsx(Checkbox, { checked: checked, disabled: isRunning || (!checked && selection.isFull), onCheckedChange: () => selection.toggle(row.id), onClick: stopRowClick, "aria-label": t("kumiko.list.bulk.select-row", { title }), "data-testid": `${rowTestId(row)}-select` }));
48
+ }
49
+ return {
50
+ active: true,
51
+ bar: (_jsx(BulkSelectionBar, { flush: flush, actions: bulk.actions, selectedCount: selection.selectedRows.length, isFull: selection.isFull, runState: run.state, resultActionLabel: resultActionLabel, pendingAction: pendingAction, t: t, onRequest: (action) => {
52
+ if (action.confirmText !== undefined)
53
+ setPendingAction(action);
54
+ else
55
+ void execute(action);
56
+ }, onConfirm: (action) => void execute(action), onConfirmClose: () => setPendingAction(undefined), onCancel: run.cancel, onClear: selection.clear, onDismissResult: run.dismissResult })),
57
+ onKeyboardSelect: isRunning ? undefined : selection.toggle,
58
+ headerCell: (pin) => (_jsx(TableHead, { "data-testid": "column-select", className: cn(EXPAND_TOGGLE_CELL_CLASS, "text-center", pin.className), style: pin.style, children: _jsx(Checkbox, { checked: selection.allState === "all"
59
+ ? true
60
+ : selection.allState === "some"
61
+ ? "indeterminate"
62
+ : false, disabled: isRunning || rows.length === 0, onCheckedChange: selection.toggleAll, "aria-label": t("kumiko.list.bulk.select-all"), "data-testid": "select-all" }) })),
63
+ bodyCell: (row, pin) => (_jsx(TableCell, { "data-testid": `cell-${row.id}-select`, className: cn(EXPAND_TOGGLE_CELL_CLASS, "text-center", pin.className), style: pin.style, children: rowCheckbox(row) })),
64
+ cardCheckbox: (row) => (_jsx("div", { className: "flex size-11 shrink-0 items-center justify-center", children: rowCheckbox(row) })),
65
+ };
66
+ }
@@ -0,0 +1,32 @@
1
+ import { type CSSProperties } from "react";
2
+ import type { DataTableViewOptions } from "../../../data-table-view-types.ts";
3
+ import type { MeasuredColumnWidths } from "./use-measured-column-widths.ts";
4
+ export declare const EXPAND_COLUMN_KEY = "__expand";
5
+ export declare const SELECT_COLUMN_KEY = "__select";
6
+ export type PinnedCellProps = {
7
+ readonly style?: CSSProperties;
8
+ readonly className?: string;
9
+ };
10
+ export type PinnedHeaderCellProps = PinnedCellProps & {
11
+ readonly columnField?: string;
12
+ };
13
+ export type ColumnPins<TColumn> = {
14
+ readonly columns: readonly TColumn[];
15
+ readonly headerCell: (field: string) => PinnedHeaderCellProps;
16
+ readonly bodyCell: (field: string) => PinnedCellProps;
17
+ readonly actionsHeaderStyle: CSSProperties | undefined;
18
+ };
19
+ /** Reorders columns (left pins first, right pins last) and computes the sticky
20
+ * styles of pinned cells. Inactive: columns unchanged, no styles. */
21
+ export declare function useColumnPins<TColumn extends {
22
+ readonly field: string;
23
+ }>({ columns, view, isNarrow, measured, hasActionsColumn, hasExpandColumn, hasSelectColumn, fillsHeight, }: {
24
+ readonly columns: readonly TColumn[];
25
+ readonly view: DataTableViewOptions | undefined;
26
+ readonly isNarrow: boolean;
27
+ readonly measured: MeasuredColumnWidths | undefined;
28
+ readonly hasActionsColumn: boolean;
29
+ readonly hasExpandColumn: boolean;
30
+ readonly hasSelectColumn?: boolean;
31
+ readonly fillsHeight: boolean;
32
+ }): ColumnPins<TColumn>;
@@ -0,0 +1,117 @@
1
+ import { useMemo } from "react";
2
+ import { cn } from "../../lib/cn.js";
3
+ import { isPinningActive } from "./column-layout-mode.js";
4
+ import { ROW_FOCUS_RING_CELL_CLASS } from "./use-row-keyboard-navigation.js";
5
+ export const EXPAND_COLUMN_KEY = "__expand";
6
+ export const SELECT_COLUMN_KEY = "__select";
7
+ // Header over pins over actions (10) over cells. Header z-index 30 also lifts
8
+ // the sticky scrollBody header, whose class selector would otherwise set 10.
9
+ const HEADER_Z_INDEX = 30;
10
+ const PINNED_HEADER_Z_INDEX = 40;
11
+ const PINNED_BODY_Z_INDEX = 20;
12
+ function pinOf(view, field) {
13
+ return view?.pins?.[field];
14
+ }
15
+ function placePins(columns, pinOfField, measured, leading, hasActionsColumn) {
16
+ const placements = new Map();
17
+ const widthOf = (field) => measured.fields[field] ?? 0;
18
+ const leftFields = columns.filter((c) => pinOfField(c.field) === "left").map((c) => c.field);
19
+ const rightFields = columns.filter((c) => pinOfField(c.field) === "right").map((c) => c.field);
20
+ let left = 0;
21
+ // Leading select and expand columns stick ahead of left pins so they stay with their row.
22
+ if (leftFields.length > 0 && leading.hasSelectColumn) {
23
+ placements.set(SELECT_COLUMN_KEY, { side: "left", offset: left, onInnerEdge: false });
24
+ left += measured.select;
25
+ }
26
+ if (leftFields.length > 0 && leading.hasExpandColumn) {
27
+ placements.set(EXPAND_COLUMN_KEY, { side: "left", offset: left, onInnerEdge: false });
28
+ left += measured.expand;
29
+ }
30
+ leftFields.forEach((field, index) => {
31
+ placements.set(field, {
32
+ side: "left",
33
+ offset: left,
34
+ onInnerEdge: index === leftFields.length - 1,
35
+ });
36
+ left += widthOf(field);
37
+ });
38
+ let right = hasActionsColumn ? measured.actions : 0;
39
+ for (let index = rightFields.length - 1; index >= 0; index--) {
40
+ const field = rightFields[index];
41
+ if (field === undefined)
42
+ continue;
43
+ placements.set(field, { side: "right", offset: right, onInnerEdge: index === 0 });
44
+ right += widthOf(field);
45
+ }
46
+ return placements;
47
+ }
48
+ function pinnedStyle(placement, part, fillsHeight) {
49
+ const edgeLine = placement.side === "left" ? "inset -1px 0 0" : "inset 1px 0 0";
50
+ const shadows = [];
51
+ if (placement.onInnerEdge)
52
+ shadows.push(`${edgeLine} var(--color-border)`);
53
+ if (part === "header" && fillsHeight)
54
+ shadows.push("inset 0 -1px 0 var(--color-border)");
55
+ return {
56
+ position: "sticky",
57
+ [placement.side]: placement.offset,
58
+ zIndex: part === "header" ? PINNED_HEADER_Z_INDEX : PINNED_BODY_Z_INDEX,
59
+ ...(shadows.length > 0 && { boxShadow: shadows.join(", ") }),
60
+ };
61
+ }
62
+ function pinnedClassName(part, fillsHeight) {
63
+ if (part === "header")
64
+ return "bg-muted";
65
+ return cn(fillsHeight ? "bg-card" : "bg-background", ROW_FOCUS_RING_CELL_CLASS);
66
+ }
67
+ /** Reorders columns (left pins first, right pins last) and computes the sticky
68
+ * styles of pinned cells. Inactive: columns unchanged, no styles. */
69
+ export function useColumnPins({ columns, view, isNarrow, measured, hasActionsColumn, hasExpandColumn, hasSelectColumn = false, fillsHeight, }) {
70
+ const active = isPinningActive(view, isNarrow);
71
+ const pins = view?.pins;
72
+ const ordered = useMemo(() => {
73
+ if (!active)
74
+ return columns;
75
+ const pinFor = (column) => pins?.[column.field];
76
+ return [
77
+ ...columns.filter((c) => pinFor(c) === "left"),
78
+ ...columns.filter((c) => pinFor(c) === undefined),
79
+ ...columns.filter((c) => pinFor(c) === "right"),
80
+ ];
81
+ }, [active, columns, pins]);
82
+ return useMemo(() => {
83
+ const placements = active
84
+ ? placePins(ordered, (field) => pinOf(view, field), measured ?? { fields: {}, select: 0, expand: 0, actions: 0 }, { hasSelectColumn, hasExpandColumn }, hasActionsColumn)
85
+ : new Map();
86
+ const cellProps = (field, part) => {
87
+ if (!active)
88
+ return {};
89
+ const placement = placements.get(field);
90
+ if (placement === undefined) {
91
+ return part === "header" ? { style: { zIndex: HEADER_Z_INDEX } } : {};
92
+ }
93
+ return {
94
+ style: pinnedStyle(placement, part, fillsHeight),
95
+ className: pinnedClassName(part, fillsHeight),
96
+ };
97
+ };
98
+ return {
99
+ columns: ordered,
100
+ headerCell: (field) => ({
101
+ ...cellProps(field, "header"),
102
+ ...(measured !== undefined && { columnField: field }),
103
+ }),
104
+ bodyCell: (field) => cellProps(field, "body"),
105
+ actionsHeaderStyle: active ? { zIndex: HEADER_Z_INDEX } : undefined,
106
+ };
107
+ }, [
108
+ active,
109
+ ordered,
110
+ view,
111
+ measured,
112
+ hasSelectColumn,
113
+ hasExpandColumn,
114
+ hasActionsColumn,
115
+ fillsHeight,
116
+ ]);
117
+ }
@@ -0,0 +1,22 @@
1
+ import { type CSSProperties } from "react";
2
+ import type { DataTableViewOptions } from "../../../data-table-view-types.ts";
3
+ import { type MeasuredColumnWidths } from "./use-measured-column-widths.ts";
4
+ export type ColumnSizing = {
5
+ readonly widthOf: (field: string) => number;
6
+ readonly minWidthOf: (field: string) => number;
7
+ readonly tableStyle: CSSProperties;
8
+ /** Live width while a drag is in progress; nothing reaches the view state until `commit`. */
9
+ readonly setLiveWidth: (field: string, width: number) => void;
10
+ readonly commit: (field: string, width: number) => void;
11
+ };
12
+ /** Fixed-layout sizing for resizable columns; undefined when resizing is off. */
13
+ export declare function useColumnSizing({ columns, view, isNarrow, measured, hasExpandColumn, hasSelectColumn, }: {
14
+ readonly columns: readonly {
15
+ readonly field: string;
16
+ }[];
17
+ readonly view: DataTableViewOptions | undefined;
18
+ readonly isNarrow: boolean;
19
+ readonly measured: MeasuredColumnWidths | undefined;
20
+ readonly hasExpandColumn: boolean;
21
+ readonly hasSelectColumn?: boolean;
22
+ }): ColumnSizing | undefined;
@@ -0,0 +1,44 @@
1
+ import { useCallback, useMemo, useState } from "react";
2
+ import { isResizingActive } from "./column-layout-mode.js";
3
+ import { EXPAND_COLUMN_WIDTH_PX, SELECT_COLUMN_WIDTH_PX, } from "./use-measured-column-widths.js";
4
+ /** Fixed-layout sizing for resizable columns; undefined when resizing is off. */
5
+ export function useColumnSizing({ columns, view, isNarrow, measured, hasExpandColumn, hasSelectColumn = false, }) {
6
+ const [live, setLive] = useState(null);
7
+ const active = isResizingActive(view, isNarrow);
8
+ const widths = view?.widths;
9
+ const minWidths = view?.minWidths;
10
+ const onWidthChange = view?.onWidthChange;
11
+ const commit = useCallback((field, width) => {
12
+ setLive(null);
13
+ onWidthChange?.(field, width);
14
+ }, [onWidthChange]);
15
+ return useMemo(() => {
16
+ if (!active || widths === undefined || minWidths === undefined)
17
+ return undefined;
18
+ const minWidthOf = (field) => minWidths[field] ?? 0;
19
+ const widthOf = (field) => live?.field === field ? live.width : (widths[field] ?? minWidthOf(field));
20
+ // The actions column keeps no width of its own: it absorbs the leftover space,
21
+ // so a dragged width holds exactly while the table is wider than its container.
22
+ const total = columns.reduce((sum, column) => sum + widthOf(column.field), 0) +
23
+ (hasExpandColumn ? EXPAND_COLUMN_WIDTH_PX : 0) +
24
+ (hasSelectColumn ? SELECT_COLUMN_WIDTH_PX : 0) +
25
+ (measured?.actions ?? 0);
26
+ return {
27
+ widthOf,
28
+ minWidthOf,
29
+ tableStyle: { tableLayout: "fixed", width: `max(100%, ${total}px)` },
30
+ setLiveWidth: (field, width) => setLive({ field, width }),
31
+ commit,
32
+ };
33
+ }, [
34
+ active,
35
+ widths,
36
+ minWidths,
37
+ live,
38
+ columns,
39
+ hasExpandColumn,
40
+ hasSelectColumn,
41
+ measured,
42
+ commit,
43
+ ]);
44
+ }
@@ -0,0 +1,15 @@
1
+ import { type RefObject } from "react";
2
+ export declare const COLUMN_FIELD_ATTRIBUTE = "data-column-field";
3
+ export declare const EXPAND_COLUMN_WIDTH_PX = 40;
4
+ export declare const SELECT_COLUMN_WIDTH_PX = 40;
5
+ export type MeasuredColumnWidths = {
6
+ readonly fields: Readonly<Record<string, number>>;
7
+ readonly select: number;
8
+ readonly expand: number;
9
+ readonly actions: number;
10
+ };
11
+ /** Header cell widths for sticky offsets and the fixed layout's total width; undefined while disabled. */
12
+ export declare function useMeasuredColumnWidths(enabled: boolean): {
13
+ readonly headerRowRef: RefObject<HTMLTableRowElement | null>;
14
+ readonly measured: MeasuredColumnWidths | undefined;
15
+ };