@entifix/react-controls 0.19.0 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/README.md +8 -0
  2. package/dist/i18n/catalog/en.d.ts.map +1 -1
  3. package/dist/i18n/catalog/en.js +55 -9
  4. package/dist/i18n/catalog/en.js.map +1 -1
  5. package/dist/i18n/catalog/es.d.ts +64 -5
  6. package/dist/i18n/catalog/es.d.ts.map +1 -1
  7. package/dist/i18n/catalog/es.js +61 -9
  8. package/dist/i18n/catalog/es.js.map +1 -1
  9. package/dist/i18n/catalog/index.d.ts +102 -10
  10. package/dist/i18n/catalog/index.d.ts.map +1 -1
  11. package/dist/i18n/entity-labels.d.ts +14 -0
  12. package/dist/i18n/entity-labels.d.ts.map +1 -1
  13. package/dist/i18n/entity-labels.js +15 -0
  14. package/dist/i18n/entity-labels.js.map +1 -1
  15. package/dist/i18n/i18n-context.js +2 -2
  16. package/dist/i18n/i18n-context.js.map +1 -1
  17. package/dist/index.d.ts +6 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +6 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/primitives.d.ts +0 -1
  22. package/dist/primitives.d.ts.map +1 -1
  23. package/dist/primitives.js +0 -1
  24. package/dist/primitives.js.map +1 -1
  25. package/dist/theme/index.d.ts +1 -0
  26. package/dist/theme/index.d.ts.map +1 -1
  27. package/dist/theme/index.js +1 -0
  28. package/dist/theme/index.js.map +1 -1
  29. package/dist/theme/theme-context.d.ts +15 -3
  30. package/dist/theme/theme-context.d.ts.map +1 -1
  31. package/dist/theme/theme-context.js +49 -23
  32. package/dist/theme/theme-context.js.map +1 -1
  33. package/dist/theme/theme-cookie.d.ts +9 -0
  34. package/dist/theme/theme-cookie.d.ts.map +1 -0
  35. package/dist/theme/theme-cookie.js +8 -0
  36. package/dist/theme/theme-cookie.js.map +1 -0
  37. package/dist/theme/theme-script.d.ts +8 -6
  38. package/dist/theme/theme-script.d.ts.map +1 -1
  39. package/dist/theme/theme-script.js +21 -12
  40. package/dist/theme/theme-script.js.map +1 -1
  41. package/dist/ui/actions/action-surfaces.d.ts +1 -1
  42. package/dist/ui/actions/action-surfaces.js +1 -1
  43. package/dist/ui/actions/action-surfaces.js.map +1 -1
  44. package/dist/ui/actions/error-actions.d.ts +32 -0
  45. package/dist/ui/actions/error-actions.d.ts.map +1 -0
  46. package/dist/ui/actions/error-actions.js +40 -0
  47. package/dist/ui/actions/error-actions.js.map +1 -0
  48. package/dist/ui/actions/index.d.ts +3 -0
  49. package/dist/ui/actions/index.d.ts.map +1 -1
  50. package/dist/ui/actions/index.js +3 -0
  51. package/dist/ui/actions/index.js.map +1 -1
  52. package/dist/ui/actions/shortcut.d.ts +13 -0
  53. package/dist/ui/actions/shortcut.d.ts.map +1 -0
  54. package/dist/ui/actions/shortcut.js +18 -0
  55. package/dist/ui/actions/shortcut.js.map +1 -0
  56. package/dist/ui/actions/use-verb-invoker.d.ts +55 -0
  57. package/dist/ui/actions/use-verb-invoker.d.ts.map +1 -0
  58. package/dist/ui/actions/use-verb-invoker.js +98 -0
  59. package/dist/ui/actions/use-verb-invoker.js.map +1 -0
  60. package/dist/ui/atoms/cell-value/cell-value.d.ts.map +1 -1
  61. package/dist/ui/atoms/cell-value/cell-value.js +28 -14
  62. package/dist/ui/atoms/cell-value/cell-value.js.map +1 -1
  63. package/dist/ui/atoms/field-control/field-control.d.ts +1 -0
  64. package/dist/ui/atoms/field-control/field-control.d.ts.map +1 -1
  65. package/dist/ui/atoms/field-control/field-control.js +2 -0
  66. package/dist/ui/atoms/field-control/field-control.js.map +1 -1
  67. package/dist/ui/atoms/number-input/quantity-input.d.ts +8 -1
  68. package/dist/ui/atoms/number-input/quantity-input.d.ts.map +1 -1
  69. package/dist/ui/atoms/number-input/quantity-input.js +2 -1
  70. package/dist/ui/atoms/number-input/quantity-input.js.map +1 -1
  71. package/dist/ui/atoms/table/table.d.ts +12 -3
  72. package/dist/ui/atoms/table/table.d.ts.map +1 -1
  73. package/dist/ui/atoms/table/table.js +59 -10
  74. package/dist/ui/atoms/table/table.js.map +1 -1
  75. package/dist/ui/atoms/text/text.d.ts +2 -2
  76. package/dist/ui/atoms/text/text.d.ts.map +1 -1
  77. package/dist/ui/atoms/text/text.js.map +1 -1
  78. package/dist/ui/molecules/action-bar/action-bar.d.ts +76 -0
  79. package/dist/ui/molecules/action-bar/action-bar.d.ts.map +1 -0
  80. package/dist/ui/molecules/action-bar/action-bar.js +218 -0
  81. package/dist/ui/molecules/action-bar/action-bar.js.map +1 -0
  82. package/dist/ui/molecules/action-bar/index.d.ts +2 -0
  83. package/dist/ui/molecules/action-bar/index.d.ts.map +1 -0
  84. package/dist/ui/molecules/action-bar/index.js +3 -0
  85. package/dist/ui/molecules/action-bar/index.js.map +1 -0
  86. package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.d.ts.map +1 -1
  87. package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.js +8 -10
  88. package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.js.map +1 -1
  89. package/dist/ui/molecules/bulk-result/bulk-result.d.ts +3 -3
  90. package/dist/ui/molecules/bulk-result/bulk-result.js +4 -4
  91. package/dist/ui/molecules/bulk-result/bulk-result.js.map +1 -1
  92. package/dist/ui/molecules/confirm-dialog/confirm-dialog.d.ts +24 -9
  93. package/dist/ui/molecules/confirm-dialog/confirm-dialog.d.ts.map +1 -1
  94. package/dist/ui/molecules/confirm-dialog/confirm-dialog.js +36 -11
  95. package/dist/ui/molecules/confirm-dialog/confirm-dialog.js.map +1 -1
  96. package/dist/ui/molecules/entity-record-card/entity-record-card.d.ts +33 -20
  97. package/dist/ui/molecules/entity-record-card/entity-record-card.d.ts.map +1 -1
  98. package/dist/ui/molecules/entity-record-card/entity-record-card.js +53 -20
  99. package/dist/ui/molecules/entity-record-card/entity-record-card.js.map +1 -1
  100. package/dist/ui/molecules/filter-builder/filter-builder.d.ts +8 -2
  101. package/dist/ui/molecules/filter-builder/filter-builder.d.ts.map +1 -1
  102. package/dist/ui/molecules/filter-builder/filter-builder.js +14 -5
  103. package/dist/ui/molecules/filter-builder/filter-builder.js.map +1 -1
  104. package/dist/ui/molecules/filter-chips/filter-chips.d.ts +16 -0
  105. package/dist/ui/molecules/filter-chips/filter-chips.d.ts.map +1 -0
  106. package/dist/ui/molecules/filter-chips/filter-chips.js +73 -0
  107. package/dist/ui/molecules/filter-chips/filter-chips.js.map +1 -0
  108. package/dist/ui/molecules/filter-chips/index.d.ts +2 -0
  109. package/dist/ui/molecules/filter-chips/index.d.ts.map +1 -0
  110. package/dist/ui/molecules/filter-chips/index.js +3 -0
  111. package/dist/ui/molecules/filter-chips/index.js.map +1 -0
  112. package/dist/ui/molecules/menu/menu.d.ts +11 -1
  113. package/dist/ui/molecules/menu/menu.d.ts.map +1 -1
  114. package/dist/ui/molecules/menu/menu.js +62 -2
  115. package/dist/ui/molecules/menu/menu.js.map +1 -1
  116. package/dist/ui/molecules/pagination/pagination.d.ts +5 -0
  117. package/dist/ui/molecules/pagination/pagination.d.ts.map +1 -1
  118. package/dist/ui/molecules/pagination/pagination.js +35 -18
  119. package/dist/ui/molecules/pagination/pagination.js.map +1 -1
  120. package/dist/ui/molecules/region-state/index.d.ts +2 -0
  121. package/dist/ui/molecules/region-state/index.d.ts.map +1 -0
  122. package/dist/ui/molecules/region-state/index.js +3 -0
  123. package/dist/ui/molecules/region-state/index.js.map +1 -0
  124. package/dist/ui/molecules/region-state/region-state.d.ts +42 -0
  125. package/dist/ui/molecules/region-state/region-state.d.ts.map +1 -0
  126. package/dist/ui/molecules/region-state/region-state.js +83 -0
  127. package/dist/ui/molecules/region-state/region-state.js.map +1 -0
  128. package/dist/ui/molecules/table-toolbar/table-toolbar.d.ts +3 -1
  129. package/dist/ui/molecules/table-toolbar/table-toolbar.d.ts.map +1 -1
  130. package/dist/ui/molecules/table-toolbar/table-toolbar.js +3 -2
  131. package/dist/ui/molecules/table-toolbar/table-toolbar.js.map +1 -1
  132. package/dist/ui/molecules/toast-region/toast-region.d.ts +63 -19
  133. package/dist/ui/molecules/toast-region/toast-region.d.ts.map +1 -1
  134. package/dist/ui/molecules/toast-region/toast-region.js +129 -42
  135. package/dist/ui/molecules/toast-region/toast-region.js.map +1 -1
  136. package/dist/ui/organisms/command-palette/command-palette.d.ts.map +1 -1
  137. package/dist/ui/organisms/command-palette/command-palette.js +19 -12
  138. package/dist/ui/organisms/command-palette/command-palette.js.map +1 -1
  139. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.d.ts.map +1 -1
  140. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js +0 -1
  141. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js.map +1 -1
  142. package/dist/ui/organisms/entity-form/entity-form.d.ts +1 -1
  143. package/dist/ui/organisms/entity-form/entity-form.d.ts.map +1 -1
  144. package/dist/ui/organisms/entity-form/entity-form.js +322 -214
  145. package/dist/ui/organisms/entity-form/entity-form.js.map +1 -1
  146. package/dist/ui/organisms/entity-form/entity-form.types.d.ts +16 -0
  147. package/dist/ui/organisms/entity-form/entity-form.types.d.ts.map +1 -1
  148. package/dist/ui/organisms/entity-form/entity-form.types.js.map +1 -1
  149. package/dist/ui/organisms/entity-table/entity-table.d.ts +1 -1
  150. package/dist/ui/organisms/entity-table/entity-table.d.ts.map +1 -1
  151. package/dist/ui/organisms/entity-table/entity-table.js +332 -197
  152. package/dist/ui/organisms/entity-table/entity-table.js.map +1 -1
  153. package/dist/ui/organisms/entity-table/entity-table.types.d.ts +19 -4
  154. package/dist/ui/organisms/entity-table/entity-table.types.d.ts.map +1 -1
  155. package/dist/ui/organisms/entity-table/entity-table.types.js.map +1 -1
  156. package/dist/ui/organisms/entity-table/header-sort.d.ts +18 -0
  157. package/dist/ui/organisms/entity-table/header-sort.d.ts.map +1 -0
  158. package/dist/ui/organisms/entity-table/header-sort.js +54 -0
  159. package/dist/ui/organisms/entity-table/header-sort.js.map +1 -0
  160. package/dist/ui/organisms/entity-table/pivot.d.ts +25 -0
  161. package/dist/ui/organisms/entity-table/pivot.d.ts.map +1 -0
  162. package/dist/ui/organisms/entity-table/pivot.js +106 -0
  163. package/dist/ui/organisms/entity-table/pivot.js.map +1 -0
  164. package/dist/ui/organisms/entity-table/use-result-announcement.d.ts +13 -0
  165. package/dist/ui/organisms/entity-table/use-result-announcement.d.ts.map +1 -0
  166. package/dist/ui/organisms/entity-table/use-result-announcement.js +31 -0
  167. package/dist/ui/organisms/entity-table/use-result-announcement.js.map +1 -0
  168. package/dist/ui/organisms/top-bar/live-indicator.d.ts +11 -0
  169. package/dist/ui/organisms/top-bar/live-indicator.d.ts.map +1 -0
  170. package/dist/ui/organisms/top-bar/live-indicator.js +46 -0
  171. package/dist/ui/organisms/top-bar/live-indicator.js.map +1 -0
  172. package/dist/ui/organisms/top-bar/top-bar.d.ts +11 -2
  173. package/dist/ui/organisms/top-bar/top-bar.d.ts.map +1 -1
  174. package/dist/ui/organisms/top-bar/top-bar.js +14 -5
  175. package/dist/ui/organisms/top-bar/top-bar.js.map +1 -1
  176. package/dist/ui/organisms/wizard/wizard.d.ts +1 -1
  177. package/dist/ui/organisms/wizard/wizard.d.ts.map +1 -1
  178. package/dist/ui/organisms/wizard/wizard.js +7 -1
  179. package/dist/ui/organisms/wizard/wizard.js.map +1 -1
  180. package/dist/ui/organisms/wizard/wizard.types.d.ts +8 -0
  181. package/dist/ui/organisms/wizard/wizard.types.d.ts.map +1 -1
  182. package/dist/ui/organisms/wizard/wizard.types.js.map +1 -1
  183. package/dist/ui/utils/live-updates.d.ts +24 -0
  184. package/dist/ui/utils/live-updates.d.ts.map +1 -0
  185. package/dist/ui/utils/live-updates.js +20 -0
  186. package/dist/ui/utils/live-updates.js.map +1 -0
  187. package/dist/ui/utils/use-focus-on-arrival.d.ts +14 -0
  188. package/dist/ui/utils/use-focus-on-arrival.d.ts.map +1 -0
  189. package/dist/ui/utils/use-focus-on-arrival.js +42 -0
  190. package/dist/ui/utils/use-focus-on-arrival.js.map +1 -0
  191. package/dist/ui/utils/use-hotkey.d.ts +44 -25
  192. package/dist/ui/utils/use-hotkey.d.ts.map +1 -1
  193. package/dist/ui/utils/use-hotkey.js +80 -48
  194. package/dist/ui/utils/use-hotkey.js.map +1 -1
  195. package/package.json +5 -5
  196. package/dist/ui/molecules/split-button/index.d.ts +0 -2
  197. package/dist/ui/molecules/split-button/index.d.ts.map +0 -1
  198. package/dist/ui/molecules/split-button/index.js +0 -3
  199. package/dist/ui/molecules/split-button/index.js.map +0 -1
  200. package/dist/ui/molecules/split-button/split-button.d.ts +0 -21
  201. package/dist/ui/molecules/split-button/split-button.d.ts.map +0 -1
  202. package/dist/ui/molecules/split-button/split-button.js +0 -35
  203. package/dist/ui/molecules/split-button/split-button.js.map +0 -1
@@ -0,0 +1,55 @@
1
+ import type { UseCaseDescriptor } from '@entifix/core';
2
+ import { type ReactNode } from 'react';
3
+ /**
4
+ * A `matching` selection larger than this asks for the count to be typed
5
+ * before a destructive verb runs over it.
6
+ */
7
+ export declare const TYPED_SELECTION_THRESHOLD = 1000;
8
+ /** One verb about to run, and what it runs on. */
9
+ export interface VerbRequest<TTarget> {
10
+ readonly descriptor: Pick<UseCaseDescriptor, 'key' | 'labelKey' | 'confirm' | 'reversible' | 'inverse'>;
11
+ readonly target: TTarget;
12
+ /** What it acts on, as the dialog names it: "Acme", "12 invoices". */
13
+ readonly object: string;
14
+ /** How many records; 1 for a single record. */
15
+ readonly count: number;
16
+ /** A selection of everything a filter matches, rather than ticked rows. */
17
+ readonly matching?: boolean;
18
+ }
19
+ export interface VerbInvokerOptions<TTarget> {
20
+ /** Runs a verb — the descriptor's own, or its inverse on Undo — by key. */
21
+ readonly run: (key: string, target: TTarget) => void;
22
+ readonly busy?: boolean;
23
+ readonly typedThreshold?: number;
24
+ /**
25
+ * The verbs this caller may run — the served document's `useCases`, which
26
+ * leave out what the caller holds no grant for. Undo is offered only when
27
+ * its `inverse` is one of them. Without it, every verb may run.
28
+ */
29
+ readonly served?: readonly Pick<UseCaseDescriptor, 'key'>[];
30
+ }
31
+ export interface VerbInvoker<TTarget> {
32
+ readonly invoke: (request: VerbRequest<TTarget>) => void;
33
+ /** The confirmation, while one is open. Render it once, anywhere. */
34
+ readonly dialog: ReactNode;
35
+ }
36
+ /**
37
+ * Where every surface sends a verb, so none can reach one around its safety
38
+ * ([ADR 0026](../../../../../../docs/adr/0026-actions-hierarchy-overflow-and-destructive-verbs.md)):
39
+ *
40
+ * - no `confirm` — it runs;
41
+ * - `reversible: 'undo'` — it runs, and a toast offers Undo, which runs its
42
+ * `inverse`. Without a toast region to offer Undo in, or an inverse this
43
+ * caller may run, it asks instead;
44
+ * - `confirm` — an alert dialog asks a question naming the object and the
45
+ * count, and the button repeats the verb with the count;
46
+ * - `typed`, or a `matching` selection over {@link TYPED_SELECTION_THRESHOLD}
47
+ * — the same dialog, and the name or the count has to be typed first.
48
+ *
49
+ * Exported for a host's own surface — a screen that is neither a form nor a
50
+ * table, rendering declared verbs on an `ActionBar` — so it reaches a verb
51
+ * through the same funnel EntityForm and EntityTable use rather than
52
+ * re-implementing the dialog's rules.
53
+ */
54
+ export declare function useVerbInvoker<TTarget>({ run, busy, typedThreshold, served, }: VerbInvokerOptions<TTarget>): VerbInvoker<TTarget>;
55
+ //# sourceMappingURL=use-verb-invoker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-verb-invoker.d.ts","sourceRoot":"","sources":["../../../src/ui/actions/use-verb-invoker.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAC;AAMjD;;;GAGG;AACH,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,kDAAkD;AAClD,MAAM,WAAW,WAAW,CAAC,OAAO;IAClC,QAAQ,CAAC,UAAU,EAAE,IAAI,CACvB,iBAAiB,EACjB,KAAK,GAAG,UAAU,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,CAC1D,CAAC;IACF,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB,CAAC,OAAO;IACzC,2EAA2E;IAC3E,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACrD,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,IAAI,CAAC,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC;CAC7D;AAED,MAAM,WAAW,WAAW,CAAC,OAAO;IAClC,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;IACzD,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,EACtC,GAAG,EACH,IAAY,EACZ,cAA0C,EAC1C,MAAM,GACP,EAAE,kBAAkB,CAAC,OAAO,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,CAyEpD"}
@@ -0,0 +1,98 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ import { useT, useTranslateKey } from '../../i18n/index.js';
5
+ import { ConfirmDialog } from '../molecules/confirm-dialog/index.js';
6
+ import { useToasts } from '../molecules/toast-region/index.js';
7
+ /**
8
+ * A `matching` selection larger than this asks for the count to be typed
9
+ * before a destructive verb runs over it.
10
+ */ export const TYPED_SELECTION_THRESHOLD = 1000;
11
+ /**
12
+ * Where every surface sends a verb, so none can reach one around its safety
13
+ * ([ADR 0026](../../../../../../docs/adr/0026-actions-hierarchy-overflow-and-destructive-verbs.md)):
14
+ *
15
+ * - no `confirm` — it runs;
16
+ * - `reversible: 'undo'` — it runs, and a toast offers Undo, which runs its
17
+ * `inverse`. Without a toast region to offer Undo in, or an inverse this
18
+ * caller may run, it asks instead;
19
+ * - `confirm` — an alert dialog asks a question naming the object and the
20
+ * count, and the button repeats the verb with the count;
21
+ * - `typed`, or a `matching` selection over {@link TYPED_SELECTION_THRESHOLD}
22
+ * — the same dialog, and the name or the count has to be typed first.
23
+ *
24
+ * Exported for a host's own surface — a screen that is neither a form nor a
25
+ * table, rendering declared verbs on an `ActionBar` — so it reaches a verb
26
+ * through the same funnel EntityForm and EntityTable use rather than
27
+ * re-implementing the dialog's rules.
28
+ */ export function useVerbInvoker({ run, busy = false, typedThreshold = TYPED_SELECTION_THRESHOLD, served }) {
29
+ var _confirm_confirmLabelKey;
30
+ const t = useT();
31
+ const translateKey = useTranslateKey();
32
+ const toasts = useToasts();
33
+ const [pending, setPending] = useState(undefined);
34
+ const invoke = (request)=>{
35
+ const { descriptor, target, object } = request;
36
+ const { confirm, inverse } = descriptor;
37
+ if (confirm === undefined) {
38
+ run(descriptor.key, target);
39
+ return;
40
+ }
41
+ if (descriptor.reversible === 'undo' && inverse !== undefined && (served === undefined || served.some((useCase)=>useCase.key === inverse)) && toasts !== undefined) {
42
+ run(descriptor.key, target);
43
+ const verb = translateKey(descriptor.labelKey);
44
+ toasts.show({
45
+ tone: 'undo',
46
+ message: confirm.doneKey === undefined ? t('confirm.done', {
47
+ verb,
48
+ object
49
+ }) : translateKey(confirm.doneKey, {
50
+ object,
51
+ count: request.count
52
+ }),
53
+ action: {
54
+ label: t('confirm.undo'),
55
+ onAction: ()=>run(inverse, target)
56
+ }
57
+ });
58
+ return;
59
+ }
60
+ setPending(request);
61
+ };
62
+ if (pending === undefined) return {
63
+ invoke,
64
+ dialog: undefined
65
+ };
66
+ const { descriptor, object, count } = pending;
67
+ const confirm = descriptor.confirm;
68
+ const verb = translateKey((_confirm_confirmLabelKey = confirm.confirmLabelKey) != null ? _confirm_confirmLabelKey : descriptor.labelKey);
69
+ const typed = descriptor.reversible === 'typed' || pending.matching === true && count > typedThreshold;
70
+ return {
71
+ invoke,
72
+ dialog: /*#__PURE__*/ _jsx(ConfirmDialog, {
73
+ open: true,
74
+ tone: confirm.tone,
75
+ title: t('confirm.question', {
76
+ verb: translateKey(descriptor.labelKey),
77
+ object
78
+ }),
79
+ message: translateKey(confirm.messageKey, {
80
+ object,
81
+ count
82
+ }),
83
+ confirmLabel: t('confirm.verbCount', {
84
+ verb,
85
+ count
86
+ }),
87
+ typed: typed ? count > 1 ? String(count) : object : undefined,
88
+ busy: busy,
89
+ onCancel: ()=>setPending(undefined),
90
+ onConfirm: ()=>{
91
+ setPending(undefined);
92
+ run(descriptor.key, pending.target);
93
+ }
94
+ })
95
+ };
96
+ }
97
+
98
+ //# sourceMappingURL=use-verb-invoker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/ui/actions/use-verb-invoker.tsx"],"sourcesContent":["'use client';\n\nimport type { UseCaseDescriptor } from '@entifix/core';\nimport { type ReactNode, useState } from 'react';\n\nimport { useT, useTranslateKey } from '../../i18n/index.js';\nimport { ConfirmDialog } from '../molecules/confirm-dialog/index.js';\nimport { useToasts } from '../molecules/toast-region/index.js';\n\n/**\n * A `matching` selection larger than this asks for the count to be typed\n * before a destructive verb runs over it.\n */\nexport const TYPED_SELECTION_THRESHOLD = 1000;\n\n/** One verb about to run, and what it runs on. */\nexport interface VerbRequest<TTarget> {\n readonly descriptor: Pick<\n UseCaseDescriptor,\n 'key' | 'labelKey' | 'confirm' | 'reversible' | 'inverse'\n >;\n readonly target: TTarget;\n /** What it acts on, as the dialog names it: \"Acme\", \"12 invoices\". */\n readonly object: string;\n /** How many records; 1 for a single record. */\n readonly count: number;\n /** A selection of everything a filter matches, rather than ticked rows. */\n readonly matching?: boolean;\n}\n\nexport interface VerbInvokerOptions<TTarget> {\n /** Runs a verb — the descriptor's own, or its inverse on Undo — by key. */\n readonly run: (key: string, target: TTarget) => void;\n readonly busy?: boolean;\n readonly typedThreshold?: number;\n /**\n * The verbs this caller may run — the served document's `useCases`, which\n * leave out what the caller holds no grant for. Undo is offered only when\n * its `inverse` is one of them. Without it, every verb may run.\n */\n readonly served?: readonly Pick<UseCaseDescriptor, 'key'>[];\n}\n\nexport interface VerbInvoker<TTarget> {\n readonly invoke: (request: VerbRequest<TTarget>) => void;\n /** The confirmation, while one is open. Render it once, anywhere. */\n readonly dialog: ReactNode;\n}\n\n/**\n * Where every surface sends a verb, so none can reach one around its safety\n * ([ADR 0026](../../../../../../docs/adr/0026-actions-hierarchy-overflow-and-destructive-verbs.md)):\n *\n * - no `confirm` — it runs;\n * - `reversible: 'undo'` — it runs, and a toast offers Undo, which runs its\n * `inverse`. Without a toast region to offer Undo in, or an inverse this\n * caller may run, it asks instead;\n * - `confirm` — an alert dialog asks a question naming the object and the\n * count, and the button repeats the verb with the count;\n * - `typed`, or a `matching` selection over {@link TYPED_SELECTION_THRESHOLD}\n * — the same dialog, and the name or the count has to be typed first.\n *\n * Exported for a host's own surface — a screen that is neither a form nor a\n * table, rendering declared verbs on an `ActionBar` — so it reaches a verb\n * through the same funnel EntityForm and EntityTable use rather than\n * re-implementing the dialog's rules.\n */\nexport function useVerbInvoker<TTarget>({\n run,\n busy = false,\n typedThreshold = TYPED_SELECTION_THRESHOLD,\n served,\n}: VerbInvokerOptions<TTarget>): VerbInvoker<TTarget> {\n const t = useT();\n const translateKey = useTranslateKey();\n const toasts = useToasts();\n const [pending, setPending] = useState<VerbRequest<TTarget> | undefined>(\n undefined,\n );\n\n const invoke = (request: VerbRequest<TTarget>) => {\n const { descriptor, target, object } = request;\n const { confirm, inverse } = descriptor;\n if (confirm === undefined) {\n run(descriptor.key, target);\n return;\n }\n if (\n descriptor.reversible === 'undo' &&\n inverse !== undefined &&\n (served === undefined ||\n served.some(useCase => useCase.key === inverse)) &&\n toasts !== undefined\n ) {\n run(descriptor.key, target);\n const verb = translateKey(descriptor.labelKey);\n toasts.show({\n tone: 'undo',\n message:\n confirm.doneKey === undefined\n ? t('confirm.done', { verb, object })\n : translateKey(confirm.doneKey, { object, count: request.count }),\n action: {\n label: t('confirm.undo'),\n onAction: () => run(inverse, target),\n },\n });\n return;\n }\n setPending(request);\n };\n\n if (pending === undefined) return { invoke, dialog: undefined };\n\n const { descriptor, object, count } = pending;\n const confirm = descriptor.confirm as NonNullable<\n UseCaseDescriptor['confirm']\n >;\n const verb = translateKey(confirm.confirmLabelKey ?? descriptor.labelKey);\n const typed =\n descriptor.reversible === 'typed' ||\n (pending.matching === true && count > typedThreshold);\n\n return {\n invoke,\n dialog: (\n <ConfirmDialog\n open\n tone={confirm.tone}\n title={t('confirm.question', {\n verb: translateKey(descriptor.labelKey),\n object,\n })}\n message={translateKey(confirm.messageKey, { object, count })}\n confirmLabel={t('confirm.verbCount', { verb, count })}\n typed={typed ? (count > 1 ? String(count) : object) : undefined}\n busy={busy}\n onCancel={() => setPending(undefined)}\n onConfirm={() => {\n setPending(undefined);\n run(descriptor.key, pending.target);\n }}\n />\n ),\n };\n}\n"],"names":["useState","useT","useTranslateKey","ConfirmDialog","useToasts","TYPED_SELECTION_THRESHOLD","useVerbInvoker","run","busy","typedThreshold","served","confirm","t","translateKey","toasts","pending","setPending","undefined","invoke","request","descriptor","target","object","inverse","key","reversible","some","useCase","verb","labelKey","show","tone","message","doneKey","count","action","label","onAction","dialog","confirmLabelKey","typed","matching","open","title","messageKey","confirmLabel","String","onCancel","onConfirm"],"mappings":"AAAA;;AAGA,SAAyBA,QAAQ,QAAQ,QAAQ;AAEjD,SAASC,IAAI,EAAEC,eAAe,QAAQ,sBAAsB;AAC5D,SAASC,aAAa,QAAQ,uCAAuC;AACrE,SAASC,SAAS,QAAQ,qCAAqC;AAE/D;;;CAGC,GACD,OAAO,MAAMC,4BAA4B,KAAK;AAoC9C;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASC,eAAwB,EACtCC,GAAG,EACHC,OAAO,KAAK,EACZC,iBAAiBJ,yBAAyB,EAC1CK,MAAM,EACsB;QA8CFC;IA7C1B,MAAMC,IAAIX;IACV,MAAMY,eAAeX;IACrB,MAAMY,SAASV;IACf,MAAM,CAACW,SAASC,WAAW,GAAGhB,SAC5BiB;IAGF,MAAMC,SAAS,CAACC;QACd,MAAM,EAAEC,UAAU,EAAEC,MAAM,EAAEC,MAAM,EAAE,GAAGH;QACvC,MAAM,EAAER,OAAO,EAAEY,OAAO,EAAE,GAAGH;QAC7B,IAAIT,YAAYM,WAAW;YACzBV,IAAIa,WAAWI,GAAG,EAAEH;YACpB;QACF;QACA,IACED,WAAWK,UAAU,KAAK,UAC1BF,YAAYN,aACXP,CAAAA,WAAWO,aACVP,OAAOgB,IAAI,CAACC,CAAAA,UAAWA,QAAQH,GAAG,KAAKD,QAAO,KAChDT,WAAWG,WACX;YACAV,IAAIa,WAAWI,GAAG,EAAEH;YACpB,MAAMO,OAAOf,aAAaO,WAAWS,QAAQ;YAC7Cf,OAAOgB,IAAI,CAAC;gBACVC,MAAM;gBACNC,SACErB,QAAQsB,OAAO,KAAKhB,YAChBL,EAAE,gBAAgB;oBAAEgB;oBAAMN;gBAAO,KACjCT,aAAaF,QAAQsB,OAAO,EAAE;oBAAEX;oBAAQY,OAAOf,QAAQe,KAAK;gBAAC;gBACnEC,QAAQ;oBACNC,OAAOxB,EAAE;oBACTyB,UAAU,IAAM9B,IAAIgB,SAASF;gBAC/B;YACF;YACA;QACF;QACAL,WAAWG;IACb;IAEA,IAAIJ,YAAYE,WAAW,OAAO;QAAEC;QAAQoB,QAAQrB;IAAU;IAE9D,MAAM,EAAEG,UAAU,EAAEE,MAAM,EAAEY,KAAK,EAAE,GAAGnB;IACtC,MAAMJ,UAAUS,WAAWT,OAAO;IAGlC,MAAMiB,OAAOf,cAAaF,2BAAAA,QAAQ4B,eAAe,YAAvB5B,2BAA2BS,WAAWS,QAAQ;IACxE,MAAMW,QACJpB,WAAWK,UAAU,KAAK,WACzBV,QAAQ0B,QAAQ,KAAK,QAAQP,QAAQzB;IAExC,OAAO;QACLS;QACAoB,sBACE,KAACnC;YACCuC,IAAI;YACJX,MAAMpB,QAAQoB,IAAI;YAClBY,OAAO/B,EAAE,oBAAoB;gBAC3BgB,MAAMf,aAAaO,WAAWS,QAAQ;gBACtCP;YACF;YACAU,SAASnB,aAAaF,QAAQiC,UAAU,EAAE;gBAAEtB;gBAAQY;YAAM;YAC1DW,cAAcjC,EAAE,qBAAqB;gBAAEgB;gBAAMM;YAAM;YACnDM,OAAOA,QAASN,QAAQ,IAAIY,OAAOZ,SAASZ,SAAUL;YACtDT,MAAMA;YACNuC,UAAU,IAAM/B,WAAWC;YAC3B+B,WAAW;gBACThC,WAAWC;gBACXV,IAAIa,WAAWI,GAAG,EAAET,QAAQM,MAAM;YACpC;;IAGN;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"cell-value.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/cell-value/cell-value.tsx"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,qBAAqB,EAK3B,MAAM,eAAe,CAAC;AAwIvB,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,qBAAqB,CAAC;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC5C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,cAAc,+BAkDtE"}
1
+ {"version":3,"file":"cell-value.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/cell-value/cell-value.tsx"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,qBAAqB,EAK3B,MAAM,eAAe,CAAC;AAgIvB,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,qBAAqB,CAAC;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC5C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,cAAc,+BAyEtE"}
@@ -1,14 +1,15 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { EntityCollectionLink, EntityLink, isLocalizedText, localize } from '@entifix/core';
3
+ import { defaultLocale, EntityCollectionLink, EntityLink, isLocalizedText, localize } from '@entifix/core';
4
4
  import { useEnumLabel, useFormatters, useLocale, useT } from '../../../i18n/index.js';
5
5
  import { Money } from '../money/money.js';
6
6
  import { currencyOf } from '../money/money-math.js';
7
+ import { Skeleton } from '../skeleton/index.js';
7
8
  /** Stand-in for an absent value, so an empty cell still reads as intentional. */ const EMPTY = '—';
8
9
  /**
9
- * A link reads as its target's label when the target is loaded (embedded or
10
- * resolved), and as the bare foreign key when it is not — the same cell markup
11
- * covers both shapes a relation can arrive in.
10
+ * A link reads as its target's label once the target is loaded (embedded or
11
+ * resolved). Never as its id: a key is not a name, and showing one fails the
12
+ * reader who came to see which record it is (#200).
12
13
  */ function entityLabel(target, linkLabelProperty, locale) {
13
14
  const label = target == null ? void 0 : target[linkLabelProperty];
14
15
  if (label == null) return undefined;
@@ -17,17 +18,14 @@ import { currencyOf } from '../money/money-math.js';
17
18
  }
18
19
  function formatLink(link, linkLabelProperty, locale) {
19
20
  var _entityLabel;
20
- return (_entityLabel = entityLabel(link.value, linkLabelProperty, locale)) != null ? _entityLabel : link.id == null ? EMPTY : String(link.id);
21
+ return link.id == null ? EMPTY : (_entityLabel = entityLabel(link.value, linkLabelProperty, locale)) != null ? _entityLabel : EMPTY;
21
22
  }
22
23
  function formatCollectionLink(link, linkLabelProperty, locale) {
23
- const loaded = link.values;
24
- if (loaded !== undefined) {
25
- return loaded.map((target, index)=>{
26
- var _entityLabel, _ref, _target_id;
27
- return (_entityLabel = entityLabel(target, linkLabelProperty, locale)) != null ? _entityLabel : String((_ref = (_target_id = target.id) != null ? _target_id : link.ids[index]) != null ? _ref : '');
28
- }).join(', ');
29
- }
30
- return link.ids.map(String).join(', ');
24
+ var _link_values;
25
+ return ((_link_values = link.values) != null ? _link_values : []).map((target)=>{
26
+ var _entityLabel;
27
+ return (_entityLabel = entityLabel(target, linkLabelProperty, locale)) != null ? _entityLabel : EMPTY;
28
+ }).join(', ');
31
29
  }
32
30
  function formatByType(value, type, linkLabelProperty, formatting) {
33
31
  switch(type){
@@ -76,7 +74,7 @@ function formatByType(value, type, linkLabelProperty, formatting) {
76
74
  * is what lets a table stay generic: a link, a date and a number all arrive as
77
75
  * "the value of an accessor", and only the descriptor decides how they read.
78
76
  */ export function CellValue({ value, descriptor, record }) {
79
- var _NUMERIC_CLASS_descriptor_type;
77
+ var _value_locale, _NUMERIC_CLASS_descriptor_type;
80
78
  const t = useT();
81
79
  const formatters = useFormatters();
82
80
  const enumLabel = useEnumLabel();
@@ -103,6 +101,14 @@ function formatByType(value, type, linkLabelProperty, formatting) {
103
101
  });
104
102
  }
105
103
  }
104
+ // A relation whose target has not arrived yet holds its place with a
105
+ // skeleton — the id is all it has, and the id is not what it means.
106
+ if (value instanceof EntityLink && value.id != null && value.value === undefined || value instanceof EntityCollectionLink && value.ids.length > 0 && value.values === undefined) {
107
+ return /*#__PURE__*/ _jsx(Skeleton, {
108
+ shape: "line",
109
+ className: "w-16"
110
+ });
111
+ }
106
112
  const text = formatByType(value, descriptor.type, descriptor.linkLabelProperty, {
107
113
  formatters,
108
114
  yes: t('value.yes'),
@@ -119,6 +125,14 @@ function formatByType(value, type, linkLabelProperty, formatting) {
119
125
  children: EMPTY
120
126
  });
121
127
  }
128
+ // Text the reader's locale lacks falls back to the default locale's, and is
129
+ // marked as that language, so it is read in its own voice (WCAG 3.1.2).
130
+ if (descriptor.type === 'localized' && isLocalizedText(value) && ((_value_locale = value[locale]) != null ? _value_locale : '') === '') {
131
+ return /*#__PURE__*/ _jsx("span", {
132
+ lang: defaultLocale(),
133
+ children: text
134
+ });
135
+ }
122
136
  return /*#__PURE__*/ _jsx("span", {
123
137
  className: (_NUMERIC_CLASS_descriptor_type = NUMERIC_CLASS[descriptor.type]) != null ? _NUMERIC_CLASS_descriptor_type : undefined,
124
138
  children: text
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/atoms/cell-value/cell-value.tsx"],"sourcesContent":["'use client';\n\nimport type { Formatters, Locale } from '@entifix/core';\nimport {\n type Entity,\n EntityCollectionLink,\n type EntityFieldDescriptor,\n EntityLink,\n isLocalizedText,\n localize,\n type MetaAccessorType,\n} from '@entifix/core';\n\nimport {\n useEnumLabel,\n useFormatters,\n useLocale,\n useT,\n} from '../../../i18n/index.js';\nimport { Money } from '../money/money.js';\nimport { currencyOf } from '../money/money-math.js';\n\n/** Stand-in for an absent value, so an empty cell still reads as intentional. */\nconst EMPTY = '—';\n\n/**\n * A link reads as its target's label when the target is loaded (embedded or\n * resolved), and as the bare foreign key when it is not — the same cell markup\n * covers both shapes a relation can arrive in.\n */\nfunction entityLabel(\n target: Entity | undefined,\n linkLabelProperty: string,\n locale: Locale,\n): string | undefined {\n const label = (target as Record<string, unknown> | undefined)?.[\n linkLabelProperty\n ];\n if (label == null) return undefined;\n // A target labelled by a localized member reads in the reader's locale.\n return isLocalizedText(label) ? localize(label, locale) : String(label);\n}\n\nfunction formatLink(\n link: EntityLink<Entity>,\n linkLabelProperty: string,\n locale: Locale,\n): string {\n return (\n entityLabel(link.value, linkLabelProperty, locale) ??\n (link.id == null ? EMPTY : String(link.id))\n );\n}\n\nfunction formatCollectionLink(\n link: EntityCollectionLink<Entity>,\n linkLabelProperty: string,\n locale: Locale,\n): string {\n const loaded = link.values;\n if (loaded !== undefined) {\n return loaded\n .map(\n (target, index) =>\n entityLabel(target, linkLabelProperty, locale) ??\n String(target.id ?? link.ids[index] ?? ''),\n )\n .join(', ');\n }\n return link.ids.map(String).join(', ');\n}\n\n/**\n * Everything locale-dependent, resolved once by the component and handed down.\n * The formatters carry an explicit locale, which is what keeps a date rendered\n * on the server byte-identical to the same date rendered in the browser.\n */\ninterface CellFormatting {\n readonly formatters: Formatters;\n readonly yes: string;\n readonly no: string;\n readonly enumLabel: (value: string) => string;\n readonly rowCount: (count: number) => string;\n readonly locale: Locale;\n}\n\nfunction formatByType(\n value: unknown,\n type: MetaAccessorType,\n linkLabelProperty: string,\n formatting: CellFormatting,\n): string {\n switch (type) {\n case 'date':\n return formatting.formatters.date(\n value instanceof Date ? value : String(value),\n );\n case 'number':\n return typeof value === 'number'\n ? formatting.formatters.number(value)\n : String(value);\n case 'boolean':\n return value ? formatting.yes : formatting.no;\n case 'enum':\n return formatting.enumLabel(String(value));\n case 'link':\n return value instanceof EntityLink\n ? formatLink(value, linkLabelProperty, formatting.locale)\n : String(value);\n case 'linkCollection':\n return value instanceof EntityCollectionLink\n ? formatCollectionLink(value, linkLabelProperty, formatting.locale)\n : String(value);\n case 'scalarCollection':\n // The same comma list the draft holds, spaced for reading. Not `String`:\n // an array's own `toString` joins on a bare comma, which reads as one\n // run-on token in a narrow column.\n return Array.isArray(value) ? value.join(', ') : String(value);\n case 'composition':\n // Owned rows have no useful one-line form — a receipt's lines are not a\n // sentence — and the master's own screen is where they are read and\n // edited. So the cell reports the count and stops.\n return Array.isArray(value)\n ? formatting.rowCount(value.length)\n : String(value);\n case 'localized':\n // The reader's locale, falling back to the host's default; a value\n // that is not a text per locale has no reading worth guessing at.\n return isLocalizedText(value)\n ? (localize(value, formatting.locale) ?? '')\n : '';\n default:\n return String(value);\n }\n}\n\n/**\n * Tabular figures, applied once, here — not on `--font-sans` and not per call\n * site. A column of numbers or dates must align on the digit; a paragraph must\n * not, because proportional figures are what prose is set in. The descriptor\n * already knows which is which, so the cell is the only place that has to.\n */\nconst NUMERIC_CLASS: Partial<Record<MetaAccessorType, string>> = {\n number: 'tabular-nums',\n date: 'tabular-nums',\n};\n\nexport interface CellValueProps {\n value: unknown;\n descriptor: EntityFieldDescriptor;\n /**\n * The record the value belongs to. A money member whose currency is another\n * member of the record (`money.currencyMember`) reads it from here.\n */\n record?: Readonly<Record<string, unknown>>;\n}\n\n/**\n * Renders one entity member according to its metadata type. Centralizing this\n * is what lets a table stay generic: a link, a date and a number all arrive as\n * \"the value of an accessor\", and only the descriptor decides how they read.\n */\nexport function CellValue({ value, descriptor, record }: CellValueProps) {\n const t = useT();\n const formatters = useFormatters();\n const enumLabel = useEnumLabel();\n const locale = useLocale();\n\n if (value === undefined || value === null || value === '') {\n return <span className=\"text-content-muted\">{EMPTY}</span>;\n }\n\n // An amount reads as money in the reader's locale, never as the stored\n // number. A draft holds it as a string, so a numeric string counts too. A\n // currency the record does not carry leaves it a plain number rather than\n // guessing one.\n if (descriptor.money !== undefined) {\n const amount = typeof value === 'number' ? value : Number(value);\n const currency = currencyOf(descriptor.money, record);\n if (currency !== undefined && Number.isFinite(amount)) {\n return (\n <Money\n amount={amount}\n currency={currency}\n unit={descriptor.money.unit}\n locale={locale}\n />\n );\n }\n }\n\n const text = formatByType(\n value,\n descriptor.type,\n descriptor.linkLabelProperty,\n {\n formatters,\n yes: t('value.yes'),\n no: t('value.no'),\n enumLabel: raw => enumLabel(descriptor, raw),\n rowCount: count => t('value.rowCount', { count }),\n locale,\n },\n );\n\n if (text === '') {\n return <span className=\"text-content-muted\">{EMPTY}</span>;\n }\n\n return (\n <span className={NUMERIC_CLASS[descriptor.type] ?? undefined}>{text}</span>\n );\n}\n"],"names":["EntityCollectionLink","EntityLink","isLocalizedText","localize","useEnumLabel","useFormatters","useLocale","useT","Money","currencyOf","EMPTY","entityLabel","target","linkLabelProperty","locale","label","undefined","String","formatLink","link","value","id","formatCollectionLink","loaded","values","map","index","ids","join","formatByType","type","formatting","formatters","date","Date","number","yes","no","enumLabel","Array","isArray","rowCount","length","NUMERIC_CLASS","CellValue","descriptor","record","t","span","className","money","amount","Number","currency","isFinite","unit","text","raw","count"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../src/ui/atoms/cell-value/cell-value.tsx"],"sourcesContent":["'use client';\n\nimport type { Formatters, Locale } from '@entifix/core';\nimport {\n defaultLocale,\n type Entity,\n EntityCollectionLink,\n type EntityFieldDescriptor,\n EntityLink,\n isLocalizedText,\n localize,\n type MetaAccessorType,\n} from '@entifix/core';\n\nimport {\n useEnumLabel,\n useFormatters,\n useLocale,\n useT,\n} from '../../../i18n/index.js';\nimport { Money } from '../money/money.js';\nimport { currencyOf } from '../money/money-math.js';\nimport { Skeleton } from '../skeleton/index.js';\n\n/** Stand-in for an absent value, so an empty cell still reads as intentional. */\nconst EMPTY = '—';\n\n/**\n * A link reads as its target's label once the target is loaded (embedded or\n * resolved). Never as its id: a key is not a name, and showing one fails the\n * reader who came to see which record it is (#200).\n */\nfunction entityLabel(\n target: Entity | undefined,\n linkLabelProperty: string,\n locale: Locale,\n): string | undefined {\n const label = (target as Record<string, unknown> | undefined)?.[\n linkLabelProperty\n ];\n if (label == null) return undefined;\n // A target labelled by a localized member reads in the reader's locale.\n return isLocalizedText(label) ? localize(label, locale) : String(label);\n}\n\nfunction formatLink(\n link: EntityLink<Entity>,\n linkLabelProperty: string,\n locale: Locale,\n): string {\n return link.id == null\n ? EMPTY\n : (entityLabel(link.value, linkLabelProperty, locale) ?? EMPTY);\n}\n\nfunction formatCollectionLink(\n link: EntityCollectionLink<Entity>,\n linkLabelProperty: string,\n locale: Locale,\n): string {\n return (link.values ?? [])\n .map(target => entityLabel(target, linkLabelProperty, locale) ?? EMPTY)\n .join(', ');\n}\n\n/**\n * Everything locale-dependent, resolved once by the component and handed down.\n * The formatters carry an explicit locale, which is what keeps a date rendered\n * on the server byte-identical to the same date rendered in the browser.\n */\ninterface CellFormatting {\n readonly formatters: Formatters;\n readonly yes: string;\n readonly no: string;\n readonly enumLabel: (value: string) => string;\n readonly rowCount: (count: number) => string;\n readonly locale: Locale;\n}\n\nfunction formatByType(\n value: unknown,\n type: MetaAccessorType,\n linkLabelProperty: string,\n formatting: CellFormatting,\n): string {\n switch (type) {\n case 'date':\n return formatting.formatters.date(\n value instanceof Date ? value : String(value),\n );\n case 'number':\n return typeof value === 'number'\n ? formatting.formatters.number(value)\n : String(value);\n case 'boolean':\n return value ? formatting.yes : formatting.no;\n case 'enum':\n return formatting.enumLabel(String(value));\n case 'link':\n return value instanceof EntityLink\n ? formatLink(value, linkLabelProperty, formatting.locale)\n : String(value);\n case 'linkCollection':\n return value instanceof EntityCollectionLink\n ? formatCollectionLink(value, linkLabelProperty, formatting.locale)\n : String(value);\n case 'scalarCollection':\n // The same comma list the draft holds, spaced for reading. Not `String`:\n // an array's own `toString` joins on a bare comma, which reads as one\n // run-on token in a narrow column.\n return Array.isArray(value) ? value.join(', ') : String(value);\n case 'composition':\n // Owned rows have no useful one-line form — a receipt's lines are not a\n // sentence — and the master's own screen is where they are read and\n // edited. So the cell reports the count and stops.\n return Array.isArray(value)\n ? formatting.rowCount(value.length)\n : String(value);\n case 'localized':\n // The reader's locale, falling back to the host's default; a value\n // that is not a text per locale has no reading worth guessing at.\n return isLocalizedText(value)\n ? (localize(value, formatting.locale) ?? '')\n : '';\n default:\n return String(value);\n }\n}\n\n/**\n * Tabular figures, applied once, here — not on `--font-sans` and not per call\n * site. A column of numbers or dates must align on the digit; a paragraph must\n * not, because proportional figures are what prose is set in. The descriptor\n * already knows which is which, so the cell is the only place that has to.\n */\nconst NUMERIC_CLASS: Partial<Record<MetaAccessorType, string>> = {\n number: 'tabular-nums',\n date: 'tabular-nums',\n};\n\nexport interface CellValueProps {\n value: unknown;\n descriptor: EntityFieldDescriptor;\n /**\n * The record the value belongs to. A money member whose currency is another\n * member of the record (`money.currencyMember`) reads it from here.\n */\n record?: Readonly<Record<string, unknown>>;\n}\n\n/**\n * Renders one entity member according to its metadata type. Centralizing this\n * is what lets a table stay generic: a link, a date and a number all arrive as\n * \"the value of an accessor\", and only the descriptor decides how they read.\n */\nexport function CellValue({ value, descriptor, record }: CellValueProps) {\n const t = useT();\n const formatters = useFormatters();\n const enumLabel = useEnumLabel();\n const locale = useLocale();\n\n if (value === undefined || value === null || value === '') {\n return <span className=\"text-content-muted\">{EMPTY}</span>;\n }\n\n // An amount reads as money in the reader's locale, never as the stored\n // number. A draft holds it as a string, so a numeric string counts too. A\n // currency the record does not carry leaves it a plain number rather than\n // guessing one.\n if (descriptor.money !== undefined) {\n const amount = typeof value === 'number' ? value : Number(value);\n const currency = currencyOf(descriptor.money, record);\n if (currency !== undefined && Number.isFinite(amount)) {\n return (\n <Money\n amount={amount}\n currency={currency}\n unit={descriptor.money.unit}\n locale={locale}\n />\n );\n }\n }\n\n // A relation whose target has not arrived yet holds its place with a\n // skeleton — the id is all it has, and the id is not what it means.\n if (\n (value instanceof EntityLink &&\n value.id != null &&\n value.value === undefined) ||\n (value instanceof EntityCollectionLink &&\n value.ids.length > 0 &&\n value.values === undefined)\n ) {\n return <Skeleton shape=\"line\" className=\"w-16\" />;\n }\n\n const text = formatByType(\n value,\n descriptor.type,\n descriptor.linkLabelProperty,\n {\n formatters,\n yes: t('value.yes'),\n no: t('value.no'),\n enumLabel: raw => enumLabel(descriptor, raw),\n rowCount: count => t('value.rowCount', { count }),\n locale,\n },\n );\n\n if (text === '') {\n return <span className=\"text-content-muted\">{EMPTY}</span>;\n }\n\n // Text the reader's locale lacks falls back to the default locale's, and is\n // marked as that language, so it is read in its own voice (WCAG 3.1.2).\n if (\n descriptor.type === 'localized' &&\n isLocalizedText(value) &&\n (value[locale] ?? '') === ''\n ) {\n return <span lang={defaultLocale()}>{text}</span>;\n }\n\n return (\n <span className={NUMERIC_CLASS[descriptor.type] ?? undefined}>{text}</span>\n );\n}\n"],"names":["defaultLocale","EntityCollectionLink","EntityLink","isLocalizedText","localize","useEnumLabel","useFormatters","useLocale","useT","Money","currencyOf","Skeleton","EMPTY","entityLabel","target","linkLabelProperty","locale","label","undefined","String","formatLink","link","id","value","formatCollectionLink","values","map","join","formatByType","type","formatting","formatters","date","Date","number","yes","no","enumLabel","Array","isArray","rowCount","length","NUMERIC_CLASS","CellValue","descriptor","record","t","span","className","money","amount","Number","currency","isFinite","unit","ids","shape","text","raw","count","lang"],"mappings":"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"}
@@ -15,6 +15,7 @@ export interface FieldControlProps {
15
15
  'aria-labelledby'?: string;
16
16
  'aria-describedby'?: string;
17
17
  'aria-invalid'?: boolean;
18
+ 'aria-required'?: boolean;
18
19
  /**
19
20
  * The draft value as a string — the shape a native input round-trips. A form
20
21
  * holds its whole draft as `Record<string, string>` and reconstructs typed
@@ -1 +1 @@
1
- {"version":3,"file":"field-control.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/field-control/field-control.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAoB,MAAM,eAAe,CAAC;AAO7E,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,qBAAqB,CAAC;IAClC;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC5C;AAQD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,EAC3B,UAAU,EACV,KAAK,EACL,QAAQ,EACR,EAAE,EACF,MAAM,EACN,GAAG,IAAI,EACR,EAAE,iBAAiB,+BAyEnB"}
1
+ {"version":3,"file":"field-control.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/field-control/field-control.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAoB,MAAM,eAAe,CAAC;AAO7E,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,qBAAqB,CAAC;IAClC;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC5C;AAQD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,EAC3B,UAAU,EACV,KAAK,EACL,QAAQ,EACR,EAAE,EACF,MAAM,EACN,GAAG,IAAI,EACR,EAAE,iBAAiB,+BA2EnB"}
@@ -92,6 +92,8 @@ import { currencyOf } from '../money/money-math.js';
92
92
  return /*#__PURE__*/ _jsx(TextInput, _extends({
93
93
  id: id
94
94
  }, aria, {
95
+ // What the field is for, so a browser can fill it (WCAG 1.3.5).
96
+ autoComplete: descriptor.autocomplete,
95
97
  type: (_INPUT_TYPE_descriptor_type = INPUT_TYPE[descriptor.type]) != null ? _INPUT_TYPE_descriptor_type : 'text',
96
98
  value: value,
97
99
  disabled: disabled,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/atoms/field-control/field-control.tsx"],"sourcesContent":["'use client';\n\nimport type { EntityFieldDescriptor, MetaAccessorType } from '@entifix/core';\n\nimport { useEnumLabel, useLocale } from '../../../i18n/index.js';\nimport { Checkbox, Select, TextInput } from '../field/index.js';\nimport { MoneyInput } from '../money/money-input.js';\nimport { currencyOf } from '../money/money-math.js';\n\nexport interface FieldControlProps {\n descriptor: EntityFieldDescriptor;\n /**\n * ARIA wiring the caller owns, because only the caller knows the shape it is\n * rendered into.\n *\n * A form labels its control with a `<label for>` and needs none of this; a\n * detail grid's cell has no label of its own — the column header is the\n * label, and the cell's message is the description — so it points at both by\n * id. Passing them through here rather than duplicating the type→input map\n * in the grid is what keeps one control deciding what a `date` member looks\n * like.\n */\n 'aria-labelledby'?: string;\n 'aria-describedby'?: string;\n 'aria-invalid'?: boolean;\n /**\n * The draft value as a string — the shape a native input round-trips. A form\n * holds its whole draft as `Record<string, string>` and reconstructs typed\n * values only at submit, so the control never sees a domain value.\n */\n value: string;\n onChange: (value: string) => void;\n id?: string;\n /**\n * The record being edited, as its draft. A money member whose currency is\n * another member (`money.currencyMember`) reads it from here.\n */\n record?: Readonly<Record<string, unknown>>;\n}\n\n/** Native input type for the text-like controls; text is the default. */\nconst INPUT_TYPE: Partial<Record<MetaAccessorType, string>> = {\n number: 'number',\n date: 'date',\n};\n\n/**\n * The edit-mode inverse of {@link CellValue}: it turns one metadata descriptor\n * into the input that edits it. Centralizing the type→control choice is what\n * lets a form stay generic, the same way `CellValue` keeps a table generic.\n *\n * A relation (`link`/`linkCollection`) is rendered read-only here, deliberately:\n * editing one needs a picker that can search and page the target, which is a\n * different control with different dependencies. So this atom stays a pure\n * type→input map and a relation never falls back to a broken text box.\n *\n * For a to-one `link` that is only half the story — `EntityForm` wires the\n * picker in one level up from its `linkSources`, so the member does get an\n * editor, just not from here. A `linkCollection` has no editor anywhere yet\n * (#26), and read-only is all it gets. A `composition` is the same: its rows\n * need a detail grid, and a text box holding `[object Object]` would be\n * strictly worse than an honest disabled field.\n *\n * A `scalarCollection` is the exception among the collections and falls through\n * to the plain text input on purpose: a `string[]` has a lossless comma form,\n * so the draft it round-trips through *is* editable text. Splitting on the\n * comma is `reconstructEntity`'s half of that same contract.\n */\nexport function FieldControl({\n descriptor,\n value,\n onChange,\n id,\n record,\n ...aria\n}: FieldControlProps) {\n const enumLabel = useEnumLabel();\n const locale = useLocale();\n const disabled =\n descriptor.readonly ||\n descriptor.type === 'link' ||\n descriptor.type === 'linkCollection' ||\n descriptor.type === 'composition';\n\n if (descriptor.type === 'boolean') {\n return (\n <Checkbox\n id={id}\n {...aria}\n label={descriptor.label}\n checked={value === 'true'}\n disabled={disabled}\n onChange={event => onChange(String(event.currentTarget.checked))}\n />\n );\n }\n\n if (descriptor.type === 'enum') {\n return (\n <Select\n id={id}\n {...aria}\n value={value}\n disabled={disabled}\n onChange={event => onChange(event.currentTarget.value)}\n >\n <option value=\"\">—</option>\n {(descriptor.enumValues ?? []).map(option => (\n <option key={option} value={option}>\n {enumLabel(descriptor, option)}\n </option>\n ))}\n </Select>\n );\n }\n\n // An amount is edited in major units, in the reader's decimal separator,\n // and written back in the unit it is stored in. Without a currency to know\n // the minor units by, it stays a plain number field.\n const currency =\n descriptor.money === undefined\n ? undefined\n : currencyOf(descriptor.money, record);\n if (descriptor.money !== undefined && currency !== undefined) {\n return (\n <MoneyInput\n id={id}\n {...aria}\n value={value}\n currency={currency}\n unit={descriptor.money.unit}\n locale={locale}\n disabled={disabled}\n onChange={onChange}\n />\n );\n }\n\n return (\n <TextInput\n id={id}\n {...aria}\n type={INPUT_TYPE[descriptor.type] ?? 'text'}\n value={value}\n disabled={disabled}\n onChange={event => onChange(event.currentTarget.value)}\n />\n );\n}\n"],"names":["useEnumLabel","useLocale","Checkbox","Select","TextInput","MoneyInput","currencyOf","INPUT_TYPE","number","date","FieldControl","descriptor","value","onChange","id","record","aria","enumLabel","locale","disabled","readonly","type","label","checked","event","String","currentTarget","option","enumValues","map","currency","money","undefined","unit"],"mappings":"AAAA;;;;AAIA,SAASA,YAAY,EAAEC,SAAS,QAAQ,yBAAyB;AACjE,SAASC,QAAQ,EAAEC,MAAM,EAAEC,SAAS,QAAQ,oBAAoB;AAChE,SAASC,UAAU,QAAQ,0BAA0B;AACrD,SAASC,UAAU,QAAQ,yBAAyB;AAiCpD,uEAAuE,GACvE,MAAMC,aAAwD;IAC5DC,QAAQ;IACRC,MAAM;AACR;AAEA;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASC,aAAa,EAOT;QAPS,EAC3BC,UAAU,EACVC,KAAK,EACLC,QAAQ,EACRC,EAAE,EACFC,MAAM,EAEY,OADfC;;;;;;;QAoEOT;IAlEV,MAAMU,YAAYjB;IAClB,MAAMkB,SAASjB;IACf,MAAMkB,WACJR,WAAWS,QAAQ,IACnBT,WAAWU,IAAI,KAAK,UACpBV,WAAWU,IAAI,KAAK,oBACpBV,WAAWU,IAAI,KAAK;IAEtB,IAAIV,WAAWU,IAAI,KAAK,WAAW;QACjC,qBACE,KAACnB;YACCY,IAAIA;WACAE;YACJM,OAAOX,WAAWW,KAAK;YACvBC,SAASX,UAAU;YACnBO,UAAUA;YACVN,UAAUW,CAAAA,QAASX,SAASY,OAAOD,MAAME,aAAa,CAACH,OAAO;;IAGpE;IAEA,IAAIZ,WAAWU,IAAI,KAAK,QAAQ;YAUxBV;QATN,qBACE,MAACR;YACCW,IAAIA;WACAE;YACJJ,OAAOA;YACPO,UAAUA;YACVN,UAAUW,CAAAA,QAASX,SAASW,MAAME,aAAa,CAACd,KAAK;;8BAErD,KAACe;oBAAOf,OAAM;8BAAG;;gBAChB,EAACD,yBAAAA,WAAWiB,UAAU,YAArBjB,yBAAyB,EAAE,EAAEkB,GAAG,CAACF,CAAAA,uBACjC,KAACA;wBAAoBf,OAAOe;kCACzBV,UAAUN,YAAYgB;uBADZA;;;IAMrB;IAEA,yEAAyE;IACzE,2EAA2E;IAC3E,qDAAqD;IACrD,MAAMG,WACJnB,WAAWoB,KAAK,KAAKC,YACjBA,YACA1B,WAAWK,WAAWoB,KAAK,EAAEhB;IACnC,IAAIJ,WAAWoB,KAAK,KAAKC,aAAaF,aAAaE,WAAW;QAC5D,qBACE,KAAC3B;YACCS,IAAIA;WACAE;YACJJ,OAAOA;YACPkB,UAAUA;YACVG,MAAMtB,WAAWoB,KAAK,CAACE,IAAI;YAC3Bf,QAAQA;YACRC,UAAUA;YACVN,UAAUA;;IAGhB;IAEA,qBACE,KAACT;QACCU,IAAIA;OACAE;QACJK,IAAI,GAAEd,8BAAAA,UAAU,CAACI,WAAWU,IAAI,CAAC,YAA3Bd,8BAA+B;QACrCK,OAAOA;QACPO,UAAUA;QACVN,UAAUW,CAAAA,QAASX,SAASW,MAAME,aAAa,CAACd,KAAK;;AAG3D"}
1
+ {"version":3,"sources":["../../../../src/ui/atoms/field-control/field-control.tsx"],"sourcesContent":["'use client';\n\nimport type { EntityFieldDescriptor, MetaAccessorType } from '@entifix/core';\n\nimport { useEnumLabel, useLocale } from '../../../i18n/index.js';\nimport { Checkbox, Select, TextInput } from '../field/index.js';\nimport { MoneyInput } from '../money/money-input.js';\nimport { currencyOf } from '../money/money-math.js';\n\nexport interface FieldControlProps {\n descriptor: EntityFieldDescriptor;\n /**\n * ARIA wiring the caller owns, because only the caller knows the shape it is\n * rendered into.\n *\n * A form labels its control with a `<label for>` and needs none of this; a\n * detail grid's cell has no label of its own — the column header is the\n * label, and the cell's message is the description — so it points at both by\n * id. Passing them through here rather than duplicating the type→input map\n * in the grid is what keeps one control deciding what a `date` member looks\n * like.\n */\n 'aria-labelledby'?: string;\n 'aria-describedby'?: string;\n 'aria-invalid'?: boolean;\n 'aria-required'?: boolean;\n /**\n * The draft value as a string — the shape a native input round-trips. A form\n * holds its whole draft as `Record<string, string>` and reconstructs typed\n * values only at submit, so the control never sees a domain value.\n */\n value: string;\n onChange: (value: string) => void;\n id?: string;\n /**\n * The record being edited, as its draft. A money member whose currency is\n * another member (`money.currencyMember`) reads it from here.\n */\n record?: Readonly<Record<string, unknown>>;\n}\n\n/** Native input type for the text-like controls; text is the default. */\nconst INPUT_TYPE: Partial<Record<MetaAccessorType, string>> = {\n number: 'number',\n date: 'date',\n};\n\n/**\n * The edit-mode inverse of {@link CellValue}: it turns one metadata descriptor\n * into the input that edits it. Centralizing the type→control choice is what\n * lets a form stay generic, the same way `CellValue` keeps a table generic.\n *\n * A relation (`link`/`linkCollection`) is rendered read-only here, deliberately:\n * editing one needs a picker that can search and page the target, which is a\n * different control with different dependencies. So this atom stays a pure\n * type→input map and a relation never falls back to a broken text box.\n *\n * For a to-one `link` that is only half the story — `EntityForm` wires the\n * picker in one level up from its `linkSources`, so the member does get an\n * editor, just not from here. A `linkCollection` has no editor anywhere yet\n * (#26), and read-only is all it gets. A `composition` is the same: its rows\n * need a detail grid, and a text box holding `[object Object]` would be\n * strictly worse than an honest disabled field.\n *\n * A `scalarCollection` is the exception among the collections and falls through\n * to the plain text input on purpose: a `string[]` has a lossless comma form,\n * so the draft it round-trips through *is* editable text. Splitting on the\n * comma is `reconstructEntity`'s half of that same contract.\n */\nexport function FieldControl({\n descriptor,\n value,\n onChange,\n id,\n record,\n ...aria\n}: FieldControlProps) {\n const enumLabel = useEnumLabel();\n const locale = useLocale();\n const disabled =\n descriptor.readonly ||\n descriptor.type === 'link' ||\n descriptor.type === 'linkCollection' ||\n descriptor.type === 'composition';\n\n if (descriptor.type === 'boolean') {\n return (\n <Checkbox\n id={id}\n {...aria}\n label={descriptor.label}\n checked={value === 'true'}\n disabled={disabled}\n onChange={event => onChange(String(event.currentTarget.checked))}\n />\n );\n }\n\n if (descriptor.type === 'enum') {\n return (\n <Select\n id={id}\n {...aria}\n value={value}\n disabled={disabled}\n onChange={event => onChange(event.currentTarget.value)}\n >\n <option value=\"\">—</option>\n {(descriptor.enumValues ?? []).map(option => (\n <option key={option} value={option}>\n {enumLabel(descriptor, option)}\n </option>\n ))}\n </Select>\n );\n }\n\n // An amount is edited in major units, in the reader's decimal separator,\n // and written back in the unit it is stored in. Without a currency to know\n // the minor units by, it stays a plain number field.\n const currency =\n descriptor.money === undefined\n ? undefined\n : currencyOf(descriptor.money, record);\n if (descriptor.money !== undefined && currency !== undefined) {\n return (\n <MoneyInput\n id={id}\n {...aria}\n value={value}\n currency={currency}\n unit={descriptor.money.unit}\n locale={locale}\n disabled={disabled}\n onChange={onChange}\n />\n );\n }\n\n return (\n <TextInput\n id={id}\n {...aria}\n // What the field is for, so a browser can fill it (WCAG 1.3.5).\n autoComplete={descriptor.autocomplete}\n type={INPUT_TYPE[descriptor.type] ?? 'text'}\n value={value}\n disabled={disabled}\n onChange={event => onChange(event.currentTarget.value)}\n />\n );\n}\n"],"names":["useEnumLabel","useLocale","Checkbox","Select","TextInput","MoneyInput","currencyOf","INPUT_TYPE","number","date","FieldControl","descriptor","value","onChange","id","record","aria","enumLabel","locale","disabled","readonly","type","label","checked","event","String","currentTarget","option","enumValues","map","currency","money","undefined","unit","autoComplete","autocomplete"],"mappings":"AAAA;;;;AAIA,SAASA,YAAY,EAAEC,SAAS,QAAQ,yBAAyB;AACjE,SAASC,QAAQ,EAAEC,MAAM,EAAEC,SAAS,QAAQ,oBAAoB;AAChE,SAASC,UAAU,QAAQ,0BAA0B;AACrD,SAASC,UAAU,QAAQ,yBAAyB;AAkCpD,uEAAuE,GACvE,MAAMC,aAAwD;IAC5DC,QAAQ;IACRC,MAAM;AACR;AAEA;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASC,aAAa,EAOT;QAPS,EAC3BC,UAAU,EACVC,KAAK,EACLC,QAAQ,EACRC,EAAE,EACFC,MAAM,EAEY,OADfC;;;;;;;QAsEOT;IApEV,MAAMU,YAAYjB;IAClB,MAAMkB,SAASjB;IACf,MAAMkB,WACJR,WAAWS,QAAQ,IACnBT,WAAWU,IAAI,KAAK,UACpBV,WAAWU,IAAI,KAAK,oBACpBV,WAAWU,IAAI,KAAK;IAEtB,IAAIV,WAAWU,IAAI,KAAK,WAAW;QACjC,qBACE,KAACnB;YACCY,IAAIA;WACAE;YACJM,OAAOX,WAAWW,KAAK;YACvBC,SAASX,UAAU;YACnBO,UAAUA;YACVN,UAAUW,CAAAA,QAASX,SAASY,OAAOD,MAAME,aAAa,CAACH,OAAO;;IAGpE;IAEA,IAAIZ,WAAWU,IAAI,KAAK,QAAQ;YAUxBV;QATN,qBACE,MAACR;YACCW,IAAIA;WACAE;YACJJ,OAAOA;YACPO,UAAUA;YACVN,UAAUW,CAAAA,QAASX,SAASW,MAAME,aAAa,CAACd,KAAK;;8BAErD,KAACe;oBAAOf,OAAM;8BAAG;;gBAChB,EAACD,yBAAAA,WAAWiB,UAAU,YAArBjB,yBAAyB,EAAE,EAAEkB,GAAG,CAACF,CAAAA,uBACjC,KAACA;wBAAoBf,OAAOe;kCACzBV,UAAUN,YAAYgB;uBADZA;;;IAMrB;IAEA,yEAAyE;IACzE,2EAA2E;IAC3E,qDAAqD;IACrD,MAAMG,WACJnB,WAAWoB,KAAK,KAAKC,YACjBA,YACA1B,WAAWK,WAAWoB,KAAK,EAAEhB;IACnC,IAAIJ,WAAWoB,KAAK,KAAKC,aAAaF,aAAaE,WAAW;QAC5D,qBACE,KAAC3B;YACCS,IAAIA;WACAE;YACJJ,OAAOA;YACPkB,UAAUA;YACVG,MAAMtB,WAAWoB,KAAK,CAACE,IAAI;YAC3Bf,QAAQA;YACRC,UAAUA;YACVN,UAAUA;;IAGhB;IAEA,qBACE,KAACT;QACCU,IAAIA;OACAE;QACJ,gEAAgE;QAChEkB,cAAcvB,WAAWwB,YAAY;QACrCd,IAAI,GAAEd,8BAAAA,UAAU,CAACI,WAAWU,IAAI,CAAC,YAA3Bd,8BAA+B;QACrCK,OAAOA;QACPO,UAAUA;QACVN,UAAUW,CAAAA,QAASX,SAASW,MAAME,aAAa,CAACd,KAAK;;AAG3D"}
@@ -8,6 +8,13 @@ export interface QuantityInputProps {
8
8
  step?: number;
9
9
  disabled?: boolean;
10
10
  id?: string;
11
+ /**
12
+ * Posts the count with the form it sits in, under this name — a server
13
+ * action's form works without JavaScript, where the field is the only part
14
+ * a reader can change, so the field itself carries it. The server clamps
15
+ * what it receives: a typed value is only clamped here once committed.
16
+ */
17
+ name?: string;
11
18
  className?: string;
12
19
  }
13
20
  /**
@@ -18,5 +25,5 @@ export interface QuantityInputProps {
18
25
  * out of the Tab order, as the pattern has them, because the arrow keys
19
26
  * already do what they do.
20
27
  */
21
- export declare function QuantityInput({ label, value, onChange, min, max, step, disabled, id, className, }: QuantityInputProps): import("react").JSX.Element;
28
+ export declare function QuantityInput({ label, value, onChange, min, max, step, disabled, id, name, className, }: QuantityInputProps): import("react").JSX.Element;
22
29
  //# sourceMappingURL=quantity-input.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"quantity-input.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/number-input/quantity-input.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAKD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,GAAO,EACP,GAAG,EACH,IAAQ,EACR,QAAgB,EAChB,EAAE,EACF,SAAS,GACV,EAAE,kBAAkB,+BA6EpB"}
1
+ {"version":3,"file":"quantity-input.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/number-input/quantity-input.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAKD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,GAAO,EACP,GAAG,EACH,IAAQ,EACR,QAAgB,EAChB,EAAE,EACF,IAAI,EACJ,SAAS,GACV,EAAE,kBAAkB,+BA8EpB"}
@@ -11,7 +11,7 @@ const STEP_BUTTON = 'flex min-h-(--target-min) min-w-(--target-min) shrink-0 ite
11
11
  * "Decrease {label}" and "Increase {label}" and sit at `--target-min`; they are
12
12
  * out of the Tab order, as the pattern has them, because the arrow keys
13
13
  * already do what they do.
14
- */ export function QuantityInput({ label, value, onChange, min = 0, max, step = 1, disabled = false, id, className }) {
14
+ */ export function QuantityInput({ label, value, onChange, min = 0, max, step = 1, disabled = false, id, name, className }) {
15
15
  const t = useT();
16
16
  const [text, setText] = useState(String(value));
17
17
  useEffect(()=>{
@@ -64,6 +64,7 @@ const STEP_BUTTON = 'flex min-h-(--target-min) min-w-(--target-min) shrink-0 ite
64
64
  }),
65
65
  /*#__PURE__*/ _jsx("input", {
66
66
  id: id,
67
+ name: name,
67
68
  type: "text",
68
69
  role: "spinbutton",
69
70
  inputMode: "numeric",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/atoms/number-input/quantity-input.tsx"],"sourcesContent":["'use client';\n\nimport { type KeyboardEvent, useEffect, useState } from 'react';\n\nimport { useT } from '../../../i18n/index.js';\nimport { cn } from '../../utils/cn.js';\n\nexport interface QuantityInputProps {\n /** What is being counted; names the spinbutton and both of its buttons. */\n label: string;\n value: number;\n onChange: (value: number) => void;\n min?: number;\n max?: number;\n step?: number;\n disabled?: boolean;\n id?: string;\n className?: string;\n}\n\nconst STEP_BUTTON =\n 'flex min-h-(--target-min) min-w-(--target-min) shrink-0 items-center justify-center rounded-md border border-border-control bg-surface-elevated text-content hover:bg-surface disabled:cursor-not-allowed disabled:opacity-50';\n\n/**\n * A count, on the WAI-ARIA spinbutton pattern: Up and Down step it, Home and\n * End jump to its bounds, and what is typed is committed on Enter or on\n * leaving the field, clamped to the bounds. The − and + beside it are named\n * \"Decrease {label}\" and \"Increase {label}\" and sit at `--target-min`; they are\n * out of the Tab order, as the pattern has them, because the arrow keys\n * already do what they do.\n */\nexport function QuantityInput({\n label,\n value,\n onChange,\n min = 0,\n max,\n step = 1,\n disabled = false,\n id,\n className,\n}: QuantityInputProps) {\n const t = useT();\n const [text, setText] = useState(String(value));\n\n useEffect(() => {\n // eslint-disable-next-line react-hooks/set-state-in-effect -- the value moved from outside (a step, a reset)\n setText(String(value));\n }, [value]);\n\n const clamp = (next: number) =>\n Math.min(max ?? Number.POSITIVE_INFINITY, Math.max(min, next));\n const set = (next: number) => {\n const clamped = clamp(next);\n setText(String(clamped));\n if (clamped !== value) onChange(clamped);\n };\n const commit = () => {\n const typed = Number.parseInt(text, 10);\n set(Number.isNaN(typed) ? value : typed);\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n const next = {\n ArrowUp: value + step,\n ArrowDown: value - step,\n Home: min,\n End: max,\n }[event.key];\n if (event.key === 'Enter') {\n commit();\n return;\n }\n if (next === undefined) return;\n event.preventDefault();\n set(next);\n };\n\n return (\n <span className={cn('inline-flex items-center gap-3xs', className)}>\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={t('quantity.decrease', { label })}\n disabled={disabled || value <= min}\n onClick={() => set(value - step)}\n className={STEP_BUTTON}\n >\n <span aria-hidden=\"true\">−</span>\n </button>\n <input\n id={id}\n type=\"text\"\n role=\"spinbutton\"\n inputMode=\"numeric\"\n aria-label={label}\n aria-valuenow={value}\n aria-valuemin={min}\n aria-valuemax={max}\n disabled={disabled}\n value={text}\n onChange={event => setText(event.currentTarget.value)}\n onBlur={commit}\n onKeyDown={onKeyDown}\n className=\"w-16 min-h-(--target-min) rounded-lg border border-border-control bg-surface-elevated px-2xs py-3xs text-center text-step-sm text-content tabular-nums focus-ring disabled:cursor-not-allowed disabled:opacity-50\"\n />\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={t('quantity.increase', { label })}\n disabled={disabled || (max !== undefined && value >= max)}\n onClick={() => set(value + step)}\n className={STEP_BUTTON}\n >\n <span aria-hidden=\"true\">+</span>\n </button>\n </span>\n );\n}\n"],"names":["useEffect","useState","useT","cn","STEP_BUTTON","QuantityInput","label","value","onChange","min","max","step","disabled","id","className","t","text","setText","String","clamp","next","Math","Number","POSITIVE_INFINITY","set","clamped","commit","typed","parseInt","isNaN","onKeyDown","event","ArrowUp","ArrowDown","Home","End","key","undefined","preventDefault","span","button","type","tabIndex","aria-label","onClick","aria-hidden","input","role","inputMode","aria-valuenow","aria-valuemin","aria-valuemax","currentTarget","onBlur"],"mappings":"AAAA;;AAEA,SAA6BA,SAAS,EAAEC,QAAQ,QAAQ,QAAQ;AAEhE,SAASC,IAAI,QAAQ,yBAAyB;AAC9C,SAASC,EAAE,QAAQ,oBAAoB;AAevC,MAAMC,cACJ;AAEF;;;;;;;CAOC,GACD,OAAO,SAASC,cAAc,EAC5BC,KAAK,EACLC,KAAK,EACLC,QAAQ,EACRC,MAAM,CAAC,EACPC,GAAG,EACHC,OAAO,CAAC,EACRC,WAAW,KAAK,EAChBC,EAAE,EACFC,SAAS,EACU;IACnB,MAAMC,IAAIb;IACV,MAAM,CAACc,MAAMC,QAAQ,GAAGhB,SAASiB,OAAOX;IAExCP,UAAU;QACR,6GAA6G;QAC7GiB,QAAQC,OAAOX;IACjB,GAAG;QAACA;KAAM;IAEV,MAAMY,QAAQ,CAACC,OACbC,KAAKZ,GAAG,CAACC,cAAAA,MAAOY,OAAOC,iBAAiB,EAAEF,KAAKX,GAAG,CAACD,KAAKW;IAC1D,MAAMI,MAAM,CAACJ;QACX,MAAMK,UAAUN,MAAMC;QACtBH,QAAQC,OAAOO;QACf,IAAIA,YAAYlB,OAAOC,SAASiB;IAClC;IACA,MAAMC,SAAS;QACb,MAAMC,QAAQL,OAAOM,QAAQ,CAACZ,MAAM;QACpCQ,IAAIF,OAAOO,KAAK,CAACF,SAASpB,QAAQoB;IACpC;IAEA,MAAMG,YAAY,CAACC;QACjB,MAAMX,OAAO;YACXY,SAASzB,QAAQI;YACjBsB,WAAW1B,QAAQI;YACnBuB,MAAMzB;YACN0B,KAAKzB;QACP,CAAC,CAACqB,MAAMK,GAAG,CAAC;QACZ,IAAIL,MAAMK,GAAG,KAAK,SAAS;YACzBV;YACA;QACF;QACA,IAAIN,SAASiB,WAAW;QACxBN,MAAMO,cAAc;QACpBd,IAAIJ;IACN;IAEA,qBACE,MAACmB;QAAKzB,WAAWX,GAAG,oCAAoCW;;0BACtD,KAAC0B;gBACCC,MAAK;gBACLC,UAAU,CAAC;gBACXC,cAAY5B,EAAE,qBAAqB;oBAAET;gBAAM;gBAC3CM,UAAUA,YAAYL,SAASE;gBAC/BmC,SAAS,IAAMpB,IAAIjB,QAAQI;gBAC3BG,WAAWV;0BAEX,cAAA,KAACmC;oBAAKM,eAAY;8BAAO;;;0BAE3B,KAACC;gBACCjC,IAAIA;gBACJ4B,MAAK;gBACLM,MAAK;gBACLC,WAAU;gBACVL,cAAYrC;gBACZ2C,iBAAe1C;gBACf2C,iBAAezC;gBACf0C,iBAAezC;gBACfE,UAAUA;gBACVL,OAAOS;gBACPR,UAAUuB,CAAAA,QAASd,QAAQc,MAAMqB,aAAa,CAAC7C,KAAK;gBACpD8C,QAAQ3B;gBACRI,WAAWA;gBACXhB,WAAU;;0BAEZ,KAAC0B;gBACCC,MAAK;gBACLC,UAAU,CAAC;gBACXC,cAAY5B,EAAE,qBAAqB;oBAAET;gBAAM;gBAC3CM,UAAUA,YAAaF,QAAQ2B,aAAa9B,SAASG;gBACrDkC,SAAS,IAAMpB,IAAIjB,QAAQI;gBAC3BG,WAAWV;0BAEX,cAAA,KAACmC;oBAAKM,eAAY;8BAAO;;;;;AAIjC"}
1
+ {"version":3,"sources":["../../../../src/ui/atoms/number-input/quantity-input.tsx"],"sourcesContent":["'use client';\n\nimport { type KeyboardEvent, useEffect, useState } from 'react';\n\nimport { useT } from '../../../i18n/index.js';\nimport { cn } from '../../utils/cn.js';\n\nexport interface QuantityInputProps {\n /** What is being counted; names the spinbutton and both of its buttons. */\n label: string;\n value: number;\n onChange: (value: number) => void;\n min?: number;\n max?: number;\n step?: number;\n disabled?: boolean;\n id?: string;\n /**\n * Posts the count with the form it sits in, under this name — a server\n * action's form works without JavaScript, where the field is the only part\n * a reader can change, so the field itself carries it. The server clamps\n * what it receives: a typed value is only clamped here once committed.\n */\n name?: string;\n className?: string;\n}\n\nconst STEP_BUTTON =\n 'flex min-h-(--target-min) min-w-(--target-min) shrink-0 items-center justify-center rounded-md border border-border-control bg-surface-elevated text-content hover:bg-surface disabled:cursor-not-allowed disabled:opacity-50';\n\n/**\n * A count, on the WAI-ARIA spinbutton pattern: Up and Down step it, Home and\n * End jump to its bounds, and what is typed is committed on Enter or on\n * leaving the field, clamped to the bounds. The − and + beside it are named\n * \"Decrease {label}\" and \"Increase {label}\" and sit at `--target-min`; they are\n * out of the Tab order, as the pattern has them, because the arrow keys\n * already do what they do.\n */\nexport function QuantityInput({\n label,\n value,\n onChange,\n min = 0,\n max,\n step = 1,\n disabled = false,\n id,\n name,\n className,\n}: QuantityInputProps) {\n const t = useT();\n const [text, setText] = useState(String(value));\n\n useEffect(() => {\n // eslint-disable-next-line react-hooks/set-state-in-effect -- the value moved from outside (a step, a reset)\n setText(String(value));\n }, [value]);\n\n const clamp = (next: number) =>\n Math.min(max ?? Number.POSITIVE_INFINITY, Math.max(min, next));\n const set = (next: number) => {\n const clamped = clamp(next);\n setText(String(clamped));\n if (clamped !== value) onChange(clamped);\n };\n const commit = () => {\n const typed = Number.parseInt(text, 10);\n set(Number.isNaN(typed) ? value : typed);\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n const next = {\n ArrowUp: value + step,\n ArrowDown: value - step,\n Home: min,\n End: max,\n }[event.key];\n if (event.key === 'Enter') {\n commit();\n return;\n }\n if (next === undefined) return;\n event.preventDefault();\n set(next);\n };\n\n return (\n <span className={cn('inline-flex items-center gap-3xs', className)}>\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={t('quantity.decrease', { label })}\n disabled={disabled || value <= min}\n onClick={() => set(value - step)}\n className={STEP_BUTTON}\n >\n <span aria-hidden=\"true\">−</span>\n </button>\n <input\n id={id}\n name={name}\n type=\"text\"\n role=\"spinbutton\"\n inputMode=\"numeric\"\n aria-label={label}\n aria-valuenow={value}\n aria-valuemin={min}\n aria-valuemax={max}\n disabled={disabled}\n value={text}\n onChange={event => setText(event.currentTarget.value)}\n onBlur={commit}\n onKeyDown={onKeyDown}\n className=\"w-16 min-h-(--target-min) rounded-lg border border-border-control bg-surface-elevated px-2xs py-3xs text-center text-step-sm text-content tabular-nums focus-ring disabled:cursor-not-allowed disabled:opacity-50\"\n />\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={t('quantity.increase', { label })}\n disabled={disabled || (max !== undefined && value >= max)}\n onClick={() => set(value + step)}\n className={STEP_BUTTON}\n >\n <span aria-hidden=\"true\">+</span>\n </button>\n </span>\n );\n}\n"],"names":["useEffect","useState","useT","cn","STEP_BUTTON","QuantityInput","label","value","onChange","min","max","step","disabled","id","name","className","t","text","setText","String","clamp","next","Math","Number","POSITIVE_INFINITY","set","clamped","commit","typed","parseInt","isNaN","onKeyDown","event","ArrowUp","ArrowDown","Home","End","key","undefined","preventDefault","span","button","type","tabIndex","aria-label","onClick","aria-hidden","input","role","inputMode","aria-valuenow","aria-valuemin","aria-valuemax","currentTarget","onBlur"],"mappings":"AAAA;;AAEA,SAA6BA,SAAS,EAAEC,QAAQ,QAAQ,QAAQ;AAEhE,SAASC,IAAI,QAAQ,yBAAyB;AAC9C,SAASC,EAAE,QAAQ,oBAAoB;AAsBvC,MAAMC,cACJ;AAEF;;;;;;;CAOC,GACD,OAAO,SAASC,cAAc,EAC5BC,KAAK,EACLC,KAAK,EACLC,QAAQ,EACRC,MAAM,CAAC,EACPC,GAAG,EACHC,OAAO,CAAC,EACRC,WAAW,KAAK,EAChBC,EAAE,EACFC,IAAI,EACJC,SAAS,EACU;IACnB,MAAMC,IAAId;IACV,MAAM,CAACe,MAAMC,QAAQ,GAAGjB,SAASkB,OAAOZ;IAExCP,UAAU;QACR,6GAA6G;QAC7GkB,QAAQC,OAAOZ;IACjB,GAAG;QAACA;KAAM;IAEV,MAAMa,QAAQ,CAACC,OACbC,KAAKb,GAAG,CAACC,cAAAA,MAAOa,OAAOC,iBAAiB,EAAEF,KAAKZ,GAAG,CAACD,KAAKY;IAC1D,MAAMI,MAAM,CAACJ;QACX,MAAMK,UAAUN,MAAMC;QACtBH,QAAQC,OAAOO;QACf,IAAIA,YAAYnB,OAAOC,SAASkB;IAClC;IACA,MAAMC,SAAS;QACb,MAAMC,QAAQL,OAAOM,QAAQ,CAACZ,MAAM;QACpCQ,IAAIF,OAAOO,KAAK,CAACF,SAASrB,QAAQqB;IACpC;IAEA,MAAMG,YAAY,CAACC;QACjB,MAAMX,OAAO;YACXY,SAAS1B,QAAQI;YACjBuB,WAAW3B,QAAQI;YACnBwB,MAAM1B;YACN2B,KAAK1B;QACP,CAAC,CAACsB,MAAMK,GAAG,CAAC;QACZ,IAAIL,MAAMK,GAAG,KAAK,SAAS;YACzBV;YACA;QACF;QACA,IAAIN,SAASiB,WAAW;QACxBN,MAAMO,cAAc;QACpBd,IAAIJ;IACN;IAEA,qBACE,MAACmB;QAAKzB,WAAWZ,GAAG,oCAAoCY;;0BACtD,KAAC0B;gBACCC,MAAK;gBACLC,UAAU,CAAC;gBACXC,cAAY5B,EAAE,qBAAqB;oBAAEV;gBAAM;gBAC3CM,UAAUA,YAAYL,SAASE;gBAC/BoC,SAAS,IAAMpB,IAAIlB,QAAQI;gBAC3BI,WAAWX;0BAEX,cAAA,KAACoC;oBAAKM,eAAY;8BAAO;;;0BAE3B,KAACC;gBACClC,IAAIA;gBACJC,MAAMA;gBACN4B,MAAK;gBACLM,MAAK;gBACLC,WAAU;gBACVL,cAAYtC;gBACZ4C,iBAAe3C;gBACf4C,iBAAe1C;gBACf2C,iBAAe1C;gBACfE,UAAUA;gBACVL,OAAOU;gBACPT,UAAUwB,CAAAA,QAASd,QAAQc,MAAMqB,aAAa,CAAC9C,KAAK;gBACpD+C,QAAQ3B;gBACRI,WAAWA;gBACXhB,WAAU;;0BAEZ,KAAC0B;gBACCC,MAAK;gBACLC,UAAU,CAAC;gBACXC,cAAY5B,EAAE,qBAAqB;oBAAEV;gBAAM;gBAC3CM,UAAUA,YAAaF,QAAQ4B,aAAa/B,SAASG;gBACrDmC,SAAS,IAAMpB,IAAIlB,QAAQI;gBAC3BI,WAAWX;0BAEX,cAAA,KAACoC;oBAAKM,eAAY;8BAAO;;;;;AAIjC"}
@@ -1,16 +1,25 @@
1
- import type { ComponentPropsWithoutRef } from 'react';
1
+ import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
2
2
  export interface TableProps extends ComponentPropsWithoutRef<'table'> {
3
3
  /** Class names for the scroll container wrapping the `<table>`. */
4
4
  containerClassName?: string;
5
+ /**
6
+ * What the table holds — "Clientes, 12 resultados". It names the table and,
7
+ * once the table overflows its container, the scroll region too.
8
+ */
9
+ caption?: ReactNode;
10
+ /** Class names for the caption; screen-reader only unless overridden. */
11
+ captionClassName?: string;
5
12
  }
6
13
  /**
7
14
  * Styled table primitive — no entity knowledge, no data fetching. `EntityTable`
8
15
  * composes these; anything rendering non-entity tabular data can use them too.
9
16
  *
10
17
  * Wide tables scroll inside their own container rather than pushing the page
11
- * sideways.
18
+ * sideways. A container that scrolls is a **focusable region** named by the
19
+ * caption, so a keyboard can scroll it — and one that fits is neither, so it
20
+ * adds no tab stop that does nothing.
12
21
  */
13
- export declare function Table({ className, containerClassName, ...props }: TableProps): import("react").JSX.Element;
22
+ export declare function Table({ className, containerClassName, caption, captionClassName, children, ...props }: TableProps): import("react").JSX.Element;
14
23
  export declare function TableHead({ className, ...props }: ComponentPropsWithoutRef<'thead'>): import("react").JSX.Element;
15
24
  export declare function TableBody({ className, ...props }: ComponentPropsWithoutRef<'tbody'>): import("react").JSX.Element;
16
25
  export declare function TableRow({ className, ...props }: ComponentPropsWithoutRef<'tr'>): import("react").JSX.Element;
@@ -1 +1 @@
1
- {"version":3,"file":"table.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/table/table.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,OAAO,CAAC;AAItD,MAAM,WAAW,UAAW,SAAQ,wBAAwB,CAAC,OAAO,CAAC;IACnE,mEAAmE;IACnE,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,wBAAgB,KAAK,CAAC,EAAE,SAAS,EAAE,kBAAkB,EAAE,GAAG,KAAK,EAAE,EAAE,UAAU,+BAiB5E;AAED,wBAAgB,SAAS,CAAC,EACxB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,wBAAwB,CAAC,OAAO,CAAC,+BAOnC;AAED,wBAAgB,SAAS,CAAC,EACxB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,wBAAwB,CAAC,OAAO,CAAC,+BAInC;AAED,wBAAgB,QAAQ,CAAC,EACvB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,wBAAwB,CAAC,IAAI,CAAC,+BAUhC;AAED,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;AAUxD,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAChD,wBAAwB,CAAC,IAAI,CAAC,EAC9B,OAAO,CACR;IACC,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED,wBAAgB,eAAe,CAAC,EAC9B,KAAe,EACf,SAAS,EACT,GAAG,KAAK,EACT,EAAE,oBAAoB,+BAYtB;AAED,MAAM,WAAW,cAAe,SAAQ,IAAI,CAC1C,wBAAwB,CAAC,IAAI,CAAC,EAC9B,OAAO,CACR;IACC,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED,wBAAgB,SAAS,CAAC,EACxB,KAAe,EACf,SAAS,EACT,GAAG,KAAK,EACT,EAAE,cAAc,+BAOhB;AAED,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,EAC9B,OAAO,EACP,SAAS,EACT,GAAG,KAAK,EACT,EAAE,wBAAwB,CAAC,IAAI,CAAC,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,+BAatD"}
1
+ {"version":3,"file":"table.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/table/table.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,wBAAwB,EAC7B,KAAK,SAAS,EAMf,MAAM,OAAO,CAAC;AAIf,MAAM,WAAW,UAAW,SAAQ,wBAAwB,CAAC,OAAO,CAAC;IACnE,mEAAmE;IACnE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAyBD;;;;;;;;GAQG;AACH,wBAAgB,KAAK,CAAC,EACpB,SAAS,EACT,kBAAkB,EAClB,OAAO,EACP,gBAA4B,EAC5B,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,UAAU,+BAoCZ;AAED,wBAAgB,SAAS,CAAC,EACxB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,wBAAwB,CAAC,OAAO,CAAC,+BAanC;AAED,wBAAgB,SAAS,CAAC,EACxB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,wBAAwB,CAAC,OAAO,CAAC,+BAInC;AAED,wBAAgB,QAAQ,CAAC,EACvB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,wBAAwB,CAAC,IAAI,CAAC,+BAUhC;AAED,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;AAUxD,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAChD,wBAAwB,CAAC,IAAI,CAAC,EAC9B,OAAO,CACR;IACC,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED,wBAAgB,eAAe,CAAC,EAC9B,KAAe,EACf,SAAS,EACT,GAAG,KAAK,EACT,EAAE,oBAAoB,+BAYtB;AAED,MAAM,WAAW,cAAe,SAAQ,IAAI,CAC1C,wBAAwB,CAAC,IAAI,CAAC,EAC9B,OAAO,CACR;IACC,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED,wBAAgB,SAAS,CAAC,EACxB,KAAe,EACf,SAAS,EACT,GAAG,KAAK,EACT,EAAE,cAAc,+BAOhB;AAED,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,EAC9B,OAAO,EACP,SAAS,EACT,GAAG,KAAK,EACT,EAAE,wBAAwB,CAAC,IAAI,CAAC,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,+BAatD"}
@@ -1,31 +1,80 @@
1
+ 'use client';
1
2
  import { _ as _extends } from "@swc/helpers/_/_extends";
2
3
  import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
- import { jsx as _jsx } from "react/jsx-runtime";
4
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
+ import { useEffect, useId, useRef, useState } from 'react';
4
6
  import { cn } from '../../utils/cn.js';
7
+ /**
8
+ * Whether an element's content is larger than its box, on either axis —
9
+ * measured again whenever the box or the content it holds resizes.
10
+ */ function useOverflows(ref) {
11
+ const [overflows, setOverflows] = useState(false);
12
+ useEffect(()=>{
13
+ const node = ref.current;
14
+ const measure = ()=>setOverflows(node.scrollWidth > node.clientWidth || node.scrollHeight > node.clientHeight);
15
+ measure();
16
+ const observer = new ResizeObserver(measure);
17
+ observer.observe(node);
18
+ // The table grows with its rows while the container keeps its size.
19
+ for (const child of Array.from(node.children))observer.observe(child);
20
+ return ()=>observer.disconnect();
21
+ }, [
22
+ ref
23
+ ]);
24
+ return overflows;
25
+ }
5
26
  /**
6
27
  * Styled table primitive — no entity knowledge, no data fetching. `EntityTable`
7
28
  * composes these; anything rendering non-entity tabular data can use them too.
8
29
  *
9
30
  * Wide tables scroll inside their own container rather than pushing the page
10
- * sideways.
31
+ * sideways. A container that scrolls is a **focusable region** named by the
32
+ * caption, so a keyboard can scroll it — and one that fits is neither, so it
33
+ * adds no tab stop that does nothing.
11
34
  */ export function Table(_0) {
12
- let { className, containerClassName } = _0, props = _object_without_properties_loose(_0, [
35
+ let { className, containerClassName, caption, captionClassName = 'sr-only', children } = _0, props = _object_without_properties_loose(_0, [
13
36
  "className",
14
- "containerClassName"
37
+ "containerClassName",
38
+ "caption",
39
+ "captionClassName",
40
+ "children"
15
41
  ]);
16
- return /*#__PURE__*/ _jsx("div", {
17
- className: cn('w-full overflow-x-auto rounded-xl border border-border bg-surface-elevated', containerClassName),
18
- children: /*#__PURE__*/ _jsx("table", _extends({
42
+ const container = useRef(null);
43
+ const captionId = useId();
44
+ const overflows = useOverflows(container);
45
+ const named = caption !== undefined;
46
+ return /*#__PURE__*/ _jsx("div", _extends({
47
+ ref: container,
48
+ "data-overflows": overflows || undefined
49
+ }, overflows && {
50
+ tabIndex: 0,
51
+ role: named ? 'region' : undefined,
52
+ 'aria-labelledby': named ? captionId : undefined
53
+ }, {
54
+ className: cn('w-full overflow-auto rounded-xl border border-border bg-surface-elevated focus-ring', containerClassName),
55
+ children: /*#__PURE__*/ _jsxs("table", _extends({
19
56
  className: cn('w-full border-collapse text-step-0 text-content', className)
20
- }, props))
21
- });
57
+ }, props, {
58
+ children: [
59
+ named && /*#__PURE__*/ _jsx("caption", {
60
+ id: captionId,
61
+ className: captionClassName,
62
+ children: caption
63
+ }),
64
+ children
65
+ ]
66
+ }))
67
+ }));
22
68
  }
23
69
  export function TableHead(_0) {
24
70
  let { className } = _0, props = _object_without_properties_loose(_0, [
25
71
  "className"
26
72
  ]);
27
73
  return /*#__PURE__*/ _jsx("thead", _extends({
28
- className: cn('border-b border-border bg-surface', className)
74
+ // Sticky inside a container that scrolls: the column names stay in view
75
+ // over a long page of rows. One layer above the rows, and no higher: a
76
+ // row menu opened under it must still paint over it.
77
+ className: cn('top-0 sticky z-1 border-b border-border bg-surface', className)
29
78
  }, props));
30
79
  }
31
80
  export function TableBody(_0) {