@cosmicdrift/kumiko-renderer 0.387.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 (210) hide show
  1. package/dist/app/action-form-body.js +10 -2
  2. package/dist/app/action-form-support.d.ts +1 -0
  3. package/dist/app/action-form-support.js +1 -1
  4. package/dist/app/base-path-context.d.ts +9 -0
  5. package/dist/app/base-path-context.js +13 -0
  6. package/dist/app/base-path.d.ts +3 -0
  7. package/dist/app/base-path.js +27 -0
  8. package/dist/app/config-edit-body.d.ts +1 -1
  9. package/dist/app/config-edit-body.js +19 -6
  10. package/dist/app/config-edit-without-secret-fields.d.ts +2 -0
  11. package/dist/app/config-edit-without-secret-fields.js +28 -0
  12. package/dist/app/entity-edit-prefill.d.ts +11 -0
  13. package/dist/app/entity-edit-prefill.js +9 -4
  14. package/dist/app/entity-list-facet-specs.d.ts +3 -0
  15. package/dist/app/entity-list-facet-specs.js +65 -0
  16. package/dist/app/entity-list-screen.js +74 -206
  17. package/dist/app/extension-sections.d.ts +5 -3
  18. package/dist/app/form-schema.js +1 -1
  19. package/dist/app/header-actions-bar.js +1 -1
  20. package/dist/app/kumiko-screen.js +6 -14
  21. package/dist/app/list-bulk-actions.d.ts +18 -0
  22. package/dist/app/list-bulk-actions.js +54 -0
  23. package/dist/app/locked-screen-header.d.ts +9 -0
  24. package/dist/app/locked-screen-header.js +33 -0
  25. package/dist/app/projection-list-body.js +51 -46
  26. package/dist/app/row-actions.d.ts +3 -0
  27. package/dist/app/row-actions.js +10 -5
  28. package/dist/app/screen-shared.js +9 -2
  29. package/dist/app/use-entity-range-facets.d.ts +24 -0
  30. package/dist/app/use-entity-range-facets.js +64 -0
  31. package/dist/app/use-projection-date-range.d.ts +12 -0
  32. package/dist/app/use-projection-date-range.js +21 -0
  33. package/dist/app/use-screen-list-rows.d.ts +37 -0
  34. package/dist/app/use-screen-list-rows.js +104 -0
  35. package/dist/app/use-screen-list-table.d.ts +24 -0
  36. package/dist/app/use-screen-list-table.js +46 -0
  37. package/dist/components/edit-mounts.d.ts +2 -2
  38. package/dist/components/embedded-list-field.js +1 -1
  39. package/dist/components/list-group-page-bridges.d.ts +3 -0
  40. package/dist/components/list-group-page-bridges.js +31 -0
  41. package/dist/components/list-state-store.d.ts +16 -0
  42. package/dist/components/list-state-store.js +31 -0
  43. package/dist/components/related-list-section.js +51 -90
  44. package/dist/components/render-edit-action-button.d.ts +1 -3
  45. package/dist/components/render-edit-action-button.js +2 -2
  46. package/dist/components/render-edit.js +11 -8
  47. package/dist/components/render-field.js +11 -7
  48. package/dist/components/render-list-toolbar-action.d.ts +27 -0
  49. package/dist/components/render-list-toolbar-action.js +40 -0
  50. package/dist/components/render-list.d.ts +13 -29
  51. package/dist/components/render-list.js +72 -67
  52. package/dist/current-user.d.ts +2 -0
  53. package/dist/data-table-aggregate-types.d.ts +47 -0
  54. package/dist/data-table-aggregate-types.js +1 -0
  55. package/dist/data-table-bulk-types.d.ts +27 -0
  56. package/dist/data-table-bulk-types.js +1 -0
  57. package/dist/data-table-facet-types.d.ts +36 -0
  58. package/dist/data-table-facet-types.js +1 -0
  59. package/dist/data-table-view-types.d.ts +73 -0
  60. package/dist/data-table-view-types.js +1 -0
  61. package/dist/flow-diagram/render.js +9 -1
  62. package/dist/headless/form/form-controller.js +8 -0
  63. package/dist/headless/form/types.d.ts +1 -0
  64. package/dist/headless/format/index.js +89 -64
  65. package/dist/headless/view-model/edit.js +5 -1
  66. package/dist/headless/view-model/list.js +184 -149
  67. package/dist/headless/view-model/types.d.ts +7 -2
  68. package/dist/hooks/list-column-layout.d.ts +19 -0
  69. package/dist/hooks/list-column-layout.js +36 -0
  70. package/dist/hooks/list-group-pages.d.ts +26 -0
  71. package/dist/hooks/list-group-pages.js +37 -0
  72. package/dist/hooks/list-reset.d.ts +8 -0
  73. package/dist/hooks/list-reset.js +15 -0
  74. package/dist/hooks/list-server-groups.d.ts +39 -0
  75. package/dist/hooks/list-server-groups.js +84 -0
  76. package/dist/hooks/list-state-schema.d.ts +28 -0
  77. package/dist/hooks/list-state-schema.js +76 -0
  78. package/dist/hooks/live-refetch-scheduler.js +4 -0
  79. package/dist/hooks/use-bulk-run.d.ts +41 -0
  80. package/dist/hooks/use-bulk-run.js +65 -0
  81. package/dist/hooks/use-group-load-state.d.ts +11 -0
  82. package/dist/hooks/use-group-load-state.js +31 -0
  83. package/dist/hooks/use-list-aggregates.d.ts +38 -0
  84. package/dist/hooks/use-list-aggregates.js +107 -0
  85. package/dist/hooks/use-list-table.d.ts +38 -0
  86. package/dist/hooks/use-list-table.js +91 -0
  87. package/dist/hooks/use-list-view-state.d.ts +43 -0
  88. package/dist/hooks/use-list-view-state.js +161 -0
  89. package/dist/hooks/use-reference-lookup.d.ts +9 -1
  90. package/dist/hooks/use-reference-lookup.js +17 -3
  91. package/dist/hooks/use-related-list-rows.d.ts +27 -0
  92. package/dist/hooks/use-related-list-rows.js +81 -0
  93. package/dist/hooks/use-related-list-state.d.ts +15 -0
  94. package/dist/hooks/use-related-list-state.js +31 -0
  95. package/dist/hooks/use-remembered-list-params.d.ts +13 -0
  96. package/dist/hooks/use-remembered-list-params.js +53 -0
  97. package/dist/hooks/use-remembered-local-list-state.d.ts +26 -0
  98. package/dist/hooks/use-remembered-local-list-state.js +63 -0
  99. package/dist/i18n-defaults.js +59 -2
  100. package/dist/index.d.ts +4 -1
  101. package/dist/index.js +3 -0
  102. package/dist/primitives.d.ts +30 -47
  103. package/dist/web/app/create-app.d.ts +5 -0
  104. package/dist/web/app/create-app.js +15 -7
  105. package/dist/web/app/dashboard-body.js +17 -1
  106. package/dist/web/app/dashboard-panels.js +12 -2
  107. package/dist/web/app/history-leave-guard.d.ts +6 -0
  108. package/dist/web/app/history-leave-guard.js +115 -0
  109. package/dist/web/app/list-state-store-boot.d.ts +6 -0
  110. package/dist/web/app/list-state-store-boot.js +15 -0
  111. package/dist/web/app/nav.js +7 -54
  112. package/dist/web/app/session-list-state-store.d.ts +15 -0
  113. package/dist/web/app/session-list-state-store.js +117 -0
  114. package/dist/web/index.d.ts +2 -1
  115. package/dist/web/index.js +2 -1
  116. package/dist/web/layout/shell-header.js +2 -1
  117. package/dist/web/layout/uniform-bar-controls.d.ts +1 -0
  118. package/dist/web/layout/uniform-bar-controls.js +4 -0
  119. package/dist/web/lib/export-xlsx-request.d.ts +11 -0
  120. package/dist/web/lib/export-xlsx-request.js +46 -0
  121. package/dist/web/lib/file-ref-download.js +4 -1
  122. package/dist/web/primitives/action-menu.js +1 -1
  123. package/dist/web/primitives/code-input.d.ts +6 -0
  124. package/dist/web/primitives/code-input.js +31 -0
  125. package/dist/web/primitives/data-table/actions-column.d.ts +16 -0
  126. package/dist/web/primitives/data-table/actions-column.js +34 -0
  127. package/dist/web/primitives/data-table/bulk-selection-bar.d.ts +20 -0
  128. package/dist/web/primitives/data-table/bulk-selection-bar.js +37 -0
  129. package/dist/web/primitives/data-table/card-sort-select.d.ts +14 -0
  130. package/dist/web/primitives/data-table/card-sort-select.js +40 -0
  131. package/dist/web/primitives/data-table/column-layout-mode.d.ts +5 -0
  132. package/dist/web/primitives/data-table/column-layout-mode.js +10 -0
  133. package/dist/web/primitives/data-table/column-picker-section.d.ts +7 -0
  134. package/dist/web/primitives/data-table/column-picker-section.js +35 -0
  135. package/dist/web/primitives/data-table/column-resize-handle.d.ts +14 -0
  136. package/dist/web/primitives/data-table/column-resize-handle.js +46 -0
  137. package/dist/web/primitives/data-table/density.d.ts +14 -0
  138. package/dist/web/primitives/data-table/density.js +20 -0
  139. package/dist/web/primitives/data-table/export-button.d.ts +5 -0
  140. package/dist/web/primitives/data-table/export-button.js +37 -0
  141. package/dist/web/primitives/data-table/facet-cluster.d.ts +8 -0
  142. package/dist/web/primitives/data-table/facet-cluster.js +37 -0
  143. package/dist/web/primitives/data-table/group-band.d.ts +28 -0
  144. package/dist/web/primitives/data-table/group-band.js +53 -0
  145. package/dist/web/primitives/data-table/measure-value.d.ts +10 -0
  146. package/dist/web/primitives/data-table/measure-value.js +28 -0
  147. package/dist/web/primitives/data-table/pin-section.d.ts +8 -0
  148. package/dist/web/primitives/data-table/pin-section.js +20 -0
  149. package/dist/web/primitives/data-table/reset-all-button.d.ts +4 -0
  150. package/dist/web/primitives/data-table/reset-all-button.js +8 -0
  151. package/dist/web/primitives/data-table/server-group-header.d.ts +11 -0
  152. package/dist/web/primitives/data-table/server-group-header.js +51 -0
  153. package/dist/web/primitives/data-table/sortable-header.d.ts +17 -0
  154. package/dist/web/primitives/data-table/sortable-header.js +41 -0
  155. package/dist/web/primitives/data-table/table-chrome.d.ts +13 -0
  156. package/dist/web/primitives/data-table/table-chrome.js +30 -0
  157. package/dist/web/primitives/data-table/table-footer.d.ts +7 -0
  158. package/dist/web/primitives/data-table/table-footer.js +20 -0
  159. package/dist/web/primitives/data-table/toolbar-end-cluster.d.ts +10 -0
  160. package/dist/web/primitives/data-table/toolbar-end-cluster.js +13 -0
  161. package/dist/web/primitives/data-table/totals-footer.d.ts +24 -0
  162. package/dist/web/primitives/data-table/totals-footer.js +48 -0
  163. package/dist/web/primitives/data-table/use-bulk-selection.d.ts +21 -0
  164. package/dist/web/primitives/data-table/use-bulk-selection.js +66 -0
  165. package/dist/web/primitives/data-table/use-column-pins.d.ts +32 -0
  166. package/dist/web/primitives/data-table/use-column-pins.js +117 -0
  167. package/dist/web/primitives/data-table/use-column-sizing.d.ts +22 -0
  168. package/dist/web/primitives/data-table/use-column-sizing.js +44 -0
  169. package/dist/web/primitives/data-table/use-measured-column-widths.d.ts +15 -0
  170. package/dist/web/primitives/data-table/use-measured-column-widths.js +83 -0
  171. package/dist/web/primitives/data-table/use-row-keyboard-navigation.d.ts +22 -0
  172. package/dist/web/primitives/data-table/use-row-keyboard-navigation.js +65 -0
  173. package/dist/web/primitives/data-table/use-row-selection.d.ts +14 -0
  174. package/dist/web/primitives/data-table/use-row-selection.js +48 -0
  175. package/dist/web/primitives/data-table/use-view-columns.d.ts +6 -0
  176. package/dist/web/primitives/data-table/use-view-columns.js +17 -0
  177. package/dist/web/primitives/data-table/view-menu.d.ts +8 -0
  178. package/dist/web/primitives/data-table/view-menu.js +45 -0
  179. package/dist/web/primitives/data-table-cell.d.ts +0 -22
  180. package/dist/web/primitives/data-table-cell.js +1 -58
  181. package/dist/web/primitives/data-table.d.ts +1 -1
  182. package/dist/web/primitives/data-table.js +125 -146
  183. package/dist/web/primitives/facet-filter.js +1 -1
  184. package/dist/web/primitives/file-thumbnail.d.ts +3 -0
  185. package/dist/web/primitives/file-thumbnail.js +5 -0
  186. package/dist/web/primitives/file-value-list.d.ts +1 -1
  187. package/dist/web/primitives/file-value-list.js +5 -5
  188. package/dist/web/primitives/input.js +6 -1
  189. package/dist/web/primitives/layout-elements.js +6 -4
  190. package/dist/web/primitives/number-range-facet.d.ts +7 -0
  191. package/dist/web/primitives/number-range-facet.js +33 -0
  192. package/dist/web/primitives/page-header.js +3 -1
  193. package/dist/web/primitives/row-actions.js +3 -3
  194. package/dist/web/primitives/use-focus-on-mount.d.ts +2 -0
  195. package/dist/web/primitives/use-focus-on-mount.js +9 -0
  196. package/dist/web/widgets/bounded-load-cache.d.ts +5 -1
  197. package/dist/web/widgets/bounded-load-cache.js +49 -21
  198. package/dist/web/widgets/choice-cards.d.ts +17 -0
  199. package/dist/web/widgets/choice-cards.js +12 -0
  200. package/dist/web/widgets/collapsible-section.js +1 -1
  201. package/dist/web/widgets/detail-list.d.ts +5 -1
  202. package/dist/web/widgets/detail-list.js +5 -1
  203. package/dist/web/widgets/index.d.ts +1 -0
  204. package/dist/web/widgets/index.js +1 -0
  205. package/dist/web/widgets/pdf-preview.js +10 -5
  206. package/dist/web/widgets/plan-card.d.ts +2 -0
  207. package/dist/web/widgets/plan-card.js +1 -1
  208. package/dist/web/widgets/upload-zone.js +13 -5
  209. package/package.json +5 -5
  210. package/src/changes.json +158 -0
@@ -0,0 +1,83 @@
1
+ import { useLayoutEffect, useRef, useState } from "react";
2
+ import { ACTIONS_CONTENT_ATTRIBUTE } from "./actions-column.js";
3
+ export const COLUMN_FIELD_ATTRIBUTE = "data-column-field";
4
+ export const EXPAND_COLUMN_WIDTH_PX = 40;
5
+ export const SELECT_COLUMN_WIDTH_PX = 40;
6
+ const NOT_MEASURED = { fields: {}, select: 0, expand: 0, actions: 0 };
7
+ function roundToHundredth(value) {
8
+ return Math.round(value * 100) / 100;
9
+ }
10
+ function widthOfElement(element) {
11
+ return roundToHundredth(element.getBoundingClientRect().width);
12
+ }
13
+ function horizontalPadding(element) {
14
+ const style = getComputedStyle(element);
15
+ return (Number.parseFloat(style.paddingLeft) || 0) + (Number.parseFloat(style.paddingRight) || 0);
16
+ }
17
+ // Under a fixed table layout the actions column has no width of its own, so its
18
+ // header cell reports the leftover space. The wrapped content gives its natural width.
19
+ function naturalActionsWidth(headerRow, headerCell) {
20
+ const contents = headerRow
21
+ .closest("table")
22
+ ?.querySelectorAll(`:scope > tbody > tr > td > [${ACTIONS_CONTENT_ATTRIBUTE}]`);
23
+ if (contents === undefined || contents.length === 0)
24
+ return widthOfElement(headerCell);
25
+ let widest = 0;
26
+ for (const content of contents) {
27
+ const cell = content.parentElement;
28
+ const width = widthOfElement(content) + (cell === null ? 0 : horizontalPadding(cell));
29
+ widest = Math.max(widest, width);
30
+ }
31
+ return roundToHundredth(widest);
32
+ }
33
+ function measureHeaderRow(headerRow) {
34
+ const fields = {};
35
+ let select = 0;
36
+ let expand = 0;
37
+ let actions = 0;
38
+ for (const cell of headerRow.children) {
39
+ const field = cell.getAttribute(COLUMN_FIELD_ATTRIBUTE);
40
+ const testId = cell.getAttribute("data-testid");
41
+ if (field !== null)
42
+ fields[field] = widthOfElement(cell);
43
+ else if (testId === "column-select")
44
+ select = widthOfElement(cell);
45
+ else if (testId === "column-expand")
46
+ expand = widthOfElement(cell);
47
+ else if (testId === "column-actions")
48
+ actions = naturalActionsWidth(headerRow, cell);
49
+ }
50
+ return { fields, select, expand, actions };
51
+ }
52
+ function isSameMeasurement(a, b) {
53
+ if (a.select !== b.select || a.expand !== b.expand || a.actions !== b.actions)
54
+ return false;
55
+ const aKeys = Object.keys(a.fields);
56
+ return (aKeys.length === Object.keys(b.fields).length && aKeys.every((k) => a.fields[k] === b.fields[k]));
57
+ }
58
+ /** Header cell widths for sticky offsets and the fixed layout's total width; undefined while disabled. */
59
+ export function useMeasuredColumnWidths(enabled) {
60
+ const headerRowRef = useRef(null);
61
+ const [measured, setMeasured] = useState(NOT_MEASURED);
62
+ // Every render: rows and columns change without a stable key, and setMeasured
63
+ // bails out on equal widths, so this cannot loop.
64
+ useLayoutEffect(() => {
65
+ const headerRow = headerRowRef.current;
66
+ // skip: disabled or not mounted yet means there is no header row to measure
67
+ if (!enabled || headerRow === null)
68
+ return;
69
+ const update = () => {
70
+ const next = measureHeaderRow(headerRow);
71
+ setMeasured((previous) => (isSameMeasurement(previous, next) ? previous : next));
72
+ };
73
+ update();
74
+ // skip: without ResizeObserver the one measurement above is all we get
75
+ if (typeof ResizeObserver === "undefined")
76
+ return;
77
+ const observer = new ResizeObserver(update);
78
+ for (const cell of headerRow.children)
79
+ observer.observe(cell);
80
+ return () => observer.disconnect();
81
+ });
82
+ return { headerRowRef, measured: enabled ? measured : undefined };
83
+ }
@@ -0,0 +1,22 @@
1
+ import { type KeyboardEvent } from "react";
2
+ import type { DataTableViewOptions } from "../../../data-table-view-types.ts";
3
+ declare const KEYBOARD_ROW_ATTRIBUTE = "data-keyboard-row";
4
+ export declare const ROW_FOCUS_RING_CELL_CLASS = "relative after:pointer-events-none after:absolute after:inset-0 after:border-y-2 after:border-transparent group-focus-visible/row:after:border-ring";
5
+ export type KeyboardRow = {
6
+ readonly props: {
7
+ readonly tabIndex?: number;
8
+ readonly onFocus?: () => void;
9
+ readonly onKeyDown?: (event: KeyboardEvent<HTMLTableRowElement>) => void;
10
+ readonly [KEYBOARD_ROW_ATTRIBUTE]?: "";
11
+ };
12
+ readonly className: string | undefined;
13
+ };
14
+ /** Roving tabindex over the data rows: one tab stop, arrows/Home/End move, Enter opens,
15
+ * Space selects when the table has a selection column. Inactive: no attributes at all. */
16
+ export declare function useRowKeyboardNavigation({ view, isNarrow, rowIds, onSelect, }: {
17
+ readonly view: DataTableViewOptions | undefined;
18
+ readonly isNarrow: boolean;
19
+ readonly rowIds: readonly string[];
20
+ readonly onSelect?: ((rowId: string) => void) | undefined;
21
+ }): (rowId: string, open: (() => void) | undefined) => KeyboardRow;
22
+ export {};
@@ -0,0 +1,65 @@
1
+ import { useState } from "react";
2
+ import { isKeyboardNavigationActive } from "./column-layout-mode.js";
3
+ const KEYBOARD_ROW_ATTRIBUTE = "data-keyboard-row";
4
+ const FOCUS_RING_CLASS = "group/row focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring";
5
+ // Opaque sticky cells paint over the row outline, so they draw the top and bottom edge themselves.
6
+ export const ROW_FOCUS_RING_CELL_CLASS = "relative after:pointer-events-none after:absolute after:inset-0 after:border-y-2 after:border-transparent group-focus-visible/row:after:border-ring";
7
+ const INACTIVE_ROW = { props: {}, className: undefined };
8
+ function keyboardRowsAround(row) {
9
+ const siblings = Array.from(row.parentElement?.children ?? []);
10
+ return siblings.filter((el) => el instanceof HTMLElement && el.hasAttribute(KEYBOARD_ROW_ATTRIBUTE));
11
+ }
12
+ function targetRow(event) {
13
+ const rows = keyboardRowsAround(event.currentTarget);
14
+ const index = rows.indexOf(event.currentTarget);
15
+ switch (event.key) {
16
+ case "ArrowDown":
17
+ return rows[Math.min(rows.length - 1, index + 1)];
18
+ case "ArrowUp":
19
+ return rows[Math.max(0, index - 1)];
20
+ case "Home":
21
+ return rows[0];
22
+ case "End":
23
+ return rows[rows.length - 1];
24
+ default:
25
+ return undefined;
26
+ }
27
+ }
28
+ /** Roving tabindex over the data rows: one tab stop, arrows/Home/End move, Enter opens,
29
+ * Space selects when the table has a selection column. Inactive: no attributes at all. */
30
+ export function useRowKeyboardNavigation({ view, isNarrow, rowIds, onSelect, }) {
31
+ const active = isKeyboardNavigationActive(view, isNarrow);
32
+ const [focusedId, setFocusedId] = useState(undefined);
33
+ const tabStopId = focusedId !== undefined && rowIds.includes(focusedId) ? focusedId : rowIds[0];
34
+ if (!active)
35
+ return () => INACTIVE_ROW;
36
+ return (rowId, open) => ({
37
+ props: {
38
+ tabIndex: rowId === tabStopId ? 0 : -1,
39
+ onFocus: () => setFocusedId(rowId),
40
+ onKeyDown: (event) => {
41
+ // skip: keys inside an editable cell, the first-cell link or a resize handle belong to that element
42
+ if (event.target !== event.currentTarget)
43
+ return;
44
+ if (event.key === "Enter" && open !== undefined) {
45
+ event.preventDefault();
46
+ open();
47
+ }
48
+ else if (event.key === " " && onSelect !== undefined) {
49
+ event.preventDefault();
50
+ onSelect(rowId);
51
+ }
52
+ else {
53
+ const target = targetRow(event);
54
+ // skip: not a navigation key, or no row in that direction
55
+ if (target === undefined)
56
+ return;
57
+ event.preventDefault();
58
+ target.focus();
59
+ }
60
+ },
61
+ [KEYBOARD_ROW_ATTRIBUTE]: "",
62
+ },
63
+ className: FOCUS_RING_CLASS,
64
+ });
65
+ }
@@ -0,0 +1,14 @@
1
+ import type { ListRowViewModel } from "../../../headless/index.ts";
2
+ export type SelectAllState = "all" | "some" | "none";
3
+ export type RowSelection = {
4
+ readonly selectedRows: readonly ListRowViewModel[];
5
+ readonly isSelected: (rowId: string) => boolean;
6
+ readonly isFull: boolean;
7
+ readonly allState: SelectAllState;
8
+ readonly toggle: (rowId: string) => void;
9
+ readonly toggleAll: () => void;
10
+ readonly retainOnly: (rowIds: ReadonlySet<string>) => void;
11
+ readonly clear: () => void;
12
+ };
13
+ /** Checkbox selection over the loaded rows, capped at MAX_BULK_SELECTION. Rows that leave the list leave the selection. */
14
+ export declare function useRowSelection(rows: readonly ListRowViewModel[]): RowSelection;
@@ -0,0 +1,48 @@
1
+ import { MAX_BULK_SELECTION } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import { useCallback, useEffect, useMemo, useState } from "react";
3
+ const NOTHING_SELECTED = new Set();
4
+ function retained(previous, keep) {
5
+ const next = new Set([...previous].filter(keep));
6
+ return next.size === previous.size ? previous : next;
7
+ }
8
+ /** Checkbox selection over the loaded rows, capped at MAX_BULK_SELECTION. Rows that leave the list leave the selection. */
9
+ export function useRowSelection(rows) {
10
+ const [selectedIds, setSelectedIds] = useState(NOTHING_SELECTED);
11
+ useEffect(() => {
12
+ // skip: an empty list is the reload gap between two fetches; pruning then would drop the failed rows a run keeps selected
13
+ if (rows.length === 0)
14
+ return;
15
+ const loaded = new Set(rows.map((row) => row.id));
16
+ setSelectedIds((previous) => retained(previous, (id) => loaded.has(id)));
17
+ }, [rows]);
18
+ const selectedRows = useMemo(() => rows.filter((row) => selectedIds.has(row.id)), [rows, selectedIds]);
19
+ const selectableCount = Math.min(rows.length, MAX_BULK_SELECTION);
20
+ const allState = selectedRows.length === 0 ? "none" : selectedRows.length >= selectableCount ? "all" : "some";
21
+ const toggle = useCallback((rowId) => {
22
+ setSelectedIds((previous) => {
23
+ const next = new Set(previous);
24
+ if (next.has(rowId))
25
+ next.delete(rowId);
26
+ else if (next.size < MAX_BULK_SELECTION)
27
+ next.add(rowId);
28
+ return next;
29
+ });
30
+ }, []);
31
+ const toggleAll = useCallback(() => {
32
+ setSelectedIds(allState === "all"
33
+ ? NOTHING_SELECTED
34
+ : new Set(rows.slice(0, MAX_BULK_SELECTION).map((row) => row.id)));
35
+ }, [allState, rows]);
36
+ const retainOnly = useCallback((rowIds) => setSelectedIds((previous) => retained(previous, (id) => rowIds.has(id))), []);
37
+ const clear = useCallback(() => setSelectedIds(NOTHING_SELECTED), []);
38
+ return {
39
+ selectedRows,
40
+ isSelected: (rowId) => selectedIds.has(rowId),
41
+ isFull: selectedIds.size >= MAX_BULK_SELECTION,
42
+ allState,
43
+ toggle,
44
+ toggleAll,
45
+ retainOnly,
46
+ clear,
47
+ };
48
+ }
@@ -0,0 +1,6 @@
1
+ import type { DataTableViewOptions } from "../../../data-table-view-types.ts";
2
+ /** Visibility always applies; the order only in the table layout, since the card
3
+ * layout derives its title/status/meta roles from the column sequence. */
4
+ export declare function useViewColumns<TColumn extends {
5
+ readonly field: string;
6
+ }>(columns: readonly TColumn[], view: DataTableViewOptions | undefined, isNarrow: boolean): readonly TColumn[];
@@ -0,0 +1,17 @@
1
+ import { useMemo } from "react";
2
+ /** Visibility always applies; the order only in the table layout, since the card
3
+ * layout derives its title/status/meta roles from the column sequence. */
4
+ export function useViewColumns(columns, view, isNarrow) {
5
+ const viewColumns = view?.columns;
6
+ return useMemo(() => {
7
+ if (viewColumns === undefined)
8
+ return columns;
9
+ const hidden = new Set(viewColumns.filter((c) => !c.visible).map((c) => c.field));
10
+ const visible = columns.filter((column) => !hidden.has(column.field));
11
+ if (isNarrow)
12
+ return visible;
13
+ const position = new Map(viewColumns.map((c, index) => [c.field, index]));
14
+ const positionOf = (column) => position.get(column.field) ?? Number.MAX_SAFE_INTEGER;
15
+ return [...visible].sort((a, b) => positionOf(a) - positionOf(b));
16
+ }, [columns, viewColumns, isNarrow]);
17
+ }
@@ -0,0 +1,8 @@
1
+ import type { ReactNode } from "react";
2
+ import type { DataTableViewOptions } from "../../../data-table-view-types.ts";
3
+ /** The menu also opens for pinning or resizing alone: pins get a section, widths only the reset. */
4
+ export declare function viewHasMenu(view: DataTableViewOptions | undefined): view is DataTableViewOptions;
5
+ export declare function ViewMenu({ view, columnLabels, }: {
6
+ readonly view: DataTableViewOptions;
7
+ readonly columnLabels: ReadonlyMap<string, string>;
8
+ }): ReactNode;
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
3
+ import { Columns3 } from "lucide-react";
4
+ import { useOptionalTranslation } from "../../../index.js";
5
+ import { Button as UiButton } from "../../ui/button.js";
6
+ import { SegmentedControl } from "../segmented-select.js";
7
+ import { ColumnPickerSection } from "./column-picker-section.js";
8
+ import { PinSection } from "./pin-section.js";
9
+ const POPOVER_CLASS = "z-50 flex w-72 max-w-[calc(100vw-2rem)] flex-col gap-3 rounded-md border bg-popover p-3 " +
10
+ "text-popover-foreground shadow-md max-h-[var(--radix-popover-content-available-height)] overflow-y-auto";
11
+ const SECTION_TITLE_CLASS = "m-0 text-xs font-medium text-muted-foreground";
12
+ /** The menu also opens for pinning or resizing alone: pins get a section, widths only the reset. */
13
+ export function viewHasMenu(view) {
14
+ if (view === undefined)
15
+ return false;
16
+ return ((view.columns !== undefined && view.onColumnsChange !== undefined) ||
17
+ view.onDensityChange !== undefined ||
18
+ view.groupBy !== undefined ||
19
+ (view.pins !== undefined && view.onPinsChange !== undefined) ||
20
+ (view.widths !== undefined && view.onWidthChange !== undefined));
21
+ }
22
+ export function ViewMenu({ view, columnLabels, }) {
23
+ const translate = useOptionalTranslation();
24
+ const label = (key, fallback) => translate?.(key) ?? fallback;
25
+ const { columns, onColumnsChange, onDensityChange, pins, onPinsChange } = view;
26
+ const hasColumns = columns !== undefined && onColumnsChange !== undefined;
27
+ if (!viewHasMenu(view))
28
+ return null;
29
+ const pinnableFields = columns !== undefined
30
+ ? columns.filter((column) => column.visible).map((column) => column.field)
31
+ : [...columnLabels.keys()];
32
+ return (_jsxs(PopoverPrimitive.Root, { children: [_jsx(PopoverPrimitive.Trigger, { asChild: true, children: _jsx(UiButton, { type: "button", variant: "outline", size: "icon", className: "size-9 max-md:size-11", "aria-label": label("kumiko.list.view.menu", "View"), "data-testid": "list-view-menu", children: _jsx(Columns3, { className: "size-4", "aria-hidden": "true" }) }) }), _jsx(PopoverPrimitive.Portal, { children: _jsxs(PopoverPrimitive.Content, { align: "end", sideOffset: 4, className: POPOVER_CLASS, "data-testid": "list-view-menu-content", children: [hasColumns && (_jsxs("section", { className: "flex flex-col gap-1", children: [_jsx("h3", { className: SECTION_TITLE_CLASS, children: label("kumiko.list.view.columns", "Columns") }), _jsx(ColumnPickerSection, { columns: columns, labels: columnLabels, onChange: onColumnsChange })] })), onDensityChange !== undefined && (_jsxs("section", { className: "flex flex-col gap-1", children: [_jsx("h3", { className: SECTION_TITLE_CLASS, children: label("kumiko.list.view.density", "Density") }), _jsx(SegmentedControl, { id: "list-view-density", name: "list-view-density", value: view.density, onChange: (next) => onDensityChange(next === "compact" ? "compact" : "comfortable"), options: [
33
+ {
34
+ value: "comfortable",
35
+ label: label("kumiko.list.view.density.comfortable", "Comfortable"),
36
+ },
37
+ { value: "compact", label: label("kumiko.list.view.density.compact", "Compact") },
38
+ ], labelProps: { "aria-label": label("kumiko.list.view.density", "Density") } })] })), view.groupBy !== undefined && (_jsxs("section", { className: "flex flex-col gap-1", children: [_jsx("h3", { className: SECTION_TITLE_CLASS, children: label("kumiko.list.group.by", "Group by") }), _jsx(SegmentedControl, { id: "list-view-group-by", name: "list-view-group-by", value: view.groupBy.value, onChange: view.groupBy.onChange, options: [
39
+ { value: "", label: label("kumiko.list.group.none", "None") },
40
+ ...view.groupBy.fields.map((field) => ({
41
+ value: field,
42
+ label: columnLabels.get(field) ?? field,
43
+ })),
44
+ ], labelProps: { "aria-label": label("kumiko.list.group.by", "Group by") } })] })), pins !== undefined && onPinsChange !== undefined && (_jsxs("section", { className: "flex flex-col gap-1", children: [_jsx("h3", { className: SECTION_TITLE_CLASS, children: label("kumiko.list.view.pinning", "Pinned columns") }), _jsx(PinSection, { fields: pinnableFields, labels: columnLabels, pins: pins, onChange: onPinsChange })] })), _jsx(UiButton, { type: "button", variant: "ghost", size: "sm", disabled: view.viewIsDefault, onClick: view.onViewReset, "data-testid": "list-view-reset", children: label("kumiko.list.view.reset", "Reset view") })] }) })] }));
45
+ }
@@ -1,8 +1,6 @@
1
1
  import type { SelectOptionTone } from "@cosmicdrift/kumiko-framework/ui-types";
2
2
  import { type ReactNode } from "react";
3
3
  import type { CellLink, CellLinkItem, ListColumnFileDisplay, ListRowViewModel, RuntimeCellLink, RuntimeCellLinkItems } from "../../headless/index.ts";
4
- import type { DataTableSort } from "../../index.ts";
5
- import { type DataTableRowGrouping } from "../../index.ts";
6
4
  import "../react-css-custom-properties.ts";
7
5
  export declare function isEmptyCellValue(value: unknown): boolean;
8
6
  export declare function isUnlabeledFalse(type: string, value: unknown, renderer: unknown): boolean;
@@ -19,16 +17,6 @@ export declare function FirstCellLink({ enabled, empty, emptyLabel, onOpen, chil
19
17
  readonly children: ReactNode;
20
18
  }): ReactNode;
21
19
  export declare const FIT_CONTENT_CELL_CLASS = "w-px";
22
- export declare function SortableHeader({ field, label, sortable, highlighted, numeric, fitContent, sort, onSortChange, }: {
23
- readonly field: string;
24
- readonly label: string;
25
- readonly sortable: boolean;
26
- readonly highlighted?: boolean;
27
- readonly numeric?: boolean;
28
- readonly fitContent?: boolean;
29
- readonly sort?: DataTableSort;
30
- readonly onSortChange?: (next: DataTableSort | null) => void;
31
- }): ReactNode;
32
20
  export declare function isComponentRendererRef(renderer: unknown): {
33
21
  readonly name: string;
34
22
  } | undefined;
@@ -59,16 +47,6 @@ export declare function cellLinkProp(col: {
59
47
  readonly link?: CellLink;
60
48
  readonly linkItems?: readonly CellLinkItem[];
61
49
  };
62
- export type TableItem = {
63
- readonly kind: "group";
64
- readonly key: string;
65
- readonly label: string;
66
- readonly collapsed: boolean;
67
- } | {
68
- readonly kind: "row";
69
- readonly row: ListRowViewModel;
70
- };
71
- export declare function buildTableItems(rows: readonly ListRowViewModel[], grouping: DataTableRowGrouping | undefined, toggledGroups: ReadonlySet<string>): readonly TableItem[];
72
50
  export declare function rowToneClass(tone: SelectOptionTone | undefined): string;
73
51
  export declare const NUMERIC_COLUMN_TYPES: ReadonlySet<string>;
74
52
  export declare const TABULAR_COLUMN_TYPES: ReadonlySet<string>;
@@ -1,14 +1,12 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-types/kms-adapter-types";
3
3
  import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
4
- import { ArrowDown, ArrowUp, ArrowUpDown, Check } from "lucide-react";
4
+ import { Check } from "lucide-react";
5
5
  import { Fragment } from "react";
6
6
  import { rendererWarn } from "../../dev-console.js";
7
7
  import { applyFormatSpec, EMPTY_VALUE_PLACEHOLDER, isSafeHref } from "../../headless/index.js";
8
8
  import { statusToneForOptionTone, statusToneForValue, useColumnRenderer, useOptionalPrimitives, } from "../../index.js";
9
- import { cn } from "../lib/cn.js";
10
9
  import { Badge } from "../ui/badge.js";
11
- import { TableHead } from "../ui/table.js";
12
10
  import { StatusBadge } from "../widgets/status-badge.js";
13
11
  import { FileDisplayCell } from "./file-thumbnail.js";
14
12
  import { currencyDecimals, formatMoney } from "./money-input.js";
@@ -88,41 +86,9 @@ export function FirstCellLink({ enabled, empty, emptyLabel, onOpen, children, })
88
86
  onOpen();
89
87
  }, children: empty ? _jsx("span", { className: "sr-only", children: emptyLabel }) : children }));
90
88
  }
91
- // SortableHeader: renders the th header per column, with or without
92
- // click-sort. Three paths:
93
- // (a) sortable=false OR no onSortChange -> plain label, no cursor
94
- // interaction (DataTable purely as a view without sort wiring).
95
- // (b) sortable=true + onSortChange -> header is a button, click
96
- // cycles asc -> desc -> null. aria-sort mirrors the state.
97
- // (c) sortable=true in the schema but no onSortChange prop -> still
98
- // label-only, but data-sortable=true stays so tests that know the
99
- // schema view keep working.
100
89
  // w-px on an auto-layout table cell shrinks the column to its content (the
101
90
  // actions column relies on the same behaviour).
102
91
  export const FIT_CONTENT_CELL_CLASS = "w-px";
103
- export function SortableHeader({ field, label, sortable, highlighted, numeric, fitContent, sort, onSortChange, }) {
104
- const active = sort?.field === field ? sort : undefined;
105
- const ariaSort = active?.dir === "asc" ? "ascending" : active?.dir === "desc" ? "descending" : "none";
106
- if (!sortable || onSortChange === undefined) {
107
- return (_jsx(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"), children: label }));
108
- }
109
- const Icon = active?.dir === "asc" ? ArrowUp : active?.dir === "desc" ? ArrowDown : ArrowUpDown;
110
- const next = nextSortState(active?.dir, field);
111
- return (_jsx(TableHead, { "data-testid": `column-${field}`, "data-sortable": "true", "data-highlighted": highlighted === true ? "true" : undefined, "aria-sort": ariaSort, className: cn(active !== undefined
112
- ? "font-semibold text-foreground"
113
- : "font-medium text-muted-foreground", "whitespace-normal", numeric === true && "text-right", fitContent === true && FIT_CONTENT_CELL_CLASS, highlighted === true && "bg-accent/40"), 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 &&
114
- "opacity-0 group-hover/sort:opacity-40 group-focus-visible/sort:opacity-40"), "aria-hidden": "true" })] }) }));
115
- }
116
- // 3-state toggle: no sort -> asc -> desc -> no sort. Idiomatic for
117
- // power-user lists where "I want the server default order back" is a
118
- // real action (instead of toggling asc<->desc forever).
119
- function nextSortState(current, field) {
120
- if (current === undefined)
121
- return { field, dir: "asc" };
122
- if (current === "asc")
123
- return { field, dir: "desc" };
124
- return null;
125
- }
126
92
  // Type guard for the `{ react: { __component: "Name" } }` form in which
127
93
  // PlatformComponent renderers arrive serialized in the schema. Schemas
128
94
  // travel over the wire (server -> client), real component refs would break
@@ -272,29 +238,6 @@ export function cellLinkProp(col, row, allowed) {
272
238
  ...(linkItems !== undefined && { linkItems }),
273
239
  };
274
240
  }
275
- export function buildTableItems(rows, grouping, toggledGroups) {
276
- if (grouping === undefined)
277
- return rows.map((row) => ({ kind: "row", row }));
278
- const groups = new Map();
279
- for (const row of rows) {
280
- const key = grouping.keyOf(row);
281
- const members = groups.get(key);
282
- if (members === undefined)
283
- groups.set(key, [row]);
284
- else
285
- members.push(row);
286
- }
287
- return [...groups].flatMap(([key, members]) => {
288
- const label = grouping.headerLabel(key, members);
289
- if (label === undefined)
290
- return members.map((row) => ({ kind: "row", row }));
291
- const collapsed = grouping.startsCollapsed(key) !== toggledGroups.has(key);
292
- return [
293
- { kind: "group", key, label, collapsed },
294
- ...(collapsed ? [] : members.map((row) => ({ kind: "row", row }))),
295
- ];
296
- });
297
- }
298
241
  const ROW_TONE_CLASS = {
299
242
  bad: "bg-status-bad/10 hover:bg-status-bad/15",
300
243
  warn: "bg-status-warn/10 hover:bg-status-warn/15",
@@ -1,4 +1,4 @@
1
1
  import { type ReactNode } from "react";
2
2
  import { type DataTableProps } from "../../index.ts";
3
3
  import "../react-css-custom-properties.ts";
4
- export declare function DefaultDataTable({ columns, rows, onRowClick, sort, onSortChange, emptyState, toolbarStart, toolbarDescription, toolbarEnd, pager, onReachEnd, loadingMore, hasMore, itemNoun, rowActions, rowActionMode, rowGrouping, rowTone, filterFacets, filterValues, onFilterChange, onFilterReset, dateRangeFacets, onDateRangeChange, testId, onCellChange, getRowTestId, getCellTestId, chromeless, scrollBody, screenPadding, expandedRowIds, onToggleRowExpanded, renderExpandedRow, }: DataTableProps): ReactNode;
4
+ export declare function DefaultDataTable({ columns: schemaColumns, rows, onRowClick, sort, onSortChange, emptyState, toolbarStart, toolbarDescription, toolbarEnd, pager, onReachEnd, loadingMore, hasMore, itemNoun, rowActions, rowActionMode, bulk, rowTone, filterFacets, filterValues, onFilterChange, onFilterReset, dateRangeFacets, onDateRangeChange, testId, onCellChange, getRowTestId, getCellTestId, chromeless, scrollBody, screenPadding, expandedRowIds, onToggleRowExpanded, renderExpandedRow, view, resetAll, exportList, serverGroups, totals, notice, rangeFacets, onRangeChange, }: DataTableProps): ReactNode;