@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
@@ -1,8 +1,9 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cardColumnRoles } from "@cosmicdrift/kumiko-framework/ui-types";
3
- import { ArrowDown, ArrowUp, ChevronDown, ChevronRight, Search, SlidersHorizontal, X, } from "lucide-react";
4
- import { Fragment, useId, useState, } from "react";
3
+ import { ChevronRight, Search, SlidersHorizontal } from "lucide-react";
4
+ import { Fragment, useId, useState } from "react";
5
5
  import { useOptionalLocale, useOptionalTranslation } from "../../index.js";
6
+ import { UNIFORM_BAR_CONTROLS_CLASSES } from "../layout/uniform-bar-controls.js";
6
7
  import { cn } from "../lib/cn.js";
7
8
  import { Button as UiButton } from "../ui/button.js";
8
9
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table.js";
@@ -10,54 +11,28 @@ import { CardLeadingImage } from "./file-thumbnail.js";
10
11
  import { screenPaddingClassName } from "./layout.js";
11
12
  import { useIsNarrowViewport } from "./use-narrow-viewport.js";
12
13
  import "../react-css-custom-properties.js";
13
- import { buildTableItems, cellLinkProp, cellTitle, DataTableCell, FIT_CONTENT_CELL_CLASS, FirstCellLink, isBadgeColumn, isEmptyCellValue, isUnlabeledFalse, NUMERIC_COLUMN_TYPES, rowToneClass, SortableHeader, showsBareCheckMark, TABULAR_COLUMN_TYPES, } from "./data-table-cell.js";
14
- import { DateRangeFacetFilter, FacetFilter } from "./facet-filter.js";
15
- import { InfiniteSentinel, Pager, TABLE_FOOTER_BAR_CLASS } from "./pager.js";
14
+ import { ActionsBodyCell, ActionsHeaderCell } from "./data-table/actions-column.js";
15
+ import { CardSortSelect } from "./data-table/card-sort-select.js";
16
+ import { isPinningActive, isResizingActive } from "./data-table/column-layout-mode.js";
17
+ import { ColumnResizeHandle, ColumnWidthGroup } from "./data-table/column-resize-handle.js";
18
+ import { densityClasses } from "./data-table/density.js";
19
+ import { buildFacetCluster } from "./data-table/facet-cluster.js";
20
+ import { bandClassName, bandReactKey, bandTestId, buildDataTableItems, GroupBand, } from "./data-table/group-band.js";
21
+ import { SortableHeader } from "./data-table/sortable-header.js";
22
+ import { EXPAND_TOGGLE_CELL_CLASS, EXPANSION_CELL_CLASS, EXPANSION_CONTENT_CLASS, FILL_TABLE_CLASS, FlushTable, RowExpandToggle, } from "./data-table/table-chrome.js";
23
+ import { DataTableFooter, showFullTextOnHover } from "./data-table/table-footer.js";
24
+ import { ToolbarEndCluster, toolbarEndHasContent } from "./data-table/toolbar-end-cluster.js";
25
+ import { TotalsCard, TotalsFooterRow } from "./data-table/totals-footer.js";
26
+ import { useBulkSelection } from "./data-table/use-bulk-selection.js";
27
+ import { EXPAND_COLUMN_KEY, SELECT_COLUMN_KEY, useColumnPins, } from "./data-table/use-column-pins.js";
28
+ import { useColumnSizing } from "./data-table/use-column-sizing.js";
29
+ import { useMeasuredColumnWidths } from "./data-table/use-measured-column-widths.js";
30
+ import { useRowKeyboardNavigation } from "./data-table/use-row-keyboard-navigation.js";
31
+ import { useViewColumns } from "./data-table/use-view-columns.js";
32
+ import { cellLinkProp, cellTitle, DataTableCell, FIT_CONTENT_CELL_CLASS, FirstCellLink, isBadgeColumn, isEmptyCellValue, isUnlabeledFalse, NUMERIC_COLUMN_TYPES, rowToneClass, showsBareCheckMark, TABULAR_COLUMN_TYPES, } from "./data-table-cell.js";
33
+ import { InfiniteSentinel, Pager } from "./pager.js";
16
34
  import { RowActionsCell } from "./row-actions.js";
17
- // The vendored Table wraps <table> in an `overflow-x-auto` container, which
18
- // would become the scrollport of a sticky header. A scrollBody table scrolls
19
- // in its parent instead, so it renders the bare <table>.
20
- function FlushTable({ className, ...props }) {
21
- return (_jsx("table", { "data-slot": "table", className: cn("w-full caption-bottom text-sm", className), ...props }));
22
- }
23
- // Header cells stick to the scroll surface; the bottom rule is an inset
24
- // shadow because a border on a sticky cell scrolls away under border-collapse.
25
- // Column weight (medium vs. semibold for the sorted one) lives on the th itself
26
- // in SortableHeader: a `[&_th]:font-*` rule here would outrank it.
27
- // Child selectors only: an expanded row nests a whole table inside a cell, and
28
- // descendant selectors would make its header sticky and re-pad its cells.
29
- 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");
30
- // `!`: beats the first/last-child padding of FILL_TABLE_CLASS, which would
31
- // otherwise widen the narrow toggle column / pad the full-width expansion cell.
32
- // The nested table's sticky actions cell (last td) gets the muted surface
33
- // instead of its own md:bg-background, so it blends into the area.
34
- const EXPAND_TOGGLE_CELL_CLASS = "w-10 px-1! py-0!";
35
- const EXPANSION_CELL_CLASS = "whitespace-normal bg-muted p-0! align-top md:[&_td:last-child]:bg-muted";
36
- const EXPANSION_CONTENT_CLASS = "pt-1 pr-4 pb-3.5 pl-[52px]";
37
- function RowExpandToggle({ expanded, controlsId, label, onToggle, testId, }) {
38
- 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) => {
39
- e.stopPropagation();
40
- onToggle();
41
- }, onKeyDown: (e) => e.stopPropagation(), children: _jsx(ChevronRight, { className: cn("size-4 transition-transform", expanded && "rotate-90"), "aria-hidden": "true" }) }));
42
- }
43
- function DataTableFooter({ count, itemNoun, testId, }) {
44
- const t = useOptionalTranslation();
45
- const noun = itemNoun?.(count);
46
- const label = noun !== undefined
47
- ? (t?.("kumiko.list.count.noun", { count: count.toLocaleString(), noun }) ??
48
- `${count.toLocaleString()} ${noun}`)
49
- : count === 1
50
- ? (t?.("kumiko.list.count.one") ?? "1 entry")
51
- : (t?.("kumiko.list.count.other", { count: count.toLocaleString() }) ??
52
- `${count.toLocaleString()} entries`);
53
- return (_jsx("div", { "data-testid": testId, className: TABLE_FOOTER_BAR_CLASS, children: _jsx("span", { "data-testid": `${testId}-count`, children: label }) }));
54
- }
55
- // Cell content is a ReactNode, so the tooltip text is read from the rendered DOM.
56
- function showFullTextOnHover(event) {
57
- const element = event.currentTarget;
58
- element.title = element.scrollWidth > element.clientWidth ? (element.textContent ?? "") : "";
59
- }
60
- export 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, }) {
35
+ export 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, }) {
61
36
  // One locale/translate subscription per table — not per cell.
62
37
  // Optional hooks: a bare DataTable outside LocaleProvider must not crash.
63
38
  const tableTranslate = useOptionalTranslation();
@@ -66,8 +41,9 @@ export function DefaultDataTable({ columns, rows, onRowClick, sort, onSortChange
66
41
  // affordance. Cards replace the table entirely below the breakpoint — same
67
42
  // single-mount pattern as EmbeddedListInput/embedded-list-input.tsx.
68
43
  const isNarrow = useIsNarrowViewport();
44
+ const viewColumns = useViewColumns(schemaColumns, view, isNarrow);
45
+ const density = densityClasses(view, scrollBody === true);
69
46
  const [facetsOpenNarrow, setFacetsOpenNarrow] = useState(false);
70
- const [toggledGroups, setToggledGroups] = useState(new Set());
71
47
  // Toolbar wrapper: shared container for toolbar + table so the two belong
72
48
  // together visually. The toolbar is NOT sticky: lists typically scroll with
73
49
  // the page container, not internally. Sticky would compete with the topbar.
@@ -86,10 +62,39 @@ export function DefaultDataTable({ columns, rows, onRowClick, sort, onSortChange
86
62
  const rowClickLabel = rowClickAction?.label;
87
63
  const rowClickIsHidden = (row) => rowClickAction?.isVisible !== undefined && !rowClickAction.isVisible(row);
88
64
  const hasTableActions = tableActions !== undefined && tableActions.length > 0;
89
- const isEmpty = rows.length === 0;
65
+ const isEmpty = rows.length === 0 && (serverGroups?.groups.length ?? 0) === 0;
90
66
  const expansionIdPrefix = useId();
91
- const isRowExpandable = renderExpandedRow !== undefined && onToggleRowExpanded !== undefined;
92
67
  const rowTestId = (row) => getRowTestId?.(row) ?? `row-${row.id}`;
68
+ const isRowExpandable = renderExpandedRow !== undefined && onToggleRowExpanded !== undefined;
69
+ const fillsHeight = scrollBody === true;
70
+ const selection = useBulkSelection({
71
+ bulk,
72
+ rows,
73
+ titleField: schemaColumns[0]?.field,
74
+ rowTestId,
75
+ flush: fillsHeight,
76
+ });
77
+ const { headerRowRef, measured } = useMeasuredColumnWidths(isPinningActive(view, isNarrow) || isResizingActive(view, isNarrow));
78
+ const pins = useColumnPins({
79
+ columns: viewColumns,
80
+ view,
81
+ isNarrow,
82
+ measured,
83
+ hasActionsColumn: hasTableActions,
84
+ hasExpandColumn: isRowExpandable,
85
+ hasSelectColumn: selection.active,
86
+ fillsHeight,
87
+ });
88
+ const columns = pins.columns;
89
+ const sizing = useColumnSizing({
90
+ columns,
91
+ view,
92
+ isNarrow,
93
+ measured,
94
+ hasExpandColumn: isRowExpandable,
95
+ hasSelectColumn: selection.active,
96
+ });
97
+ const fullColSpan = columns.length + [isRowExpandable, hasTableActions, selection.active].filter(Boolean).length;
93
98
  const expansionDomId = (row) => `${expansionIdPrefix}-${row.id}`;
94
99
  // The expand toggle of a row, shared by table and card layout. Named after
95
100
  // the first data column, the one a user recognizes the row by.
@@ -111,75 +116,59 @@ export function DefaultDataTable({ columns, rows, onRowClick, sort, onSortChange
111
116
  }
112
117
  return (_jsx(TableRow, { id: expansionDomId(row), "data-testid": `${rowTestId(row)}-expansion`, className: "hover:bg-transparent", children: _jsx(TableCell, { colSpan: colSpan, className: EXPANSION_CELL_CLASS, children: _jsx("div", { className: EXPANSION_CONTENT_CLASS, children: content }) }) }));
113
118
  }
114
- const fillsHeight = scrollBody === true;
115
119
  const TableRoot = fillsHeight ? FlushTable : Table;
116
120
  const emptyBlock = (_jsx("div", { "data-testid": testId !== undefined ? `${testId}-empty` : "render-list-empty", className: cn("flex flex-col items-center justify-center p-12 text-sm text-muted-foreground gap-3", chromeless !== true && !fillsHeight && "rounded-md border border-dashed"), children: emptyState ?? _jsx("span", { children: tableTranslate?.("kumiko.list.no-entries") ?? "No entries." }) }));
117
- const tableItems = buildTableItems(rows, rowGrouping, toggledGroups);
118
- function renderGroupToggle(item) {
119
- return (_jsxs("button", { type: "button", "aria-expanded": !item.collapsed, "data-testid": `row-group-${item.key}-toggle`, onClick: () => setToggledGroups((prev) => {
120
- const next = new Set(prev);
121
- if (!next.delete(item.key))
122
- next.add(item.key);
123
- return next;
124
- }), className: "flex w-full items-center gap-2 px-4 py-2 text-left text-xs font-medium text-foreground-secondary", children: [item.collapsed ? (_jsx(ChevronRight, { className: "size-3.5" })) : (_jsx(ChevronDown, { className: "size-3.5" })), item.label] }));
121
+ const tableItems = buildDataTableItems(rows, serverGroups);
122
+ const keyboardRow = useRowKeyboardNavigation({
123
+ view,
124
+ isNarrow,
125
+ rowIds: tableItems.flatMap((item) => (item.kind === "row" ? [item.row.id] : [])),
126
+ onSelect: selection.onKeyboardSelect,
127
+ });
128
+ const schemaColumnByField = new Map(schemaColumns.map((col) => [col.field, col]));
129
+ const columnOf = (field) => schemaColumnByField.get(field);
130
+ function renderBand(item) {
131
+ return _jsx(GroupBand, { item: item, columnOf: columnOf });
125
132
  }
126
133
  function tableInner() {
127
134
  if (isEmpty)
128
135
  return emptyBlock;
129
- return (_jsx("div", { className: cn(!fillsHeight && "overflow-hidden", !fillsHeight && chromeless !== true && "rounded-lg border bg-card"), children: _jsxs(TableRoot, { "data-testid": testId, className: fillsHeight ? FILL_TABLE_CLASS : undefined, children: [_jsx(TableHeader, { className: "bg-muted", children: _jsxs(TableRow, { className: "hover:bg-transparent", children: [isRowExpandable && (_jsx(TableHead, { "data-testid": "column-expand", className: EXPAND_TOGGLE_CELL_CLASS })), columns.map((col) => (_jsx(SortableHeader, { field: col.field, label: col.label, sortable: col.sortable === true, highlighted: col.highlighted === true, numeric: NUMERIC_COLUMN_TYPES.has(col.type), fitContent: col.fileDisplay !== undefined, ...(sort !== undefined && sort !== null && { sort }), ...(onSortChange !== undefined && { onSortChange }) }, col.field))), hasTableActions && (_jsx(TableHead, { "data-testid": "column-actions",
130
- // From md: sticky right-0 + bg-muted (= header tone) so the
131
- // action column stays at the right edge during horizontal
132
- // scroll on wider screens. Below md, actions scroll in
133
- // normal flow — sticky there would pin the column over the
134
- // preceding data column instead of the (already narrow)
135
- // page edge. No border-l: a permanent divider looks heavy;
136
- // the sticky bg already sets the column apart during scroll.
137
- className: "w-px text-right text-muted-foreground md:sticky md:right-0 md:z-10 md:bg-muted", "aria-label": tableTranslate?.("kumiko.list.actions") ?? "Actions" }))] }) }), _jsx(TableBody, { children: tableItems.map((item) => {
138
- if (item.kind === "group") {
139
- return (_jsx(TableRow, { "data-testid": `row-group-${item.key}`, children: _jsx(TableCell, { colSpan: columns.length + (isRowExpandable ? 1 : 0) + (hasTableActions ? 1 : 0), className: "bg-muted/40 p-0", children: renderGroupToggle(item) }) }, `group-${item.key}`));
136
+ const expandHeaderPin = pins.headerCell(EXPAND_COLUMN_KEY);
137
+ const expandBodyPin = pins.bodyCell(EXPAND_COLUMN_KEY);
138
+ return (_jsx("div", { className: cn(!fillsHeight && "overflow-hidden", !fillsHeight && chromeless !== true && "rounded-lg border bg-card"), children: _jsxs(TableRoot, { "data-testid": testId, className: fillsHeight ? FILL_TABLE_CLASS : undefined, ...(sizing !== undefined && { style: sizing.tableStyle }), children: [sizing !== undefined && (_jsx(ColumnWidthGroup, { fields: columns.map((col) => col.field), sizing: sizing, hasExpandColumn: isRowExpandable, hasSelectColumn: selection.active, hasActionsColumn: hasTableActions })), _jsx(TableHeader, { className: "bg-muted", children: _jsxs(TableRow, { ref: headerRowRef, className: "hover:bg-transparent", children: [selection.headerCell(pins.headerCell(SELECT_COLUMN_KEY)), isRowExpandable && (_jsx(TableHead, { "data-testid": "column-expand", className: cn(EXPAND_TOGGLE_CELL_CLASS, expandHeaderPin.className), style: expandHeaderPin.style })), columns.map((col) => {
139
+ const headerPin = pins.headerCell(col.field);
140
+ return (_jsx(SortableHeader, { field: col.field, label: col.label, sortable: col.sortable === true, highlighted: col.highlighted === true, numeric: NUMERIC_COLUMN_TYPES.has(col.type), fitContent: col.fileDisplay !== undefined, className: cn(density.header, headerPin.className), style: headerPin.style, columnField: headerPin.columnField, trailing: sizing && (_jsx(ColumnResizeHandle, { field: col.field, label: col.label, sizing: sizing })), ...(sort !== undefined && sort !== null && { sort }), ...(onSortChange !== undefined && { onSortChange }) }, col.field));
141
+ }), hasTableActions && (_jsx(ActionsHeaderCell, { label: tableTranslate?.("kumiko.list.actions") ?? "Actions", style: pins.actionsHeaderStyle, absorbsLeftover: sizing !== undefined }))] }) }), _jsx(TableBody, { children: tableItems.map((item) => {
142
+ if (item.kind !== "row") {
143
+ return (_jsx(TableRow, { "data-testid": bandTestId(item), children: _jsx(TableCell, { colSpan: fullColSpan, className: bandClassName(item), children: renderBand(item) }) }, bandReactKey(item)));
140
144
  }
141
145
  const row = item.row;
142
146
  const opensOnClick = rowIsLink && !rowClickIsHidden(row);
143
- return (_jsxs(Fragment, { children: [_jsxs(TableRow, { "data-testid": rowTestId(row), "data-tone": rowTone?.(row), onClick: opensOnClick ? () => onRowClick(row) : undefined, className: cn(rowToneClass(rowTone?.(row)), opensOnClick && "cursor-pointer", fillsHeight && "h-10 border-border-row hover:bg-muted"), children: [isRowExpandable && (_jsx(TableCell, { "data-testid": getCellTestId?.(row, "expand") ?? `cell-${row.id}-expand`, className: EXPAND_TOGGLE_CELL_CLASS, children: renderExpandToggle(row) })), columns.map((col, colIndex) => (_jsx(TableCell, { "data-testid": getCellTestId?.(row, col.field) ?? `cell-${row.id}-${col.field}`, "data-highlighted": col.highlighted === true ? "true" : undefined,
144
- // Text wraps instead of truncating: nowrap cells keep their
145
- // full width, so a wide table scrolled sideways and its
146
- // last column slid under the sticky actions column with
147
- // nothing visibly cut off. max-w-xs bounds a single column;
148
- // badges and dates stay nowrap by their own classes.
149
- className: cn(col.fileDisplay === undefined
150
- ? "max-w-xs whitespace-normal break-words"
151
- : FIT_CONTENT_CELL_CLASS, colIndex === 0
152
- ? "font-medium text-foreground"
153
- : "text-foreground-secondary", NUMERIC_COLUMN_TYPES.has(col.type) && "text-right", TABULAR_COLUMN_TYPES.has(col.type) && "tabular-nums", col.highlighted === true && "bg-accent/40"), title: cellTitle(row.values[col.field]), children: _jsx(FirstCellLink, { enabled: colIndex === 0 && opensOnClick && onCellChange === undefined, onOpen: () => onRowClick?.(row), empty: isEmptyCellValue(row.values[col.field]), emptyLabel: rowClickLabel, children: _jsx(DataTableCell, { value: row.values[col.field], row: row.values, field: col.field, type: col.type, renderer: col.renderer, translate: tableTranslate, locale: tableLocale, ...(col.optionLabels !== undefined && {
154
- optionLabels: col.optionLabels,
155
- }), ...(col.optionTones !== undefined && { optionTones: col.optionTones }), ...(col.grouping !== undefined && { grouping: col.grouping }), ...(col.fileDisplay !== undefined && { fileDisplay: col.fileDisplay }), ...(onCellChange !== undefined && {
156
- onChange: (value) => onCellChange(row.id, col.field, value),
157
- }), ...cellLinkProp(col, row, !(colIndex === 0 && opensOnClick && onCellChange === undefined)) }) }) }, col.field))), hasTableActions && (_jsx(TableCell, { "data-testid": getCellTestId?.(row, "actions") ?? `cell-${row.id}-actions`,
158
- // From md: sticky-right so the actions stay visible on the
159
- // right edge during horizontal scroll. Below md, actions
160
- // scroll with the row like any other cell — sticky there
161
- // would hide the neighboring data column on narrow
162
- // viewports. bg-background sets the column apart during
163
- // scroll — no border-l (divider too heavy).
164
- className: cn("whitespace-nowrap text-right md:sticky md:right-0 md:z-10 md:bg-background", fillsHeight && "md:bg-card"),
165
- // Action-cell events must not trigger the row click/activation
166
- // (typically "Open Detail" — the user wanted the action,
167
- // not navigation). We stopPropagation for mouse and
168
- // keyboard so a11y stays consistent.
169
- onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: _jsx(RowActionsCell, { row: row, actions: tableActions, mode: rowActionMode, rowIsLink: rowIsLink }) }))] }), renderExpansion(row, "row", columns.length + (isRowExpandable ? 1 : 0) + (hasTableActions ? 1 : 0))] }, row.id));
170
- }) })] }) }));
147
+ const keyboard = keyboardRow(row.id, opensOnClick ? () => onRowClick(row) : undefined);
148
+ return (_jsxs(Fragment, { children: [_jsxs(TableRow, { "data-testid": rowTestId(row), "data-tone": rowTone?.(row), onClick: opensOnClick ? () => onRowClick(row) : undefined, className: cn(rowToneClass(rowTone?.(row)), opensOnClick && "cursor-pointer", fillsHeight && "h-10 border-border-row hover:bg-muted", density.row, keyboard.className), ...keyboard.props, children: [selection.bodyCell(row, pins.bodyCell(SELECT_COLUMN_KEY)), isRowExpandable && (_jsx(TableCell, { "data-testid": getCellTestId?.(row, "expand") ?? `cell-${row.id}-expand`, className: cn(EXPAND_TOGGLE_CELL_CLASS, expandBodyPin.className), style: expandBodyPin.style, children: renderExpandToggle(row) })), columns.map((col, colIndex) => {
149
+ const bodyPin = pins.bodyCell(col.field);
150
+ return (_jsx(TableCell, { "data-testid": getCellTestId?.(row, col.field) ?? `cell-${row.id}-${col.field}`, "data-highlighted": col.highlighted === true ? "true" : undefined,
151
+ // Text wraps instead of truncating: nowrap cells keep their
152
+ // full width, so a wide table scrolled sideways and its
153
+ // last column slid under the sticky actions column with
154
+ // nothing visibly cut off. max-w-xs bounds a single column;
155
+ // badges and dates stay nowrap by their own classes.
156
+ className: cn(col.fileDisplay === undefined
157
+ ? cn(sizing ? "overflow-hidden" : "max-w-xs", "whitespace-normal break-words")
158
+ : FIT_CONTENT_CELL_CLASS, colIndex === 0
159
+ ? "font-medium text-foreground"
160
+ : "text-foreground-secondary", NUMERIC_COLUMN_TYPES.has(col.type) && "text-right", TABULAR_COLUMN_TYPES.has(col.type) && "tabular-nums", col.highlighted === true && "bg-accent/40", density.cell, bodyPin.className), style: bodyPin.style, title: cellTitle(row.values[col.field]), children: _jsx(FirstCellLink, { enabled: colIndex === 0 && opensOnClick && onCellChange === undefined, onOpen: () => onRowClick?.(row), empty: isEmptyCellValue(row.values[col.field]), emptyLabel: rowClickLabel, children: _jsx(DataTableCell, { value: row.values[col.field], row: row.values, field: col.field, type: col.type, renderer: col.renderer, translate: tableTranslate, locale: tableLocale, ...(col.optionLabels !== undefined && {
161
+ optionLabels: col.optionLabels,
162
+ }), ...(col.optionTones !== undefined && {
163
+ optionTones: col.optionTones,
164
+ }), ...(col.grouping !== undefined && { grouping: col.grouping }), ...(col.fileDisplay !== undefined && {
165
+ fileDisplay: col.fileDisplay,
166
+ }), ...(onCellChange !== undefined && {
167
+ onChange: (value) => onCellChange(row.id, col.field, value),
168
+ }), ...cellLinkProp(col, row, !(colIndex === 0 && opensOnClick && onCellChange === undefined)) }) }) }, col.field));
169
+ }), hasTableActions && (_jsx(ActionsBodyCell, { testId: getCellTestId?.(row, "actions") ?? `cell-${row.id}-actions`, fillsHeight: fillsHeight, measureContent: sizing !== undefined, children: _jsx(RowActionsCell, { row: row, actions: tableActions, mode: rowActionMode, rowIsLink: rowIsLink }) }))] }), renderExpansion(row, "row", fullColSpan)] }, row.id));
170
+ }) }), totals !== undefined && (_jsx(TotalsFooterRow, { totals: totals, columns: columns, cellPin: pins.bodyCell, leadingCells: { select: selection.active, expand: isRowExpandable }, actionsCell: hasTableActions ? { fillsHeight } : undefined, cellClassName: density.cell }))] }) }));
171
171
  }
172
- // No column headers below the breakpoint, so the click-to-sort affordance
173
- // on SortableHeader has nothing to attach to. A single native <select> is
174
- // the whole fix — no custom widget, no menu — fed straight from the
175
- // columns marked sortable in the ViewModel. Options carry the resolved
176
- // {field, dir} directly so onChange can look the pick up by value instead
177
- // of parsing/casting the option string back apart.
178
- const sortableColumns = columns.filter((col) => col.sortable);
179
- const sortOptions = sortableColumns.flatMap((col) => [
180
- { value: `${col.field}:asc`, field: col.field, dir: "asc", label: `${col.label} ↑` },
181
- { value: `${col.field}:desc`, field: col.field, dir: "desc", label: `${col.label} ↓` },
182
- ]);
183
172
  const { image: cardImageColumn, title: cardTitleColumn, status: cardStatusColumn, meta: cardMetaColumns, } = cardColumnRoles(columns);
184
173
  function cardCell(row, col) {
185
174
  // A bare check mark in the subtitle line says nothing without its column.
@@ -198,7 +187,7 @@ export function DefaultDataTable({ columns, rows, onRowClick, sort, onSortChange
198
187
  const body = (_jsxs(_Fragment, { children: [cardImageColumn?.fileDisplay !== undefined && (_jsx("div", { "data-testid": `card-image-${row.id}`, className: "shrink-0", children: _jsx(CardLeadingImage, { value: row.values[cardImageColumn.field], display: cardImageColumn.fileDisplay }) })), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [_jsxs("div", { className: "flex min-w-0 items-start justify-between gap-2", children: [cardTitleColumn !== undefined && (_jsx("span", { "data-testid": getCellTestId?.(row, cardTitleColumn.field) ??
199
188
  `cell-${row.id}-${cardTitleColumn.field}`, className: "min-w-0 break-words text-base font-semibold text-foreground", children: cardCell(row, cardTitleColumn) })), showStatus && (_jsx("span", { "data-testid": getCellTestId?.(row, cardStatusColumn.field) ??
200
189
  `cell-${row.id}-${cardStatusColumn.field}`, className: "shrink-0", children: cardCell(row, cardStatusColumn) }))] }), metaColumns.length > 0 && (_jsx("div", { className: "min-w-0 overflow-x-clip", children: _jsx("div", { "data-testid": `card-meta-${row.id}`, className: "-ms-3 flex min-w-0 flex-wrap items-center text-[13px] leading-5 tabular-nums text-foreground-secondary", children: metaColumns.map((col) => (_jsxs("div", { className: "flex h-5 min-w-0 max-w-full items-center", children: [_jsx("span", { "aria-hidden": "true", className: "w-3 shrink-0 text-center", children: "\u00B7" }), _jsx("div", { "data-testid": getCellTestId?.(row, col.field) ?? `cell-${row.id}-${col.field}`, className: "min-w-0 truncate leading-5", onPointerEnter: showFullTextOnHover, children: isBadgeColumn(col) ? (_jsx("span", { className: "inline-flex align-middle", children: cardCell(row, col) })) : (cardCell(row, col)) })] }, col.field))) }) }))] }), opensOnClick && (_jsx(ChevronRight, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" }))] }));
201
- const rowItem = (_jsxs("li", { "data-testid": rowTestId(row), "data-tone": rowTone?.(row), className: cn("flex min-h-[72px] items-center gap-2 border-b border-border-row py-3", rowToneClass(rowTone?.(row)), isRowExpandable ? "pl-2" : "pl-4", hasMenu ? "pr-2" : "pr-4"), ...(opensOnClick && { onClick: () => onRowClick(row) }), children: [renderExpandToggle(row), opensOnClick ? (_jsx("button", { type: "button", className: "flex min-w-0 flex-1 cursor-pointer items-center gap-2 bg-transparent p-0 text-left", onKeyDown: (e) => {
190
+ const rowItem = (_jsxs("li", { "data-testid": rowTestId(row), "data-tone": rowTone?.(row), className: cn("flex min-h-[72px] items-center gap-2 border-b border-border-row py-3", rowToneClass(rowTone?.(row)), isRowExpandable || selection.active ? "pl-2" : "pl-4", hasMenu ? "pr-2" : "pr-4", density.card), ...(opensOnClick && { onClick: () => onRowClick(row) }), children: [selection.cardCheckbox(row), renderExpandToggle(row), opensOnClick ? (_jsx("button", { type: "button", className: "flex min-w-0 flex-1 cursor-pointer items-center gap-2 bg-transparent p-0 text-left", onKeyDown: (e) => {
202
191
  if (e.key !== "Enter")
203
192
  return;
204
193
  e.preventDefault();
@@ -214,47 +203,37 @@ export function DefaultDataTable({ columns, rows, onRowClick, sort, onSortChange
214
203
  function cardsInner() {
215
204
  if (isEmpty)
216
205
  return emptyBlock;
217
- const sortLabel = sort !== undefined && sort !== null
218
- ? (tableTranslate?.("kumiko.list.sort.by", {
219
- column: columns.find((col) => col.field === sort.field)?.label ?? sort.field,
220
- }) ?? `Sorted by ${sort.field}`)
221
- : (tableTranslate?.("kumiko.list.sort.unsorted") ?? "Unsorted");
222
- const SortArrow = sort?.dir === "desc" ? ArrowDown : ArrowUp;
223
- // URL state can carry a sort on a column that is not sortable; without an
224
- // option for it the select would claim "Unsorted" while the list is sorted.
225
- const currentSortValue = sort !== undefined && sort !== null ? `${sort.field}:${sort.dir}` : "";
226
- const currentSortIsUnlisted = sort !== undefined && sort !== null && !sortOptions.some((o) => o.value === currentSortValue);
227
- return (_jsx("div", { "data-testid": testId, className: "contents", children: _jsxs("div", { "data-testid": testId !== undefined ? `${testId}-cards` : "render-list-cards", className: "flex flex-col", children: [onSortChange !== undefined && sortableColumns.length > 0 && (_jsxs("div", { className: "relative flex h-9 items-center gap-1.5 px-4 text-[13px] text-foreground-secondary", children: [_jsx("span", { "aria-hidden": "true", children: sortLabel }), sort !== undefined && sort !== null && (_jsx(SortArrow, { className: "size-3.5", "aria-hidden": "true" })), _jsxs("select", { "aria-label": tableTranslate?.("kumiko.list.sort.label") ?? "Sort", "data-testid": testId !== undefined ? `${testId}-sort` : "render-list-sort", value: currentSortValue, onChange: (e) => {
228
- const raw = e.target.value;
229
- if (raw === "") {
230
- onSortChange(null);
231
- return;
232
- }
233
- const picked = sortOptions.find((o) => o.value === raw);
234
- if (picked === undefined)
235
- return;
236
- onSortChange({ field: picked.field, dir: picked.dir });
237
- }, className: "absolute inset-0 size-full cursor-pointer opacity-0", children: [_jsx("option", { value: "", children: tableTranslate?.("kumiko.list.sort.unsorted") ?? "Unsorted" }), currentSortIsUnlisted && (_jsx("option", { value: currentSortValue, children: `${columns.find((col) => col.field === sort.field)?.label ?? sort.field} ${sort.dir === "desc" ? "↓" : "↑"}` })), sortOptions.map((o) => (_jsx("option", { value: o.value, children: o.label }, o.value)))] })] })), _jsx("ul", { className: "m-0 list-none border-t border-border-row p-0", children: tableItems.map((item) => item.kind === "group" ? (_jsx("li", { "data-testid": `row-group-${item.key}`, className: "border-b border-border-row bg-muted/40", children: renderGroupToggle(item) }, `group-${item.key}`)) : (renderCard(item.row))) })] }) }));
206
+ return (_jsx("div", { "data-testid": testId, className: "contents", children: _jsxs("div", { "data-testid": testId !== undefined ? `${testId}-cards` : "render-list-cards", className: "flex flex-col", children: [_jsx(CardSortSelect, { columns: columns, sort: sort, onSortChange: onSortChange, testId: testId }), _jsx("ul", { className: "m-0 list-none border-t border-border-row p-0", children: tableItems.map((item) => item.kind === "row" ? (renderCard(item.row)) : (_jsx("li", { "data-testid": bandTestId(item), className: cn("border-b border-border-row", bandClassName(item)), children: renderBand(item) }, bandReactKey(item)))) }), totals !== undefined && _jsx(TotalsCard, { totals: totals, columnOf: columnOf })] }) }));
238
207
  }
239
208
  const tableContent = isNarrow ? cardsInner() : tableInner();
240
209
  // The pager is ALWAYS rendered below the table (even with rows=[]) so the
241
210
  // user can page back on a filter hit of zero without losing the list.
242
211
  // Except total === 0: then there is nothing to paginate. Incompatible with
243
212
  // infinite scroll: the caller sets either pager OR onReachEnd.
244
- const pagerElement = pager !== undefined && pager.total > 0 ? (_jsx(Pager, { page: pager.page, limit: pager.limit, total: pager.total, onPageChange: pager.onPageChange, ...(pager.pageSizeOptions !== undefined && { pageSizeOptions: pager.pageSizeOptions }), ...(pager.onPageSizeChange !== undefined && { onPageSizeChange: pager.onPageSizeChange }), ...(itemNoun !== undefined && { itemNoun }), testId: testId !== undefined ? `${testId}-pager` : "render-list-pager", pinned: fillsHeight })) : undefined;
245
- const sentinelElement = pagerElement === undefined && onReachEnd !== undefined ? (_jsx(InfiniteSentinel, { onReachEnd: onReachEnd, loadingMore: loadingMore === true, hasMore: hasMore !== false, rowsCount: rows.length, testId: testId !== undefined ? `${testId}-sentinel` : "render-list-sentinel" })) : undefined;
213
+ const pagerElement = pager !== undefined && pager.total > 0 && serverGroups === undefined ? (_jsx(Pager, { page: pager.page, limit: pager.limit, total: pager.total, onPageChange: pager.onPageChange, ...(pager.pageSizeOptions !== undefined && { pageSizeOptions: pager.pageSizeOptions }), ...(pager.onPageSizeChange !== undefined && { onPageSizeChange: pager.onPageSizeChange }), ...(itemNoun !== undefined && { itemNoun }), testId: testId !== undefined ? `${testId}-pager` : "render-list-pager", pinned: fillsHeight })) : undefined;
214
+ const sentinelElement = pagerElement === undefined && onReachEnd !== undefined && serverGroups === undefined ? (_jsx(InfiniteSentinel, { onReachEnd: onReachEnd, loadingMore: loadingMore === true, hasMore: hasMore !== false, rowsCount: rows.length, testId: testId !== undefined ? `${testId}-sentinel` : "render-list-sentinel" })) : undefined;
246
215
  // With scrollBody the footer (pager, else the entry count) sits OUTSIDE the
247
216
  // scroll surface so it stays put at any row count; the infinite sentinel
248
217
  // stays INSIDE, or it would never intersect the scrollport.
249
- const content = fillsHeight ? (_jsxs(_Fragment, { children: [_jsxs("div", { "data-testid": testId !== undefined ? `${testId}-scroll` : "render-list-scroll", className: "min-h-0 flex-1 overflow-auto border-t border-border bg-card", children: [tableContent, sentinelElement] }), pagerElement ?? (_jsx(DataTableFooter, { count: rows.length, ...(itemNoun !== undefined && { itemNoun }), testId: testId !== undefined ? `${testId}-footer` : "render-list-footer" }))] })) : (_jsxs(_Fragment, { children: [tableContent, pagerElement, sentinelElement] }));
250
- const hasFacets = filterFacets !== undefined && filterFacets.length > 0 && onFilterChange !== undefined;
251
- const hasDateRangeFacets = dateRangeFacets !== undefined && dateRangeFacets.length > 0 && onDateRangeChange !== undefined;
252
- const hasActiveFilters = filterValues !== undefined && Object.values(filterValues).some((v) => v.length > 0);
253
- const facetCluster = hasFacets || hasDateRangeFacets ? (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [hasFacets &&
254
- filterFacets.map((facet) => (_jsx(FacetFilter, { facet: facet, selected: filterValues?.[facet.field] ?? [], onChange: onFilterChange, dense: fillsHeight }, facet.field))), hasDateRangeFacets &&
255
- dateRangeFacets.map((facet) => (_jsx(DateRangeFacetFilter, { facet: facet, onChange: onDateRangeChange }, facet.field))), hasActiveFilters && onFilterReset !== undefined && (_jsxs(UiButton, { variant: "ghost", size: "sm", className: "h-9 px-2", onClick: onFilterReset, "data-testid": "facet-reset", children: [tableTranslate?.("kumiko.list.filter.reset") ?? "Reset", _jsx(X, {})] }))] })) : undefined;
218
+ const content = fillsHeight ? (_jsxs(_Fragment, { children: [selection.bar, _jsxs("div", { "data-testid": testId !== undefined ? `${testId}-scroll` : "render-list-scroll", className: "min-h-0 flex-1 overflow-auto border-t border-border bg-card", children: [tableContent, sentinelElement] }), pagerElement ?? (_jsx(DataTableFooter, { count: serverGroups?.totalCount ?? rows.length, ...(itemNoun !== undefined && { itemNoun }), testId: testId !== undefined ? `${testId}-footer` : "render-list-footer" }))] })) : (_jsxs(_Fragment, { children: [selection.bar, tableContent, pagerElement, sentinelElement] }));
219
+ const facetCluster = buildFacetCluster({
220
+ filterFacets,
221
+ filterValues,
222
+ onFilterChange,
223
+ onFilterReset,
224
+ dateRangeFacets,
225
+ onDateRangeChange,
226
+ rangeFacets,
227
+ onRangeChange,
228
+ resetAll,
229
+ notice,
230
+ dense: fillsHeight,
231
+ });
232
+ const columnLabels = new Map(schemaColumns.map((col) => [col.field, col.label]));
256
233
  const collapsesFacetsBehindButton = isNarrow && fillsHeight && facetCluster !== undefined;
257
- const hasToolbar = toolbarStart !== undefined || toolbarEnd !== undefined || facetCluster !== undefined;
234
+ const hasToolbar = toolbarStart !== undefined ||
235
+ facetCluster !== undefined ||
236
+ toolbarEndHasContent(view, exportList, toolbarEnd);
258
237
  // dashboard-01 pattern: the toolbar (search + facets + "+ New") sits ABOVE
259
238
  // the table in the same padding block; no separate bg bar, no screen title
260
239
  // (that lives in the shell's breadcrumb).
@@ -268,11 +247,11 @@ export function DefaultDataTable({ columns, rows, onRowClick, sort, onSortChange
268
247
  ? screenPaddingClassName
269
248
  : screenPadding === false
270
249
  ? undefined
271
- : "p-6")), children: [hasToolbar && (_jsxs("div", { "data-testid": testId !== undefined ? `${testId}-toolbar` : "render-list-toolbar", className: cn("flex flex-wrap items-center", fillsHeight
250
+ : "p-6")), children: [hasToolbar && (_jsxs("div", { "data-testid": testId !== undefined ? `${testId}-toolbar` : "render-list-toolbar", className: cn("flex flex-wrap items-center", UNIFORM_BAR_CONTROLS_CLASSES, fillsHeight
272
251
  ? isNarrow
273
252
  ? "shrink-0 gap-2 p-3"
274
253
  : "min-h-13 shrink-0 gap-2 px-6 py-2 md:py-0"
275
254
  : "gap-3"), children: [toolbarStart !== undefined && (_jsxs("div", { className: cn(fillsHeight
276
- ? "relative w-full md:w-[300px] max-md:flex-1 [&_input]:border-border-strong [&_input]:pl-8 [&_input]:max-md:h-11 md:[&_input]:h-8"
277
- : "flex-1 min-w-48 max-w-sm"), children: [fillsHeight && (_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 z-10 size-4 -translate-y-1/2 text-muted-foreground", "aria-hidden": "true" })), toolbarStart] })), collapsesFacetsBehindButton ? (_jsxs(_Fragment, { children: [_jsx(UiButton, { type: "button", variant: "outline", size: "icon", className: "size-11", "aria-label": tableTranslate?.("kumiko.list.filter.toggle") ?? "Filter", "aria-expanded": facetsOpenNarrow, "data-testid": testId !== undefined ? `${testId}-filter-toggle` : "filter-toggle", onClick: () => setFacetsOpenNarrow((open) => !open), children: _jsx(SlidersHorizontal, { className: "size-4", "aria-hidden": "true" }) }), facetsOpenNarrow && _jsx("div", { className: "w-full", children: facetCluster })] })) : (facetCluster), toolbarEnd !== undefined && (_jsx("div", { className: "flex flex-wrap items-center gap-2 ml-auto", children: toolbarEnd }))] })), toolbarDescription !== undefined && (_jsx("p", { "data-testid": testId !== undefined ? `${testId}-description` : "render-list-description", className: cn("m-0 pb-2 text-sm text-muted-foreground", fillsHeight && (isNarrow ? "px-3" : "px-6")), children: toolbarDescription })), content] }));
255
+ ? "relative w-full md:w-[300px] max-md:flex-1 [&_input]:border-border-strong [&_input]:pl-8 [&_input]:max-md:h-11"
256
+ : "flex-1 min-w-48 max-w-sm"), children: [fillsHeight && (_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 z-10 size-4 -translate-y-1/2 text-muted-foreground", "aria-hidden": "true" })), toolbarStart] })), collapsesFacetsBehindButton ? (_jsxs(_Fragment, { children: [_jsx(UiButton, { type: "button", variant: "outline", size: "icon", className: "size-11", "aria-label": tableTranslate?.("kumiko.list.filter.toggle") ?? "Filter", "aria-expanded": facetsOpenNarrow, "data-testid": testId !== undefined ? `${testId}-filter-toggle` : "filter-toggle", onClick: () => setFacetsOpenNarrow((open) => !open), children: _jsx(SlidersHorizontal, { className: "size-4", "aria-hidden": "true" }) }), facetsOpenNarrow && _jsx("div", { className: "w-full", children: facetCluster })] })) : (facetCluster), _jsx(ToolbarEndCluster, { view: view, columnLabels: columnLabels, exportList: exportList, toolbarEnd: toolbarEnd })] })), toolbarDescription !== undefined && (_jsx("p", { "data-testid": testId !== undefined ? `${testId}-description` : "render-list-description", className: cn("m-0 pb-2 text-sm text-muted-foreground", fillsHeight && (isNarrow ? "px-3" : "px-6"), fillsHeight && !hasToolbar && "pt-4"), children: toolbarDescription })), content] }));
278
257
  }
@@ -26,7 +26,7 @@ export function FacetFilter({ facet, selected, onChange, dense = false, }) {
26
26
  if (facet.chips !== undefined) {
27
27
  return _jsx(FacetChips, { facet: facet, selected: selected, onChange: onChange });
28
28
  }
29
- return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(UiButton, { variant: "outline", size: "sm", className: cn("h-9", dense && "h-8 border-border-strong px-2.5 max-md:h-11"), "data-testid": `facet-${facet.field}`, children: [facet.label, selected.length > 0 && (_jsx(Badge, { variant: "secondary", className: "ml-1 rounded-sm px-1 font-normal tabular-nums", children: selected.length })), _jsx(ChevronDown, { className: "text-muted-foreground" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "max-h-[var(--radix-dropdown-menu-content-available-height)] w-44 overflow-y-auto", children: facet.options.map((opt) => (_jsx(DropdownMenuCheckboxItem, { checked: selected.includes(opt.value), onCheckedChange: (checked) => toggle(opt.value, checked), onSelect: (e) => e.preventDefault(), "data-testid": `facet-${facet.field}-${opt.value}`, children: opt.label }, opt.value))) })] }));
29
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(UiButton, { variant: "outline", size: "sm", className: cn("h-9", dense && "border-border-strong px-2.5"), "data-testid": `facet-${facet.field}`, children: [facet.label, selected.length > 0 && (_jsx(Badge, { variant: "secondary", className: "ml-1 rounded-sm px-1 font-normal tabular-nums", children: selected.length })), _jsx(ChevronDown, { className: "text-muted-foreground" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "max-h-[var(--radix-dropdown-menu-content-available-height)] w-44 overflow-y-auto", children: facet.options.map((opt) => (_jsx(DropdownMenuCheckboxItem, { checked: selected.includes(opt.value), onCheckedChange: (checked) => toggle(opt.value, checked), onSelect: (e) => e.preventDefault(), "data-testid": `facet-${facet.field}-${opt.value}`, children: opt.label }, opt.value))) })] }));
30
30
  }
31
31
  // Time-range filter: two native <input type="date"> instead of a
32
32
  // dropdown — the browser supplies the calendar, the locale and the keyboard
@@ -12,6 +12,9 @@ type FileMeta = {
12
12
  readonly fileName: string;
13
13
  readonly mimeType: string | undefined;
14
14
  };
15
+ /** File names are per-user data and the server re-checks access per request, so
16
+ * the cache must not outlive the session that filled it. */
17
+ export declare function clearFileMetaCache(): void;
15
18
  export declare function useFileMeta(fileId: string): FileMeta | null;
16
19
  export declare function FileNameLink({ fileId, fallbackLabel, className, }: {
17
20
  readonly fileId: string;
@@ -46,6 +46,11 @@ const META_BATCH_SIZE = 100;
46
46
  const META_CACHE_LIMIT = 500;
47
47
  const metaCache = new Map();
48
48
  let pendingMetaIds = new Map();
49
+ /** File names are per-user data and the server re-checks access per request, so
50
+ * the cache must not outlive the session that filled it. */
51
+ export function clearFileMetaCache() {
52
+ metaCache.clear();
53
+ }
49
54
  function parseBatchMeta(json) {
50
55
  const metaById = new Map();
51
56
  if (typeof json !== "object" || json === null || !("files" in json))
@@ -1,3 +1,3 @@
1
1
  import { type ReactNode } from "react";
2
2
  import { type FileValueListProps } from "../../index.ts";
3
- export declare function FileValueList({ fileIds, testId }: FileValueListProps): ReactNode;
3
+ export declare function FileValueList({ fileIds, previewOpenByDefault, testId, }: FileValueListProps): ReactNode;
@@ -6,17 +6,17 @@ import { Button } from "../ui/button.js";
6
6
  import { PdfPreview } from "../widgets/pdf-preview.js";
7
7
  import { fileOriginalUrl, useFileMeta } from "./file-thumbnail.js";
8
8
  const PDF_MIME_TYPE = "application/pdf";
9
- function FileValueRow({ fileId, testId, }) {
9
+ function FileValueRow({ fileId, previewOpenInitially, testId, }) {
10
10
  const t = useTranslation();
11
11
  const meta = useFileMeta(fileId);
12
- const [previewOpen, setPreviewOpen] = useState(false);
12
+ const [previewOpen, setPreviewOpen] = useState(previewOpenInitially);
13
13
  if (!isUuid(fileId))
14
14
  return _jsx("span", { className: "text-sm", children: fileId });
15
15
  const downloadUrl = fileOriginalUrl(fileId);
16
- return (_jsxs("li", { className: "flex flex-col gap-2", "data-testid": testId, children: [_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx("span", { className: "text-sm", children: meta?.fileName ?? fileId.slice(0, 8) }), _jsx("a", { href: downloadUrl, download: meta?.fileName ?? "", className: "text-primary text-sm underline underline-offset-2", ...(testId !== undefined && { "data-testid": `${testId}-download` }), children: t("kumiko.field.file-list.download") }), meta?.mimeType === PDF_MIME_TYPE && (_jsx(Button, { type: "button", variant: "link", size: "sm", "aria-expanded": previewOpen, onClick: () => setPreviewOpen((open) => !open), ...(testId !== undefined && { "data-testid": `${testId}-preview-toggle` }), children: t(previewOpen ? "kumiko.field.file-list.hidePreview" : "kumiko.field.file-list.preview") }))] }), previewOpen && (_jsx(PdfPreview, { source: { url: downloadUrl }, testId: `file-preview-${fileId}` }))] }));
16
+ return (_jsxs("li", { className: "flex flex-col gap-2", "data-testid": testId, children: [_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx("span", { className: "text-sm", children: meta?.fileName ?? fileId.slice(0, 8) }), _jsx("a", { href: downloadUrl, download: meta?.fileName ?? "", className: "text-primary text-sm underline underline-offset-2", ...(testId !== undefined && { "data-testid": `${testId}-download` }), children: t("kumiko.field.file-list.download") }), meta?.mimeType === PDF_MIME_TYPE && (_jsx(Button, { type: "button", variant: "link", size: "sm", "aria-expanded": previewOpen, onClick: () => setPreviewOpen((open) => !open), ...(testId !== undefined && { "data-testid": `${testId}-preview-toggle` }), children: t(previewOpen ? "kumiko.field.file-list.hidePreview" : "kumiko.field.file-list.preview") }))] }), previewOpen && meta?.mimeType === PDF_MIME_TYPE && (_jsx(PdfPreview, { source: { url: downloadUrl }, testId: `file-preview-${fileId}` }))] }));
17
17
  }
18
- export function FileValueList({ fileIds, testId }) {
18
+ export function FileValueList({ fileIds, previewOpenByDefault = false, testId, }) {
19
19
  if (fileIds.length === 0)
20
20
  return null;
21
- return (_jsx("ul", { className: "m-0 flex list-none flex-col gap-3 p-0", "data-testid": testId, children: fileIds.map((fileId) => (_jsx(FileValueRow, { fileId: fileId, ...(testId !== undefined && { testId: `${testId}-${fileId}` }) }, fileId))) }));
21
+ return (_jsx("ul", { className: "m-0 flex list-none flex-col gap-3 p-0", "data-testid": testId, children: fileIds.map((fileId, index) => (_jsx(FileValueRow, { fileId: fileId, previewOpenInitially: previewOpenByDefault && index === 0, ...(testId !== undefined && { testId: `${testId}-${fileId}` }) }, fileId))) }));
22
22
  }
@@ -5,6 +5,7 @@ import { Checkbox } from "../ui/checkbox.js";
5
5
  import { Input as UiInput } from "../ui/input.js";
6
6
  import { Switch } from "../ui/switch.js";
7
7
  import { Textarea } from "../ui/textarea.js";
8
+ import { CodeInput } from "./code-input.js";
8
9
  import { ComboboxInput } from "./combobox.js";
9
10
  import { DateInput } from "./date-input.js";
10
11
  import { FileUploadInput } from "./file-upload.js";
@@ -15,6 +16,7 @@ import { NumberInput } from "./number-input.js";
15
16
  import { hasDescriptionOrGroup } from "./option-groups.js";
16
17
  import { TimestampInput } from "./timestamp-input.js";
17
18
  import { TzInput } from "./tz-input.js";
19
+ import { useFocusOnMount } from "./use-focus-on-mount.js";
18
20
  import "../react-css-custom-properties.js";
19
21
  import { FieldDescriptionContext, FieldLayoutContext } from "./contexts.js";
20
22
  import { defaultSelectPresentation, FIELD_ICON_CLASS, fieldIconFor, RadioListSelect, SegmentedSelect, textareaMinHeight, unitSuffixId, withFieldIcon, withInlineDisabledHint, withInputAddons, withoutSegmentedSelectPlaceholder, withTrailingAction, } from "./field.js";
@@ -23,6 +25,7 @@ export function DefaultInput(props) {
23
25
  // themselves, no manual border-destructive needed anymore.
24
26
  const booleanLayout = useContext(FieldLayoutContext);
25
27
  const describedBy = useContext(FieldDescriptionContext);
28
+ const textInputRef = useFocusOnMount(props.kind === "text" ? props.autoFocus : undefined);
26
29
  const common = {
27
30
  id: props.id,
28
31
  name: props.name,
@@ -33,7 +36,9 @@ export function DefaultInput(props) {
33
36
  };
34
37
  switch (props.kind) {
35
38
  case "text":
36
- return withInputAddons(props, withFieldIcon(props.icon, _jsx(UiInput, { type: "text", ...common, ...props.dataAttributes, "data-testid": props.testId, readOnly: props.readOnly, value: props.value, onChange: (e) => props.onChange(e.target.value), onKeyDown: props.onKeyDown, ...(props.placeholder !== undefined && { placeholder: props.placeholder }), ...(props.autoComplete !== undefined && { autoComplete: props.autoComplete }), className: cn(fieldIconFor(props.icon) !== undefined ? "pl-8" : undefined) })));
39
+ return withInputAddons(props, withFieldIcon(props.icon, _jsx(UiInput, { type: "text", ref: textInputRef, ...common, ...props.dataAttributes, "data-testid": props.testId, readOnly: props.readOnly, value: props.value, onChange: (e) => props.onChange(e.target.value), onKeyDown: props.onKeyDown, ...(props.placeholder !== undefined && { placeholder: props.placeholder }), ...(props.autoComplete !== undefined && { autoComplete: props.autoComplete }), className: cn(fieldIconFor(props.icon) !== undefined ? "pl-8" : undefined) })));
40
+ case "code":
41
+ return _jsx(CodeInput, { ...props });
37
42
  case "email":
38
43
  return (_jsx(UiInput, { type: "email", ...common, "data-testid": props.testId, value: props.value, onChange: (e) => props.onChange(e.target.value), ...(props.placeholder !== undefined && { placeholder: props.placeholder }), autoComplete: props.autoComplete ?? "email" }));
39
44
  case "month":
@@ -61,7 +61,7 @@ const FIELD_CELL_WIDTH_CLASS = {
61
61
  money: "w-40 shrink-0 [&_label]:whitespace-nowrap",
62
62
  date: "w-full sm:w-[200px]",
63
63
  timestamp: "w-full sm:w-[328px]",
64
- select: "w-full sm:w-auto sm:min-w-[200px] sm:has-[[data-radio-list]]:w-full",
64
+ select: "w-full sm:w-60 sm:has-[[data-radio-list]]:w-full",
65
65
  full: "w-full",
66
66
  // self-start: the label shares the top line with neighbouring labels. The
67
67
  // switch (1.15rem) gets (h-9 input − switch) / 2 margin to sit on the input line.
@@ -75,7 +75,8 @@ export function DefaultGrid({ columns, children, testId, maxRows, flow, list, ar
75
75
  if (list === true) {
76
76
  return (_jsx("div", { ...groupProps, "data-testid": testId, className: "flex flex-col divide-y divide-border", children: children }));
77
77
  }
78
- if (flow === true) {
78
+ const declaresColumns = typeof columns === "number" && columns >= 2;
79
+ if (flow === true && (insideDrawerBody || !declaresColumns)) {
79
80
  return (_jsx("div", { ...groupProps, "data-testid": testId, className: cn("flex gap-4", insideDrawerBody
80
81
  ? "flex-col items-start"
81
82
  : // items-end aligns inputs when labels wrap to different heights; a
@@ -116,12 +117,13 @@ export function DefaultGrid({ columns, children, testId, maxRows, flow, list, ar
116
117
  overflowY: "auto",
117
118
  }),
118
119
  };
119
- return (_jsx("div", { ...groupProps, "data-testid": testId, className: "grid gap-[var(--kumiko-grid-gap,1rem)] grid-cols-1 sm:[grid-template-columns:var(--grid-cols)]", style: style, children: children }));
120
+ return (_jsx("div", { ...groupProps, "data-testid": testId, className: cn("grid gap-[var(--kumiko-grid-gap,1rem)] grid-cols-1 sm:[grid-template-columns:var(--grid-cols)]", flow === true &&
121
+ "items-end has-[[data-appendix-below]]:items-start has-[[role=alert]]:items-start"), style: style, children: children }));
120
122
  }
121
123
  export function DefaultGridCell({ span, width, children }) {
122
124
  const insideDrawerBody = useContext(DrawerBodyContext);
123
125
  if (width !== undefined) {
124
- return (_jsx("div", { className: cn("min-w-0 max-w-full", insideDrawerBody
126
+ return (_jsx("div", { className: cn("min-w-0 max-w-full", width === "full" && "sm:[grid-column:1/-1]", insideDrawerBody
125
127
  ? (DRAWER_FIELD_CELL_WIDTH_CLASS[width] ?? FIELD_CELL_WIDTH_CLASS[width])
126
128
  : FIELD_CELL_WIDTH_CLASS[width]), children: children }));
127
129
  }
@@ -0,0 +1,7 @@
1
+ import { type ReactNode } from "react";
2
+ import type { DataTableRangeBound, DataTableRangeFacet } from "../../data-table-aggregate-types.ts";
3
+ /** "From/to" bounds of a number, decimal, money or bigInt field. */
4
+ export declare function NumberRangeFacetFilter({ facet, onChange, }: {
5
+ readonly facet: DataTableRangeFacet;
6
+ readonly onChange: (field: string, bound: DataTableRangeBound, value: string) => void;
7
+ }): ReactNode;
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { useOptionalTranslation } from "../../index.js";
4
+ const INPUT_CLASS = "h-9 w-24 rounded-md border border-input bg-transparent px-2 text-sm tabular-nums shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 max-md:min-h-11";
5
+ // A keystroke must not refetch the list, so a bound commits on blur or Enter;
6
+ // a value changed from outside (reset, back navigation) replaces the draft.
7
+ function RangeBoundInput({ facet, bound, label, onCommit, }) {
8
+ const committed = facet[bound];
9
+ const [draft, setDraft] = useState(committed);
10
+ const [seen, setSeen] = useState(committed);
11
+ if (seen !== committed) {
12
+ setSeen(committed);
13
+ setDraft(committed);
14
+ }
15
+ const commit = () => {
16
+ const trimmed = draft.trim();
17
+ if (trimmed !== "" && !Number.isFinite(Number(trimmed))) {
18
+ setDraft(committed);
19
+ return;
20
+ }
21
+ if (trimmed !== committed)
22
+ onCommit(facet.field, bound, trimmed);
23
+ };
24
+ return (_jsx("input", { type: "number", inputMode: "decimal", step: "any", className: INPUT_CLASS, value: draft, placeholder: label, "aria-label": `${facet.label} ${label}`, "data-testid": `facet-range-${facet.field}-${bound}`, onChange: (event) => setDraft(event.target.value), onBlur: commit, onKeyDown: (event) => {
25
+ if (event.key === "Enter")
26
+ commit();
27
+ } }));
28
+ }
29
+ /** "From/to" bounds of a number, decimal, money or bigInt field. */
30
+ export function NumberRangeFacetFilter({ facet, onChange, }) {
31
+ const translate = useOptionalTranslation();
32
+ return (_jsxs("div", { className: "flex items-center gap-2", "data-testid": `facet-range-${facet.field}`, children: [_jsx("span", { className: "text-sm text-muted-foreground", children: facet.label }), _jsx(RangeBoundInput, { facet: facet, bound: "min", label: translate?.("kumiko.list.range.min") ?? "From", onCommit: onChange }), _jsx("span", { className: "text-sm text-muted-foreground", children: "\u2013" }), _jsx(RangeBoundInput, { facet: facet, bound: "max", label: translate?.("kumiko.list.range.max") ?? "To", onCommit: onChange }), facet.unit !== undefined && (_jsx("span", { className: "text-sm text-muted-foreground", children: facet.unit }))] }));
33
+ }