@entifix/react-controls 0.18.2 → 0.19.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 (180) hide show
  1. package/README.md +5 -5
  2. package/dist/i18n/catalog/en.d.ts.map +1 -1
  3. package/dist/i18n/catalog/en.js +13 -1
  4. package/dist/i18n/catalog/en.js.map +1 -1
  5. package/dist/i18n/catalog/es.d.ts +12 -0
  6. package/dist/i18n/catalog/es.d.ts.map +1 -1
  7. package/dist/i18n/catalog/es.js +13 -1
  8. package/dist/i18n/catalog/es.js.map +1 -1
  9. package/dist/i18n/catalog/index.d.ts +24 -0
  10. package/dist/i18n/catalog/index.d.ts.map +1 -1
  11. package/dist/index.d.ts +7 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +7 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/primitives.d.ts +7 -0
  16. package/dist/primitives.d.ts.map +1 -1
  17. package/dist/primitives.js +7 -0
  18. package/dist/primitives.js.map +1 -1
  19. package/dist/theme/theme-context.d.ts +23 -7
  20. package/dist/theme/theme-context.d.ts.map +1 -1
  21. package/dist/theme/theme-context.js +76 -14
  22. package/dist/theme/theme-context.js.map +1 -1
  23. package/dist/theme/theme-script.d.ts +4 -3
  24. package/dist/theme/theme-script.d.ts.map +1 -1
  25. package/dist/theme/theme-script.js +8 -3
  26. package/dist/theme/theme-script.js.map +1 -1
  27. package/dist/theme/types.d.ts +19 -3
  28. package/dist/theme/types.d.ts.map +1 -1
  29. package/dist/theme/types.js.map +1 -1
  30. package/dist/ui/atoms/alert/alert-dismiss.d.ts +9 -0
  31. package/dist/ui/atoms/alert/alert-dismiss.d.ts.map +1 -0
  32. package/dist/ui/atoms/alert/alert-dismiss.js +28 -0
  33. package/dist/ui/atoms/alert/alert-dismiss.js.map +1 -0
  34. package/dist/ui/atoms/alert/alert.d.ts +24 -0
  35. package/dist/ui/atoms/alert/alert.d.ts.map +1 -0
  36. package/dist/ui/atoms/alert/alert.js +50 -0
  37. package/dist/ui/atoms/alert/alert.js.map +1 -0
  38. package/dist/ui/atoms/alert/index.d.ts +3 -0
  39. package/dist/ui/atoms/alert/index.d.ts.map +1 -0
  40. package/dist/ui/atoms/alert/index.js +4 -0
  41. package/dist/ui/atoms/alert/index.js.map +1 -0
  42. package/dist/ui/atoms/alert/tone.d.ts +8 -0
  43. package/dist/ui/atoms/alert/tone.d.ts.map +1 -0
  44. package/dist/ui/atoms/alert/tone.js +11 -0
  45. package/dist/ui/atoms/alert/tone.js.map +1 -0
  46. package/dist/ui/atoms/button/button-recipe.js +1 -1
  47. package/dist/ui/atoms/button/button-recipe.js.map +1 -1
  48. package/dist/ui/atoms/button/index.d.ts +1 -0
  49. package/dist/ui/atoms/button/index.d.ts.map +1 -1
  50. package/dist/ui/atoms/button/index.js +1 -0
  51. package/dist/ui/atoms/button/index.js.map +1 -1
  52. package/dist/ui/atoms/button/submit-button.d.ts +13 -0
  53. package/dist/ui/atoms/button/submit-button.d.ts.map +1 -0
  54. package/dist/ui/atoms/button/submit-button.js +29 -0
  55. package/dist/ui/atoms/button/submit-button.js.map +1 -0
  56. package/dist/ui/atoms/cell-value/cell-value.d.ts +6 -1
  57. package/dist/ui/atoms/cell-value/cell-value.d.ts.map +1 -1
  58. package/dist/ui/atoms/cell-value/cell-value.js +20 -2
  59. package/dist/ui/atoms/cell-value/cell-value.js.map +1 -1
  60. package/dist/ui/atoms/field/checkbox.d.ts.map +1 -1
  61. package/dist/ui/atoms/field/checkbox.js +21 -1
  62. package/dist/ui/atoms/field/checkbox.js.map +1 -1
  63. package/dist/ui/atoms/field/field.js +1 -1
  64. package/dist/ui/atoms/field/field.js.map +1 -1
  65. package/dist/ui/atoms/field-control/field-control.d.ts +6 -1
  66. package/dist/ui/atoms/field-control/field-control.d.ts.map +1 -1
  67. package/dist/ui/atoms/field-control/field-control.js +23 -3
  68. package/dist/ui/atoms/field-control/field-control.js.map +1 -1
  69. package/dist/ui/atoms/money/index.d.ts +4 -0
  70. package/dist/ui/atoms/money/index.d.ts.map +1 -0
  71. package/dist/ui/atoms/money/index.js +5 -0
  72. package/dist/ui/atoms/money/index.js.map +1 -0
  73. package/dist/ui/atoms/money/money-input.d.ts +19 -0
  74. package/dist/ui/atoms/money/money-input.d.ts.map +1 -0
  75. package/dist/ui/atoms/money/money-input.js +75 -0
  76. package/dist/ui/atoms/money/money-input.js.map +1 -0
  77. package/dist/ui/atoms/money/money-math.d.ts +21 -0
  78. package/dist/ui/atoms/money/money-math.d.ts.map +1 -0
  79. package/dist/ui/atoms/money/money-math.js +36 -0
  80. package/dist/ui/atoms/money/money-math.js.map +1 -0
  81. package/dist/ui/atoms/money/money.d.ts +19 -0
  82. package/dist/ui/atoms/money/money.d.ts.map +1 -0
  83. package/dist/ui/atoms/money/money.js +26 -0
  84. package/dist/ui/atoms/money/money.js.map +1 -0
  85. package/dist/ui/atoms/number-input/index.d.ts +3 -0
  86. package/dist/ui/atoms/number-input/index.d.ts.map +1 -0
  87. package/dist/ui/atoms/number-input/index.js +4 -0
  88. package/dist/ui/atoms/number-input/index.js.map +1 -0
  89. package/dist/ui/atoms/number-input/number-input.d.ts +10 -0
  90. package/dist/ui/atoms/number-input/number-input.d.ts.map +1 -0
  91. package/dist/ui/atoms/number-input/number-input.js +23 -0
  92. package/dist/ui/atoms/number-input/number-input.js.map +1 -0
  93. package/dist/ui/atoms/number-input/quantity-input.d.ts +22 -0
  94. package/dist/ui/atoms/number-input/quantity-input.d.ts.map +1 -0
  95. package/dist/ui/atoms/number-input/quantity-input.js +99 -0
  96. package/dist/ui/atoms/number-input/quantity-input.js.map +1 -0
  97. package/dist/ui/atoms/skeleton/skeleton.js +1 -1
  98. package/dist/ui/atoms/skeleton/skeleton.js.map +1 -1
  99. package/dist/ui/atoms/status-tag/index.d.ts +2 -0
  100. package/dist/ui/atoms/status-tag/index.d.ts.map +1 -0
  101. package/dist/ui/atoms/status-tag/index.js +3 -0
  102. package/dist/ui/atoms/status-tag/index.js.map +1 -0
  103. package/dist/ui/atoms/status-tag/status-tag.d.ts +17 -0
  104. package/dist/ui/atoms/status-tag/status-tag.d.ts.map +1 -0
  105. package/dist/ui/atoms/status-tag/status-tag.js +37 -0
  106. package/dist/ui/atoms/status-tag/status-tag.js.map +1 -0
  107. package/dist/ui/atoms/switch/switch.d.ts.map +1 -1
  108. package/dist/ui/atoms/switch/switch.js +6 -3
  109. package/dist/ui/atoms/switch/switch.js.map +1 -1
  110. package/dist/ui/molecules/breadcrumbs/breadcrumbs.d.ts +11 -2
  111. package/dist/ui/molecules/breadcrumbs/breadcrumbs.d.ts.map +1 -1
  112. package/dist/ui/molecules/breadcrumbs/breadcrumbs.js +9 -7
  113. package/dist/ui/molecules/breadcrumbs/breadcrumbs.js.map +1 -1
  114. package/dist/ui/molecules/column-settings/column-settings.js +1 -1
  115. package/dist/ui/molecules/column-settings/column-settings.js.map +1 -1
  116. package/dist/ui/molecules/confirm-dialog/confirm-dialog.js +2 -2
  117. package/dist/ui/molecules/confirm-dialog/confirm-dialog.js.map +1 -1
  118. package/dist/ui/molecules/description-list/description-list.d.ts +17 -0
  119. package/dist/ui/molecules/description-list/description-list.d.ts.map +1 -0
  120. package/dist/ui/molecules/description-list/description-list.js +36 -0
  121. package/dist/ui/molecules/description-list/description-list.js.map +1 -0
  122. package/dist/ui/molecules/description-list/index.d.ts +2 -0
  123. package/dist/ui/molecules/description-list/index.d.ts.map +1 -0
  124. package/dist/ui/molecules/description-list/index.js +3 -0
  125. package/dist/ui/molecules/description-list/index.js.map +1 -0
  126. package/dist/ui/molecules/drawer/drawer.js +3 -3
  127. package/dist/ui/molecules/drawer/drawer.js.map +1 -1
  128. package/dist/ui/molecules/empty-state/empty-state.d.ts +15 -0
  129. package/dist/ui/molecules/empty-state/empty-state.d.ts.map +1 -0
  130. package/dist/ui/molecules/empty-state/empty-state.js +29 -0
  131. package/dist/ui/molecules/empty-state/empty-state.js.map +1 -0
  132. package/dist/ui/molecules/empty-state/index.d.ts +2 -0
  133. package/dist/ui/molecules/empty-state/index.d.ts.map +1 -0
  134. package/dist/ui/molecules/empty-state/index.js +3 -0
  135. package/dist/ui/molecules/empty-state/index.js.map +1 -0
  136. package/dist/ui/molecules/entity-link-input/entity-link-input.d.ts.map +1 -1
  137. package/dist/ui/molecules/entity-link-input/entity-link-input.js +7 -4
  138. package/dist/ui/molecules/entity-link-input/entity-link-input.js.map +1 -1
  139. package/dist/ui/molecules/entity-link-picker/entity-link-picker.js +2 -2
  140. package/dist/ui/molecules/entity-link-picker/entity-link-picker.js.map +1 -1
  141. package/dist/ui/molecules/media-gallery/index.d.ts +2 -0
  142. package/dist/ui/molecules/media-gallery/index.d.ts.map +1 -0
  143. package/dist/ui/molecules/media-gallery/index.js +3 -0
  144. package/dist/ui/molecules/media-gallery/index.js.map +1 -0
  145. package/dist/ui/molecules/media-gallery/media-gallery.d.ts +31 -0
  146. package/dist/ui/molecules/media-gallery/media-gallery.d.ts.map +1 -0
  147. package/dist/ui/molecules/media-gallery/media-gallery.js +64 -0
  148. package/dist/ui/molecules/media-gallery/media-gallery.js.map +1 -0
  149. package/dist/ui/molecules/menu/menu.d.ts.map +1 -1
  150. package/dist/ui/molecules/menu/menu.js +6 -4
  151. package/dist/ui/molecules/menu/menu.js.map +1 -1
  152. package/dist/ui/molecules/nav-menu/nav-menu.js +4 -4
  153. package/dist/ui/molecules/nav-menu/nav-menu.js.map +1 -1
  154. package/dist/ui/molecules/segmented-nav/segment-classes.d.ts +3 -3
  155. package/dist/ui/molecules/segmented-nav/segment-classes.d.ts.map +1 -1
  156. package/dist/ui/molecules/segmented-nav/segment-classes.js +2 -2
  157. package/dist/ui/molecules/segmented-nav/segment-classes.js.map +1 -1
  158. package/dist/ui/molecules/segmented-nav/segmented-nav.d.ts +14 -4
  159. package/dist/ui/molecules/segmented-nav/segmented-nav.d.ts.map +1 -1
  160. package/dist/ui/molecules/segmented-nav/segmented-nav.js +6 -5
  161. package/dist/ui/molecules/segmented-nav/segmented-nav.js.map +1 -1
  162. package/dist/ui/molecules/split-view/split-view.js +3 -3
  163. package/dist/ui/molecules/split-view/split-view.js.map +1 -1
  164. package/dist/ui/molecules/tab-strip/tab-strip.d.ts +51 -12
  165. package/dist/ui/molecules/tab-strip/tab-strip.d.ts.map +1 -1
  166. package/dist/ui/molecules/tab-strip/tab-strip.js +125 -34
  167. package/dist/ui/molecules/tab-strip/tab-strip.js.map +1 -1
  168. package/dist/ui/molecules/toast-region/toast-region.js +1 -1
  169. package/dist/ui/molecules/toast-region/toast-region.js.map +1 -1
  170. package/dist/ui/organisms/command-palette/command-palette.js +6 -6
  171. package/dist/ui/organisms/command-palette/command-palette.js.map +1 -1
  172. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.d.ts.map +1 -1
  173. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js +8 -2
  174. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js.map +1 -1
  175. package/dist/ui/organisms/entity-form/entity-form.js +4 -2
  176. package/dist/ui/organisms/entity-form/entity-form.js.map +1 -1
  177. package/dist/ui/organisms/entity-table/entity-table.d.ts.map +1 -1
  178. package/dist/ui/organisms/entity-table/entity-table.js +2 -1
  179. package/dist/ui/organisms/entity-table/entity-table.js.map +1 -1
  180. package/package.json +5 -5
@@ -18,7 +18,7 @@ function GroupHeading({ label }) {
18
18
  function CommandRow({ option }) {
19
19
  return /*#__PURE__*/ _jsxs(ComboboxOption, {
20
20
  value: option,
21
- className: cn('flex cursor-pointer items-baseline gap-2xs rounded-md px-2xs py-3xs', 'text-step-sm text-content', 'data-focus:bg-surface'),
21
+ className: cn('flex min-h-(--target-min) cursor-pointer items-baseline gap-2xs rounded-md px-2xs py-3xs', 'text-step-sm text-content', 'focus-ring data-focus:bg-surface data-focus:-outline-offset-2'),
22
22
  children: [
23
23
  /*#__PURE__*/ _jsx("span", {
24
24
  className: "truncate",
@@ -163,15 +163,15 @@ function Group({ group, labels }) {
163
163
  // Headless UI routes Escape here. Popping rather than closing is what
164
164
  // makes the stack feel like a stack; at the root the two are the same.
165
165
  onClose: pop,
166
- className: "inset-0 fixed z-50 flex items-start justify-center overflow-y-auto p-s pt-[12vh]",
166
+ className: "inset-0 fixed z-(--z-palette) flex items-start justify-center overflow-y-auto p-s pt-(--overlay-offset-block)",
167
167
  children: [
168
168
  /*#__PURE__*/ _jsx("div", {
169
169
  "aria-hidden": true,
170
- className: "inset-0 bg-black/40 fixed"
170
+ className: "inset-0 fixed bg-scrim"
171
171
  }),
172
172
  /*#__PURE__*/ _jsxs(DialogPanel, {
173
173
  "data-testid": "command-palette",
174
- className: "relative w-full max-w-xl overflow-hidden rounded-lg border border-border bg-surface-elevated shadow-overlay",
174
+ className: "relative w-full max-w-(--container-xl) overflow-hidden rounded-lg border border-border bg-surface-elevated shadow-overlay",
175
175
  children: [
176
176
  /*#__PURE__*/ _jsx(DialogTitle, {
177
177
  className: "sr-only",
@@ -197,13 +197,13 @@ function Group({ group, labels }) {
197
197
  value: term,
198
198
  onChange: (event)=>onTermChange(event.currentTarget.value),
199
199
  onKeyDown: handleKeyDown,
200
- className: "flex-1 bg-transparent py-3xs text-step-1 text-content placeholder:text-content-muted focus:outline-none"
200
+ className: "flex-1 rounded-sm bg-transparent py-3xs text-step-1 text-content focus-ring placeholder:text-content-muted"
201
201
  })
202
202
  ]
203
203
  }),
204
204
  /*#__PURE__*/ _jsx(ComboboxOptions, {
205
205
  static: true,
206
- className: cn('max-h-[60vh] overflow-y-auto p-3xs', // The panel is a focus-managed CONTAINER, not a target: the ring
206
+ className: cn('max-h-(--overlay-max-block) overflow-y-auto p-3xs', // The panel is a focus-managed CONTAINER, not a target: the ring
207
207
  // belongs on the row inside it.
208
208
  'focus:outline-none'),
209
209
  children: isEmpty ? /*#__PURE__*/ _jsx("div", {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/organisms/command-palette/command-palette.tsx"],"sourcesContent":["'use client';\n\nimport {\n commandEffectOf,\n type CommandGroup,\n type CommandOption,\n type CommandPage,\n} from '@entifix/core';\nimport {\n Combobox,\n ComboboxInput,\n ComboboxOption,\n ComboboxOptions,\n Dialog,\n DialogPanel,\n DialogTitle,\n} from '@headlessui/react';\nimport type { KeyboardEvent } from 'react';\n\nimport { SkeletonText } from '../../atoms/skeleton/index.js';\nimport { cn } from '../../utils/cn.js';\n\n/** Every string the palette renders. Resolved — this control does not translate. */\nexport interface CommandPaletteLabels {\n /** Names the dialog. A dialog with no accessible name is announced as \"dialog\". */\n readonly title: string;\n /** Nothing matched, and every source said so. */\n readonly empty: string;\n /** Announced while a group is still answering. */\n readonly loading: string;\n /** \"N más\" — a function, so the plural rule stays in the catalog. */\n readonly more: (remaining: number) => string;\n /** Leaves a pushed page. Also the chip's accessible name. */\n readonly back: string;\n}\n\nexport interface CommandPaletteProps {\n open: boolean;\n onClose: () => void;\n /** Every page this palette can show. The root is the first. */\n pages: readonly CommandPage[];\n /**\n * The pushed page ids, outermost first — empty means the root.\n *\n * Controlled, so the host can empty it in the same handler that opens the\n * palette. An internal stack would have to be reset from an effect keyed on\n * `open`, which is setting state in a passive effect for something that is a\n * fact about the event that opened it.\n */\n stack: readonly string[];\n onStackChange: (stack: readonly string[]) => void;\n term: string;\n onTermChange: (term: string) => void;\n /**\n * An option was chosen. The host runs it — this control has no router and\n * cannot navigate, which is the `hrefFor` rule.\n */\n onSelect: (option: CommandOption) => void;\n labels: CommandPaletteLabels;\n}\n\nconst groupIsEmpty = (group: CommandGroup): boolean =>\n group.options.length === 0 &&\n !group.isLoading &&\n group.unavailable === undefined;\n\nfunction GroupHeading({ label }: { label: string }) {\n return (\n // `presentation`, not a heading: everything inside a `listbox` that is not\n // an `option` must be presentational, or assistive tech counts the heading\n // as a selectable row and the option count it announces is wrong.\n <div\n role=\"presentation\"\n className=\"tracking-wide px-2xs pt-2xs pb-3xs text-step-xs font-semibold text-content-muted uppercase\"\n >\n {label}\n </div>\n );\n}\n\nfunction CommandRow({ option }: { option: CommandOption }) {\n return (\n <ComboboxOption\n value={option}\n className={cn(\n 'flex cursor-pointer items-baseline gap-2xs rounded-md px-2xs py-3xs',\n 'text-step-sm text-content',\n 'data-focus:bg-surface',\n )}\n >\n <span className=\"truncate\">{option.label}</span>\n {option.sublabel !== undefined && (\n <span className=\"truncate text-step-xs text-content-muted\">\n {option.sublabel}\n </span>\n )}\n {option.hint !== undefined && (\n <span className=\"ml-auto shrink-0 text-step-xs text-content-muted\">\n {option.hint}\n </span>\n )}\n </ComboboxOption>\n );\n}\n\nfunction Group({\n group,\n labels,\n}: {\n group: CommandGroup;\n labels: CommandPaletteLabels;\n}) {\n const remaining =\n group.total === undefined ? 0 : group.total - group.options.length;\n\n return (\n <div data-testid={`command-group-${group.key}`}>\n <GroupHeading label={group.label} />\n\n {group.isLoading && (\n <div role=\"presentation\" className=\"px-2xs py-3xs\">\n {/* The skeleton is `aria-hidden`, so the wait needs its own text or a\n screen reader hears a group that simply has no rows. */}\n <span className=\"sr-only\">{labels.loading}</span>\n <SkeletonText lines={2} />\n </div>\n )}\n\n {group.options.map(option => (\n <CommandRow key={option.id} option={option} />\n ))}\n\n {remaining > 0 && (\n <div\n role=\"presentation\"\n className=\"px-2xs py-3xs text-step-xs text-content-muted\"\n >\n {labels.more(remaining)}\n </div>\n )}\n\n {/*\n A degraded source is NAMED, never a missing group: an absent group reads\n as \"nothing matched\", which nobody can honestly claim about a source they\n could not reach. The two severities look different on purpose — `scope`\n is the ordinary state for an operator with no organization, and painting\n that as a warning on every keystroke teaches people to ignore the one\n that means something is broken.\n */}\n {group.unavailable !== undefined && (\n <div\n role=\"presentation\"\n data-testid={`command-unavailable-${group.key}`}\n className={cn(\n 'px-2xs py-3xs text-step-xs',\n group.unavailable.severity === 'reachability'\n ? 'text-danger'\n : 'text-content-muted',\n )}\n >\n {group.unavailable.message}\n </div>\n )}\n </div>\n );\n}\n\n/**\n * A floating command bar: search destinations, records and commands, and run\n * them from the keyboard.\n *\n * Presentational, and strictly so. It holds no index, performs no fetch and\n * knows no router — every group arrives already filtered, already authorized and\n * already translated, from hooks in the shell layer that meet this component at\n * the framework-free `CommandSource` port in core. That is the same arrangement\n * `EntityLinkSource` uses, and it exists because `@entifix/react-controls` may\n * not import `@entifix/react-integration`.\n *\n * **The sources filter, not this component.** A record group is filtered by the\n * service that answered it; a command group is filtered by the hook that built\n * it. Filtering again here would mean the palette had to know which groups were\n * pre-filtered, and a group that answered a *different* term would be silently\n * emptied rather than shown as stale.\n *\n * Depth is a **stack of pages**: selecting an option carrying `push` makes the\n * palette become that page. `Escape` pops one level and closes at the root;\n * `Backspace` on an empty term pops too, which is the gesture Raycast trained\n * people on and costs nothing when there is nothing to pop.\n *\n * Built on Headless UI's `Dialog` + `Combobox`, so the focus trap, focus\n * restoration, `role=\"combobox\"`, `aria-expanded`, `aria-activedescendant` and\n * the arrow-key contract are the library's rather than seven hand-written\n * behaviours that each have to be right.\n */\nexport function CommandPalette({\n open,\n onClose,\n pages,\n stack,\n onStackChange,\n term,\n onTermChange,\n onSelect,\n labels,\n}: CommandPaletteProps) {\n const activeId = stack[stack.length - 1];\n const page =\n (activeId === undefined\n ? pages[0]\n : pages.find(candidate => candidate.id === activeId)) ?? pages[0];\n\n const groups = page.sources.flatMap(source => source.groups);\n // Runs on every render of the palette, not on the click: an option with no\n // effect — or with two — is a line that looks selectable and misbehaves, and\n // the check that only fires when someone already selected it is the check\n // that fires after the bug is reported.\n groups.forEach(group => group.options.forEach(commandEffectOf));\n\n const isEmpty = groups.every(groupIsEmpty);\n\n const pop = () => {\n if (stack.length === 0) {\n onClose();\n return;\n }\n onStackChange(stack.slice(0, -1));\n onTermChange('');\n };\n\n const handleSelect = (option: CommandOption | null) => {\n // Headless UI types `onChange` as nullable — it reports `null` when a search\n // is abandoned rather than chosen, and acting on that would run whatever\n // happened to be highlighted.\n //\n // ⚠️ Unreachable *here*, and ignored for coverage rather than contorted\n // around: this combobox is driven entirely from the outside (`value` is the\n // caller's `term`, there is no `displayValue`) and Escape is handled on the\n // input above, so the library never produces the null it admits. The guard\n // stays because the type is the library's contract, not ours to assume away.\n /* v8 ignore next 2 */\n if (option === null) return;\n if (option.push !== undefined) {\n onStackChange([...stack, option.push]);\n onTermChange('');\n return;\n }\n onSelect(option);\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n // ⚠️ Escape is handled here rather than left to the `Dialog`, because\n // `Combobox` consumes it first once a term has been typed — it treats the\n // key as \"close the suggestion list\", which for a `static` list means\n // nothing at all, and the palette then refuses to close for exactly the\n // person who has typed something. Measured, not defensive.\n if (event.key === 'Escape') {\n event.preventDefault();\n event.stopPropagation();\n pop();\n return;\n }\n // Only when there is nothing to delete, so the key keeps its ordinary\n // meaning for as long as the person is still editing a term.\n if (event.key === 'Backspace' && term === '' && stack.length > 0) {\n event.preventDefault();\n pop();\n }\n };\n\n return (\n <Dialog\n open={open}\n // Headless UI routes Escape here. Popping rather than closing is what\n // makes the stack feel like a stack; at the root the two are the same.\n onClose={pop}\n className=\"inset-0 fixed z-50 flex items-start justify-center overflow-y-auto p-s pt-[12vh]\"\n >\n <div aria-hidden className=\"inset-0 bg-black/40 fixed\" />\n <DialogPanel\n data-testid=\"command-palette\"\n className=\"relative w-full max-w-xl overflow-hidden rounded-lg border border-border bg-surface-elevated shadow-overlay\"\n >\n <DialogTitle className=\"sr-only\">{labels.title}</DialogTitle>\n\n {/*\n Uncontrolled: this combobox has no value worth holding — an option is\n an act, not a stored choice. Every render rebuilds its options from the\n sources, so the identities differ and selecting the same row twice\n still fires `onChange` both times.\n */}\n <Combobox<CommandOption> onChange={handleSelect}>\n <div className=\"flex items-center gap-2xs border-b border-border px-s py-2xs\">\n {page.title !== undefined && (\n <button\n type=\"button\"\n aria-label={labels.back}\n onClick={pop}\n className=\"shrink-0 rounded-md bg-surface px-2xs py-3xs text-step-xs text-content-muted focus-ring\"\n >\n {page.title}\n </button>\n )}\n <ComboboxInput\n data-autofocus\n aria-label={labels.title}\n placeholder={page.placeholder}\n value={term}\n onChange={event => onTermChange(event.currentTarget.value)}\n onKeyDown={handleKeyDown}\n className=\"flex-1 bg-transparent py-3xs text-step-1 text-content placeholder:text-content-muted focus:outline-none\"\n />\n </div>\n\n <ComboboxOptions\n static\n className={cn(\n 'max-h-[60vh] overflow-y-auto p-3xs',\n // The panel is a focus-managed CONTAINER, not a target: the ring\n // belongs on the row inside it.\n 'focus:outline-none',\n )}\n >\n {isEmpty ? (\n <div\n role=\"presentation\"\n className=\"px-2xs py-s text-step-sm text-content-muted\"\n >\n {labels.empty}\n </div>\n ) : (\n groups\n .filter(group => !groupIsEmpty(group))\n .map(group => (\n <Group key={group.key} group={group} labels={labels} />\n ))\n )}\n </ComboboxOptions>\n </Combobox>\n </DialogPanel>\n </Dialog>\n );\n}\n"],"names":["commandEffectOf","Combobox","ComboboxInput","ComboboxOption","ComboboxOptions","Dialog","DialogPanel","DialogTitle","SkeletonText","cn","groupIsEmpty","group","options","length","isLoading","unavailable","undefined","GroupHeading","label","div","role","className","CommandRow","option","value","span","sublabel","hint","Group","labels","remaining","total","data-testid","key","loading","lines","map","id","more","severity","message","CommandPalette","open","onClose","pages","stack","onStackChange","term","onTermChange","onSelect","activeId","page","find","candidate","groups","sources","flatMap","source","forEach","isEmpty","every","pop","slice","handleSelect","push","handleKeyDown","event","preventDefault","stopPropagation","aria-hidden","title","onChange","button","type","aria-label","back","onClick","data-autofocus","placeholder","currentTarget","onKeyDown","static","empty","filter"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../src/ui/organisms/command-palette/command-palette.tsx"],"sourcesContent":["'use client';\n\nimport {\n commandEffectOf,\n type CommandGroup,\n type CommandOption,\n type CommandPage,\n} from '@entifix/core';\nimport {\n Combobox,\n ComboboxInput,\n ComboboxOption,\n ComboboxOptions,\n Dialog,\n DialogPanel,\n DialogTitle,\n} from '@headlessui/react';\nimport type { KeyboardEvent } from 'react';\n\nimport { SkeletonText } from '../../atoms/skeleton/index.js';\nimport { cn } from '../../utils/cn.js';\n\n/** Every string the palette renders. Resolved — this control does not translate. */\nexport interface CommandPaletteLabels {\n /** Names the dialog. A dialog with no accessible name is announced as \"dialog\". */\n readonly title: string;\n /** Nothing matched, and every source said so. */\n readonly empty: string;\n /** Announced while a group is still answering. */\n readonly loading: string;\n /** \"N más\" — a function, so the plural rule stays in the catalog. */\n readonly more: (remaining: number) => string;\n /** Leaves a pushed page. Also the chip's accessible name. */\n readonly back: string;\n}\n\nexport interface CommandPaletteProps {\n open: boolean;\n onClose: () => void;\n /** Every page this palette can show. The root is the first. */\n pages: readonly CommandPage[];\n /**\n * The pushed page ids, outermost first — empty means the root.\n *\n * Controlled, so the host can empty it in the same handler that opens the\n * palette. An internal stack would have to be reset from an effect keyed on\n * `open`, which is setting state in a passive effect for something that is a\n * fact about the event that opened it.\n */\n stack: readonly string[];\n onStackChange: (stack: readonly string[]) => void;\n term: string;\n onTermChange: (term: string) => void;\n /**\n * An option was chosen. The host runs it — this control has no router and\n * cannot navigate, which is the `hrefFor` rule.\n */\n onSelect: (option: CommandOption) => void;\n labels: CommandPaletteLabels;\n}\n\nconst groupIsEmpty = (group: CommandGroup): boolean =>\n group.options.length === 0 &&\n !group.isLoading &&\n group.unavailable === undefined;\n\nfunction GroupHeading({ label }: { label: string }) {\n return (\n // `presentation`, not a heading: everything inside a `listbox` that is not\n // an `option` must be presentational, or assistive tech counts the heading\n // as a selectable row and the option count it announces is wrong.\n <div\n role=\"presentation\"\n className=\"tracking-wide px-2xs pt-2xs pb-3xs text-step-xs font-semibold text-content-muted uppercase\"\n >\n {label}\n </div>\n );\n}\n\nfunction CommandRow({ option }: { option: CommandOption }) {\n return (\n <ComboboxOption\n value={option}\n className={cn(\n 'flex min-h-(--target-min) cursor-pointer items-baseline gap-2xs rounded-md px-2xs py-3xs',\n 'text-step-sm text-content',\n 'focus-ring data-focus:bg-surface data-focus:-outline-offset-2',\n )}\n >\n <span className=\"truncate\">{option.label}</span>\n {option.sublabel !== undefined && (\n <span className=\"truncate text-step-xs text-content-muted\">\n {option.sublabel}\n </span>\n )}\n {option.hint !== undefined && (\n <span className=\"ml-auto shrink-0 text-step-xs text-content-muted\">\n {option.hint}\n </span>\n )}\n </ComboboxOption>\n );\n}\n\nfunction Group({\n group,\n labels,\n}: {\n group: CommandGroup;\n labels: CommandPaletteLabels;\n}) {\n const remaining =\n group.total === undefined ? 0 : group.total - group.options.length;\n\n return (\n <div data-testid={`command-group-${group.key}`}>\n <GroupHeading label={group.label} />\n\n {group.isLoading && (\n <div role=\"presentation\" className=\"px-2xs py-3xs\">\n {/* The skeleton is `aria-hidden`, so the wait needs its own text or a\n screen reader hears a group that simply has no rows. */}\n <span className=\"sr-only\">{labels.loading}</span>\n <SkeletonText lines={2} />\n </div>\n )}\n\n {group.options.map(option => (\n <CommandRow key={option.id} option={option} />\n ))}\n\n {remaining > 0 && (\n <div\n role=\"presentation\"\n className=\"px-2xs py-3xs text-step-xs text-content-muted\"\n >\n {labels.more(remaining)}\n </div>\n )}\n\n {/*\n A degraded source is NAMED, never a missing group: an absent group reads\n as \"nothing matched\", which nobody can honestly claim about a source they\n could not reach. The two severities look different on purpose — `scope`\n is the ordinary state for an operator with no organization, and painting\n that as a warning on every keystroke teaches people to ignore the one\n that means something is broken.\n */}\n {group.unavailable !== undefined && (\n <div\n role=\"presentation\"\n data-testid={`command-unavailable-${group.key}`}\n className={cn(\n 'px-2xs py-3xs text-step-xs',\n group.unavailable.severity === 'reachability'\n ? 'text-danger'\n : 'text-content-muted',\n )}\n >\n {group.unavailable.message}\n </div>\n )}\n </div>\n );\n}\n\n/**\n * A floating command bar: search destinations, records and commands, and run\n * them from the keyboard.\n *\n * Presentational, and strictly so. It holds no index, performs no fetch and\n * knows no router — every group arrives already filtered, already authorized and\n * already translated, from hooks in the shell layer that meet this component at\n * the framework-free `CommandSource` port in core. That is the same arrangement\n * `EntityLinkSource` uses, and it exists because `@entifix/react-controls` may\n * not import `@entifix/react-integration`.\n *\n * **The sources filter, not this component.** A record group is filtered by the\n * service that answered it; a command group is filtered by the hook that built\n * it. Filtering again here would mean the palette had to know which groups were\n * pre-filtered, and a group that answered a *different* term would be silently\n * emptied rather than shown as stale.\n *\n * Depth is a **stack of pages**: selecting an option carrying `push` makes the\n * palette become that page. `Escape` pops one level and closes at the root;\n * `Backspace` on an empty term pops too, which is the gesture Raycast trained\n * people on and costs nothing when there is nothing to pop.\n *\n * Built on Headless UI's `Dialog` + `Combobox`, so the focus trap, focus\n * restoration, `role=\"combobox\"`, `aria-expanded`, `aria-activedescendant` and\n * the arrow-key contract are the library's rather than seven hand-written\n * behaviours that each have to be right.\n */\nexport function CommandPalette({\n open,\n onClose,\n pages,\n stack,\n onStackChange,\n term,\n onTermChange,\n onSelect,\n labels,\n}: CommandPaletteProps) {\n const activeId = stack[stack.length - 1];\n const page =\n (activeId === undefined\n ? pages[0]\n : pages.find(candidate => candidate.id === activeId)) ?? pages[0];\n\n const groups = page.sources.flatMap(source => source.groups);\n // Runs on every render of the palette, not on the click: an option with no\n // effect — or with two — is a line that looks selectable and misbehaves, and\n // the check that only fires when someone already selected it is the check\n // that fires after the bug is reported.\n groups.forEach(group => group.options.forEach(commandEffectOf));\n\n const isEmpty = groups.every(groupIsEmpty);\n\n const pop = () => {\n if (stack.length === 0) {\n onClose();\n return;\n }\n onStackChange(stack.slice(0, -1));\n onTermChange('');\n };\n\n const handleSelect = (option: CommandOption | null) => {\n // Headless UI types `onChange` as nullable — it reports `null` when a search\n // is abandoned rather than chosen, and acting on that would run whatever\n // happened to be highlighted.\n //\n // ⚠️ Unreachable *here*, and ignored for coverage rather than contorted\n // around: this combobox is driven entirely from the outside (`value` is the\n // caller's `term`, there is no `displayValue`) and Escape is handled on the\n // input above, so the library never produces the null it admits. The guard\n // stays because the type is the library's contract, not ours to assume away.\n /* v8 ignore next 2 */\n if (option === null) return;\n if (option.push !== undefined) {\n onStackChange([...stack, option.push]);\n onTermChange('');\n return;\n }\n onSelect(option);\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n // ⚠️ Escape is handled here rather than left to the `Dialog`, because\n // `Combobox` consumes it first once a term has been typed — it treats the\n // key as \"close the suggestion list\", which for a `static` list means\n // nothing at all, and the palette then refuses to close for exactly the\n // person who has typed something. Measured, not defensive.\n if (event.key === 'Escape') {\n event.preventDefault();\n event.stopPropagation();\n pop();\n return;\n }\n // Only when there is nothing to delete, so the key keeps its ordinary\n // meaning for as long as the person is still editing a term.\n if (event.key === 'Backspace' && term === '' && stack.length > 0) {\n event.preventDefault();\n pop();\n }\n };\n\n return (\n <Dialog\n open={open}\n // Headless UI routes Escape here. Popping rather than closing is what\n // makes the stack feel like a stack; at the root the two are the same.\n onClose={pop}\n className=\"inset-0 fixed z-(--z-palette) flex items-start justify-center overflow-y-auto p-s pt-(--overlay-offset-block)\"\n >\n <div aria-hidden className=\"inset-0 fixed bg-scrim\" />\n <DialogPanel\n data-testid=\"command-palette\"\n className=\"relative w-full max-w-(--container-xl) overflow-hidden rounded-lg border border-border bg-surface-elevated shadow-overlay\"\n >\n <DialogTitle className=\"sr-only\">{labels.title}</DialogTitle>\n\n {/*\n Uncontrolled: this combobox has no value worth holding — an option is\n an act, not a stored choice. Every render rebuilds its options from the\n sources, so the identities differ and selecting the same row twice\n still fires `onChange` both times.\n */}\n <Combobox<CommandOption> onChange={handleSelect}>\n <div className=\"flex items-center gap-2xs border-b border-border px-s py-2xs\">\n {page.title !== undefined && (\n <button\n type=\"button\"\n aria-label={labels.back}\n onClick={pop}\n className=\"shrink-0 rounded-md bg-surface px-2xs py-3xs text-step-xs text-content-muted focus-ring\"\n >\n {page.title}\n </button>\n )}\n <ComboboxInput\n data-autofocus\n aria-label={labels.title}\n placeholder={page.placeholder}\n value={term}\n onChange={event => onTermChange(event.currentTarget.value)}\n onKeyDown={handleKeyDown}\n className=\"flex-1 rounded-sm bg-transparent py-3xs text-step-1 text-content focus-ring placeholder:text-content-muted\"\n />\n </div>\n\n <ComboboxOptions\n static\n className={cn(\n 'max-h-(--overlay-max-block) overflow-y-auto p-3xs',\n // The panel is a focus-managed CONTAINER, not a target: the ring\n // belongs on the row inside it.\n 'focus:outline-none',\n )}\n >\n {isEmpty ? (\n <div\n role=\"presentation\"\n className=\"px-2xs py-s text-step-sm text-content-muted\"\n >\n {labels.empty}\n </div>\n ) : (\n groups\n .filter(group => !groupIsEmpty(group))\n .map(group => (\n <Group key={group.key} group={group} labels={labels} />\n ))\n )}\n </ComboboxOptions>\n </Combobox>\n </DialogPanel>\n </Dialog>\n );\n}\n"],"names":["commandEffectOf","Combobox","ComboboxInput","ComboboxOption","ComboboxOptions","Dialog","DialogPanel","DialogTitle","SkeletonText","cn","groupIsEmpty","group","options","length","isLoading","unavailable","undefined","GroupHeading","label","div","role","className","CommandRow","option","value","span","sublabel","hint","Group","labels","remaining","total","data-testid","key","loading","lines","map","id","more","severity","message","CommandPalette","open","onClose","pages","stack","onStackChange","term","onTermChange","onSelect","activeId","page","find","candidate","groups","sources","flatMap","source","forEach","isEmpty","every","pop","slice","handleSelect","push","handleKeyDown","event","preventDefault","stopPropagation","aria-hidden","title","onChange","button","type","aria-label","back","onClick","data-autofocus","placeholder","currentTarget","onKeyDown","static","empty","filter"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"entity-detail-grid.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/entity-detail-grid/entity-detail-grid.tsx"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAK3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,OAAe,EACf,SAAiB,EACjB,QAAe,EACf,MAAM,EACN,SAAS,GACV,EAAE,qBAAqB,+BAyMvB"}
1
+ {"version":3,"file":"entity-detail-grid.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/entity-detail-grid/entity-detail-grid.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAK3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,OAAe,EACf,SAAiB,EACjB,QAAe,EACf,MAAM,EACN,SAAS,GACV,EAAE,qBAAqB,+BAgNvB"}
@@ -5,6 +5,7 @@ import { describeChildColumns, editableChildColumns, newRowKey, parseRowFieldPat
5
5
  import { useId, useMemo } from 'react';
6
6
  import { useLocalizedDescriptors, useT } from '../../../i18n/index.js';
7
7
  import { Button } from '../../atoms/button/index.js';
8
+ import { CellValue } from '../../atoms/cell-value/index.js';
8
9
  import { FieldControl } from '../../atoms/field-control/index.js';
9
10
  import { Skeleton } from '../../atoms/skeleton/index.js';
10
11
  import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableMessageRow, TableRow } from '../../atoms/table/index.js';
@@ -180,9 +181,14 @@ import { cn } from '../../utils/cn.js';
180
181
  "aria-labelledby": `${gridId}-${column.name}`,
181
182
  "aria-invalid": error !== undefined || undefined,
182
183
  "aria-describedby": error === undefined ? undefined : `${cellId}-error`,
183
- onChange: (next)=>setRow(index, column.name, next)
184
- }) : /*#__PURE__*/ _jsx("span", {
184
+ onChange: (next)=>setRow(index, column.name, next),
185
+ record: row
186
+ }) : column.money === undefined ? /*#__PURE__*/ _jsx("span", {
185
187
  children: (_row_column_name1 = row[column.name]) != null ? _row_column_name1 : ''
188
+ }) : /*#__PURE__*/ _jsx(CellValue, {
189
+ value: row[column.name],
190
+ descriptor: column,
191
+ record: row
186
192
  }),
187
193
  editing && error !== undefined && /*#__PURE__*/ _jsx("span", {
188
194
  id: `${cellId}-error`,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/organisms/entity-detail-grid/entity-detail-grid.tsx"],"sourcesContent":["'use client';\n\nimport {\n describeChildColumns,\n editableChildColumns,\n type EntityFieldDescriptor,\n type EntityRowDraft,\n newRowKey,\n parseRowFieldPath,\n ROW_KEY,\n rowFieldPath,\n} from '@entifix/core';\nimport { type KeyboardEvent, useId, useMemo } from 'react';\n\nimport { useLocalizedDescriptors, useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { FieldControl } from '../../atoms/field-control/index.js';\nimport { Skeleton } from '../../atoms/skeleton/index.js';\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeaderCell,\n TableMessageRow,\n TableRow,\n} from '../../atoms/table/index.js';\nimport { Stack } from '../../molecules/stack/index.js';\nimport { cn } from '../../utils/cn.js';\nimport type { EntityDetailGridProps } from './entity-detail-grid.types.js';\n\n/** Rows the placeholder stands in for. Enough to read as a grid, not a page. */\nconst SKELETON_ROW_COUNT = 3;\n\n/**\n * The rows a record **owns**, edited in place.\n *\n * A `composition` — an order and its lines — where the rows have no life outside\n * the master and go out in the same write\n *. Not\n * a `linkCollection`, which is association to records that exist on their own\n * and save separately; that editor is still #26.\n *\n * ## Why this is not `EntityTable`\n *\n * `EntityTable` is a *server* listing: pages, RSQL filtering and sorting,\n * column personalization, a selection model and a bulk bar. A detail grid has\n * none of those and cannot have them — its rows are local, unsaved, and\n * unqueryable by construction, since a collection member is never `filterable`.\n * Sharing the organism would mean every one of those features growing a\n * \"detail\" branch. It shares the `Table` **atoms** instead, which is the level\n * the two actually have in common.\n *\n * ## Inline cells, and why Enter is free\n *\n * Each cell is a {@link FieldControl}, the same descriptor→input map the form\n * uses, so a child's `enum` gets a select and its `date` a date input with no\n * code here. `Tab` walks the row natively and crosses into the next; `Enter` on\n * the last row appends and focuses the new row's first cell.\n *\n * That last binding is available only because `EntityForm` renders a `<Card>`\n * with a `<button onClick>` Save rather than a `<form>` — inside a real form,\n * `Enter` in a text input submits, and appending a row would fight the browser\n * for it. Wrapping the form in a `<form>` element later would break this, which\n * is the one thing worth knowing before doing it.\n *\n * ## Errors\n *\n * A cell carries `aria-invalid` and points at its own message; the grid carries\n * **one** summary above it, which is the only `role=\"alert\"` in the component.\n * A live region per cell would announce the whole grid on every keystroke, and\n * a message rendered only in the cell is unreachable when the failing row is\n * scrolled out — the case a long grid always meets.\n */\nexport function EntityDetailGrid({\n descriptor,\n rows,\n onRowsChange,\n errors,\n editing = false,\n isLoading = false,\n skeleton = true,\n footer,\n className,\n}: EntityDetailGridProps) {\n const t = useT();\n const gridId = useId();\n\n const described = useMemo(\n () =>\n descriptor.childType === undefined\n ? []\n : editableChildColumns(describeChildColumns(descriptor.childType)),\n [descriptor.childType],\n );\n const columns = useLocalizedDescriptors(described);\n\n // Only the messages that name *this* member. The form hands over its whole\n // error map so a record with two owned collections needs no bookkeeping at\n // the call site, and a grid that filtered nothing would show the other's.\n const rowErrors = useMemo(\n () => readRowErrors(descriptor.name, errors),\n [descriptor.name, errors],\n );\n\n const showSkeleton = isLoading && skeleton !== false;\n const customSkeleton = typeof skeleton === 'boolean' ? undefined : skeleton;\n // One more than the columns: every row carries its remove control.\n const columnCount = columns.length + (editing ? 1 : 0);\n\n const setRow = (index: number, member: string, value: string) => {\n onRowsChange(\n rows.map((row, position) =>\n position === index ? { ...row, [member]: value } : row,\n ),\n );\n };\n\n const firstColumn = columns[0]?.name;\n\n const appendRow = () => {\n onRowsChange([...rows, seedEmptyRow(columns)]);\n focusCell(gridId, rows.length, firstColumn);\n };\n\n const removeRow = (index: number) => {\n onRowsChange(rows.filter((_row, position) => position !== index));\n // Focus the row that slid into this position, or the one above it when the\n // removed row was last — never nothing, which drops the user to the page.\n focusCell(gridId, Math.min(index, rows.length - 2), firstColumn);\n };\n\n /**\n * `Enter` anywhere in the **last row** appends the next one.\n *\n * Bound to the row rather than to its final cell, which is what it looked\n * like it should be: the last *column* is whatever the child declared last,\n * and a read-only member there renders a disabled input that never receives a\n * key at all — so the binding would exist and be unreachable, on exactly the\n * entities that have a computed column. The row is the unit the user is\n * finishing anyway.\n *\n * Only from a text input: a `<select>` opens on Enter and a checkbox toggles\n * on Space, so taking the key from those would break a control's own\n * behaviour to add a row the user did not ask for. A read-only column needs\n * no test of its own — its input is `disabled`, so it never takes focus and\n * never raises a key here at all.\n */\n const onLastRowKeyDown = (event: KeyboardEvent<HTMLTableRowElement>) => {\n if (event.key !== 'Enter') return;\n if (!(event.target instanceof HTMLInputElement)) return;\n event.preventDefault();\n appendRow();\n };\n\n return (\n <Stack gap=\"2xs\" className={className}>\n <div className=\"flex items-center justify-between gap-s\">\n <span className=\"text-step-sm font-medium text-content-muted\">\n {descriptor.label}\n </span>\n {editing && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={appendRow}\n disabled={isLoading}\n >\n {t('detail.addRow')}\n </Button>\n )}\n </div>\n\n {editing && rowErrors.size > 0 && (\n <div role=\"alert\" className=\"text-step-sm text-danger\">\n {t('detail.errorSummary', { count: rowErrors.size })}\n </div>\n )}\n\n <Table\n aria-label={descriptor.label}\n aria-busy={isLoading || undefined}\n aria-rowcount={rows.length}\n >\n <TableHead>\n <TableRow>\n {columns.map(column => (\n <TableHeaderCell\n key={column.name}\n id={`${gridId}-${column.name}`}\n >\n {column.label}\n </TableHeaderCell>\n ))}\n {editing && (\n <TableHeaderCell align=\"end\">\n <span className=\"sr-only\">{t('detail.rowActions')}</span>\n </TableHeaderCell>\n )}\n </TableRow>\n </TableHead>\n\n <TableBody>\n {showSkeleton\n ? (customSkeleton ?? (\n <SkeletonRows\n columns={columns.length}\n extra={editing ? 1 : 0}\n />\n ))\n : rows.map((row, index) => (\n <TableRow\n key={row[ROW_KEY]}\n onKeyDown={\n editing && index === rows.length - 1\n ? onLastRowKeyDown\n : undefined\n }\n >\n {columns.map(column => {\n const error = rowErrors.get(\n rowFieldPath(descriptor.name, index, column.name),\n );\n const cellId = `${gridId}-${index}-${column.name}`;\n return (\n <TableCell key={column.name}>\n {editing ? (\n <FieldControl\n descriptor={column}\n value={row[column.name] ?? ''}\n id={cellId}\n aria-labelledby={`${gridId}-${column.name}`}\n aria-invalid={error !== undefined || undefined}\n aria-describedby={\n error === undefined\n ? undefined\n : `${cellId}-error`\n }\n onChange={next => setRow(index, column.name, next)}\n />\n ) : (\n <span>{row[column.name] ?? ''}</span>\n )}\n {editing && error !== undefined && (\n <span\n id={`${cellId}-error`}\n className=\"mt-3xs block text-step-sm text-danger\"\n >\n {error}\n </span>\n )}\n </TableCell>\n );\n })}\n {editing && (\n <TableCell align=\"end\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => removeRow(index)}\n >\n {t('detail.removeRow', { row: index + 1 })}\n </Button>\n </TableCell>\n )}\n </TableRow>\n ))}\n\n {!showSkeleton && rows.length === 0 && (\n <TableMessageRow colSpan={Math.max(columnCount, 1)}>\n {t('detail.empty')}\n </TableMessageRow>\n )}\n </TableBody>\n </Table>\n\n {footer !== undefined && !showSkeleton && (\n <div className={cn('flex justify-end text-step-sm text-content')}>\n {footer(rows)}\n </div>\n )}\n </Stack>\n );\n}\n\n/** The placeholder, shaped like the grid it stands in for. */\nfunction SkeletonRows({ columns, extra }: { columns: number; extra: number }) {\n return (\n <>\n {Array.from({ length: SKELETON_ROW_COUNT }, (_row, index) => (\n <TableRow key={index}>\n {Array.from({ length: columns + extra }, (_cell, cellIndex) => (\n <TableCell key={cellIndex}>\n <Skeleton className=\"h-8 w-full\" />\n </TableCell>\n ))}\n </TableRow>\n ))}\n </>\n );\n}\n\n/**\n * A blank row, seeded with every column the grid edits.\n *\n * Every member is present and empty rather than absent, for the reason a form's\n * seed carries every key: a control handed `undefined` flips from controlled to\n * uncontrolled the moment the user types into it.\n */\nfunction seedEmptyRow(\n columns: readonly EntityFieldDescriptor[],\n): EntityRowDraft {\n const row: EntityRowDraft = { [ROW_KEY]: newRowKey() };\n for (const column of columns) row[column.name] = '';\n return row;\n}\n\n/**\n * Moves focus into a cell after the list changed.\n *\n * By element id rather than through a ref into the `<tbody>`, because the row\n * being focused does not exist yet when this is called — it is the row the\n * caller has just asked React to render. Deferring to a microtask lets that\n * render commit first; `getElementById` then finds the input the same way the\n * cell's own `aria-describedby` does.\n *\n * A row index that names nothing simply focuses nothing, which is the right\n * outcome for removing the only row: there is no cell to land in, and stealing\n * focus somewhere arbitrary is worse than leaving it where the user put it.\n */\nfunction focusCell(gridId: string, index: number, column?: string): void {\n if (column === undefined || index < 0) return;\n\n queueMicrotask(() => {\n document.getElementById(`${gridId}-${index}-${column}`)?.focus();\n });\n}\n\n/** The messages naming a row of `member`, keyed by their full path. */\nfunction readRowErrors(\n member: string,\n errors: Record<string, string> | undefined,\n): Map<string, string> {\n const found = new Map<string, string>();\n if (errors === undefined) return found;\n\n for (const [path, message] of Object.entries(errors)) {\n if (parseRowFieldPath(path)?.member === member) found.set(path, message);\n }\n return found;\n}\n"],"names":["describeChildColumns","editableChildColumns","newRowKey","parseRowFieldPath","ROW_KEY","rowFieldPath","useId","useMemo","useLocalizedDescriptors","useT","Button","FieldControl","Skeleton","Table","TableBody","TableCell","TableHead","TableHeaderCell","TableMessageRow","TableRow","Stack","cn","SKELETON_ROW_COUNT","EntityDetailGrid","descriptor","rows","onRowsChange","errors","editing","isLoading","skeleton","footer","className","columns","t","gridId","described","childType","undefined","rowErrors","readRowErrors","name","showSkeleton","customSkeleton","columnCount","length","setRow","index","member","value","map","row","position","firstColumn","appendRow","seedEmptyRow","focusCell","removeRow","filter","_row","Math","min","onLastRowKeyDown","event","key","target","HTMLInputElement","preventDefault","gap","div","span","label","type","variant","size","onClick","disabled","role","count","aria-label","aria-busy","aria-rowcount","column","id","align","SkeletonRows","extra","onKeyDown","error","get","cellId","aria-labelledby","aria-invalid","aria-describedby","onChange","next","colSpan","max","Array","from","_cell","cellIndex","queueMicrotask","document","getElementById","focus","found","Map","path","message","Object","entries","set"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../src/ui/organisms/entity-detail-grid/entity-detail-grid.tsx"],"sourcesContent":["'use client';\n\nimport {\n describeChildColumns,\n editableChildColumns,\n type EntityFieldDescriptor,\n type EntityRowDraft,\n newRowKey,\n parseRowFieldPath,\n ROW_KEY,\n rowFieldPath,\n} from '@entifix/core';\nimport { type KeyboardEvent, useId, useMemo } from 'react';\n\nimport { useLocalizedDescriptors, useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { CellValue } from '../../atoms/cell-value/index.js';\nimport { FieldControl } from '../../atoms/field-control/index.js';\nimport { Skeleton } from '../../atoms/skeleton/index.js';\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeaderCell,\n TableMessageRow,\n TableRow,\n} from '../../atoms/table/index.js';\nimport { Stack } from '../../molecules/stack/index.js';\nimport { cn } from '../../utils/cn.js';\nimport type { EntityDetailGridProps } from './entity-detail-grid.types.js';\n\n/** Rows the placeholder stands in for. Enough to read as a grid, not a page. */\nconst SKELETON_ROW_COUNT = 3;\n\n/**\n * The rows a record **owns**, edited in place.\n *\n * A `composition` — an order and its lines — where the rows have no life outside\n * the master and go out in the same write\n *. Not\n * a `linkCollection`, which is association to records that exist on their own\n * and save separately; that editor is still #26.\n *\n * ## Why this is not `EntityTable`\n *\n * `EntityTable` is a *server* listing: pages, RSQL filtering and sorting,\n * column personalization, a selection model and a bulk bar. A detail grid has\n * none of those and cannot have them — its rows are local, unsaved, and\n * unqueryable by construction, since a collection member is never `filterable`.\n * Sharing the organism would mean every one of those features growing a\n * \"detail\" branch. It shares the `Table` **atoms** instead, which is the level\n * the two actually have in common.\n *\n * ## Inline cells, and why Enter is free\n *\n * Each cell is a {@link FieldControl}, the same descriptor→input map the form\n * uses, so a child's `enum` gets a select and its `date` a date input with no\n * code here. `Tab` walks the row natively and crosses into the next; `Enter` on\n * the last row appends and focuses the new row's first cell.\n *\n * That last binding is available only because `EntityForm` renders a `<Card>`\n * with a `<button onClick>` Save rather than a `<form>` — inside a real form,\n * `Enter` in a text input submits, and appending a row would fight the browser\n * for it. Wrapping the form in a `<form>` element later would break this, which\n * is the one thing worth knowing before doing it.\n *\n * ## Errors\n *\n * A cell carries `aria-invalid` and points at its own message; the grid carries\n * **one** summary above it, which is the only `role=\"alert\"` in the component.\n * A live region per cell would announce the whole grid on every keystroke, and\n * a message rendered only in the cell is unreachable when the failing row is\n * scrolled out — the case a long grid always meets.\n */\nexport function EntityDetailGrid({\n descriptor,\n rows,\n onRowsChange,\n errors,\n editing = false,\n isLoading = false,\n skeleton = true,\n footer,\n className,\n}: EntityDetailGridProps) {\n const t = useT();\n const gridId = useId();\n\n const described = useMemo(\n () =>\n descriptor.childType === undefined\n ? []\n : editableChildColumns(describeChildColumns(descriptor.childType)),\n [descriptor.childType],\n );\n const columns = useLocalizedDescriptors(described);\n\n // Only the messages that name *this* member. The form hands over its whole\n // error map so a record with two owned collections needs no bookkeeping at\n // the call site, and a grid that filtered nothing would show the other's.\n const rowErrors = useMemo(\n () => readRowErrors(descriptor.name, errors),\n [descriptor.name, errors],\n );\n\n const showSkeleton = isLoading && skeleton !== false;\n const customSkeleton = typeof skeleton === 'boolean' ? undefined : skeleton;\n // One more than the columns: every row carries its remove control.\n const columnCount = columns.length + (editing ? 1 : 0);\n\n const setRow = (index: number, member: string, value: string) => {\n onRowsChange(\n rows.map((row, position) =>\n position === index ? { ...row, [member]: value } : row,\n ),\n );\n };\n\n const firstColumn = columns[0]?.name;\n\n const appendRow = () => {\n onRowsChange([...rows, seedEmptyRow(columns)]);\n focusCell(gridId, rows.length, firstColumn);\n };\n\n const removeRow = (index: number) => {\n onRowsChange(rows.filter((_row, position) => position !== index));\n // Focus the row that slid into this position, or the one above it when the\n // removed row was last — never nothing, which drops the user to the page.\n focusCell(gridId, Math.min(index, rows.length - 2), firstColumn);\n };\n\n /**\n * `Enter` anywhere in the **last row** appends the next one.\n *\n * Bound to the row rather than to its final cell, which is what it looked\n * like it should be: the last *column* is whatever the child declared last,\n * and a read-only member there renders a disabled input that never receives a\n * key at all — so the binding would exist and be unreachable, on exactly the\n * entities that have a computed column. The row is the unit the user is\n * finishing anyway.\n *\n * Only from a text input: a `<select>` opens on Enter and a checkbox toggles\n * on Space, so taking the key from those would break a control's own\n * behaviour to add a row the user did not ask for. A read-only column needs\n * no test of its own — its input is `disabled`, so it never takes focus and\n * never raises a key here at all.\n */\n const onLastRowKeyDown = (event: KeyboardEvent<HTMLTableRowElement>) => {\n if (event.key !== 'Enter') return;\n if (!(event.target instanceof HTMLInputElement)) return;\n event.preventDefault();\n appendRow();\n };\n\n return (\n <Stack gap=\"2xs\" className={className}>\n <div className=\"flex items-center justify-between gap-s\">\n <span className=\"text-step-sm font-medium text-content-muted\">\n {descriptor.label}\n </span>\n {editing && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={appendRow}\n disabled={isLoading}\n >\n {t('detail.addRow')}\n </Button>\n )}\n </div>\n\n {editing && rowErrors.size > 0 && (\n <div role=\"alert\" className=\"text-step-sm text-danger\">\n {t('detail.errorSummary', { count: rowErrors.size })}\n </div>\n )}\n\n <Table\n aria-label={descriptor.label}\n aria-busy={isLoading || undefined}\n aria-rowcount={rows.length}\n >\n <TableHead>\n <TableRow>\n {columns.map(column => (\n <TableHeaderCell\n key={column.name}\n id={`${gridId}-${column.name}`}\n >\n {column.label}\n </TableHeaderCell>\n ))}\n {editing && (\n <TableHeaderCell align=\"end\">\n <span className=\"sr-only\">{t('detail.rowActions')}</span>\n </TableHeaderCell>\n )}\n </TableRow>\n </TableHead>\n\n <TableBody>\n {showSkeleton\n ? (customSkeleton ?? (\n <SkeletonRows\n columns={columns.length}\n extra={editing ? 1 : 0}\n />\n ))\n : rows.map((row, index) => (\n <TableRow\n key={row[ROW_KEY]}\n onKeyDown={\n editing && index === rows.length - 1\n ? onLastRowKeyDown\n : undefined\n }\n >\n {columns.map(column => {\n const error = rowErrors.get(\n rowFieldPath(descriptor.name, index, column.name),\n );\n const cellId = `${gridId}-${index}-${column.name}`;\n return (\n <TableCell key={column.name}>\n {editing ? (\n <FieldControl\n descriptor={column}\n value={row[column.name] ?? ''}\n id={cellId}\n aria-labelledby={`${gridId}-${column.name}`}\n aria-invalid={error !== undefined || undefined}\n aria-describedby={\n error === undefined\n ? undefined\n : `${cellId}-error`\n }\n onChange={next => setRow(index, column.name, next)}\n record={row}\n />\n ) : column.money === undefined ? (\n <span>{row[column.name] ?? ''}</span>\n ) : (\n <CellValue\n value={row[column.name]}\n descriptor={column}\n record={row}\n />\n )}\n {editing && error !== undefined && (\n <span\n id={`${cellId}-error`}\n className=\"mt-3xs block text-step-sm text-danger\"\n >\n {error}\n </span>\n )}\n </TableCell>\n );\n })}\n {editing && (\n <TableCell align=\"end\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => removeRow(index)}\n >\n {t('detail.removeRow', { row: index + 1 })}\n </Button>\n </TableCell>\n )}\n </TableRow>\n ))}\n\n {!showSkeleton && rows.length === 0 && (\n <TableMessageRow colSpan={Math.max(columnCount, 1)}>\n {t('detail.empty')}\n </TableMessageRow>\n )}\n </TableBody>\n </Table>\n\n {footer !== undefined && !showSkeleton && (\n <div className={cn('flex justify-end text-step-sm text-content')}>\n {footer(rows)}\n </div>\n )}\n </Stack>\n );\n}\n\n/** The placeholder, shaped like the grid it stands in for. */\nfunction SkeletonRows({ columns, extra }: { columns: number; extra: number }) {\n return (\n <>\n {Array.from({ length: SKELETON_ROW_COUNT }, (_row, index) => (\n <TableRow key={index}>\n {Array.from({ length: columns + extra }, (_cell, cellIndex) => (\n <TableCell key={cellIndex}>\n <Skeleton className=\"h-8 w-full\" />\n </TableCell>\n ))}\n </TableRow>\n ))}\n </>\n );\n}\n\n/**\n * A blank row, seeded with every column the grid edits.\n *\n * Every member is present and empty rather than absent, for the reason a form's\n * seed carries every key: a control handed `undefined` flips from controlled to\n * uncontrolled the moment the user types into it.\n */\nfunction seedEmptyRow(\n columns: readonly EntityFieldDescriptor[],\n): EntityRowDraft {\n const row: EntityRowDraft = { [ROW_KEY]: newRowKey() };\n for (const column of columns) row[column.name] = '';\n return row;\n}\n\n/**\n * Moves focus into a cell after the list changed.\n *\n * By element id rather than through a ref into the `<tbody>`, because the row\n * being focused does not exist yet when this is called — it is the row the\n * caller has just asked React to render. Deferring to a microtask lets that\n * render commit first; `getElementById` then finds the input the same way the\n * cell's own `aria-describedby` does.\n *\n * A row index that names nothing simply focuses nothing, which is the right\n * outcome for removing the only row: there is no cell to land in, and stealing\n * focus somewhere arbitrary is worse than leaving it where the user put it.\n */\nfunction focusCell(gridId: string, index: number, column?: string): void {\n if (column === undefined || index < 0) return;\n\n queueMicrotask(() => {\n document.getElementById(`${gridId}-${index}-${column}`)?.focus();\n });\n}\n\n/** The messages naming a row of `member`, keyed by their full path. */\nfunction readRowErrors(\n member: string,\n errors: Record<string, string> | undefined,\n): Map<string, string> {\n const found = new Map<string, string>();\n if (errors === undefined) return found;\n\n for (const [path, message] of Object.entries(errors)) {\n if (parseRowFieldPath(path)?.member === member) found.set(path, message);\n }\n return found;\n}\n"],"names":["describeChildColumns","editableChildColumns","newRowKey","parseRowFieldPath","ROW_KEY","rowFieldPath","useId","useMemo","useLocalizedDescriptors","useT","Button","CellValue","FieldControl","Skeleton","Table","TableBody","TableCell","TableHead","TableHeaderCell","TableMessageRow","TableRow","Stack","cn","SKELETON_ROW_COUNT","EntityDetailGrid","descriptor","rows","onRowsChange","errors","editing","isLoading","skeleton","footer","className","columns","t","gridId","described","childType","undefined","rowErrors","readRowErrors","name","showSkeleton","customSkeleton","columnCount","length","setRow","index","member","value","map","row","position","firstColumn","appendRow","seedEmptyRow","focusCell","removeRow","filter","_row","Math","min","onLastRowKeyDown","event","key","target","HTMLInputElement","preventDefault","gap","div","span","label","type","variant","size","onClick","disabled","role","count","aria-label","aria-busy","aria-rowcount","column","id","align","SkeletonRows","extra","onKeyDown","error","get","cellId","aria-labelledby","aria-invalid","aria-describedby","onChange","next","record","money","colSpan","max","Array","from","_cell","cellIndex","queueMicrotask","document","getElementById","focus","found","Map","path","message","Object","entries","set"],"mappings":"AAAA;;;AAEA,SACEA,oBAAoB,EACpBC,oBAAoB,EAGpBC,SAAS,EACTC,iBAAiB,EACjBC,OAAO,EACPC,YAAY,QACP,gBAAgB;AACvB,SAA6BC,KAAK,EAAEC,OAAO,QAAQ,QAAQ;AAE3D,SAASC,uBAAuB,EAAEC,IAAI,QAAQ,yBAAyB;AACvE,SAASC,MAAM,QAAQ,8BAA8B;AACrD,SAASC,SAAS,QAAQ,kCAAkC;AAC5D,SAASC,YAAY,QAAQ,qCAAqC;AAClE,SAASC,QAAQ,QAAQ,gCAAgC;AACzD,SACEC,KAAK,EACLC,SAAS,EACTC,SAAS,EACTC,SAAS,EACTC,eAAe,EACfC,eAAe,EACfC,QAAQ,QACH,6BAA6B;AACpC,SAASC,KAAK,QAAQ,iCAAiC;AACvD,SAASC,EAAE,QAAQ,oBAAoB;AAGvC,8EAA8E,GAC9E,MAAMC,qBAAqB;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCC,GACD,OAAO,SAASC,iBAAiB,EAC/BC,UAAU,EACVC,IAAI,EACJC,YAAY,EACZC,MAAM,EACNC,UAAU,KAAK,EACfC,YAAY,KAAK,EACjBC,WAAW,IAAI,EACfC,MAAM,EACNC,SAAS,EACa;QAkCFC;IAjCpB,MAAMC,IAAI1B;IACV,MAAM2B,SAAS9B;IAEf,MAAM+B,YAAY9B,QAChB,IACEkB,WAAWa,SAAS,KAAKC,YACrB,EAAE,GACFtC,qBAAqBD,qBAAqByB,WAAWa,SAAS,IACpE;QAACb,WAAWa,SAAS;KAAC;IAExB,MAAMJ,UAAU1B,wBAAwB6B;IAExC,2EAA2E;IAC3E,2EAA2E;IAC3E,0EAA0E;IAC1E,MAAMG,YAAYjC,QAChB,IAAMkC,cAAchB,WAAWiB,IAAI,EAAEd,SACrC;QAACH,WAAWiB,IAAI;QAAEd;KAAO;IAG3B,MAAMe,eAAeb,aAAaC,aAAa;IAC/C,MAAMa,iBAAiB,OAAOb,aAAa,YAAYQ,YAAYR;IACnE,mEAAmE;IACnE,MAAMc,cAAcX,QAAQY,MAAM,GAAIjB,CAAAA,UAAU,IAAI,CAAA;IAEpD,MAAMkB,SAAS,CAACC,OAAeC,QAAgBC;QAC7CvB,aACED,KAAKyB,GAAG,CAAC,CAACC,KAAKC,WACbA,aAAaL,QAAQ,aAAKI;gBAAK,CAACH,OAAO,EAAEC;iBAAUE;IAGzD;IAEA,MAAME,eAAcpB,YAAAA,OAAO,CAAC,EAAE,qBAAVA,UAAYQ,IAAI;IAEpC,MAAMa,YAAY;QAChB5B,aAAa;eAAID;YAAM8B,aAAatB;SAAS;QAC7CuB,UAAUrB,QAAQV,KAAKoB,MAAM,EAAEQ;IACjC;IAEA,MAAMI,YAAY,CAACV;QACjBrB,aAAaD,KAAKiC,MAAM,CAAC,CAACC,MAAMP,WAAaA,aAAaL;QAC1D,2EAA2E;QAC3E,0EAA0E;QAC1ES,UAAUrB,QAAQyB,KAAKC,GAAG,CAACd,OAAOtB,KAAKoB,MAAM,GAAG,IAAIQ;IACtD;IAEA;;;;;;;;;;;;;;;GAeC,GACD,MAAMS,mBAAmB,CAACC;QACxB,IAAIA,MAAMC,GAAG,KAAK,SAAS;QAC3B,IAAI,CAAED,CAAAA,MAAME,MAAM,YAAYC,gBAAe,GAAI;QACjDH,MAAMI,cAAc;QACpBb;IACF;IAEA,qBACE,MAAClC;QAAMgD,KAAI;QAAMpC,WAAWA;;0BAC1B,MAACqC;gBAAIrC,WAAU;;kCACb,KAACsC;wBAAKtC,WAAU;kCACbR,WAAW+C,KAAK;;oBAElB3C,yBACC,KAACnB;wBACC+D,MAAK;wBACLC,SAAQ;wBACRC,MAAK;wBACLC,SAASrB;wBACTsB,UAAU/C;kCAETK,EAAE;;;;YAKRN,WAAWW,UAAUmC,IAAI,GAAG,mBAC3B,KAACL;gBAAIQ,MAAK;gBAAQ7C,WAAU;0BACzBE,EAAE,uBAAuB;oBAAE4C,OAAOvC,UAAUmC,IAAI;gBAAC;;0BAItD,MAAC7D;gBACCkE,cAAYvD,WAAW+C,KAAK;gBAC5BS,aAAWnD,aAAaS;gBACxB2C,iBAAexD,KAAKoB,MAAM;;kCAE1B,KAAC7B;kCACC,cAAA,MAACG;;gCACEc,QAAQiB,GAAG,CAACgC,CAAAA,uBACX,KAACjE;wCAECkE,IAAI,GAAGhD,OAAO,CAAC,EAAE+C,OAAOzC,IAAI,EAAE;kDAE7ByC,OAAOX,KAAK;uCAHRW,OAAOzC,IAAI;gCAMnBb,yBACC,KAACX;oCAAgBmE,OAAM;8CACrB,cAAA,KAACd;wCAAKtC,WAAU;kDAAWE,EAAE;;;;;;kCAMrC,MAACpB;;4BACE4B,eACIC,yBAAAA,+BACC,KAAC0C;gCACCpD,SAASA,QAAQY,MAAM;gCACvByC,OAAO1D,UAAU,IAAI;iCAGzBH,KAAKyB,GAAG,CAAC,CAACC,KAAKJ,sBACb,MAAC5B;oCAECoE,WACE3D,WAAWmB,UAAUtB,KAAKoB,MAAM,GAAG,IAC/BiB,mBACAxB;;wCAGLL,QAAQiB,GAAG,CAACgC,CAAAA;gDAUI/B,kBAaFA;4CAtBb,MAAMqC,QAAQjD,UAAUkD,GAAG,CACzBrF,aAAaoB,WAAWiB,IAAI,EAAEM,OAAOmC,OAAOzC,IAAI;4CAElD,MAAMiD,SAAS,GAAGvD,OAAO,CAAC,EAAEY,MAAM,CAAC,EAAEmC,OAAOzC,IAAI,EAAE;4CAClD,qBACE,MAAC1B;;oDACEa,wBACC,KAACjB;wDACCa,YAAY0D;wDACZjC,KAAK,GAAEE,mBAAAA,GAAG,CAAC+B,OAAOzC,IAAI,CAAC,YAAhBU,mBAAoB;wDAC3BgC,IAAIO;wDACJC,mBAAiB,GAAGxD,OAAO,CAAC,EAAE+C,OAAOzC,IAAI,EAAE;wDAC3CmD,gBAAcJ,UAAUlD,aAAaA;wDACrCuD,oBACEL,UAAUlD,YACNA,YACA,GAAGoD,OAAO,MAAM,CAAC;wDAEvBI,UAAUC,CAAAA,OAAQjD,OAAOC,OAAOmC,OAAOzC,IAAI,EAAEsD;wDAC7CC,QAAQ7C;yDAER+B,OAAOe,KAAK,KAAK3D,0BACnB,KAACgC;mEAAMnB,oBAAAA,GAAG,CAAC+B,OAAOzC,IAAI,CAAC,YAAhBU,oBAAoB;uEAE3B,KAACzC;wDACCuC,OAAOE,GAAG,CAAC+B,OAAOzC,IAAI,CAAC;wDACvBjB,YAAY0D;wDACZc,QAAQ7C;;oDAGXvB,WAAW4D,UAAUlD,2BACpB,KAACgC;wDACCa,IAAI,GAAGO,OAAO,MAAM,CAAC;wDACrB1D,WAAU;kEAETwD;;;+CA9BSN,OAAOzC,IAAI;wCAmC/B;wCACCb,yBACC,KAACb;4CAAUqE,OAAM;sDACf,cAAA,KAAC3E;gDACC+D,MAAK;gDACLC,SAAQ;gDACRC,MAAK;gDACLC,SAAS,IAAMlB,UAAUV;0DAExBb,EAAE,oBAAoB;oDAAEiB,KAAKJ,QAAQ;gDAAE;;;;mCAzDzCI,GAAG,CAAChD,QAAQ;4BAgExB,CAACuC,gBAAgBjB,KAAKoB,MAAM,KAAK,mBAChC,KAAC3B;gCAAgBgF,SAAStC,KAAKuC,GAAG,CAACvD,aAAa;0CAC7CV,EAAE;;;;;;YAMVH,WAAWO,aAAa,CAACI,8BACxB,KAAC2B;gBAAIrC,WAAWX,GAAG;0BAChBU,OAAON;;;;AAKlB;AAEA,4DAA4D,GAC5D,SAAS4D,aAAa,EAAEpD,OAAO,EAAEqD,KAAK,EAAsC;IAC1E,qBACE;kBACGc,MAAMC,IAAI,CAAC;YAAExD,QAAQvB;QAAmB,GAAG,CAACqC,MAAMZ,sBACjD,KAAC5B;0BACEiF,MAAMC,IAAI,CAAC;oBAAExD,QAAQZ,UAAUqD;gBAAM,GAAG,CAACgB,OAAOC,0BAC/C,KAACxF;kCACC,cAAA,KAACH;4BAASoB,WAAU;;uBADNuE;eAFLxD;;AAUvB;AAEA;;;;;;CAMC,GACD,SAASQ,aACPtB,OAAyC;IAEzC,MAAMkB,MAAsB;QAAE,CAAChD,QAAQ,EAAEF;IAAY;IACrD,KAAK,MAAMiF,UAAUjD,QAASkB,GAAG,CAAC+B,OAAOzC,IAAI,CAAC,GAAG;IACjD,OAAOU;AACT;AAEA;;;;;;;;;;;;CAYC,GACD,SAASK,UAAUrB,MAAc,EAAEY,KAAa,EAAEmC,MAAe;IAC/D,IAAIA,WAAW5C,aAAaS,QAAQ,GAAG;IAEvCyD,eAAe;YACbC;SAAAA,2BAAAA,SAASC,cAAc,CAAC,GAAGvE,OAAO,CAAC,EAAEY,MAAM,CAAC,EAAEmC,QAAQ,sBAAtDuB,yBAAyDE,KAAK;IAChE;AACF;AAEA,qEAAqE,GACrE,SAASnE,cACPQ,MAAc,EACdrB,MAA0C;IAE1C,MAAMiF,QAAQ,IAAIC;IAClB,IAAIlF,WAAWW,WAAW,OAAOsE;IAEjC,KAAK,MAAM,CAACE,MAAMC,QAAQ,IAAIC,OAAOC,OAAO,CAACtF,QAAS;YAChDzB;QAAJ,IAAIA,EAAAA,qBAAAA,kBAAkB4G,0BAAlB5G,mBAAyB8C,MAAM,MAAKA,QAAQ4D,MAAMM,GAAG,CAACJ,MAAMC;IAClE;IACA,OAAOH;AACT"}
@@ -423,7 +423,8 @@ linkSources) {
423
423
  */ function FieldRow({ field, entity, draft, value, editing, error, setField, linkSource, onLinkChange, id }) {
424
424
  const readNode = field.readRender ? field.readRender(entity) : /*#__PURE__*/ _jsx(CellValue, {
425
425
  value: entity ? entity[field.name] : undefined,
426
- descriptor: field
426
+ descriptor: field,
427
+ record: entity
427
428
  });
428
429
  // Members with no editor of their own here: a to-one `link` whose picker was
429
430
  // never wired, a `linkCollection` (#26) and a `composition`. All three
@@ -473,7 +474,8 @@ linkSources) {
473
474
  descriptor: field,
474
475
  value: value,
475
476
  onChange: (next)=>setField(field.name, next),
476
- id: id
477
+ id: id,
478
+ record: draft
477
479
  });
478
480
  }
479
481
  const ownsLabel = editing && field.type === 'boolean' && !field.render && !field.virtual;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/organisms/entity-form/entity-form.tsx"],"sourcesContent":["'use client';\n\nimport {\n cloneEntityDraft,\n describeEntityColumns,\n type Entity,\n type EntityAction,\n type EntityDraft,\n type EntityDraftValue,\n type EntityLinkSource,\n type MetaAccessorType,\n readDraftString,\n readLocalizedDraft,\n readRowDrafts,\n type UseCaseDescriptor,\n} from '@entifix/core';\nimport { type ReactNode, useId, useState } from 'react';\n\nimport {\n useErrorMessage,\n useLocalizedDescriptors,\n useT,\n useTranslateKey,\n} from '../../../i18n/index.js';\nimport {\n OVERFLOW_GLYPH,\n renderAnchor,\n useCasesForSurface,\n} from '../../actions/index.js';\nimport { Button, button } from '../../atoms/button/index.js';\nimport { CellValue } from '../../atoms/cell-value/index.js';\nimport { FieldControl } from '../../atoms/field-control/index.js';\nimport { LocalizedFieldControl } from '../../atoms/localized-field-control/index.js';\nimport { Skeleton } from '../../atoms/skeleton/index.js';\nimport { Text } from '../../atoms/text/index.js';\nimport { Card } from '../../molecules/card/index.js';\nimport { ConfirmDialog } from '../../molecules/confirm-dialog/index.js';\nimport { EntityLinkInput } from '../../molecules/entity-link-input/index.js';\nimport { LoadingBoundary } from '../../molecules/loading-boundary/index.js';\nimport { Menu } from '../../molecules/menu/index.js';\nimport { Stack } from '../../molecules/stack/index.js';\nimport { cn } from '../../utils/cn.js';\nimport { EntityDetailGrid } from '../entity-detail-grid/index.js';\nimport type {\n EntityFormField,\n EntityFormMode,\n EntityFormProps,\n} from './entity-form.types.js';\nimport { readEntityFormFields } from './entity-form-slots.js';\nimport { resolveEntityFormFields } from './use-entity-form-fields.js';\n\n/**\n * The member types a `linkSources` entry can actually produce an editor for.\n *\n * `link` is the obvious one. `string` is here because a foreign key does not\n * have to be a typed relation: when the target lives in **another slice's\n * store**, an `EntityLink` would be an illegal edge and a storage-layer join\n * across a store boundary, so the member is a plain id and the relation exists\n * only in the operator's head — a cross-store reference is an id, never a\n * link. Nothing about the editor cares which of the two it is —\n * `EntityLinkInput` writes `String(target.id)` into the draft either way, and\n * the *only* place `link`-ness still matters is `applyEntityLinks` at submit,\n * which skips a non-`link` descriptor and so leaves the id as the truth.\n *\n * Everything else is excluded on purpose rather than by omission: a `boolean`,\n * `enum`, `number` or `date` member cannot name another record, so a source\n * pointed at one is a wiring mistake — see {@link assertLinkSourcesAreEditable}.\n */\nconst PICKABLE_TYPES: ReadonlySet<MetaAccessorType> = new Set<MetaAccessorType>(\n ['link', 'string'],\n);\n\n/**\n * A form that builds itself from an entity's metadata: fields, labels and value\n * formatting all come from `@accessor()` declarations, so editing a new entity\n * needs no bespoke form. The read/edit toggle is the whole point — the same\n * component shows a record as text or as inputs.\n *\n * Two things layer on top of that default:\n * - **modes** — `read` renders each member through `CellValue`; `edit` renders\n * the matching `FieldControl`. A member in {@link PICKABLE_TYPES} with an\n * entry in `linkSources` gets the full picker instead — a to-one `link`, or a\n * `string` holding a foreign key into another slice's store. Without a source,\n * a relation stays read-only, and a `linkCollection` always does, the to-many\n * editor being a follow-up. A source aimed at a member that can render no\n * picker is a wiring mistake rather than a no-op, and throws.\n * - **slots** — `<EntityField>` children override one field's label, control or\n * read display, or add a computed field.\n *\n * It is presentational: the draft, its errors and the save/delete actions are\n * all props, so the same form hosts on a plain route and inside a workspace tab.\n */\n/**\n * How many verbs a surface shows before the rest fold into an overflow menu.\n *\n * Four, because that is roughly what fits beside a title at a laptop width\n * without the row wrapping — the same number Fiori's object-page header settles\n * on. It is a layout constant, not a policy: raising it does not grant anything.\n */\nconst VISIBLE_ACTION_LIMIT = 4;\n\n/** A verb waiting on its confirmation — narrowed so `confirm` is not optional. */\ninterface PendingConfirmation {\n key: string;\n labelKey: string;\n confirm: NonNullable<UseCaseDescriptor['confirm']>;\n}\n\nexport function EntityForm<TEntity extends Entity>({\n entityConstructor,\n entity,\n values,\n onFieldChange,\n linkSources,\n onLinkChange,\n mode: modeProp,\n defaultMode,\n onModeChange,\n errors,\n formError,\n onSubmit,\n onDelete,\n embedded = false,\n isLoading = false,\n skeleton = true,\n isSaving = false,\n isPending = false,\n isDeleting = false,\n error,\n backHref,\n renderLink = renderAnchor,\n title,\n metadata,\n isMetadataLoading = false,\n onUseCase,\n onClone,\n children,\n}: EntityFormProps<TEntity>) {\n const formId = useId();\n const slots = readEntityFormFields<TEntity>(children);\n // The *field* descriptors, which stay local and synchronous — a column is a\n // property of the class every caller holds. Only an action's availability is\n // a property of the caller, which is why that half arrives as `metadata`.\n const columnDescriptors = describeEntityColumns(\n entityConstructor,\n entity,\n ) as Array<EntityFormField<TEntity>>;\n // Before slots resolve, so an `<EntityField label>` override still wins.\n const described = useLocalizedDescriptors(columnDescriptors);\n const resolved = resolveEntityFormFields(described, slots.fields);\n assertLinkSourcesAreEditable(resolved, linkSources);\n\n // **Form above, table below**, and it is a partition rather than an ordering\n // hint. A grid rendered between two labelled inputs reads as a field, which\n // it is not — it is a second record list — so every owned collection is\n // pulled out of the field stack whatever `order` it declared, and rendered\n // full width beneath it. A composition that named no child has nothing to\n // render and stays where it was, showing its row count as before.\n const fields = resolved.filter(field => !isOwnedCollection(field));\n const ownedCollections = resolved.filter(isOwnedCollection);\n\n /**\n * `true`/omitted → the built-in default, which draws one label+control pair\n * per resolved field; a node → that node; `false` → nothing. The field list\n * comes from the constructor's metadata, not from the record, so it is already\n * complete while the record is still in flight — which is what lets the\n * placeholder match the form it is standing in for.\n */\n const customSkeleton = typeof skeleton === 'boolean' ? undefined : skeleton;\n const showSkeleton = isLoading && skeleton !== false;\n\n const [internalMode, setInternalMode] = useState<EntityFormMode>(\n defaultMode ?? (entity ? 'read' : 'edit'),\n );\n const mode = modeProp ?? internalMode;\n const editing = mode === 'edit';\n\n const draft: EntityDraft = values ?? {};\n const setField = (name: string, value: EntityDraftValue) =>\n onFieldChange?.(name, value);\n\n const toggleMode = () => {\n const next: EntityFormMode = editing ? 'read' : 'edit';\n setInternalMode(next);\n onModeChange?.(next);\n };\n\n const t = useT();\n const translateKey = useTranslateKey();\n const errorMessage = useErrorMessage();\n const busy = isSaving || isPending || isDeleting;\n\n // A draft restored from storage holds relation ids and no instances, so every\n // picker is resolving its target for a moment after mount. Saving inside that\n // window is exactly the case `applyEntityLinks` refuses: an `embedded` member\n // with an id and nothing to inline. Gating the save is the honest answer —\n // the form is still assembling itself, and the wait is one lookup long.\n //\n // Deliberately not narrowed to `embedded` members: a descriptor's\n // serialization is not what makes an in-flight lookup a bad moment to submit,\n // and a form that saves half-resolved is confusing whichever shape it writes.\n const resolvingLinks = Object.values(linkSources ?? {}).some(\n source => source.selected.isLoading,\n );\n\n // Absent metadata keeps the pre-ADR-0026 behaviour, so an un-migrated call\n // site renders exactly as before. Present metadata is authoritative: it is\n // what the service already decided this caller may do.\n const may = (action: EntityAction) =>\n metadata === undefined || metadata.actions.includes(action);\n\n // A single-record form shows the verbs that act on *this* record. Which cell\n // lands where is `action-surfaces`' business and nobody else's — a\n // `context-dependent` entity verb belongs to a row's overflow menu and a\n // `collection`-bound one to the bulk bar, and resolving that here as well is\n // how the two surfaces drifted apart in the first place.\n //\n // ⚠️ **Only when there is somewhere for them to go.** Every cell these two\n // surfaces render is `binding: 'entity'`, so without an `onUseCase` the\n // button has no handler and clicking it does nothing at all. That is not a\n // hypothetical: `makeEntityCrud` withholds the handler on a create, because\n // there is no record for a verb to act on — and Publish and Unpublish\n // rendered on a new `ProductOffering` and were inert, which reads as a broken\n // feature rather than an inapplicable one.\n //\n // The hooks are called unconditionally and their *results* are gated: React's\n // hook count must stay fixed across renders.\n const declaredHeaderUseCases = useCasesForSurface(\n 'form-header',\n metadata?.useCases,\n );\n const declaredFooterUseCases = useCasesForSurface(\n 'form-footer',\n metadata?.useCases,\n );\n const headerUseCases = onUseCase ? declaredHeaderUseCases : [];\n const footerUseCases = onUseCase ? declaredFooterUseCases : [];\n\n // A descriptor carrying `confirm` must be asked about before it fires;\n // `revoke-sessions` ends every session a user holds. The state holds the\n // confirmation itself rather than the descriptor, so the dialog cannot be\n // opened for a verb that never asked for one.\n const [pending, setPending] = useState<PendingConfirmation | undefined>(\n undefined,\n );\n const invoke = (descriptor: UseCaseDescriptor) => {\n const confirm = descriptor.confirm;\n if (confirm) {\n setPending({\n key: descriptor.key,\n labelKey: descriptor.labelKey,\n confirm,\n });\n return;\n }\n onUseCase?.(descriptor.key);\n };\n\n const useCaseButton = (descriptor: UseCaseDescriptor) => (\n <Button\n key={descriptor.key}\n type=\"button\"\n variant={\n descriptor.confirm?.tone === 'destructive' ? 'destructive' : 'secondary'\n }\n size={descriptor.placement === 'context-independent' ? 'sm' : 'md'}\n disabled={busy}\n onClick={() => invoke(descriptor)}\n >\n {/* A descriptor's labels are *runtime* catalog keys — they never resolve\n at compile time, which is why a host's catalog check is what catches a\n typo here rather than the type system. */}\n {translateKey(descriptor.labelKey)}\n </Button>\n );\n\n const useCaseMenuItem = (descriptor: UseCaseDescriptor) => (\n <Menu.Item\n key={descriptor.key}\n tone={\n descriptor.confirm?.tone === 'destructive' ? 'destructive' : 'neutral'\n }\n disabled={busy}\n onClick={() => invoke(descriptor)}\n >\n {translateKey(descriptor.labelKey)}\n </Menu.Item>\n );\n\n /**\n * Four actions fit a row; twelve do not.\n *\n * So a surface renders its first {@link VISIBLE_ACTION_LIMIT} verbs as\n * buttons and folds the rest behind one overflow trigger, in **declaration\n * order** — the entity's author decided which verbs matter by writing them\n * first, and a rule that sorted by anything else (alphabetical, destructive\n * last) would reorder the row every time a verb was added.\n *\n * The split is only applied when there is something to fold: a fifth action\n * is the point at which a menu is cheaper than a wider row, and a menu\n * holding one entry is strictly worse than the button it replaced.\n */\n const withOverflow = (descriptors: UseCaseDescriptor[]) => {\n if (descriptors.length <= VISIBLE_ACTION_LIMIT) {\n return descriptors.map(useCaseButton);\n }\n\n const visible = descriptors.slice(0, VISIBLE_ACTION_LIMIT);\n const folded = descriptors.slice(VISIBLE_ACTION_LIMIT);\n\n return [\n ...visible.map(useCaseButton),\n <Menu key=\"overflow\">\n <Menu.Trigger aria-label={t('form.moreActions')}>\n {OVERFLOW_GLYPH}\n </Menu.Trigger>\n <Menu.Items>{folded.map(useCaseMenuItem)}</Menu.Items>\n </Menu>,\n ];\n };\n\n return (\n <Card>\n <Stack gap=\"s\">\n {/* An embedded form brings only its fields: the heading and the action\n row belong to whatever is hosting it — a wizard step sits under the\n wizard's own heading and advances with the wizard's own footer, so\n rendering a second of each is two controls for one job. */}\n {!embedded && (\n <Stack direction=\"row\" gap=\"xs\" align=\"center\">\n <Text as=\"h2\" step={1} weight=\"semibold\">\n {/* `entity` is undefined while the record is still in flight, so\n testing it alone made a loading edit form announce itself as\n \"New\" for as long as the fetch took, then relabel when the\n record landed. A form that is loading is never a create. */}\n {title ??\n (entity || isLoading ? t('form.details') : t('form.new'))}\n </Text>\n {/* The built-in toggle only appears when the form owns its mode and\n there is a record to view — a create form has nothing to read. */}\n {modeProp === undefined && entity && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={toggleMode}\n >\n {editing ? t('form.view') : t('form.edit')}\n </Button>\n )}\n <LoadingBoundary isLoading={isMetadataLoading} lines={0}>\n <>\n {withOverflow(headerUseCases)}\n {slots.headerActions}\n </>\n </LoadingBoundary>\n </Stack>\n )}\n\n {/* One announcement for the form; the shimmer below is aria-hidden. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {isLoading ? t('form.loading') : ''}\n </span>\n\n {error && (\n <p\n role=\"alert\"\n data-testid=\"entity-form-error\"\n className=\"rounded-sm border border-danger bg-danger-subtle px-s py-2xs text-step-sm text-danger\"\n >\n {errorMessage(error)}\n </p>\n )}\n\n {/* The placeholder *replaces* the rows rather than sitting above them.\n Rendering both meant a shimmer stacked on a full set of empty fields,\n which is two loading signals for one region and twice the height the\n loaded form settles at. */}\n {showSkeleton\n ? (customSkeleton ??\n fields.map(field => (\n <Stack key={field.name} gap=\"3xs\">\n <Skeleton shape=\"line\" className=\"w-24\" />\n <Skeleton className=\"h-9 w-full\" />\n </Stack>\n )))\n : fields.map(field => (\n <FieldRow\n key={field.name}\n field={field}\n entity={entity}\n draft={draft}\n value={readDraftString(draft, field.name)}\n editing={editing}\n error={editing ? errors?.[field.name] : undefined}\n setField={setField}\n linkSource={linkSources?.[field.name]}\n onLinkChange={onLinkChange}\n id={`${formId}-${field.name}`}\n />\n ))}\n\n {/* The rows the record owns, full width and below every field. Each\n grid is handed the *whole* error map and picks out the keys naming\n its own member, so a record owning two collections needs no\n bookkeeping here. */}\n {ownedCollections.map(field => (\n <EntityDetailGrid\n key={field.name}\n descriptor={field}\n rows={readRowDrafts(draft[field.name])}\n onRowsChange={rows => setField(field.name, rows)}\n errors={editing ? errors : undefined}\n editing={editing}\n isLoading={isLoading}\n skeleton={skeleton}\n />\n ))}\n\n {/* A cross-field rule has no row to sit under, so it sits with the\n actions it blocks. */}\n {editing && formError && (\n <span role=\"alert\" className=\"text-step-sm text-danger\">\n {formError}\n </span>\n )}\n\n {editing && !embedded && (\n <Stack direction=\"row\" gap=\"xs\">\n {may('write') && (\n <Button\n type=\"button\"\n onClick={() => onSubmit?.(draft)}\n disabled={busy || resolvingLinks}\n >\n {isPending\n ? t('form.pending')\n : isSaving\n ? t('form.saving')\n : t('form.save')}\n </Button>\n )}\n {onDelete && may('delete') && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={onDelete}\n disabled={busy}\n >\n {isDeleting ? t('form.deleting') : t('form.delete')}\n </Button>\n )}\n {withOverflow(footerUseCases)}\n {/* Clone is the CRUD triple's, not a declared verb's: every entity\n can be copied, so giving it a `@useCase()` would mean declaring\n the same verb on all 28 of them. It needs `write`, because what\n it produces is a create. */}\n {onClone && entity && may('write') && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n disabled={busy}\n onClick={() =>\n onClone(cloneEntityDraft(entityConstructor, draft))\n }\n >\n {t('form.clone')}\n </Button>\n )}\n {slots.footerActions}\n {/* A link styled as a button, not a button inside a link: the old\n nesting was two interactive elements for one action, and its\n `disabled` stopped nothing, because the anchor around it still\n navigated. While busy the link is inert instead. */}\n {backHref &&\n renderLink({\n href: backHref,\n className: cn(\n button({ variant: 'ghost' }),\n busy && 'pointer-events-none opacity-50',\n ),\n children: t('form.back'),\n intent: { kind: 'back' },\n })}\n </Stack>\n )}\n\n {slots.rest}\n </Stack>\n\n {pending && (\n <ConfirmDialog\n open\n tone={pending.confirm.tone}\n title={translateKey(pending.labelKey)}\n message={translateKey(pending.confirm.messageKey)}\n busy={busy}\n onCancel={() => setPending(undefined)}\n onConfirm={() => {\n const key = pending.key;\n setPending(undefined);\n onUseCase?.(key);\n }}\n />\n )}\n </Card>\n );\n}\n\n/**\n * A member the detail grid renders rather than the field stack.\n *\n * The `childType` half is not defensive: a `composition` that declared no child\n * has no columns, so there is nothing for a grid to draw. It keeps its place in\n * the field stack and reads as its row count, which is what it did before this\n * control existed — an honest \"some rows\" rather than an empty table implying\n * there are none.\n */\nfunction isOwnedCollection<TEntity extends Entity>(\n field: EntityFormField<TEntity>,\n): boolean {\n return field.type === 'composition' && field.childType !== undefined;\n}\n\n/**\n * Refuse a `linkSources` entry that names a member no picker can be built for.\n *\n * `FieldRow` asks for {@link PICKABLE_TYPES} before it builds an editor, so a\n * source handed to anything else used to fall through to the read-only display\n * or a plain `FieldControl` with nothing said: the caller wired a picker, no\n * picker appeared, and the field was indistinguishable from one the entity had\n * declared read-only. That is a wiring mistake rather than a state — the entry\n * is dead in *every* shape such a field can render, since an\n * `<EntityField render>` slot is handed the draft and never the source — so it\n * costs the render instead of a warning nobody reads. `useEntityLinkSource`\n * already throws at render for the sibling mistake (a search\n * property the target does not declare `filterable`); this is the same fault\n * caught one level up.\n *\n * Two messages, because the two faults are not the same thing. A\n * `linkCollection` is a member the editor has simply not been built for yet\n * (#26), so the fix is to wait or to point at the to-one you meant. Any other\n * type is a member that can never name another record at all, so the fix is to\n * remove the entry.\n *\n * Here rather than inside `FieldRow`, and that is the whole reason it is a\n * separate pass: a row only reaches its editor branch in **edit** mode, so a\n * per-row check would stay silent on a form that opens in `read` — the default\n * whenever there is a record — and then throw on a click, minutes later. One\n * pass over the registry fires on the first render either way.\n *\n * Against the resolved fields rather than the raw metadata: a slot may drop a\n * member, and a source left behind for a field that no longer renders is not\n * worth a page.\n */\nfunction assertLinkSourcesAreEditable<TEntity extends Entity>(\n fields: Array<EntityFormField<TEntity>>,\n // Same reason as the prop's own declaration: a source's target is another\n // entity entirely and cannot be expressed in terms of `TEntity`.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n linkSources: Record<string, EntityLinkSource<any>> | undefined,\n): void {\n if (linkSources === undefined) return;\n\n const sourced = fields.filter(\n field =>\n linkSources[field.name] !== undefined && !PICKABLE_TYPES.has(field.type),\n );\n if (sourced.length === 0) return;\n\n const toMany = sourced\n .filter(field => field.type === 'linkCollection')\n .map(field => field.name);\n if (toMany.length > 0) {\n throw new Error(\n `EntityForm was given a link source for a to-many member: ${toMany.join(', ')}. ` +\n 'A `linkCollection` has no editor yet, so the source can only be dropped ' +\n 'and the field would render read-only with no sign anything was wired. ' +\n 'Remove the entry, or point it at the to-one `link` you meant.',\n );\n }\n\n const owned = sourced\n .filter(field => field.type === 'composition')\n .map(field => field.name);\n if (owned.length > 0) {\n throw new Error(\n `EntityForm was given a link source for an owned collection: ${owned.join(', ')}. ` +\n 'A `composition` is not a reference — its rows have no life outside this ' +\n 'record and are never picked from existing ones, so there is nothing for a ' +\n 'picker to look up. The detail grid is what edits them.',\n );\n }\n\n const unpickable = sourced.map(field => `${field.name} (${field.type})`);\n throw new Error(\n `EntityForm was given a link source for a member that cannot hold one: ${unpickable.join(', ')}. ` +\n 'A picker writes the target’s id into the draft, so the member has to be a ' +\n '`link` or the `string` that carries a foreign key. Remove the entry, or ' +\n 'point it at the member that actually holds the reference.',\n );\n}\n\ninterface FieldRowProps<TEntity extends Entity> {\n field: EntityFormField<TEntity>;\n entity: TEntity | undefined;\n draft: EntityDraft;\n value: string;\n editing: boolean;\n error: string | undefined;\n setField: (name: string, value: EntityDraftValue) => void;\n // The target of a relation is a different entity than the form's own.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n linkSource: EntityLinkSource<any> | undefined;\n onLinkChange:\n ((name: string, entity: Entity | undefined) => void) | undefined;\n id: string;\n}\n\n/**\n * One label + control (or value) row. A `readonly` member renders its read\n * display even in edit mode, and so does a relation with no source; everything\n * else edits through `FieldControl` — or, when a source was supplied for a\n * member in {@link PICKABLE_TYPES}, through `EntityLinkInput`. A boolean edited\n * inline carries its own label, so the row does not add a second one.\n *\n * The source branch sits above the `virtual || isRelation` fallback and below\n * `field.render`, which is what lets a slot still override a picked field.\n */\nfunction FieldRow<TEntity extends Entity>({\n field,\n entity,\n draft,\n value,\n editing,\n error,\n setField,\n linkSource,\n onLinkChange,\n id,\n}: FieldRowProps<TEntity>) {\n const readNode: ReactNode = field.readRender ? (\n field.readRender(entity)\n ) : (\n <CellValue\n value={\n entity ? (entity as Record<string, unknown>)[field.name] : undefined\n }\n descriptor={field}\n />\n );\n\n // Members with no editor of their own here: a to-one `link` whose picker was\n // never wired, a `linkCollection` (#26) and a `composition`. All three\n // fall back to the read display, which is honest — a disabled text box\n // holding `[object Object]` is not.\n const isRelation =\n field.type === 'link' ||\n field.type === 'linkCollection' ||\n field.type === 'composition';\n\n let control: ReactNode;\n if (!editing) {\n control = readNode;\n } else if (field.render) {\n control = field.render({ draft, value, setField, id });\n } else if (linkSource && PICKABLE_TYPES.has(field.type)) {\n control = (\n <EntityLinkInput\n descriptor={field}\n value={value}\n source={linkSource}\n id={id}\n disabled={field.readonly}\n onSelect={target => {\n setField(field.name, target.id == null ? '' : String(target.id));\n onLinkChange?.(field.name, target);\n }}\n onClear={() => {\n setField(field.name, '');\n onLinkChange?.(field.name, undefined);\n }}\n />\n );\n } else if (field.type === 'localized' && !field.virtual) {\n control = (\n <LocalizedFieldControl\n descriptor={field}\n value={readLocalizedDraft(draft, field.name)}\n onChange={next => setField(field.name, next)}\n id={id}\n />\n );\n } else if (field.virtual || isRelation) {\n // A relation with no source has no input to offer, and a virtual field has\n // no member to edit unless a `render` was supplied — both fall back to the\n // read display.\n control = readNode;\n } else {\n control = (\n <FieldControl\n descriptor={field}\n value={value}\n onChange={next => setField(field.name, next)}\n id={id}\n />\n );\n }\n\n const ownsLabel =\n editing && field.type === 'boolean' && !field.render && !field.virtual;\n\n return (\n <Stack gap=\"3xs\">\n {!ownsLabel && (\n <label htmlFor={id} className=\"text-step-sm text-content-muted\">\n {field.label}\n </label>\n )}\n {control}\n {error && (\n <span role=\"alert\" className=\"text-step-sm text-danger\">\n {error}\n </span>\n )}\n </Stack>\n );\n}\n"],"names":["cloneEntityDraft","describeEntityColumns","readDraftString","readLocalizedDraft","readRowDrafts","useId","useState","useErrorMessage","useLocalizedDescriptors","useT","useTranslateKey","OVERFLOW_GLYPH","renderAnchor","useCasesForSurface","Button","button","CellValue","FieldControl","LocalizedFieldControl","Skeleton","Text","Card","ConfirmDialog","EntityLinkInput","LoadingBoundary","Menu","Stack","cn","EntityDetailGrid","readEntityFormFields","resolveEntityFormFields","PICKABLE_TYPES","Set","VISIBLE_ACTION_LIMIT","EntityForm","entityConstructor","entity","values","onFieldChange","linkSources","onLinkChange","mode","modeProp","defaultMode","onModeChange","errors","formError","onSubmit","onDelete","embedded","isLoading","skeleton","isSaving","isPending","isDeleting","error","backHref","renderLink","title","metadata","isMetadataLoading","onUseCase","onClone","children","formId","slots","columnDescriptors","described","resolved","fields","assertLinkSourcesAreEditable","filter","field","isOwnedCollection","ownedCollections","customSkeleton","undefined","showSkeleton","internalMode","setInternalMode","editing","draft","setField","name","value","toggleMode","next","t","translateKey","errorMessage","busy","resolvingLinks","Object","some","source","selected","may","action","actions","includes","declaredHeaderUseCases","useCases","declaredFooterUseCases","headerUseCases","footerUseCases","pending","setPending","invoke","descriptor","confirm","key","labelKey","useCaseButton","type","variant","tone","size","placement","disabled","onClick","useCaseMenuItem","Item","withOverflow","descriptors","length","map","visible","slice","folded","Trigger","aria-label","Items","gap","direction","align","as","step","weight","lines","headerActions","span","role","aria-live","className","p","data-testid","shape","FieldRow","linkSource","id","rows","onRowsChange","footerActions","href","intent","kind","rest","open","message","messageKey","onCancel","onConfirm","childType","sourced","has","toMany","Error","join","owned","unpickable","readNode","readRender","isRelation","control","render","readonly","onSelect","target","String","onClear","virtual","onChange","ownsLabel","label","htmlFor"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../src/ui/organisms/entity-form/entity-form.tsx"],"sourcesContent":["'use client';\n\nimport {\n cloneEntityDraft,\n describeEntityColumns,\n type Entity,\n type EntityAction,\n type EntityDraft,\n type EntityDraftValue,\n type EntityLinkSource,\n type MetaAccessorType,\n readDraftString,\n readLocalizedDraft,\n readRowDrafts,\n type UseCaseDescriptor,\n} from '@entifix/core';\nimport { type ReactNode, useId, useState } from 'react';\n\nimport {\n useErrorMessage,\n useLocalizedDescriptors,\n useT,\n useTranslateKey,\n} from '../../../i18n/index.js';\nimport {\n OVERFLOW_GLYPH,\n renderAnchor,\n useCasesForSurface,\n} from '../../actions/index.js';\nimport { Button, button } from '../../atoms/button/index.js';\nimport { CellValue } from '../../atoms/cell-value/index.js';\nimport { FieldControl } from '../../atoms/field-control/index.js';\nimport { LocalizedFieldControl } from '../../atoms/localized-field-control/index.js';\nimport { Skeleton } from '../../atoms/skeleton/index.js';\nimport { Text } from '../../atoms/text/index.js';\nimport { Card } from '../../molecules/card/index.js';\nimport { ConfirmDialog } from '../../molecules/confirm-dialog/index.js';\nimport { EntityLinkInput } from '../../molecules/entity-link-input/index.js';\nimport { LoadingBoundary } from '../../molecules/loading-boundary/index.js';\nimport { Menu } from '../../molecules/menu/index.js';\nimport { Stack } from '../../molecules/stack/index.js';\nimport { cn } from '../../utils/cn.js';\nimport { EntityDetailGrid } from '../entity-detail-grid/index.js';\nimport type {\n EntityFormField,\n EntityFormMode,\n EntityFormProps,\n} from './entity-form.types.js';\nimport { readEntityFormFields } from './entity-form-slots.js';\nimport { resolveEntityFormFields } from './use-entity-form-fields.js';\n\n/**\n * The member types a `linkSources` entry can actually produce an editor for.\n *\n * `link` is the obvious one. `string` is here because a foreign key does not\n * have to be a typed relation: when the target lives in **another slice's\n * store**, an `EntityLink` would be an illegal edge and a storage-layer join\n * across a store boundary, so the member is a plain id and the relation exists\n * only in the operator's head — a cross-store reference is an id, never a\n * link. Nothing about the editor cares which of the two it is —\n * `EntityLinkInput` writes `String(target.id)` into the draft either way, and\n * the *only* place `link`-ness still matters is `applyEntityLinks` at submit,\n * which skips a non-`link` descriptor and so leaves the id as the truth.\n *\n * Everything else is excluded on purpose rather than by omission: a `boolean`,\n * `enum`, `number` or `date` member cannot name another record, so a source\n * pointed at one is a wiring mistake — see {@link assertLinkSourcesAreEditable}.\n */\nconst PICKABLE_TYPES: ReadonlySet<MetaAccessorType> = new Set<MetaAccessorType>(\n ['link', 'string'],\n);\n\n/**\n * A form that builds itself from an entity's metadata: fields, labels and value\n * formatting all come from `@accessor()` declarations, so editing a new entity\n * needs no bespoke form. The read/edit toggle is the whole point — the same\n * component shows a record as text or as inputs.\n *\n * Two things layer on top of that default:\n * - **modes** — `read` renders each member through `CellValue`; `edit` renders\n * the matching `FieldControl`. A member in {@link PICKABLE_TYPES} with an\n * entry in `linkSources` gets the full picker instead — a to-one `link`, or a\n * `string` holding a foreign key into another slice's store. Without a source,\n * a relation stays read-only, and a `linkCollection` always does, the to-many\n * editor being a follow-up. A source aimed at a member that can render no\n * picker is a wiring mistake rather than a no-op, and throws.\n * - **slots** — `<EntityField>` children override one field's label, control or\n * read display, or add a computed field.\n *\n * It is presentational: the draft, its errors and the save/delete actions are\n * all props, so the same form hosts on a plain route and inside a workspace tab.\n */\n/**\n * How many verbs a surface shows before the rest fold into an overflow menu.\n *\n * Four, because that is roughly what fits beside a title at a laptop width\n * without the row wrapping — the same number Fiori's object-page header settles\n * on. It is a layout constant, not a policy: raising it does not grant anything.\n */\nconst VISIBLE_ACTION_LIMIT = 4;\n\n/** A verb waiting on its confirmation — narrowed so `confirm` is not optional. */\ninterface PendingConfirmation {\n key: string;\n labelKey: string;\n confirm: NonNullable<UseCaseDescriptor['confirm']>;\n}\n\nexport function EntityForm<TEntity extends Entity>({\n entityConstructor,\n entity,\n values,\n onFieldChange,\n linkSources,\n onLinkChange,\n mode: modeProp,\n defaultMode,\n onModeChange,\n errors,\n formError,\n onSubmit,\n onDelete,\n embedded = false,\n isLoading = false,\n skeleton = true,\n isSaving = false,\n isPending = false,\n isDeleting = false,\n error,\n backHref,\n renderLink = renderAnchor,\n title,\n metadata,\n isMetadataLoading = false,\n onUseCase,\n onClone,\n children,\n}: EntityFormProps<TEntity>) {\n const formId = useId();\n const slots = readEntityFormFields<TEntity>(children);\n // The *field* descriptors, which stay local and synchronous — a column is a\n // property of the class every caller holds. Only an action's availability is\n // a property of the caller, which is why that half arrives as `metadata`.\n const columnDescriptors = describeEntityColumns(\n entityConstructor,\n entity,\n ) as Array<EntityFormField<TEntity>>;\n // Before slots resolve, so an `<EntityField label>` override still wins.\n const described = useLocalizedDescriptors(columnDescriptors);\n const resolved = resolveEntityFormFields(described, slots.fields);\n assertLinkSourcesAreEditable(resolved, linkSources);\n\n // **Form above, table below**, and it is a partition rather than an ordering\n // hint. A grid rendered between two labelled inputs reads as a field, which\n // it is not — it is a second record list — so every owned collection is\n // pulled out of the field stack whatever `order` it declared, and rendered\n // full width beneath it. A composition that named no child has nothing to\n // render and stays where it was, showing its row count as before.\n const fields = resolved.filter(field => !isOwnedCollection(field));\n const ownedCollections = resolved.filter(isOwnedCollection);\n\n /**\n * `true`/omitted → the built-in default, which draws one label+control pair\n * per resolved field; a node → that node; `false` → nothing. The field list\n * comes from the constructor's metadata, not from the record, so it is already\n * complete while the record is still in flight — which is what lets the\n * placeholder match the form it is standing in for.\n */\n const customSkeleton = typeof skeleton === 'boolean' ? undefined : skeleton;\n const showSkeleton = isLoading && skeleton !== false;\n\n const [internalMode, setInternalMode] = useState<EntityFormMode>(\n defaultMode ?? (entity ? 'read' : 'edit'),\n );\n const mode = modeProp ?? internalMode;\n const editing = mode === 'edit';\n\n const draft: EntityDraft = values ?? {};\n const setField = (name: string, value: EntityDraftValue) =>\n onFieldChange?.(name, value);\n\n const toggleMode = () => {\n const next: EntityFormMode = editing ? 'read' : 'edit';\n setInternalMode(next);\n onModeChange?.(next);\n };\n\n const t = useT();\n const translateKey = useTranslateKey();\n const errorMessage = useErrorMessage();\n const busy = isSaving || isPending || isDeleting;\n\n // A draft restored from storage holds relation ids and no instances, so every\n // picker is resolving its target for a moment after mount. Saving inside that\n // window is exactly the case `applyEntityLinks` refuses: an `embedded` member\n // with an id and nothing to inline. Gating the save is the honest answer —\n // the form is still assembling itself, and the wait is one lookup long.\n //\n // Deliberately not narrowed to `embedded` members: a descriptor's\n // serialization is not what makes an in-flight lookup a bad moment to submit,\n // and a form that saves half-resolved is confusing whichever shape it writes.\n const resolvingLinks = Object.values(linkSources ?? {}).some(\n source => source.selected.isLoading,\n );\n\n // Absent metadata keeps the pre-ADR-0026 behaviour, so an un-migrated call\n // site renders exactly as before. Present metadata is authoritative: it is\n // what the service already decided this caller may do.\n const may = (action: EntityAction) =>\n metadata === undefined || metadata.actions.includes(action);\n\n // A single-record form shows the verbs that act on *this* record. Which cell\n // lands where is `action-surfaces`' business and nobody else's — a\n // `context-dependent` entity verb belongs to a row's overflow menu and a\n // `collection`-bound one to the bulk bar, and resolving that here as well is\n // how the two surfaces drifted apart in the first place.\n //\n // ⚠️ **Only when there is somewhere for them to go.** Every cell these two\n // surfaces render is `binding: 'entity'`, so without an `onUseCase` the\n // button has no handler and clicking it does nothing at all. That is not a\n // hypothetical: `makeEntityCrud` withholds the handler on a create, because\n // there is no record for a verb to act on — and Publish and Unpublish\n // rendered on a new `ProductOffering` and were inert, which reads as a broken\n // feature rather than an inapplicable one.\n //\n // The hooks are called unconditionally and their *results* are gated: React's\n // hook count must stay fixed across renders.\n const declaredHeaderUseCases = useCasesForSurface(\n 'form-header',\n metadata?.useCases,\n );\n const declaredFooterUseCases = useCasesForSurface(\n 'form-footer',\n metadata?.useCases,\n );\n const headerUseCases = onUseCase ? declaredHeaderUseCases : [];\n const footerUseCases = onUseCase ? declaredFooterUseCases : [];\n\n // A descriptor carrying `confirm` must be asked about before it fires;\n // `revoke-sessions` ends every session a user holds. The state holds the\n // confirmation itself rather than the descriptor, so the dialog cannot be\n // opened for a verb that never asked for one.\n const [pending, setPending] = useState<PendingConfirmation | undefined>(\n undefined,\n );\n const invoke = (descriptor: UseCaseDescriptor) => {\n const confirm = descriptor.confirm;\n if (confirm) {\n setPending({\n key: descriptor.key,\n labelKey: descriptor.labelKey,\n confirm,\n });\n return;\n }\n onUseCase?.(descriptor.key);\n };\n\n const useCaseButton = (descriptor: UseCaseDescriptor) => (\n <Button\n key={descriptor.key}\n type=\"button\"\n variant={\n descriptor.confirm?.tone === 'destructive' ? 'destructive' : 'secondary'\n }\n size={descriptor.placement === 'context-independent' ? 'sm' : 'md'}\n disabled={busy}\n onClick={() => invoke(descriptor)}\n >\n {/* A descriptor's labels are *runtime* catalog keys — they never resolve\n at compile time, which is why a host's catalog check is what catches a\n typo here rather than the type system. */}\n {translateKey(descriptor.labelKey)}\n </Button>\n );\n\n const useCaseMenuItem = (descriptor: UseCaseDescriptor) => (\n <Menu.Item\n key={descriptor.key}\n tone={\n descriptor.confirm?.tone === 'destructive' ? 'destructive' : 'neutral'\n }\n disabled={busy}\n onClick={() => invoke(descriptor)}\n >\n {translateKey(descriptor.labelKey)}\n </Menu.Item>\n );\n\n /**\n * Four actions fit a row; twelve do not.\n *\n * So a surface renders its first {@link VISIBLE_ACTION_LIMIT} verbs as\n * buttons and folds the rest behind one overflow trigger, in **declaration\n * order** — the entity's author decided which verbs matter by writing them\n * first, and a rule that sorted by anything else (alphabetical, destructive\n * last) would reorder the row every time a verb was added.\n *\n * The split is only applied when there is something to fold: a fifth action\n * is the point at which a menu is cheaper than a wider row, and a menu\n * holding one entry is strictly worse than the button it replaced.\n */\n const withOverflow = (descriptors: UseCaseDescriptor[]) => {\n if (descriptors.length <= VISIBLE_ACTION_LIMIT) {\n return descriptors.map(useCaseButton);\n }\n\n const visible = descriptors.slice(0, VISIBLE_ACTION_LIMIT);\n const folded = descriptors.slice(VISIBLE_ACTION_LIMIT);\n\n return [\n ...visible.map(useCaseButton),\n <Menu key=\"overflow\">\n <Menu.Trigger aria-label={t('form.moreActions')}>\n {OVERFLOW_GLYPH}\n </Menu.Trigger>\n <Menu.Items>{folded.map(useCaseMenuItem)}</Menu.Items>\n </Menu>,\n ];\n };\n\n return (\n <Card>\n <Stack gap=\"s\">\n {/* An embedded form brings only its fields: the heading and the action\n row belong to whatever is hosting it — a wizard step sits under the\n wizard's own heading and advances with the wizard's own footer, so\n rendering a second of each is two controls for one job. */}\n {!embedded && (\n <Stack direction=\"row\" gap=\"xs\" align=\"center\">\n <Text as=\"h2\" step={1} weight=\"semibold\">\n {/* `entity` is undefined while the record is still in flight, so\n testing it alone made a loading edit form announce itself as\n \"New\" for as long as the fetch took, then relabel when the\n record landed. A form that is loading is never a create. */}\n {title ??\n (entity || isLoading ? t('form.details') : t('form.new'))}\n </Text>\n {/* The built-in toggle only appears when the form owns its mode and\n there is a record to view — a create form has nothing to read. */}\n {modeProp === undefined && entity && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={toggleMode}\n >\n {editing ? t('form.view') : t('form.edit')}\n </Button>\n )}\n <LoadingBoundary isLoading={isMetadataLoading} lines={0}>\n <>\n {withOverflow(headerUseCases)}\n {slots.headerActions}\n </>\n </LoadingBoundary>\n </Stack>\n )}\n\n {/* One announcement for the form; the shimmer below is aria-hidden. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {isLoading ? t('form.loading') : ''}\n </span>\n\n {error && (\n <p\n role=\"alert\"\n data-testid=\"entity-form-error\"\n className=\"rounded-sm border border-danger bg-danger-subtle px-s py-2xs text-step-sm text-danger\"\n >\n {errorMessage(error)}\n </p>\n )}\n\n {/* The placeholder *replaces* the rows rather than sitting above them.\n Rendering both meant a shimmer stacked on a full set of empty fields,\n which is two loading signals for one region and twice the height the\n loaded form settles at. */}\n {showSkeleton\n ? (customSkeleton ??\n fields.map(field => (\n <Stack key={field.name} gap=\"3xs\">\n <Skeleton shape=\"line\" className=\"w-24\" />\n <Skeleton className=\"h-9 w-full\" />\n </Stack>\n )))\n : fields.map(field => (\n <FieldRow\n key={field.name}\n field={field}\n entity={entity}\n draft={draft}\n value={readDraftString(draft, field.name)}\n editing={editing}\n error={editing ? errors?.[field.name] : undefined}\n setField={setField}\n linkSource={linkSources?.[field.name]}\n onLinkChange={onLinkChange}\n id={`${formId}-${field.name}`}\n />\n ))}\n\n {/* The rows the record owns, full width and below every field. Each\n grid is handed the *whole* error map and picks out the keys naming\n its own member, so a record owning two collections needs no\n bookkeeping here. */}\n {ownedCollections.map(field => (\n <EntityDetailGrid\n key={field.name}\n descriptor={field}\n rows={readRowDrafts(draft[field.name])}\n onRowsChange={rows => setField(field.name, rows)}\n errors={editing ? errors : undefined}\n editing={editing}\n isLoading={isLoading}\n skeleton={skeleton}\n />\n ))}\n\n {/* A cross-field rule has no row to sit under, so it sits with the\n actions it blocks. */}\n {editing && formError && (\n <span role=\"alert\" className=\"text-step-sm text-danger\">\n {formError}\n </span>\n )}\n\n {editing && !embedded && (\n <Stack direction=\"row\" gap=\"xs\">\n {may('write') && (\n <Button\n type=\"button\"\n onClick={() => onSubmit?.(draft)}\n disabled={busy || resolvingLinks}\n >\n {isPending\n ? t('form.pending')\n : isSaving\n ? t('form.saving')\n : t('form.save')}\n </Button>\n )}\n {onDelete && may('delete') && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={onDelete}\n disabled={busy}\n >\n {isDeleting ? t('form.deleting') : t('form.delete')}\n </Button>\n )}\n {withOverflow(footerUseCases)}\n {/* Clone is the CRUD triple's, not a declared verb's: every entity\n can be copied, so giving it a `@useCase()` would mean declaring\n the same verb on all 28 of them. It needs `write`, because what\n it produces is a create. */}\n {onClone && entity && may('write') && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n disabled={busy}\n onClick={() =>\n onClone(cloneEntityDraft(entityConstructor, draft))\n }\n >\n {t('form.clone')}\n </Button>\n )}\n {slots.footerActions}\n {/* A link styled as a button, not a button inside a link: the old\n nesting was two interactive elements for one action, and its\n `disabled` stopped nothing, because the anchor around it still\n navigated. While busy the link is inert instead. */}\n {backHref &&\n renderLink({\n href: backHref,\n className: cn(\n button({ variant: 'ghost' }),\n busy && 'pointer-events-none opacity-50',\n ),\n children: t('form.back'),\n intent: { kind: 'back' },\n })}\n </Stack>\n )}\n\n {slots.rest}\n </Stack>\n\n {pending && (\n <ConfirmDialog\n open\n tone={pending.confirm.tone}\n title={translateKey(pending.labelKey)}\n message={translateKey(pending.confirm.messageKey)}\n busy={busy}\n onCancel={() => setPending(undefined)}\n onConfirm={() => {\n const key = pending.key;\n setPending(undefined);\n onUseCase?.(key);\n }}\n />\n )}\n </Card>\n );\n}\n\n/**\n * A member the detail grid renders rather than the field stack.\n *\n * The `childType` half is not defensive: a `composition` that declared no child\n * has no columns, so there is nothing for a grid to draw. It keeps its place in\n * the field stack and reads as its row count, which is what it did before this\n * control existed — an honest \"some rows\" rather than an empty table implying\n * there are none.\n */\nfunction isOwnedCollection<TEntity extends Entity>(\n field: EntityFormField<TEntity>,\n): boolean {\n return field.type === 'composition' && field.childType !== undefined;\n}\n\n/**\n * Refuse a `linkSources` entry that names a member no picker can be built for.\n *\n * `FieldRow` asks for {@link PICKABLE_TYPES} before it builds an editor, so a\n * source handed to anything else used to fall through to the read-only display\n * or a plain `FieldControl` with nothing said: the caller wired a picker, no\n * picker appeared, and the field was indistinguishable from one the entity had\n * declared read-only. That is a wiring mistake rather than a state — the entry\n * is dead in *every* shape such a field can render, since an\n * `<EntityField render>` slot is handed the draft and never the source — so it\n * costs the render instead of a warning nobody reads. `useEntityLinkSource`\n * already throws at render for the sibling mistake (a search\n * property the target does not declare `filterable`); this is the same fault\n * caught one level up.\n *\n * Two messages, because the two faults are not the same thing. A\n * `linkCollection` is a member the editor has simply not been built for yet\n * (#26), so the fix is to wait or to point at the to-one you meant. Any other\n * type is a member that can never name another record at all, so the fix is to\n * remove the entry.\n *\n * Here rather than inside `FieldRow`, and that is the whole reason it is a\n * separate pass: a row only reaches its editor branch in **edit** mode, so a\n * per-row check would stay silent on a form that opens in `read` — the default\n * whenever there is a record — and then throw on a click, minutes later. One\n * pass over the registry fires on the first render either way.\n *\n * Against the resolved fields rather than the raw metadata: a slot may drop a\n * member, and a source left behind for a field that no longer renders is not\n * worth a page.\n */\nfunction assertLinkSourcesAreEditable<TEntity extends Entity>(\n fields: Array<EntityFormField<TEntity>>,\n // Same reason as the prop's own declaration: a source's target is another\n // entity entirely and cannot be expressed in terms of `TEntity`.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n linkSources: Record<string, EntityLinkSource<any>> | undefined,\n): void {\n if (linkSources === undefined) return;\n\n const sourced = fields.filter(\n field =>\n linkSources[field.name] !== undefined && !PICKABLE_TYPES.has(field.type),\n );\n if (sourced.length === 0) return;\n\n const toMany = sourced\n .filter(field => field.type === 'linkCollection')\n .map(field => field.name);\n if (toMany.length > 0) {\n throw new Error(\n `EntityForm was given a link source for a to-many member: ${toMany.join(', ')}. ` +\n 'A `linkCollection` has no editor yet, so the source can only be dropped ' +\n 'and the field would render read-only with no sign anything was wired. ' +\n 'Remove the entry, or point it at the to-one `link` you meant.',\n );\n }\n\n const owned = sourced\n .filter(field => field.type === 'composition')\n .map(field => field.name);\n if (owned.length > 0) {\n throw new Error(\n `EntityForm was given a link source for an owned collection: ${owned.join(', ')}. ` +\n 'A `composition` is not a reference — its rows have no life outside this ' +\n 'record and are never picked from existing ones, so there is nothing for a ' +\n 'picker to look up. The detail grid is what edits them.',\n );\n }\n\n const unpickable = sourced.map(field => `${field.name} (${field.type})`);\n throw new Error(\n `EntityForm was given a link source for a member that cannot hold one: ${unpickable.join(', ')}. ` +\n 'A picker writes the target’s id into the draft, so the member has to be a ' +\n '`link` or the `string` that carries a foreign key. Remove the entry, or ' +\n 'point it at the member that actually holds the reference.',\n );\n}\n\ninterface FieldRowProps<TEntity extends Entity> {\n field: EntityFormField<TEntity>;\n entity: TEntity | undefined;\n draft: EntityDraft;\n value: string;\n editing: boolean;\n error: string | undefined;\n setField: (name: string, value: EntityDraftValue) => void;\n // The target of a relation is a different entity than the form's own.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n linkSource: EntityLinkSource<any> | undefined;\n onLinkChange:\n ((name: string, entity: Entity | undefined) => void) | undefined;\n id: string;\n}\n\n/**\n * One label + control (or value) row. A `readonly` member renders its read\n * display even in edit mode, and so does a relation with no source; everything\n * else edits through `FieldControl` — or, when a source was supplied for a\n * member in {@link PICKABLE_TYPES}, through `EntityLinkInput`. A boolean edited\n * inline carries its own label, so the row does not add a second one.\n *\n * The source branch sits above the `virtual || isRelation` fallback and below\n * `field.render`, which is what lets a slot still override a picked field.\n */\nfunction FieldRow<TEntity extends Entity>({\n field,\n entity,\n draft,\n value,\n editing,\n error,\n setField,\n linkSource,\n onLinkChange,\n id,\n}: FieldRowProps<TEntity>) {\n const readNode: ReactNode = field.readRender ? (\n field.readRender(entity)\n ) : (\n <CellValue\n value={\n entity ? (entity as Record<string, unknown>)[field.name] : undefined\n }\n descriptor={field}\n record={entity as Record<string, unknown> | undefined}\n />\n );\n\n // Members with no editor of their own here: a to-one `link` whose picker was\n // never wired, a `linkCollection` (#26) and a `composition`. All three\n // fall back to the read display, which is honest — a disabled text box\n // holding `[object Object]` is not.\n const isRelation =\n field.type === 'link' ||\n field.type === 'linkCollection' ||\n field.type === 'composition';\n\n let control: ReactNode;\n if (!editing) {\n control = readNode;\n } else if (field.render) {\n control = field.render({ draft, value, setField, id });\n } else if (linkSource && PICKABLE_TYPES.has(field.type)) {\n control = (\n <EntityLinkInput\n descriptor={field}\n value={value}\n source={linkSource}\n id={id}\n disabled={field.readonly}\n onSelect={target => {\n setField(field.name, target.id == null ? '' : String(target.id));\n onLinkChange?.(field.name, target);\n }}\n onClear={() => {\n setField(field.name, '');\n onLinkChange?.(field.name, undefined);\n }}\n />\n );\n } else if (field.type === 'localized' && !field.virtual) {\n control = (\n <LocalizedFieldControl\n descriptor={field}\n value={readLocalizedDraft(draft, field.name)}\n onChange={next => setField(field.name, next)}\n id={id}\n />\n );\n } else if (field.virtual || isRelation) {\n // A relation with no source has no input to offer, and a virtual field has\n // no member to edit unless a `render` was supplied — both fall back to the\n // read display.\n control = readNode;\n } else {\n control = (\n <FieldControl\n descriptor={field}\n value={value}\n onChange={next => setField(field.name, next)}\n id={id}\n record={draft}\n />\n );\n }\n\n const ownsLabel =\n editing && field.type === 'boolean' && !field.render && !field.virtual;\n\n return (\n <Stack gap=\"3xs\">\n {!ownsLabel && (\n <label htmlFor={id} className=\"text-step-sm text-content-muted\">\n {field.label}\n </label>\n )}\n {control}\n {error && (\n <span role=\"alert\" className=\"text-step-sm text-danger\">\n {error}\n </span>\n )}\n </Stack>\n );\n}\n"],"names":["cloneEntityDraft","describeEntityColumns","readDraftString","readLocalizedDraft","readRowDrafts","useId","useState","useErrorMessage","useLocalizedDescriptors","useT","useTranslateKey","OVERFLOW_GLYPH","renderAnchor","useCasesForSurface","Button","button","CellValue","FieldControl","LocalizedFieldControl","Skeleton","Text","Card","ConfirmDialog","EntityLinkInput","LoadingBoundary","Menu","Stack","cn","EntityDetailGrid","readEntityFormFields","resolveEntityFormFields","PICKABLE_TYPES","Set","VISIBLE_ACTION_LIMIT","EntityForm","entityConstructor","entity","values","onFieldChange","linkSources","onLinkChange","mode","modeProp","defaultMode","onModeChange","errors","formError","onSubmit","onDelete","embedded","isLoading","skeleton","isSaving","isPending","isDeleting","error","backHref","renderLink","title","metadata","isMetadataLoading","onUseCase","onClone","children","formId","slots","columnDescriptors","described","resolved","fields","assertLinkSourcesAreEditable","filter","field","isOwnedCollection","ownedCollections","customSkeleton","undefined","showSkeleton","internalMode","setInternalMode","editing","draft","setField","name","value","toggleMode","next","t","translateKey","errorMessage","busy","resolvingLinks","Object","some","source","selected","may","action","actions","includes","declaredHeaderUseCases","useCases","declaredFooterUseCases","headerUseCases","footerUseCases","pending","setPending","invoke","descriptor","confirm","key","labelKey","useCaseButton","type","variant","tone","size","placement","disabled","onClick","useCaseMenuItem","Item","withOverflow","descriptors","length","map","visible","slice","folded","Trigger","aria-label","Items","gap","direction","align","as","step","weight","lines","headerActions","span","role","aria-live","className","p","data-testid","shape","FieldRow","linkSource","id","rows","onRowsChange","footerActions","href","intent","kind","rest","open","message","messageKey","onCancel","onConfirm","childType","sourced","has","toMany","Error","join","owned","unpickable","readNode","readRender","record","isRelation","control","render","readonly","onSelect","target","String","onClear","virtual","onChange","ownsLabel","label","htmlFor"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"entity-table.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/entity-table/entity-table.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,MAAM,EAWZ,MAAM,eAAe,CAAC;AAkCvB,OAAO,KAAK,EAGV,gBAAgB,EACjB,MAAM,yBAAyB,CAAC;AAyDjC,wBAAgB,WAAW,CAAC,OAAO,SAAS,MAAM,EAAE,EAClD,iBAAiB,EACjB,SAAS,EACT,QAAe,EACf,KAAK,EACL,KAAK,EACL,UAAU,EACV,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,gBAAgB,EAChB,OAAO,EACP,OAAO,EACP,UAAyB,EACzB,QAAQ,EACR,cAAc,EACd,YAAmB,EACnB,eAAsB,EACtB,SAAS,EACT,OAAO,EACP,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,iBAAyB,EACzB,aAAa,EACb,SAAS,EACT,YAAY,EACZ,aAAa,EACb,WAAW,EACX,aAAqB,EACrB,QAAQ,GACT,EAAE,gBAAgB,CAAC,OAAO,CAAC,+BAysB3B"}
1
+ {"version":3,"file":"entity-table.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/entity-table/entity-table.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,MAAM,EAWZ,MAAM,eAAe,CAAC;AAkCvB,OAAO,KAAK,EAGV,gBAAgB,EACjB,MAAM,yBAAyB,CAAC;AAyDjC,wBAAgB,WAAW,CAAC,OAAO,SAAS,MAAM,EAAE,EAClD,iBAAiB,EACjB,SAAS,EACT,QAAe,EACf,KAAK,EACL,KAAK,EACL,UAAU,EACV,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,gBAAgB,EAChB,OAAO,EACP,OAAO,EACP,UAAyB,EACzB,QAAQ,EACR,cAAc,EACd,YAAmB,EACnB,eAAsB,EACtB,SAAS,EACT,OAAO,EACP,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,iBAAyB,EACzB,aAAa,EACb,SAAS,EACT,YAAY,EACZ,aAAa,EACb,WAAW,EACX,aAAqB,EACrB,QAAQ,GACT,EAAE,gBAAgB,CAAC,OAAO,CAAC,+BA0sB3B"}
@@ -136,7 +136,8 @@ export function EntityTable({ entityConstructor, isLoading, skeleton = true, err
136
136
  */ const busyClass = isRefetching ? 'opacity-60 transition-opacity duration-200 ease-smooth' : '';
137
137
  const renderCell = (column, item)=>column.render ? column.render(item) : /*#__PURE__*/ _jsx(CellValue, {
138
138
  value: item[column.name],
139
- descriptor: column
139
+ descriptor: column,
140
+ record: item
140
141
  });
141
142
  /**
142
143
  * The row a shift-click ranges *from* — the last box the user actually