@entifix/react-controls 0.19.0 → 0.20.1

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 (203) hide show
  1. package/README.md +8 -0
  2. package/dist/i18n/catalog/en.d.ts.map +1 -1
  3. package/dist/i18n/catalog/en.js +55 -9
  4. package/dist/i18n/catalog/en.js.map +1 -1
  5. package/dist/i18n/catalog/es.d.ts +64 -5
  6. package/dist/i18n/catalog/es.d.ts.map +1 -1
  7. package/dist/i18n/catalog/es.js +61 -9
  8. package/dist/i18n/catalog/es.js.map +1 -1
  9. package/dist/i18n/catalog/index.d.ts +102 -10
  10. package/dist/i18n/catalog/index.d.ts.map +1 -1
  11. package/dist/i18n/entity-labels.d.ts +14 -0
  12. package/dist/i18n/entity-labels.d.ts.map +1 -1
  13. package/dist/i18n/entity-labels.js +15 -0
  14. package/dist/i18n/entity-labels.js.map +1 -1
  15. package/dist/i18n/i18n-context.js +2 -2
  16. package/dist/i18n/i18n-context.js.map +1 -1
  17. package/dist/index.d.ts +6 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +6 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/primitives.d.ts +0 -1
  22. package/dist/primitives.d.ts.map +1 -1
  23. package/dist/primitives.js +0 -1
  24. package/dist/primitives.js.map +1 -1
  25. package/dist/theme/index.d.ts +1 -0
  26. package/dist/theme/index.d.ts.map +1 -1
  27. package/dist/theme/index.js +1 -0
  28. package/dist/theme/index.js.map +1 -1
  29. package/dist/theme/theme-context.d.ts +15 -3
  30. package/dist/theme/theme-context.d.ts.map +1 -1
  31. package/dist/theme/theme-context.js +49 -23
  32. package/dist/theme/theme-context.js.map +1 -1
  33. package/dist/theme/theme-cookie.d.ts +9 -0
  34. package/dist/theme/theme-cookie.d.ts.map +1 -0
  35. package/dist/theme/theme-cookie.js +8 -0
  36. package/dist/theme/theme-cookie.js.map +1 -0
  37. package/dist/theme/theme-script.d.ts +8 -6
  38. package/dist/theme/theme-script.d.ts.map +1 -1
  39. package/dist/theme/theme-script.js +21 -12
  40. package/dist/theme/theme-script.js.map +1 -1
  41. package/dist/ui/actions/action-surfaces.d.ts +1 -1
  42. package/dist/ui/actions/action-surfaces.js +1 -1
  43. package/dist/ui/actions/action-surfaces.js.map +1 -1
  44. package/dist/ui/actions/error-actions.d.ts +32 -0
  45. package/dist/ui/actions/error-actions.d.ts.map +1 -0
  46. package/dist/ui/actions/error-actions.js +40 -0
  47. package/dist/ui/actions/error-actions.js.map +1 -0
  48. package/dist/ui/actions/index.d.ts +3 -0
  49. package/dist/ui/actions/index.d.ts.map +1 -1
  50. package/dist/ui/actions/index.js +3 -0
  51. package/dist/ui/actions/index.js.map +1 -1
  52. package/dist/ui/actions/shortcut.d.ts +13 -0
  53. package/dist/ui/actions/shortcut.d.ts.map +1 -0
  54. package/dist/ui/actions/shortcut.js +18 -0
  55. package/dist/ui/actions/shortcut.js.map +1 -0
  56. package/dist/ui/actions/use-verb-invoker.d.ts +55 -0
  57. package/dist/ui/actions/use-verb-invoker.d.ts.map +1 -0
  58. package/dist/ui/actions/use-verb-invoker.js +98 -0
  59. package/dist/ui/actions/use-verb-invoker.js.map +1 -0
  60. package/dist/ui/atoms/cell-value/cell-value.d.ts.map +1 -1
  61. package/dist/ui/atoms/cell-value/cell-value.js +28 -14
  62. package/dist/ui/atoms/cell-value/cell-value.js.map +1 -1
  63. package/dist/ui/atoms/field-control/field-control.d.ts +1 -0
  64. package/dist/ui/atoms/field-control/field-control.d.ts.map +1 -1
  65. package/dist/ui/atoms/field-control/field-control.js +2 -0
  66. package/dist/ui/atoms/field-control/field-control.js.map +1 -1
  67. package/dist/ui/atoms/number-input/quantity-input.d.ts +8 -1
  68. package/dist/ui/atoms/number-input/quantity-input.d.ts.map +1 -1
  69. package/dist/ui/atoms/number-input/quantity-input.js +2 -1
  70. package/dist/ui/atoms/number-input/quantity-input.js.map +1 -1
  71. package/dist/ui/atoms/table/table.d.ts +12 -3
  72. package/dist/ui/atoms/table/table.d.ts.map +1 -1
  73. package/dist/ui/atoms/table/table.js +59 -10
  74. package/dist/ui/atoms/table/table.js.map +1 -1
  75. package/dist/ui/atoms/text/text.d.ts +2 -2
  76. package/dist/ui/atoms/text/text.d.ts.map +1 -1
  77. package/dist/ui/atoms/text/text.js.map +1 -1
  78. package/dist/ui/molecules/action-bar/action-bar.d.ts +76 -0
  79. package/dist/ui/molecules/action-bar/action-bar.d.ts.map +1 -0
  80. package/dist/ui/molecules/action-bar/action-bar.js +218 -0
  81. package/dist/ui/molecules/action-bar/action-bar.js.map +1 -0
  82. package/dist/ui/molecules/action-bar/index.d.ts +2 -0
  83. package/dist/ui/molecules/action-bar/index.d.ts.map +1 -0
  84. package/dist/ui/molecules/action-bar/index.js +3 -0
  85. package/dist/ui/molecules/action-bar/index.js.map +1 -0
  86. package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.d.ts.map +1 -1
  87. package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.js +8 -10
  88. package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.js.map +1 -1
  89. package/dist/ui/molecules/bulk-result/bulk-result.d.ts +3 -3
  90. package/dist/ui/molecules/bulk-result/bulk-result.js +4 -4
  91. package/dist/ui/molecules/bulk-result/bulk-result.js.map +1 -1
  92. package/dist/ui/molecules/confirm-dialog/confirm-dialog.d.ts +24 -9
  93. package/dist/ui/molecules/confirm-dialog/confirm-dialog.d.ts.map +1 -1
  94. package/dist/ui/molecules/confirm-dialog/confirm-dialog.js +36 -11
  95. package/dist/ui/molecules/confirm-dialog/confirm-dialog.js.map +1 -1
  96. package/dist/ui/molecules/entity-record-card/entity-record-card.d.ts +33 -20
  97. package/dist/ui/molecules/entity-record-card/entity-record-card.d.ts.map +1 -1
  98. package/dist/ui/molecules/entity-record-card/entity-record-card.js +53 -20
  99. package/dist/ui/molecules/entity-record-card/entity-record-card.js.map +1 -1
  100. package/dist/ui/molecules/filter-builder/filter-builder.d.ts +8 -2
  101. package/dist/ui/molecules/filter-builder/filter-builder.d.ts.map +1 -1
  102. package/dist/ui/molecules/filter-builder/filter-builder.js +14 -5
  103. package/dist/ui/molecules/filter-builder/filter-builder.js.map +1 -1
  104. package/dist/ui/molecules/filter-chips/filter-chips.d.ts +16 -0
  105. package/dist/ui/molecules/filter-chips/filter-chips.d.ts.map +1 -0
  106. package/dist/ui/molecules/filter-chips/filter-chips.js +73 -0
  107. package/dist/ui/molecules/filter-chips/filter-chips.js.map +1 -0
  108. package/dist/ui/molecules/filter-chips/index.d.ts +2 -0
  109. package/dist/ui/molecules/filter-chips/index.d.ts.map +1 -0
  110. package/dist/ui/molecules/filter-chips/index.js +3 -0
  111. package/dist/ui/molecules/filter-chips/index.js.map +1 -0
  112. package/dist/ui/molecules/menu/menu.d.ts +11 -1
  113. package/dist/ui/molecules/menu/menu.d.ts.map +1 -1
  114. package/dist/ui/molecules/menu/menu.js +62 -2
  115. package/dist/ui/molecules/menu/menu.js.map +1 -1
  116. package/dist/ui/molecules/pagination/pagination.d.ts +5 -0
  117. package/dist/ui/molecules/pagination/pagination.d.ts.map +1 -1
  118. package/dist/ui/molecules/pagination/pagination.js +35 -18
  119. package/dist/ui/molecules/pagination/pagination.js.map +1 -1
  120. package/dist/ui/molecules/region-state/index.d.ts +2 -0
  121. package/dist/ui/molecules/region-state/index.d.ts.map +1 -0
  122. package/dist/ui/molecules/region-state/index.js +3 -0
  123. package/dist/ui/molecules/region-state/index.js.map +1 -0
  124. package/dist/ui/molecules/region-state/region-state.d.ts +42 -0
  125. package/dist/ui/molecules/region-state/region-state.d.ts.map +1 -0
  126. package/dist/ui/molecules/region-state/region-state.js +83 -0
  127. package/dist/ui/molecules/region-state/region-state.js.map +1 -0
  128. package/dist/ui/molecules/table-toolbar/table-toolbar.d.ts +3 -1
  129. package/dist/ui/molecules/table-toolbar/table-toolbar.d.ts.map +1 -1
  130. package/dist/ui/molecules/table-toolbar/table-toolbar.js +3 -2
  131. package/dist/ui/molecules/table-toolbar/table-toolbar.js.map +1 -1
  132. package/dist/ui/molecules/toast-region/toast-region.d.ts +63 -19
  133. package/dist/ui/molecules/toast-region/toast-region.d.ts.map +1 -1
  134. package/dist/ui/molecules/toast-region/toast-region.js +129 -42
  135. package/dist/ui/molecules/toast-region/toast-region.js.map +1 -1
  136. package/dist/ui/organisms/command-palette/command-palette.d.ts.map +1 -1
  137. package/dist/ui/organisms/command-palette/command-palette.js +19 -12
  138. package/dist/ui/organisms/command-palette/command-palette.js.map +1 -1
  139. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.d.ts.map +1 -1
  140. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js +0 -1
  141. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js.map +1 -1
  142. package/dist/ui/organisms/entity-form/entity-form.d.ts +1 -1
  143. package/dist/ui/organisms/entity-form/entity-form.d.ts.map +1 -1
  144. package/dist/ui/organisms/entity-form/entity-form.js +322 -214
  145. package/dist/ui/organisms/entity-form/entity-form.js.map +1 -1
  146. package/dist/ui/organisms/entity-form/entity-form.types.d.ts +16 -0
  147. package/dist/ui/organisms/entity-form/entity-form.types.d.ts.map +1 -1
  148. package/dist/ui/organisms/entity-form/entity-form.types.js.map +1 -1
  149. package/dist/ui/organisms/entity-table/entity-table.d.ts +1 -1
  150. package/dist/ui/organisms/entity-table/entity-table.d.ts.map +1 -1
  151. package/dist/ui/organisms/entity-table/entity-table.js +332 -197
  152. package/dist/ui/organisms/entity-table/entity-table.js.map +1 -1
  153. package/dist/ui/organisms/entity-table/entity-table.types.d.ts +19 -4
  154. package/dist/ui/organisms/entity-table/entity-table.types.d.ts.map +1 -1
  155. package/dist/ui/organisms/entity-table/entity-table.types.js.map +1 -1
  156. package/dist/ui/organisms/entity-table/header-sort.d.ts +18 -0
  157. package/dist/ui/organisms/entity-table/header-sort.d.ts.map +1 -0
  158. package/dist/ui/organisms/entity-table/header-sort.js +54 -0
  159. package/dist/ui/organisms/entity-table/header-sort.js.map +1 -0
  160. package/dist/ui/organisms/entity-table/pivot.d.ts +25 -0
  161. package/dist/ui/organisms/entity-table/pivot.d.ts.map +1 -0
  162. package/dist/ui/organisms/entity-table/pivot.js +106 -0
  163. package/dist/ui/organisms/entity-table/pivot.js.map +1 -0
  164. package/dist/ui/organisms/entity-table/use-result-announcement.d.ts +13 -0
  165. package/dist/ui/organisms/entity-table/use-result-announcement.d.ts.map +1 -0
  166. package/dist/ui/organisms/entity-table/use-result-announcement.js +31 -0
  167. package/dist/ui/organisms/entity-table/use-result-announcement.js.map +1 -0
  168. package/dist/ui/organisms/top-bar/live-indicator.d.ts +11 -0
  169. package/dist/ui/organisms/top-bar/live-indicator.d.ts.map +1 -0
  170. package/dist/ui/organisms/top-bar/live-indicator.js +46 -0
  171. package/dist/ui/organisms/top-bar/live-indicator.js.map +1 -0
  172. package/dist/ui/organisms/top-bar/top-bar.d.ts +11 -2
  173. package/dist/ui/organisms/top-bar/top-bar.d.ts.map +1 -1
  174. package/dist/ui/organisms/top-bar/top-bar.js +14 -5
  175. package/dist/ui/organisms/top-bar/top-bar.js.map +1 -1
  176. package/dist/ui/organisms/wizard/wizard.d.ts +1 -1
  177. package/dist/ui/organisms/wizard/wizard.d.ts.map +1 -1
  178. package/dist/ui/organisms/wizard/wizard.js +7 -1
  179. package/dist/ui/organisms/wizard/wizard.js.map +1 -1
  180. package/dist/ui/organisms/wizard/wizard.types.d.ts +8 -0
  181. package/dist/ui/organisms/wizard/wizard.types.d.ts.map +1 -1
  182. package/dist/ui/organisms/wizard/wizard.types.js.map +1 -1
  183. package/dist/ui/utils/live-updates.d.ts +24 -0
  184. package/dist/ui/utils/live-updates.d.ts.map +1 -0
  185. package/dist/ui/utils/live-updates.js +20 -0
  186. package/dist/ui/utils/live-updates.js.map +1 -0
  187. package/dist/ui/utils/use-focus-on-arrival.d.ts +14 -0
  188. package/dist/ui/utils/use-focus-on-arrival.d.ts.map +1 -0
  189. package/dist/ui/utils/use-focus-on-arrival.js +42 -0
  190. package/dist/ui/utils/use-focus-on-arrival.js.map +1 -0
  191. package/dist/ui/utils/use-hotkey.d.ts +44 -25
  192. package/dist/ui/utils/use-hotkey.d.ts.map +1 -1
  193. package/dist/ui/utils/use-hotkey.js +80 -48
  194. package/dist/ui/utils/use-hotkey.js.map +1 -1
  195. package/package.json +5 -5
  196. package/dist/ui/molecules/split-button/index.d.ts +0 -2
  197. package/dist/ui/molecules/split-button/index.d.ts.map +0 -1
  198. package/dist/ui/molecules/split-button/index.js +0 -3
  199. package/dist/ui/molecules/split-button/index.js.map +0 -1
  200. package/dist/ui/molecules/split-button/split-button.d.ts +0 -21
  201. package/dist/ui/molecules/split-button/split-button.d.ts.map +0 -1
  202. package/dist/ui/molecules/split-button/split-button.js +0 -35
  203. package/dist/ui/molecules/split-button/split-button.js.map +0 -1
@@ -1,48 +1,34 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { emptySelection, extractMetaEntity, hierarchyOf, isSelected, selectionSize, toggleSelected } from '@entifix/core';
4
- import { Fragment, useMemo, useRef, useState } from 'react';
5
- import { useErrorMessage, useT, useTranslateKey } from '../../../i18n/index.js';
6
- import { OVERFLOW_GLYPH, renderAnchor, useCasesForSurface } from '../../actions/index.js';
7
- import { Button } from '../../atoms/button/index.js';
3
+ import { emptySelection, extractMetaEntity, hierarchyOf, isSelected, queryPropertyFor, selectionSize, toggleSelected } from '@entifix/core';
4
+ import { Fragment, useId, useMemo, useRef, useState } from 'react';
5
+ import { useEntityNoun, useLocale, useT, useTranslateKey } from '../../../i18n/index.js';
6
+ import { renderAnchor, useCasesForSurface } from '../../actions/index.js';
7
+ import { useVerbInvoker } from '../../actions/use-verb-invoker.js';
8
+ import { Button, button } from '../../atoms/button/index.js';
8
9
  import { CellValue } from '../../atoms/cell-value/index.js';
9
10
  import { Checkbox } from '../../atoms/field/index.js';
10
11
  import { Skeleton } from '../../atoms/skeleton/index.js';
11
12
  import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableMessageRow, TableRow } from '../../atoms/table/index.js';
12
13
  import { linkClassName } from '../../atoms/text/index.js';
14
+ import { ActionBar, itemsForUseCases } from '../../molecules/action-bar/index.js';
13
15
  import { BulkActionBar } from '../../molecules/bulk-action-bar/index.js';
14
16
  import { BulkResult } from '../../molecules/bulk-result/index.js';
15
17
  import { ColumnSettings } from '../../molecules/column-settings/index.js';
16
- import { ConfirmDialog } from '../../molecules/confirm-dialog/index.js';
17
18
  import { EntityRecordCard } from '../../molecules/entity-record-card/index.js';
18
19
  import { FilterBuilder } from '../../molecules/filter-builder/index.js';
20
+ import { FilterChips } from '../../molecules/filter-chips/index.js';
19
21
  import { LoadingBoundary } from '../../molecules/loading-boundary/index.js';
20
- import { Menu } from '../../molecules/menu/index.js';
21
22
  import { Pagination } from '../../molecules/pagination/index.js';
23
+ import { RegionState } from '../../molecules/region-state/index.js';
22
24
  import { SortBuilder } from '../../molecules/sort-builder/index.js';
23
25
  import { TableToolbar } from '../../molecules/table-toolbar/index.js';
24
26
  import { readEntityTableSlots } from './entity-table-slots.js';
25
27
  import { shownTreeRows, TreeCell, treeRows } from './entity-table-tree.js';
28
+ import { nextHeaderSorting, sortKeys } from './header-sort.js';
29
+ import { ACTIONS_WIDTH, pivotClasses, pivotWidth, SELECTION_WIDTH } from './pivot.js';
26
30
  import { useEntityTableColumns } from './use-entity-table-columns.js';
27
- /**
28
- * The grid and the card list are both rendered, and CSS picks one. A JS
29
- * breakpoint hook would have to guess during SSR and correct after mount —
30
- * a hydration mismatch on every page load. Classes are written out in full
31
- * because Tailwind cannot see a class name that is concatenated at runtime.
32
- */ const PIVOT_CLASS = {
33
- sm: {
34
- grid: 'hidden sm:block',
35
- cards: 'sm:hidden'
36
- },
37
- md: {
38
- grid: 'hidden md:block',
39
- cards: 'md:hidden'
40
- },
41
- lg: {
42
- grid: 'hidden lg:block',
43
- cards: 'lg:hidden'
44
- }
45
- };
31
+ import { useResultAnnouncement } from './use-result-announcement.js';
46
32
  /**
47
33
  * How many placeholder rows the built-in skeleton draws.
48
34
  *
@@ -51,36 +37,18 @@ import { useEntityTableColumns } from './use-entity-table-columns.js';
51
37
  * a fifty-row page would otherwise paint fifty shimmer rows to stand in for
52
38
  * content that arrives below the fold anyway.
53
39
  */ const SKELETON_ROW_CAP = 5;
54
- export function EntityTable({ entityConstructor, isLoading, skeleton = true, error, items, totalItems, currentPage, pageSize, onPageChange, onPageSizeChange, hrefFor, newHref, renderLink = renderAnchor, onSelect, preferencesKey, showControls = true, pivotBreakpoint = 'md', filtering, sorting, onFilteringChange, onSortingChange, selection, onSelectionChange, metadata, isMetadataLoading = false, onBulkUseCase, onUseCase, bulkOutcomes, onBulkDismiss, onBulkRetry, isBulkRunning = false, children }) {
55
- var _ref;
40
+ /** How many members a card shows as meta when none declares a `card` role. */ const UNDECLARED_META_CAP = 3;
41
+ export function EntityTable({ entityConstructor, isLoading, skeleton = true, error, items, totalItems, currentPage, pageSize, onPageChange, onPageSizeChange, hrefFor, newHref, renderLink = renderAnchor, onSelect, preferencesKey, showControls = true, pivotBreakpoint, filtering, sorting, onFilteringChange, onSortingChange, facets, selection, onSelectionChange, metadata, isMetadataLoading = false, onBulkUseCase, onUseCase, onRetry, bulkOutcomes, onBulkDismiss, onBulkRetry, isBulkRunning = false, children }) {
42
+ var _ref, _ref1;
56
43
  const t = useT();
57
44
  // A descriptor's label is a runtime catalog key — the typed gate cannot see
58
45
  // it, which is why a host's catalog check is what catches a typo.
59
46
  const translateKey = useTranslateKey();
60
- const errorMessage = useErrorMessage();
47
+ const entityNoun = useEntityNoun();
61
48
  const metaEntity = extractMetaEntity(entityConstructor);
62
49
  const slots = readEntityTableSlots(children);
63
50
  const [panel, setPanel] = useState('none');
64
- /**
65
- * A verb waiting to be confirmed, and what it will act on.
66
- *
67
- * ⚠️ **The table had no confirmation flow at all** until a change moved a
68
- * destructive verb onto it. It read `confirm.tone` — only to pick a tone for
69
- * a button or a menu item — and then fired on the first click. `unpublish` is
70
- * declared `destructive`, so a verb reachable from a row menu without this
71
- * would have been a one-click unpublish with a red label explaining what had
72
- * already happened.
73
- *
74
- * The state holds the confirmation itself rather than the descriptor, so the
75
- * dialog cannot open for a verb that never asked for one — the same shape
76
- * `EntityForm` uses, and for the same reason.
77
- *
78
- * It carries the **target** too, which the form's does not need: a form has
79
- * one record and a table has a row or a selection, and those run through two
80
- * different handlers with two different payloads.
81
- */ const [pending, setPending] = useState(undefined);
82
51
  const { columns, visibleColumns, hidden, setPersonalization } = useEntityTableColumns(entityConstructor, slots.columns, items[0], `entity-table:${(_ref = preferencesKey != null ? preferencesKey : metaEntity.key) != null ? _ref : metaEntity.name}`);
83
- const pivot = PIVOT_CLASS[pivotBreakpoint];
84
52
  // A picker's table is choosing *one* value; a multi-selection there would
85
53
  // offer a set to a form member that holds a single reference, and the two
86
54
  // controls would fight over the same row. This is a wiring mistake rather
@@ -116,30 +84,91 @@ export function EntityTable({ entityConstructor, isLoading, skeleton = true, err
116
84
  total: totalItems,
117
85
  excluded: new Set()
118
86
  };
119
- const hasRowAction = onSelect !== undefined || hrefFor !== undefined || hasRowMenu;
87
+ // Opening a record is its title's link, so a row needs an actions column
88
+ // only to pick it or to run a verb on it.
89
+ const hasRowAction = onSelect !== undefined || hasRowMenu;
120
90
  const columnCount = visibleColumns.length + (hasRowAction ? 1 : 0) + (selectable ? 1 : 0);
121
91
  /**
92
+ * The grid and the cards are both rendered, and a container query on the
93
+ * table's own box picks one — at the width its visible columns need. A
94
+ * viewport breakpoint was wrong inside a split view, a drawer or a wizard
95
+ * step, and a JS measurement would guess during SSR and correct after mount.
96
+ */ const pivot = pivotClasses(pivotWidth(visibleColumns, (selectable ? SELECTION_WIDTH : 0) + (hasRowAction ? ACTIONS_WIDTH : 0), pivotBreakpoint));
97
+ /**
122
98
  * The first load has nothing to show and is held with a skeleton; a refetch
123
99
  * already has rows, so it keeps them and only marks the region busy. Splitting
124
100
  * the two is what stops every pagination click flashing grey.
125
101
  */ const isFirstLoad = isLoading && items.length === 0;
126
- const isRefetching = isLoading && items.length > 0;
127
102
  /** `true`/omitted → the built-in default; a node → that node; `false` → none. */ const customSkeleton = typeof skeleton === 'boolean' ? undefined : skeleton;
128
103
  const showSkeleton = isFirstLoad && skeleton !== false;
129
104
  const skeletonRows = Array.from({
130
105
  length: Math.min(pageSize, SKELETON_ROW_CAP)
131
106
  }, (_, index)=>index);
132
107
  /**
133
- * The refetch hint, and it is deliberately the only one: rows stay readable
134
- * and simply dim, because a second skeleton over live data would be two
135
- * loading signals in one region.
136
- */ const busyClass = isRefetching ? 'opacity-60 transition-opacity duration-200 ease-smooth' : '';
137
- const renderCell = (column, item)=>column.render ? column.render(item) : /*#__PURE__*/ _jsx(CellValue, {
108
+ * A refetch keeps its rows at full contrast and marks them `aria-busy`:
109
+ * dimming them through opacity took the text under the contrast a reader
110
+ * needs, on every page change.
111
+ */ const busy = isLoading || undefined;
112
+ /** The column a row reads *as*: the first non-identifier one it filled in. */ const titleColumn = (item)=>visibleColumns.find((column)=>{
113
+ if (column.type === 'id') return false;
114
+ const value = item[column.name];
115
+ return value != null && String(value) !== '';
116
+ });
117
+ /** The column that links to the record: one declared `card: 'title'`, else the row's title. */ const declaredTitle = visibleColumns.find((column)=>column.card === 'title');
118
+ const linkColumn = (item)=>declaredTitle != null ? declaredTitle : titleColumn(item);
119
+ /**
120
+ * What a record's card shows, by each member's declared `card` role or the
121
+ * default: an id is hidden, the first enum is the badge, and up to
122
+ * {@link UNDECLARED_META_CAP} more members are meta — a card is a summary,
123
+ * and the grid holds the rest.
124
+ */ const firstEnum = visibleColumns.find((column)=>column.card === undefined && column.type === 'enum');
125
+ const roleOf = (column)=>{
126
+ var _column_card;
127
+ return (_column_card = column.card) != null ? _column_card : column.type === 'id' ? 'hidden' : column === firstEnum ? 'badge' : 'meta';
128
+ };
129
+ const cardParts = (item)=>{
130
+ const title = linkColumn(item);
131
+ const rest = visibleColumns.filter((column)=>column !== title);
132
+ let undeclared = 0;
133
+ return {
134
+ title,
135
+ subtitle: rest.find((column)=>roleOf(column) === 'subtitle'),
136
+ badges: rest.filter((column)=>roleOf(column) === 'badge'),
137
+ meta: rest.filter((column)=>{
138
+ if (roleOf(column) !== 'meta') return false;
139
+ if (column.card !== undefined) return true;
140
+ undeclared += 1;
141
+ return undeclared <= UNDECLARED_META_CAP;
142
+ })
143
+ };
144
+ };
145
+ const renderValue = (column, item)=>column.render ? column.render(item) : /*#__PURE__*/ _jsx(CellValue, {
138
146
  value: item[column.name],
139
147
  descriptor: column,
140
148
  record: item
141
149
  });
142
150
  /**
151
+ * A cell, and the row's title cell is the record's link
152
+ * ([ADR 0026](../../../../../../../docs/adr/0026-actions-hierarchy-overflow-and-destructive-verbs.md)).
153
+ * A column of identical "Open" links named nothing, and a reader tabbing
154
+ * down it heard "Open" once per row; the title says which record it opens.
155
+ * A picker's rows are chosen, not opened, so they stay plain.
156
+ */ const renderCell = (column, item)=>{
157
+ var _linkColumn;
158
+ const value = renderValue(column, item);
159
+ if (hrefFor === undefined || onSelect !== undefined) return value;
160
+ if (((_linkColumn = linkColumn(item)) == null ? void 0 : _linkColumn.name) !== column.name) return value;
161
+ return renderLink({
162
+ href: hrefFor(item.id),
163
+ className: linkClassName,
164
+ children: value,
165
+ intent: {
166
+ kind: 'open',
167
+ id: item.id
168
+ }
169
+ });
170
+ };
171
+ /**
143
172
  * The row a shift-click ranges *from* — the last box the user actually
144
173
  * toggled. Held in a ref rather than state because it changes nothing on
145
174
  * screen, and re-rendering the whole table on every click to remember an
@@ -220,11 +249,7 @@ export function EntityTable({ entityConstructor, isLoading, skeleton = true, err
220
249
  //
221
250
  // The id remains the last resort, because a row with nothing rendered
222
251
  // still has to announce itself.
223
- const named = visibleColumns.find((column)=>{
224
- if (column.type === 'id') return false;
225
- const value = item[column.name];
226
- return value != null && String(value) !== '';
227
- });
252
+ const named = titleColumn(item);
228
253
  return named ? String(item[named.name]) : String(item.id);
229
254
  };
230
255
  const selectionCell = (item, index, // Passed in rather than closed over: this is only ever called under
@@ -245,113 +270,196 @@ export function EntityTable({ entityConstructor, isLoading, skeleton = true, err
245
270
  if (target.kind === 'row') onUseCase == null ? void 0 : onUseCase(key, target.item);
246
271
  else onBulkUseCase == null ? void 0 : onBulkUseCase(key, target.selection);
247
272
  };
273
+ /**
274
+ * Every verb on the table goes through here — the row menu, the bulk bar and
275
+ * the toolbar — so none reaches a destructive verb around its confirmation,
276
+ * its typed check or its Undo.
277
+ */ const verbs = useVerbInvoker({
278
+ run: runTarget,
279
+ busy: isBulkRunning,
280
+ served: metadata == null ? void 0 : metadata.useCases
281
+ });
248
282
  const invoke = (useCase, target)=>{
249
- const confirm = useCase.confirm;
250
- if (confirm) {
251
- setPending({
252
- key: useCase.key,
253
- labelKey: useCase.labelKey,
254
- confirm,
255
- target
283
+ if (target.kind === 'row') {
284
+ verbs.invoke({
285
+ descriptor: useCase,
286
+ target,
287
+ object: rowLabel(target.item),
288
+ count: 1
256
289
  });
257
290
  return;
258
291
  }
259
- runTarget(useCase.key, target);
292
+ const count = selectionSize(target.selection);
293
+ verbs.invoke({
294
+ descriptor: useCase,
295
+ target,
296
+ object: t('confirm.selection', {
297
+ count,
298
+ label: entityNoun(metaEntity),
299
+ plural: entityNoun(metaEntity, 'plural')
300
+ }),
301
+ count,
302
+ matching: target.selection.mode === 'matching'
303
+ });
260
304
  };
261
- const rowMenu = (item)=>hasRowMenu ? /*#__PURE__*/ _jsxs(Menu, {
262
- children: [
263
- /*#__PURE__*/ _jsx(Menu.Trigger, {
264
- "aria-label": t('table.actions'),
265
- children: OVERFLOW_GLYPH
266
- }),
267
- /*#__PURE__*/ _jsx(Menu.Items, {
268
- children: rowUseCases.map((useCase)=>{
269
- var _useCase_confirm;
270
- return /*#__PURE__*/ _jsx(Menu.Item, {
271
- tone: ((_useCase_confirm = useCase.confirm) == null ? void 0 : _useCase_confirm.tone) === 'destructive' ? 'destructive' : 'neutral',
272
- onClick: ()=>invoke(useCase, {
273
- kind: 'row',
274
- item
275
- }),
276
- children: translateKey(useCase.labelKey)
277
- }, useCase.key);
278
- })
279
- })
280
- ]
305
+ const rowMenu = (item)=>hasRowMenu ? /*#__PURE__*/ _jsx(ActionBar, {
306
+ surface: "row-menu",
307
+ menuLabel: t('actions.forRecord', {
308
+ record: rowLabel(item)
309
+ }),
310
+ items: itemsForUseCases(rowUseCases, translateKey, (useCase)=>invoke(useCase, {
311
+ kind: 'row',
312
+ item
313
+ }))
281
314
  }) : undefined;
282
315
  /**
283
- * Selection wins over navigation: a picker's row must set the value, not send
284
- * the user away from the form they came from.
285
- *
286
- * The row menu sits *beside* whichever of the two applies rather than
287
- * replacing it — "open this record" and "run a verb on it" are both true at
288
- * once, and folding the first into the menu would bury the action every row
289
- * of a list exists for.
316
+ * A picker's Select, then the row's verbs. Opening the record is not here:
317
+ * it is the title's link.
290
318
  */ const recordAction = (item)=>{
291
- const primary = onSelect ? /*#__PURE__*/ _jsx(Button, {
319
+ const pick = onSelect ? /*#__PURE__*/ _jsx(Button, {
292
320
  type: "button",
293
321
  variant: "secondary",
294
322
  size: "sm",
295
323
  onClick: ()=>onSelect(item),
296
324
  children: t('table.select')
297
- }) : hrefFor ? renderLink({
298
- href: hrefFor(item.id),
299
- className: linkClassName,
300
- children: t('table.open'),
301
- intent: {
302
- kind: 'open',
303
- id: item.id
304
- }
305
325
  }) : undefined;
306
326
  const menu = rowMenu(item);
307
- if (!menu) return primary;
327
+ if (!menu) return pick;
308
328
  return /*#__PURE__*/ _jsxs("span", {
309
329
  className: "flex items-center gap-2xs",
310
330
  children: [
311
- primary,
331
+ pick,
312
332
  menu
313
333
  ]
314
334
  });
315
335
  };
316
336
  /**
317
- * "No records" is a statement about the data; after a failed load it would be
318
- * a lie about it. The distinction matters most to the user who is deciding
319
- * whether their catalog is empty or their backend is down.
320
- */ const emptyMessage = error ? t('table.error') : t('table.empty');
337
+ * "New" is the list's primary, and only where something can be created
338
+ * here: the entity is not one only a flow writes (`creatable: false`), and
339
+ * the caller may write it. A list that offered "New" to a reader who could
340
+ * never save opened a form with no Save on it.
341
+ */ const mayCreate = newHref !== undefined && metaEntity.creatable && (metadata === undefined || metadata.actions.includes('write'));
342
+ const toolbarItems = itemsForUseCases(toolbarUseCases, translateKey, (useCase)=>invoke(useCase, {
343
+ kind: 'selection',
344
+ selection: everythingMatching
345
+ }), isBulkRunning);
346
+ const declaresToolbarPrimary = toolbarItems.some((item)=>item.emphasis === 'primary');
347
+ const toolbarActions = [
348
+ ...mayCreate ? [
349
+ {
350
+ key: 'new',
351
+ label: t('table.new'),
352
+ emphasis: declaresToolbarPrimary ? 'default' : 'primary',
353
+ href: newHref,
354
+ intent: {
355
+ kind: 'new'
356
+ }
357
+ }
358
+ ] : [],
359
+ ...toolbarItems
360
+ ];
361
+ const locale = useLocale();
362
+ const panelId = useId();
363
+ const keys = sortKeys(sorting, columns);
364
+ const activeFilters = (_ref1 = filtering == null ? void 0 : filtering.values.length) != null ? _ref1 : 0;
365
+ const results = t('table.results', {
366
+ count: totalItems
367
+ });
368
+ /**
369
+ * What the status region says once a filter, a sort or a page has settled —
370
+ * debounced, and silent on the first load.
371
+ */ const announcement = useResultAnnouncement(!isLoading, totalItems === 0 && activeFilters > 0 ? t('table.noResultsFiltered') : results, JSON.stringify([
372
+ filtering,
373
+ sorting,
374
+ currentPage,
375
+ pageSize
376
+ ]));
377
+ /** The caption: what the table holds, how many, and in what order. */ const primaryKey = keys[0];
378
+ const primaryColumn = primaryKey === undefined ? undefined : columns.find((column)=>column.name === primaryKey.member);
379
+ const caption = primaryKey === undefined || primaryColumn === undefined ? t('table.caption', {
380
+ entity: entityNoun(metaEntity, 'plural'),
381
+ results
382
+ }) : t('table.captionSorted', {
383
+ entity: entityNoun(metaEntity, 'plural'),
384
+ results,
385
+ column: primaryColumn.label,
386
+ direction: t(primaryKey.type === 'asc' ? 'sort.ascending' : 'sort.descending')
387
+ });
388
+ /**
389
+ * A column's header: its name, and — for a sortable one — a button that
390
+ * cycles it ascending → descending → unsorted, with Shift adding it as the
391
+ * next key. Only the primary key carries `aria-sort`.
392
+ */ const headerCell = (column)=>{
393
+ var _column_header;
394
+ var _keys_position;
395
+ const label = (_column_header = column.header) != null ? _column_header : column.label;
396
+ if (!column.sortable || onSortingChange === undefined) return /*#__PURE__*/ _jsx(TableHeaderCell, {
397
+ children: label
398
+ }, column.name);
399
+ const position = keys.findIndex((key)=>key.member === column.name);
400
+ const type = (_keys_position = keys[position]) == null ? void 0 : _keys_position.type;
401
+ return /*#__PURE__*/ _jsx(TableHeaderCell, {
402
+ "aria-sort": position === 0 ? type === 'asc' ? 'ascending' : 'descending' : undefined,
403
+ children: /*#__PURE__*/ _jsxs("button", {
404
+ type: "button",
405
+ className: "inline-flex min-h-(--target-min) items-center gap-3xs rounded-sm focus-ring hover:text-content",
406
+ onClick: (event)=>onSortingChange(nextHeaderSorting(keys, {
407
+ member: column.name,
408
+ property: queryPropertyFor(column, locale)
409
+ }, event.shiftKey)),
410
+ children: [
411
+ label,
412
+ /*#__PURE__*/ _jsx("span", {
413
+ "aria-hidden": "true",
414
+ children: type === 'asc' ? '▲' : type === 'desc' ? '▼' : '↕'
415
+ }),
416
+ keys.length > 1 && position >= 0 && /*#__PURE__*/ _jsx("span", {
417
+ "aria-hidden": "true",
418
+ className: "text-step-sm",
419
+ children: position + 1
420
+ })
421
+ ]
422
+ })
423
+ }, column.name);
424
+ };
425
+ /** A panel toggle: its state said by `aria-expanded`, and its count shown. */ const panelToggle = (which, count)=>/*#__PURE__*/ _jsxs(Button, {
426
+ type: "button",
427
+ variant: panel === which ? 'primary' : 'secondary',
428
+ size: "sm",
429
+ "aria-expanded": panel === which,
430
+ "aria-controls": panelId,
431
+ onClick: ()=>setPanel((current)=>current === which ? 'none' : which),
432
+ children: [
433
+ t(which === 'filters' ? 'table.filters' : 'table.sorting'),
434
+ count > 0 && // Its name stays "Filters": the chips say which filters, and a count
435
+ // in the name would change it under the reader with every filter.
436
+ /*#__PURE__*/ _jsx("span", {
437
+ "aria-hidden": "true",
438
+ className: "ms-3xs rounded-full bg-surface px-3xs text-step-sm text-content",
439
+ children: count
440
+ })
441
+ ]
442
+ });
321
443
  return /*#__PURE__*/ _jsxs("div", {
322
- className: "flex flex-col gap-s",
444
+ className: "@container/entity-table flex flex-col gap-s",
323
445
  children: [
324
446
  /*#__PURE__*/ _jsx("span", {
325
447
  role: "status",
326
448
  "aria-live": "polite",
327
449
  className: "sr-only",
328
- children: isFirstLoad ? t('table.loading') : ''
450
+ children: isFirstLoad ? t('table.loading') : announcement
329
451
  }),
330
452
  showControls && /*#__PURE__*/ _jsx(TableToolbar, {
331
453
  start: /*#__PURE__*/ _jsxs(_Fragment, {
332
454
  children: [
333
- newHref && renderLink({
334
- href: newHref,
335
- className: linkClassName,
336
- children: t('table.new'),
337
- intent: {
338
- kind: 'new'
339
- }
340
- }),
341
455
  /*#__PURE__*/ _jsx(LoadingBoundary, {
342
456
  isLoading: isMetadataLoading,
343
457
  lines: 0,
344
458
  children: /*#__PURE__*/ _jsx(_Fragment, {
345
- children: toolbarUseCases.map((useCase)=>{
346
- var _useCase_confirm;
347
- return /*#__PURE__*/ _jsx(Button, {
348
- type: "button",
349
- variant: ((_useCase_confirm = useCase.confirm) == null ? void 0 : _useCase_confirm.tone) === 'destructive' ? 'destructive' : 'secondary',
350
- size: "sm",
351
- disabled: isBulkRunning,
352
- onClick: ()=>onBulkUseCase == null ? void 0 : onBulkUseCase(useCase.key, everythingMatching),
353
- children: translateKey(useCase.labelKey)
354
- }, useCase.key);
459
+ children: toolbarActions.length > 0 && /*#__PURE__*/ _jsx(ActionBar, {
460
+ surface: "table-toolbar",
461
+ items: toolbarActions,
462
+ renderLink: renderLink
355
463
  })
356
464
  })
357
465
  }),
@@ -360,20 +468,8 @@ export function EntityTable({ entityConstructor, isLoading, skeleton = true, err
360
468
  }),
361
469
  end: /*#__PURE__*/ _jsxs(_Fragment, {
362
470
  children: [
363
- /*#__PURE__*/ _jsx(Button, {
364
- type: "button",
365
- variant: panel === 'filters' ? 'primary' : 'secondary',
366
- size: "sm",
367
- onClick: ()=>setPanel((current)=>current === 'filters' ? 'none' : 'filters'),
368
- children: t('table.filters')
369
- }),
370
- /*#__PURE__*/ _jsx(Button, {
371
- type: "button",
372
- variant: panel === 'sorting' ? 'primary' : 'secondary',
373
- size: "sm",
374
- onClick: ()=>setPanel((current)=>current === 'sorting' ? 'none' : 'sorting'),
375
- children: t('table.sorting')
376
- }),
471
+ panelToggle('filters', activeFilters),
472
+ panelToggle('sorting', keys.length),
377
473
  /*#__PURE__*/ _jsx(ColumnSettings, {
378
474
  columns: columns,
379
475
  hidden: hidden,
@@ -381,16 +477,23 @@ export function EntityTable({ entityConstructor, isLoading, skeleton = true, err
381
477
  })
382
478
  ]
383
479
  }),
480
+ panelId: panelId,
384
481
  panel: panel === 'filters' ? /*#__PURE__*/ _jsx(FilterBuilder, {
385
482
  descriptors: columns.filter((column)=>column.filterable),
483
+ facets: facets,
386
484
  value: filtering,
387
485
  onChange: (applied)=>onFilteringChange == null ? void 0 : onFilteringChange(applied)
388
- }) : panel === 'sorting' ? /*#__PURE__*/ _jsx(SortBuilder, {
486
+ }, JSON.stringify(filtering != null ? filtering : null)) : panel === 'sorting' ? /*#__PURE__*/ _jsx(SortBuilder, {
389
487
  descriptors: columns.filter((column)=>column.sortable),
390
488
  value: sorting,
391
489
  onChange: (applied)=>onSortingChange == null ? void 0 : onSortingChange(applied)
392
490
  }) : undefined
393
491
  }),
492
+ showControls && onFilteringChange !== undefined && /*#__PURE__*/ _jsx(FilterChips, {
493
+ descriptors: columns.filter((column)=>column.filterable),
494
+ value: filtering,
495
+ onChange: onFilteringChange
496
+ }),
394
497
  selectionState && selectionSize(selectionState) > 0 && /*#__PURE__*/ _jsx(BulkActionBar, {
395
498
  count: selectionSize(selectionState),
396
499
  useCases: bulkUseCases,
@@ -405,19 +508,7 @@ export function EntityTable({ entityConstructor, isLoading, skeleton = true, err
405
508
  selection: selectionState
406
509
  })
407
510
  }),
408
- pending && /*#__PURE__*/ _jsx(ConfirmDialog, {
409
- open: true,
410
- tone: pending.confirm.tone,
411
- title: translateKey(pending.labelKey),
412
- message: translateKey(pending.confirm.messageKey),
413
- busy: isBulkRunning,
414
- onCancel: ()=>setPending(undefined),
415
- onConfirm: ()=>{
416
- const { key, target } = pending;
417
- setPending(undefined);
418
- runTarget(key, target);
419
- }
420
- }),
511
+ verbs.dialog,
421
512
  bulkOutcomes && bulkOutcomes.length > 0 && onBulkDismiss && /*#__PURE__*/ _jsx(BulkResult, {
422
513
  outcomes: bulkOutcomes,
423
514
  busy: isBulkRunning,
@@ -428,15 +519,37 @@ export function EntityTable({ entityConstructor, isLoading, skeleton = true, err
428
519
  onRetryFailed: onBulkRetry,
429
520
  onDismiss: onBulkDismiss
430
521
  }),
431
- error && /*#__PURE__*/ _jsx("p", {
432
- role: "alert",
433
- "data-testid": "entity-table-error",
434
- className: "rounded-sm border border-danger bg-danger-subtle px-s py-2xs text-step-sm text-danger",
435
- children: errorMessage(error)
522
+ /*#__PURE__*/ _jsx(RegionState, {
523
+ plural: entityNoun(metaEntity, 'plural'),
524
+ isLoading: isLoading,
525
+ error: error,
526
+ onRetry: onRetry,
527
+ hasStaleData: items.length > 0,
528
+ isEmpty: items.length === 0,
529
+ isFiltered: activeFilters > 0,
530
+ onClearFilters: onFilteringChange === undefined ? undefined : ()=>onFilteringChange({
531
+ // Only offered under a filter, so there is one to keep the
532
+ // logic of.
533
+ operator: filtering.operator,
534
+ values: []
535
+ }),
536
+ emptyAction: mayCreate ? renderLink({
537
+ href: newHref,
538
+ className: button({
539
+ variant: 'primary'
540
+ }),
541
+ children: t('table.new'),
542
+ intent: {
543
+ kind: 'new'
544
+ }
545
+ }) : undefined
436
546
  }),
437
547
  /*#__PURE__*/ _jsx("div", {
438
- className: `${pivot.grid} ${busyClass}`,
548
+ className: pivot.grid,
549
+ "aria-busy": busy,
439
550
  children: /*#__PURE__*/ _jsxs(Table, {
551
+ caption: caption,
552
+ containerClassName: "max-h-(--table-max-block) scroll-pt-12",
440
553
  children: [
441
554
  /*#__PURE__*/ _jsx(TableHead, {
442
555
  children: slots.header ? slots.header.render(visibleColumns) : /*#__PURE__*/ _jsxs("tr", {
@@ -450,12 +563,7 @@ export function EntityTable({ entityConstructor, isLoading, skeleton = true, err
450
563
  onChange: (event)=>toggleAllOnPage(event.currentTarget.checked, selectionState)
451
564
  })
452
565
  }),
453
- visibleColumns.map((column)=>{
454
- var _column_header;
455
- return /*#__PURE__*/ _jsx(TableHeaderCell, {
456
- children: (_column_header = column.header) != null ? _column_header : column.label
457
- }, column.name);
458
- }),
566
+ visibleColumns.map(headerCell),
459
567
  hasRowAction && /*#__PURE__*/ _jsx(TableHeaderCell, {
460
568
  children: t('table.actions')
461
569
  })
@@ -463,7 +571,6 @@ export function EntityTable({ entityConstructor, isLoading, skeleton = true, err
463
571
  })
464
572
  }),
465
573
  /*#__PURE__*/ _jsxs(TableBody, {
466
- "aria-busy": isLoading || undefined,
467
574
  children: [
468
575
  showSkeleton && (customSkeleton ? /*#__PURE__*/ _jsx(TableMessageRow, {
469
576
  colSpan: columnCount,
@@ -490,10 +597,6 @@ export function EntityTable({ entityConstructor, isLoading, skeleton = true, err
490
597
  })
491
598
  ]
492
599
  }, row))),
493
- !isLoading && items.length === 0 && /*#__PURE__*/ _jsx(TableMessageRow, {
494
- colSpan: columnCount,
495
- children: emptyMessage
496
- }),
497
600
  shown.map(({ item, depth, hasChildren }, index)=>{
498
601
  var _item_id, _item_id1;
499
602
  return slots.row ? // A Fragment carries the key without wrapping the custom row in
@@ -529,30 +632,62 @@ export function EntityTable({ entityConstructor, isLoading, skeleton = true, err
529
632
  })
530
633
  }),
531
634
  /*#__PURE__*/ _jsxs("div", {
532
- className: `${pivot.cards} flex flex-col gap-2xs ${busyClass}`,
533
- "aria-busy": isLoading || undefined,
635
+ className: pivot.cards,
636
+ "aria-busy": busy,
534
637
  children: [
535
- showSkeleton && (customSkeleton != null ? customSkeleton : skeletonRows.map((row)=>/*#__PURE__*/ _jsx(EntityRecordCard, {
536
- columns: visibleColumns,
537
- renderCell: ()=>/*#__PURE__*/ _jsx(Skeleton, {
538
- shape: "line",
539
- className: "w-24"
638
+ showSkeleton && (customSkeleton != null ? customSkeleton : /*#__PURE__*/ _jsx("ul", {
639
+ role: "list",
640
+ className: "m-0 p-0 flex list-none flex-col gap-2xs",
641
+ children: skeletonRows.map((row)=>/*#__PURE__*/ _jsx("li", {
642
+ children: /*#__PURE__*/ _jsx(EntityRecordCard, {
643
+ title: /*#__PURE__*/ _jsx(Skeleton, {
644
+ shape: "line",
645
+ className: "w-32"
646
+ }),
647
+ meta: visibleColumns.slice(1).map((column)=>({
648
+ key: column.name,
649
+ label: column.label,
650
+ value: /*#__PURE__*/ _jsx(Skeleton, {
651
+ shape: "line",
652
+ className: "w-24"
653
+ })
654
+ }))
540
655
  })
541
- }, row))),
542
- !isLoading && items.length === 0 && /*#__PURE__*/ _jsx("p", {
543
- className: "text-step-sm text-content-muted",
544
- children: emptyMessage
545
- }),
546
- shown.map(({ item }, index)=>{
547
- var _item_id;
548
- return /*#__PURE__*/ _jsx(EntityRecordCard, {
549
- columns: visibleColumns,
550
- // The card is handed the resolved columns themselves, so the
551
- // narrow layout goes through the exact same renderers as the grid.
552
- renderCell: (column)=>renderCell(column, item),
553
- leading: selectionState ? selectionCell(item, index, selectionState) : undefined,
554
- actions: recordAction(item)
555
- }, String((_item_id = item.id) != null ? _item_id : index));
656
+ }, row))
657
+ })),
658
+ shown.length > 0 && /*#__PURE__*/ _jsx("ul", {
659
+ role: "list",
660
+ className: "m-0 p-0 flex list-none flex-col gap-2xs",
661
+ children: shown.map(({ item, depth }, index)=>{
662
+ var _item_id;
663
+ const parts = cardParts(item);
664
+ return /*#__PURE__*/ _jsx("li", {
665
+ // A tree's card says how deep it sits, as its row does.
666
+ "aria-level": treeKeys === undefined ? undefined : depth + 1,
667
+ style: treeKeys === undefined ? undefined : {
668
+ marginInlineStart: `${depth * 1.5}rem`
669
+ },
670
+ children: /*#__PURE__*/ _jsx(EntityRecordCard, {
671
+ // The same renderers as the grid, so the two layouts agree.
672
+ title: parts.title === undefined ? rowLabel(item) : renderCell(parts.title, item),
673
+ subtitle: parts.subtitle === undefined ? undefined : renderCell(parts.subtitle, item),
674
+ badges: parts.badges.map((column)=>({
675
+ key: column.name,
676
+ value: renderCell(column, item)
677
+ })),
678
+ meta: parts.meta.map((column)=>{
679
+ var _column_header;
680
+ return {
681
+ key: column.name,
682
+ label: (_column_header = column.header) != null ? _column_header : column.label,
683
+ value: renderCell(column, item)
684
+ };
685
+ }),
686
+ leading: selectionState ? selectionCell(item, index, selectionState) : undefined,
687
+ actions: recordAction(item)
688
+ })
689
+ }, String((_item_id = item.id) != null ? _item_id : index));
690
+ })
556
691
  })
557
692
  ]
558
693
  }),