@entifix/react-controls 0.19.0 → 0.20.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 (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 +2 -0
  49. package/dist/ui/actions/index.d.ts.map +1 -1
  50. package/dist/ui/actions/index.js +2 -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 +50 -0
  57. package/dist/ui/actions/use-verb-invoker.d.ts.map +1 -0
  58. package/dist/ui/actions/use-verb-invoker.js +93 -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 +8 -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,83 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useErrorMessage, useT } from '../../../i18n/index.js';
4
+ import { useErrorAction } from '../../actions/error-actions.js';
5
+ import { Alert } from '../../atoms/alert/index.js';
6
+ import { Button } from '../../atoms/button/index.js';
7
+ import { EmptyState } from '../empty-state/index.js';
8
+ /**
9
+ * What a region says when it is not simply showing its content
10
+ * ([ADR 0028](../../../../../../../docs/adr/0028-states-an-organism-must-render.md)):
11
+ *
12
+ * - a **load error** is an `Alert` with Retry — `forbidden` says so in its own
13
+ * words — and data kept from before it is marked possibly out of date;
14
+ * - an **empty result under a filter** says so and offers Clear filters;
15
+ * - an **empty region** says what appears here, and offers its first record.
16
+ *
17
+ * It renders nothing while loading, and nothing when there is content: the
18
+ * organism's own skeleton and rows speak then. An empty result and a dead
19
+ * backend never look alike.
20
+ */ export function RegionState({ plural, isLoading = false, error, onRetry, isEmpty = false, isFiltered = false, onClearFilters, emptyAction, hasStaleData = false }) {
21
+ const t = useT();
22
+ const errorMessage = useErrorMessage();
23
+ const errorAction = useErrorAction();
24
+ // Read inside a sentence, so as a common noun: "sus facturas".
25
+ const noun = plural.toLocaleLowerCase();
26
+ if (error !== undefined) {
27
+ // The host's next step for this code — "Choose organization" — when it
28
+ // registered one and its catalog names it.
29
+ const next = errorAction(error);
30
+ return /*#__PURE__*/ _jsxs(Alert, {
31
+ tone: "danger",
32
+ "data-testid": "region-state-error",
33
+ action: next === undefined && onRetry === undefined ? undefined : /*#__PURE__*/ _jsxs("span", {
34
+ className: "flex flex-wrap gap-3xs",
35
+ children: [
36
+ next !== undefined && /*#__PURE__*/ _jsx(Button, {
37
+ type: "button",
38
+ variant: "primary",
39
+ size: "sm",
40
+ onClick: next.onAction,
41
+ children: next.label
42
+ }),
43
+ onRetry !== undefined && /*#__PURE__*/ _jsx(Button, {
44
+ type: "button",
45
+ variant: "secondary",
46
+ size: "sm",
47
+ onClick: onRetry,
48
+ children: t('state.retry')
49
+ })
50
+ ]
51
+ }),
52
+ children: [
53
+ error.code === 'forbidden' ? t('state.forbidden', {
54
+ plural: noun
55
+ }) : errorMessage(error),
56
+ hasStaleData && ` ${t('state.stale')}`
57
+ ]
58
+ });
59
+ }
60
+ if (isLoading || !isEmpty) return null;
61
+ if (isFiltered) {
62
+ return /*#__PURE__*/ _jsx(EmptyState, {
63
+ action: onClearFilters === undefined ? undefined : /*#__PURE__*/ _jsx(Button, {
64
+ type: "button",
65
+ variant: "secondary",
66
+ size: "sm",
67
+ onClick: onClearFilters,
68
+ children: t('state.clearFilters')
69
+ }),
70
+ children: t('state.noResults', {
71
+ plural: noun
72
+ })
73
+ });
74
+ }
75
+ return /*#__PURE__*/ _jsx(EmptyState, {
76
+ action: emptyAction,
77
+ children: t('state.empty', {
78
+ plural: noun
79
+ })
80
+ });
81
+ }
82
+
83
+ //# sourceMappingURL=region-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/ui/molecules/region-state/region-state.tsx"],"sourcesContent":["'use client';\n\nimport type { ReactNode } from 'react';\n\nimport { useErrorMessage, useT } from '../../../i18n/index.js';\nimport { useErrorAction } from '../../actions/error-actions.js';\nimport { Alert } from '../../atoms/alert/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { EmptyState } from '../empty-state/index.js';\n\n/** A failure as the organisms hold it: a typed failure, or anything thrown. */\nexport interface RegionStateError {\n message: string;\n code?: unknown;\n details?: Record<string, unknown>;\n}\n\nexport interface RegionStateProps {\n /** What the region holds, plural — \"invoices\" — for the state's sentence. */\n plural: string;\n /** The first load: the organism's skeleton speaks for it, so nothing here. */\n isLoading?: boolean;\n /** A load that failed. */\n error?: RegionStateError;\n /** Loads the region again. Without it, an error offers no Retry. */\n onRetry?: () => void;\n /** The load answered, with nothing in it. */\n isEmpty?: boolean;\n /** A filter is in effect, so an empty answer is the filter's, not the data's. */\n isFiltered?: boolean;\n /** Clears every filter — what an empty filtered region offers. */\n onClearFilters?: () => void;\n /** What an empty, unfiltered region offers: creating the first record. */\n emptyAction?: ReactNode;\n /** Data from before the failure is still on screen. */\n hasStaleData?: boolean;\n}\n\n/**\n * What a region says when it is not simply showing its content\n * ([ADR 0028](../../../../../../../docs/adr/0028-states-an-organism-must-render.md)):\n *\n * - a **load error** is an `Alert` with Retry — `forbidden` says so in its own\n * words — and data kept from before it is marked possibly out of date;\n * - an **empty result under a filter** says so and offers Clear filters;\n * - an **empty region** says what appears here, and offers its first record.\n *\n * It renders nothing while loading, and nothing when there is content: the\n * organism's own skeleton and rows speak then. An empty result and a dead\n * backend never look alike.\n */\nexport function RegionState({\n plural,\n isLoading = false,\n error,\n onRetry,\n isEmpty = false,\n isFiltered = false,\n onClearFilters,\n emptyAction,\n hasStaleData = false,\n}: RegionStateProps) {\n const t = useT();\n const errorMessage = useErrorMessage();\n const errorAction = useErrorAction();\n // Read inside a sentence, so as a common noun: \"sus facturas\".\n const noun = plural.toLocaleLowerCase();\n\n if (error !== undefined) {\n // The host's next step for this code — \"Choose organization\" — when it\n // registered one and its catalog names it.\n const next = errorAction(error);\n return (\n <Alert\n tone=\"danger\"\n data-testid=\"region-state-error\"\n action={\n next === undefined && onRetry === undefined ? undefined : (\n <span className=\"flex flex-wrap gap-3xs\">\n {next !== undefined && (\n <Button\n type=\"button\"\n variant=\"primary\"\n size=\"sm\"\n onClick={next.onAction}\n >\n {next.label}\n </Button>\n )}\n {onRetry !== undefined && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={onRetry}\n >\n {t('state.retry')}\n </Button>\n )}\n </span>\n )\n }\n >\n {error.code === 'forbidden'\n ? t('state.forbidden', { plural: noun })\n : errorMessage(error)}\n {hasStaleData && ` ${t('state.stale')}`}\n </Alert>\n );\n }\n\n if (isLoading || !isEmpty) return null;\n\n if (isFiltered) {\n return (\n <EmptyState\n action={\n onClearFilters === undefined ? undefined : (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n onClick={onClearFilters}\n >\n {t('state.clearFilters')}\n </Button>\n )\n }\n >\n {t('state.noResults', { plural: noun })}\n </EmptyState>\n );\n }\n\n return (\n <EmptyState action={emptyAction}>\n {t('state.empty', { plural: noun })}\n </EmptyState>\n );\n}\n"],"names":["useErrorMessage","useT","useErrorAction","Alert","Button","EmptyState","RegionState","plural","isLoading","error","onRetry","isEmpty","isFiltered","onClearFilters","emptyAction","hasStaleData","t","errorMessage","errorAction","noun","toLocaleLowerCase","undefined","next","tone","data-testid","action","span","className","type","variant","size","onClick","onAction","label","code"],"mappings":"AAAA;;AAIA,SAASA,eAAe,EAAEC,IAAI,QAAQ,yBAAyB;AAC/D,SAASC,cAAc,QAAQ,iCAAiC;AAChE,SAASC,KAAK,QAAQ,6BAA6B;AACnD,SAASC,MAAM,QAAQ,8BAA8B;AACrD,SAASC,UAAU,QAAQ,0BAA0B;AA8BrD;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,YAAY,EAC1BC,MAAM,EACNC,YAAY,KAAK,EACjBC,KAAK,EACLC,OAAO,EACPC,UAAU,KAAK,EACfC,aAAa,KAAK,EAClBC,cAAc,EACdC,WAAW,EACXC,eAAe,KAAK,EACH;IACjB,MAAMC,IAAIf;IACV,MAAMgB,eAAejB;IACrB,MAAMkB,cAAchB;IACpB,+DAA+D;IAC/D,MAAMiB,OAAOZ,OAAOa,iBAAiB;IAErC,IAAIX,UAAUY,WAAW;QACvB,uEAAuE;QACvE,2CAA2C;QAC3C,MAAMC,OAAOJ,YAAYT;QACzB,qBACE,MAACN;YACCoB,MAAK;YACLC,eAAY;YACZC,QACEH,SAASD,aAAaX,YAAYW,YAAYA,0BAC5C,MAACK;gBAAKC,WAAU;;oBACbL,SAASD,2BACR,KAACjB;wBACCwB,MAAK;wBACLC,SAAQ;wBACRC,MAAK;wBACLC,SAAST,KAAKU,QAAQ;kCAErBV,KAAKW,KAAK;;oBAGdvB,YAAYW,2BACX,KAACjB;wBACCwB,MAAK;wBACLC,SAAQ;wBACRC,MAAK;wBACLC,SAASrB;kCAERM,EAAE;;;;;gBAOZP,MAAMyB,IAAI,KAAK,cACZlB,EAAE,mBAAmB;oBAAET,QAAQY;gBAAK,KACpCF,aAAaR;gBAChBM,gBAAgB,CAAC,CAAC,EAAEC,EAAE,gBAAgB;;;IAG7C;IAEA,IAAIR,aAAa,CAACG,SAAS,OAAO;IAElC,IAAIC,YAAY;QACd,qBACE,KAACP;YACCoB,QACEZ,mBAAmBQ,YAAYA,0BAC7B,KAACjB;gBACCwB,MAAK;gBACLC,SAAQ;gBACRC,MAAK;gBACLC,SAASlB;0BAERG,EAAE;;sBAKRA,EAAE,mBAAmB;gBAAET,QAAQY;YAAK;;IAG3C;IAEA,qBACE,KAACd;QAAWoB,QAAQX;kBACjBE,EAAE,eAAe;YAAET,QAAQY;QAAK;;AAGvC"}
@@ -6,8 +6,10 @@ export interface TableToolbarProps {
6
6
  end?: ReactNode;
7
7
  /** Expanded panel rendered below the bar (filters, sorting). */
8
8
  panel?: ReactNode;
9
+ /** The panel's id, for the toggle that opens it to name in `aria-controls`. */
10
+ panelId?: string;
9
11
  className?: string;
10
12
  }
11
13
  /** Bar above a table: actions on one line, an optional expanded panel below. */
12
- export declare function TableToolbar({ start, end, panel, className, }: TableToolbarProps): import("react").JSX.Element;
14
+ export declare function TableToolbar({ start, end, panel, panelId, className, }: TableToolbarProps): import("react").JSX.Element;
13
15
  //# sourceMappingURL=table-toolbar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"table-toolbar.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/table-toolbar/table-toolbar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,MAAM,WAAW,iBAAiB;IAChC,8DAA8D;IAC9D,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,mEAAmE;IACnE,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,gEAAgE;IAChE,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,GAAG,EACH,KAAK,EACL,SAAS,GACV,EAAE,iBAAiB,+BAcnB"}
1
+ {"version":3,"file":"table-toolbar.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/table-toolbar/table-toolbar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,MAAM,WAAW,iBAAiB;IAChC,8DAA8D;IAC9D,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,mEAAmE;IACnE,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,gEAAgE;IAChE,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,GAAG,EACH,KAAK,EACL,OAAO,EACP,SAAS,GACV,EAAE,iBAAiB,+BAmBnB"}
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cn } from '../../utils/cn.js';
3
- /** Bar above a table: actions on one line, an optional expanded panel below. */ export function TableToolbar({ start, end, panel, className }) {
3
+ /** Bar above a table: actions on one line, an optional expanded panel below. */ export function TableToolbar({ start, end, panel, panelId, className }) {
4
4
  return /*#__PURE__*/ _jsxs("div", {
5
5
  className: cn('flex flex-col gap-2xs', className),
6
6
  children: [
@@ -8,7 +8,7 @@ import { cn } from '../../utils/cn.js';
8
8
  className: "flex flex-wrap items-center justify-between gap-2xs",
9
9
  children: [
10
10
  /*#__PURE__*/ _jsx("div", {
11
- className: "flex flex-wrap items-center gap-2xs",
11
+ className: "min-w-0 flex flex-1 flex-wrap items-center gap-2xs",
12
12
  children: start
13
13
  }),
14
14
  /*#__PURE__*/ _jsx("div", {
@@ -18,6 +18,7 @@ import { cn } from '../../utils/cn.js';
18
18
  ]
19
19
  }),
20
20
  panel && /*#__PURE__*/ _jsx("div", {
21
+ id: panelId,
21
22
  className: "rounded-xl border border-border bg-surface p-s",
22
23
  children: panel
23
24
  })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/table-toolbar/table-toolbar.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\n\nimport { cn } from '../../utils/cn.js';\n\nexport interface TableToolbarProps {\n /** Actions aligned to the start — titles, primary actions. */\n start?: ReactNode;\n /** Actions aligned to the end — column settings, panel toggles. */\n end?: ReactNode;\n /** Expanded panel rendered below the bar (filters, sorting). */\n panel?: ReactNode;\n className?: string;\n}\n\n/** Bar above a table: actions on one line, an optional expanded panel below. */\nexport function TableToolbar({\n start,\n end,\n panel,\n className,\n}: TableToolbarProps) {\n return (\n <div className={cn('flex flex-col gap-2xs', className)}>\n <div className=\"flex flex-wrap items-center justify-between gap-2xs\">\n <div className=\"flex flex-wrap items-center gap-2xs\">{start}</div>\n <div className=\"flex flex-wrap items-center gap-2xs\">{end}</div>\n </div>\n {panel && (\n <div className=\"rounded-xl border border-border bg-surface p-s\">\n {panel}\n </div>\n )}\n </div>\n );\n}\n"],"names":["cn","TableToolbar","start","end","panel","className","div"],"mappings":";AAEA,SAASA,EAAE,QAAQ,oBAAoB;AAYvC,8EAA8E,GAC9E,OAAO,SAASC,aAAa,EAC3BC,KAAK,EACLC,GAAG,EACHC,KAAK,EACLC,SAAS,EACS;IAClB,qBACE,MAACC;QAAID,WAAWL,GAAG,yBAAyBK;;0BAC1C,MAACC;gBAAID,WAAU;;kCACb,KAACC;wBAAID,WAAU;kCAAuCH;;kCACtD,KAACI;wBAAID,WAAU;kCAAuCF;;;;YAEvDC,uBACC,KAACE;gBAAID,WAAU;0BACZD;;;;AAKX"}
1
+ {"version":3,"sources":["../../../../src/ui/molecules/table-toolbar/table-toolbar.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\n\nimport { cn } from '../../utils/cn.js';\n\nexport interface TableToolbarProps {\n /** Actions aligned to the start — titles, primary actions. */\n start?: ReactNode;\n /** Actions aligned to the end — column settings, panel toggles. */\n end?: ReactNode;\n /** Expanded panel rendered below the bar (filters, sorting). */\n panel?: ReactNode;\n /** The panel's id, for the toggle that opens it to name in `aria-controls`. */\n panelId?: string;\n className?: string;\n}\n\n/** Bar above a table: actions on one line, an optional expanded panel below. */\nexport function TableToolbar({\n start,\n end,\n panel,\n panelId,\n className,\n}: TableToolbarProps) {\n return (\n <div className={cn('flex flex-col gap-2xs', className)}>\n <div className=\"flex flex-wrap items-center justify-between gap-2xs\">\n <div className=\"min-w-0 flex flex-1 flex-wrap items-center gap-2xs\">\n {start}\n </div>\n <div className=\"flex flex-wrap items-center gap-2xs\">{end}</div>\n </div>\n {panel && (\n <div\n id={panelId}\n className=\"rounded-xl border border-border bg-surface p-s\"\n >\n {panel}\n </div>\n )}\n </div>\n );\n}\n"],"names":["cn","TableToolbar","start","end","panel","panelId","className","div","id"],"mappings":";AAEA,SAASA,EAAE,QAAQ,oBAAoB;AAcvC,8EAA8E,GAC9E,OAAO,SAASC,aAAa,EAC3BC,KAAK,EACLC,GAAG,EACHC,KAAK,EACLC,OAAO,EACPC,SAAS,EACS;IAClB,qBACE,MAACC;QAAID,WAAWN,GAAG,yBAAyBM;;0BAC1C,MAACC;gBAAID,WAAU;;kCACb,KAACC;wBAAID,WAAU;kCACZJ;;kCAEH,KAACK;wBAAID,WAAU;kCAAuCH;;;;YAEvDC,uBACC,KAACG;gBACCC,IAAIH;gBACJC,WAAU;0BAETF;;;;AAKX"}
@@ -5,39 +5,83 @@ import { useErrorMessage } from '../../../i18n/index.js';
5
5
  * `code`, and anything else that reached the user keeps its `message`.
6
6
  */
7
7
  export type ToastFailure = Parameters<ReturnType<typeof useErrorMessage>>[0];
8
- /** One failure waiting in the region, under the id it was shown with. */
9
- export interface FailureToast {
10
- readonly id: number;
11
- readonly failure: ToastFailure;
8
+ /**
9
+ * What a toast reports
10
+ * ([ADR 0028](../../../../../../../docs/adr/0028-states-an-organism-must-render.md)):
11
+ * a write that worked, one that can still be taken back, something worth
12
+ * knowing, or a failure. Only a failure interrupts.
13
+ */
14
+ export type ToastTone = 'success' | 'undo' | 'info' | 'failure';
15
+ /** The one thing a toast offers to do about what it reports. */
16
+ export interface ToastAction {
17
+ /** The button's text, already in the reader's language. */
18
+ readonly label: string;
19
+ readonly onAction: () => void;
12
20
  }
21
+ /**
22
+ * A toast to show. A failure carries the failure itself, rendered through the
23
+ * same {@link useErrorMessage} a form's error slot uses; every other tone
24
+ * carries a sentence already in the reader's language. An `undo` toast without
25
+ * its Undo would be a success toast, so the type asks for the action.
26
+ */
27
+ export type ToastInput = {
28
+ readonly tone: 'success' | 'info';
29
+ readonly message: string;
30
+ readonly action?: ToastAction;
31
+ } | {
32
+ readonly tone: 'undo';
33
+ readonly message: string;
34
+ readonly action: ToastAction;
35
+ } | {
36
+ readonly tone: 'failure';
37
+ readonly failure: ToastFailure;
38
+ readonly action?: ToastAction;
39
+ };
40
+ /** One toast waiting in the region, under the id it was shown with. */
41
+ export type Toast = ToastInput & {
42
+ readonly id: number;
43
+ };
13
44
  export interface Toasts {
14
- /** The failures shown, oldest first. */
15
- readonly failures: readonly FailureToast[];
16
- /** Shows a failure until it is dismissed, and answers its id. */
17
- readonly showFailure: (failure: ToastFailure) => number;
45
+ /** The toasts shown, oldest first. */
46
+ readonly toasts: readonly Toast[];
47
+ /** Shows a toast, and answers its id. */
48
+ readonly show: (toast: ToastInput) => number;
18
49
  readonly dismiss: (id: number) => void;
19
50
  }
51
+ /**
52
+ * How long a toast that is neither a failure nor offers an action stays, in
53
+ * milliseconds. A failure stays until it is dismissed: it reports something
54
+ * that already happened, often after the screen that started it was left, and
55
+ * a message that times out is one a slow reader never gets. A toast with an
56
+ * action stays too, for the same reader.
57
+ */
58
+ export declare const TOAST_TIMEOUT_MS = 8000;
59
+ export interface ToastProviderProps {
60
+ readonly children: ReactNode;
61
+ /** How long a success, undo or info toast stays. */
62
+ readonly timeoutMs?: number;
63
+ }
20
64
  /**
21
65
  * Holds the toasts one host shows. Mounted once, above everything that can
22
66
  * fail asynchronously, with a {@link ToastRegion} somewhere inside it.
23
67
  */
24
- export declare function ToastProvider({ children }: {
25
- readonly children: ReactNode;
26
- }): import("react").JSX.Element;
68
+ export declare function ToastProvider({ children, timeoutMs, }: ToastProviderProps): import("react").JSX.Element;
27
69
  /**
28
70
  * The {@link ToastProvider} above, or `undefined` outside one: a caller with
29
- * no region to show a failure in keeps it where it can, rather than having it
30
- * vanish.
71
+ * no region to show a toast in keeps what it has to say where it can, rather
72
+ * than having it vanish.
31
73
  */
32
74
  export declare const useToasts: () => Toasts | undefined;
33
75
  /**
34
- * Where a host's toasts render: each failure in the reader's language,
35
- * through the same {@link useErrorMessage} a form's error slot uses, so an
36
- * asynchronous failure reads exactly as a synchronous one would.
76
+ * Where a host's toasts render, each through {@link Alert}: a failure is
77
+ * `role="alert"` and every other tone `role="status"`.
37
78
  *
38
- * A failure stays until it is dismissed. It reports something that already
39
- * happened, often after the screen that started it was left, and a message
40
- * that times out is one a slow reader never gets.
79
+ * ⚠️ **An empty region takes no room and no clicks.** It stays mounted, so a
80
+ * toast lands in a region that already exists, but at zero size and with
81
+ * `pointer-events: none` — a fixed box that kept its padding once covered two
82
+ * focused links and swallowed clicks (WCAG 2.4.11). While toasts show, the
83
+ * document's bottom scroll padding is the region's height, so a control
84
+ * focused by keyboard scrolls clear of it. F6 moves focus into the region.
41
85
  */
42
86
  export declare function ToastRegion(): import("react").JSX.Element | null;
43
87
  //# sourceMappingURL=toast-region.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"toast-region.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/toast-region/toast-region.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,SAAS,EAMf,MAAM,OAAO,CAAC;AAEf,OAAO,EAAE,eAAe,EAAQ,MAAM,wBAAwB,CAAC;AAG/D;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE7E,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;CAChC;AAED,MAAM,WAAW,MAAM;IACrB,wCAAwC;IACxC,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;IAC3C,iEAAiE;IACjE,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,MAAM,CAAC;IACxD,QAAQ,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC;AAID;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAqB3E;AAED;;;;GAIG;AACH,eAAO,MAAM,SAAS,QAAO,MAAM,GAAG,SAAqC,CAAC;AAE5E;;;;;;;;GAQG;AACH,wBAAgB,WAAW,uCA6B1B"}
1
+ {"version":3,"file":"toast-region.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/toast-region/toast-region.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,SAAS,EAOf,MAAM,OAAO,CAAC;AAEf,OAAO,EAAE,eAAe,EAAQ,MAAM,wBAAwB,CAAC;AAM/D;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE7E;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAEhE,gEAAgE;AAChE,MAAM,WAAW,WAAW;IAC1B,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAC;CAC/B;AAED;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAClB;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B,CAAC;AAEN,uEAAuE;AACvE,MAAM,MAAM,KAAK,GAAG,UAAU,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD,MAAM,WAAW,MAAM;IACrB,sCAAsC;IACtC,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAClC,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,MAAM,CAAC;IAC7C,QAAQ,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAIrC,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,oDAAoD;IACpD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,SAA4B,GAC7B,EAAE,kBAAkB,+BA4CpB;AAED;;;;GAIG;AACH,eAAO,MAAM,SAAS,QAAO,MAAM,GAAG,SAAqC,CAAC;AA0D5E;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,uCA8C1B"}
@@ -1,37 +1,64 @@
1
1
  'use client';
2
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
3
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
4
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
4
5
  import { useErrorMessage, useT } from '../../../i18n/index.js';
6
+ import { useErrorAction } from '../../actions/error-actions.js';
7
+ import { Alert } from '../../atoms/alert/index.js';
5
8
  import { Button } from '../../atoms/button/index.js';
9
+ import { cn } from '../../utils/cn.js';
10
+ /**
11
+ * How long a toast that is neither a failure nor offers an action stays, in
12
+ * milliseconds. A failure stays until it is dismissed: it reports something
13
+ * that already happened, often after the screen that started it was left, and
14
+ * a message that times out is one a slow reader never gets. A toast with an
15
+ * action stays too, for the same reader.
16
+ */ export const TOAST_TIMEOUT_MS = 8000;
6
17
  const ToastContext = /*#__PURE__*/ createContext(undefined);
7
18
  /**
8
19
  * Holds the toasts one host shows. Mounted once, above everything that can
9
20
  * fail asynchronously, with a {@link ToastRegion} somewhere inside it.
10
- */ export function ToastProvider({ children }) {
11
- const [failures, setFailures] = useState([]);
21
+ */ export function ToastProvider({ children, timeoutMs = TOAST_TIMEOUT_MS }) {
22
+ const [toasts, setToasts] = useState([]);
12
23
  const next = useRef(0);
13
- const showFailure = useCallback((failure)=>{
24
+ const timers = useRef(new Map());
25
+ const dismiss = useCallback((id)=>{
26
+ clearTimeout(timers.current.get(id));
27
+ timers.current.delete(id);
28
+ setToasts((shown)=>shown.filter((toast)=>toast.id !== id));
29
+ }, []);
30
+ const show = useCallback((toast)=>{
14
31
  next.current += 1;
15
32
  const id = next.current;
16
- setFailures((shown)=>[
33
+ setToasts((shown)=>[
17
34
  ...shown,
18
- {
19
- id,
20
- failure
21
- }
35
+ _extends({}, toast, {
36
+ id
37
+ })
22
38
  ]);
39
+ // A failure stays until it is dismissed, and so does a toast that
40
+ // offers an action: Undo on a timer is a time limit on the reader's
41
+ // only way back (WCAG 2.2.1). The rest say their piece and go.
42
+ if (toast.tone !== 'failure' && toast.action === undefined) timers.current.set(id, setTimeout(()=>dismiss(id), timeoutMs));
23
43
  return id;
24
- }, []);
25
- const dismiss = useCallback((id)=>{
26
- setFailures((shown)=>shown.filter((toast)=>toast.id !== id));
44
+ }, [
45
+ dismiss,
46
+ timeoutMs
47
+ ]);
48
+ useEffect(()=>{
49
+ const pending = timers.current;
50
+ return ()=>{
51
+ for (const timer of pending.values())clearTimeout(timer);
52
+ pending.clear();
53
+ };
27
54
  }, []);
28
55
  const value = useMemo(()=>({
29
- failures,
30
- showFailure,
56
+ toasts,
57
+ show,
31
58
  dismiss
32
59
  }), [
33
- failures,
34
- showFailure,
60
+ toasts,
61
+ show,
35
62
  dismiss
36
63
  ]);
37
64
  return /*#__PURE__*/ _jsx(ToastContext.Provider, {
@@ -41,41 +68,101 @@ const ToastContext = /*#__PURE__*/ createContext(undefined);
41
68
  }
42
69
  /**
43
70
  * The {@link ToastProvider} above, or `undefined` outside one: a caller with
44
- * no region to show a failure in keeps it where it can, rather than having it
45
- * vanish.
71
+ * no region to show a toast in keeps what it has to say where it can, rather
72
+ * than having it vanish.
46
73
  */ export const useToasts = ()=>useContext(ToastContext);
74
+ const ALERT_TONE = {
75
+ success: 'success',
76
+ undo: 'info',
77
+ info: 'info',
78
+ failure: 'danger'
79
+ };
80
+ function ToastItem({ toast, onDismiss }) {
81
+ var _toast_action;
82
+ const t = useT();
83
+ const message = useErrorMessage();
84
+ const errorAction = useErrorAction();
85
+ // A failure with no action of its own offers the host's next step for its
86
+ // code, when there is one.
87
+ const next = toast.tone === 'failure' ? errorAction(toast.failure) : undefined;
88
+ const action = (_toast_action = toast.action) != null ? _toast_action : next === undefined ? undefined : {
89
+ label: next.label,
90
+ onAction: next.onAction
91
+ };
92
+ return /*#__PURE__*/ _jsx(Alert, {
93
+ tone: ALERT_TONE[toast.tone],
94
+ "data-toast-tone": toast.tone,
95
+ className: "pointer-events-auto bg-surface shadow-overlay",
96
+ action: /*#__PURE__*/ _jsxs("div", {
97
+ className: "flex gap-3xs",
98
+ children: [
99
+ action !== undefined && /*#__PURE__*/ _jsx(Button, {
100
+ type: "button",
101
+ variant: "secondary",
102
+ onClick: ()=>{
103
+ action.onAction();
104
+ onDismiss();
105
+ },
106
+ children: action.label
107
+ }),
108
+ /*#__PURE__*/ _jsx(Button, {
109
+ type: "button",
110
+ variant: "ghost",
111
+ onClick: onDismiss,
112
+ children: t('toast.dismiss')
113
+ })
114
+ ]
115
+ }),
116
+ children: toast.tone === 'failure' ? message(toast.failure) : toast.message
117
+ });
118
+ }
47
119
  /**
48
- * Where a host's toasts render: each failure in the reader's language,
49
- * through the same {@link useErrorMessage} a form's error slot uses, so an
50
- * asynchronous failure reads exactly as a synchronous one would.
120
+ * Where a host's toasts render, each through {@link Alert}: a failure is
121
+ * `role="alert"` and every other tone `role="status"`.
51
122
  *
52
- * A failure stays until it is dismissed. It reports something that already
53
- * happened, often after the screen that started it was left, and a message
54
- * that times out is one a slow reader never gets.
123
+ * ⚠️ **An empty region takes no room and no clicks.** It stays mounted, so a
124
+ * toast lands in a region that already exists, but at zero size and with
125
+ * `pointer-events: none` — a fixed box that kept its padding once covered two
126
+ * focused links and swallowed clicks (WCAG 2.4.11). While toasts show, the
127
+ * document's bottom scroll padding is the region's height, so a control
128
+ * focused by keyboard scrolls clear of it. F6 moves focus into the region.
55
129
  */ export function ToastRegion() {
130
+ var _ref;
56
131
  const toasts = useToasts();
57
132
  const t = useT();
58
- const message = useErrorMessage();
133
+ const region = useRef(null);
134
+ const shown = (_ref = toasts == null ? void 0 : toasts.toasts) != null ? _ref : [];
135
+ const count = shown.length;
136
+ useEffect(()=>{
137
+ const root = document.documentElement;
138
+ if (count === 0) {
139
+ root.style.removeProperty('scroll-padding-bottom');
140
+ return;
141
+ }
142
+ // The region renders whenever a toast can: the ref is set by now.
143
+ const node = region.current;
144
+ root.style.setProperty('scroll-padding-bottom', `${node.offsetHeight}px`);
145
+ const onKeyDown = (event)=>{
146
+ if (event.key !== 'F6') return;
147
+ event.preventDefault();
148
+ node.focus();
149
+ };
150
+ document.addEventListener('keydown', onKeyDown);
151
+ return ()=>document.removeEventListener('keydown', onKeyDown);
152
+ }, [
153
+ count
154
+ ]);
59
155
  if (toasts === undefined) return null;
60
156
  return /*#__PURE__*/ _jsx("section", {
157
+ ref: region,
61
158
  "aria-label": t('toast.region'),
62
- className: "bottom-0 right-0 max-w-md fixed z-(--z-toast) flex w-full flex-col gap-xs p-s",
63
- children: toasts.failures.map(({ id, failure })=>/*#__PURE__*/ _jsxs("div", {
64
- role: "alert",
65
- className: "flex items-start gap-xs rounded-lg border border-danger bg-surface p-xs shadow-overlay",
66
- children: [
67
- /*#__PURE__*/ _jsx("p", {
68
- className: "flex-1 text-step-sm text-danger",
69
- children: message(failure)
70
- }),
71
- /*#__PURE__*/ _jsx(Button, {
72
- type: "button",
73
- variant: "ghost",
74
- onClick: ()=>toasts.dismiss(id),
75
- children: t('toast.dismiss')
76
- })
77
- ]
78
- }, id))
159
+ tabIndex: -1,
160
+ "data-testid": "toast-region",
161
+ className: cn('bottom-0 right-0 pointer-events-none fixed z-(--z-toast) flex flex-col gap-xs focus-ring', count === 0 ? 'size-0 overflow-hidden' : 'max-w-md w-full p-s'),
162
+ children: shown.map((toast)=>/*#__PURE__*/ _jsx(ToastItem, {
163
+ toast: toast,
164
+ onDismiss: ()=>toasts.dismiss(toast.id)
165
+ }, toast.id))
79
166
  });
80
167
  }
81
168
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/toast-region/toast-region.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport { useErrorMessage, useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\n\n/**\n * A failure as {@link useErrorMessage} renders it: a typed failure carries its\n * `code`, and anything else that reached the user keeps its `message`.\n */\nexport type ToastFailure = Parameters<ReturnType<typeof useErrorMessage>>[0];\n\n/** One failure waiting in the region, under the id it was shown with. */\nexport interface FailureToast {\n readonly id: number;\n readonly failure: ToastFailure;\n}\n\nexport interface Toasts {\n /** The failures shown, oldest first. */\n readonly failures: readonly FailureToast[];\n /** Shows a failure until it is dismissed, and answers its id. */\n readonly showFailure: (failure: ToastFailure) => number;\n readonly dismiss: (id: number) => void;\n}\n\nconst ToastContext = createContext<Toasts | undefined>(undefined);\n\n/**\n * Holds the toasts one host shows. Mounted once, above everything that can\n * fail asynchronously, with a {@link ToastRegion} somewhere inside it.\n */\nexport function ToastProvider({ children }: { readonly children: ReactNode }) {\n const [failures, setFailures] = useState<readonly FailureToast[]>([]);\n const next = useRef(0);\n\n const showFailure = useCallback((failure: ToastFailure) => {\n next.current += 1;\n const id = next.current;\n setFailures(shown => [...shown, { id, failure }]);\n return id;\n }, []);\n const dismiss = useCallback((id: number) => {\n setFailures(shown => shown.filter(toast => toast.id !== id));\n }, []);\n\n const value = useMemo(\n () => ({ failures, showFailure, dismiss }),\n [failures, showFailure, dismiss],\n );\n return (\n <ToastContext.Provider value={value}>{children}</ToastContext.Provider>\n );\n}\n\n/**\n * The {@link ToastProvider} above, or `undefined` outside one: a caller with\n * no region to show a failure in keeps it where it can, rather than having it\n * vanish.\n */\nexport const useToasts = (): Toasts | undefined => useContext(ToastContext);\n\n/**\n * Where a host's toasts render: each failure in the reader's language,\n * through the same {@link useErrorMessage} a form's error slot uses, so an\n * asynchronous failure reads exactly as a synchronous one would.\n *\n * A failure stays until it is dismissed. It reports something that already\n * happened, often after the screen that started it was left, and a message\n * that times out is one a slow reader never gets.\n */\nexport function ToastRegion() {\n const toasts = useToasts();\n const t = useT();\n const message = useErrorMessage();\n\n if (toasts === undefined) return null;\n return (\n <section\n aria-label={t('toast.region')}\n className=\"bottom-0 right-0 max-w-md fixed z-(--z-toast) flex w-full flex-col gap-xs p-s\"\n >\n {toasts.failures.map(({ id, failure }) => (\n <div\n key={id}\n role=\"alert\"\n className=\"flex items-start gap-xs rounded-lg border border-danger bg-surface p-xs shadow-overlay\"\n >\n <p className=\"flex-1 text-step-sm text-danger\">{message(failure)}</p>\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={() => toasts.dismiss(id)}\n >\n {t('toast.dismiss')}\n </Button>\n </div>\n ))}\n </section>\n );\n}\n"],"names":["createContext","useCallback","useContext","useMemo","useRef","useState","useErrorMessage","useT","Button","ToastContext","undefined","ToastProvider","children","failures","setFailures","next","showFailure","failure","current","id","shown","dismiss","filter","toast","value","Provider","useToasts","ToastRegion","toasts","t","message","section","aria-label","className","map","div","role","p","type","variant","onClick"],"mappings":"AAAA;;AAEA,SACEA,aAAa,EAEbC,WAAW,EACXC,UAAU,EACVC,OAAO,EACPC,MAAM,EACNC,QAAQ,QACH,QAAQ;AAEf,SAASC,eAAe,EAAEC,IAAI,QAAQ,yBAAyB;AAC/D,SAASC,MAAM,QAAQ,8BAA8B;AAsBrD,MAAMC,6BAAeT,cAAkCU;AAEvD;;;CAGC,GACD,OAAO,SAASC,cAAc,EAAEC,QAAQ,EAAoC;IAC1E,MAAM,CAACC,UAAUC,YAAY,GAAGT,SAAkC,EAAE;IACpE,MAAMU,OAAOX,OAAO;IAEpB,MAAMY,cAAcf,YAAY,CAACgB;QAC/BF,KAAKG,OAAO,IAAI;QAChB,MAAMC,KAAKJ,KAAKG,OAAO;QACvBJ,YAAYM,CAAAA,QAAS;mBAAIA;gBAAO;oBAAED;oBAAIF;gBAAQ;aAAE;QAChD,OAAOE;IACT,GAAG,EAAE;IACL,MAAME,UAAUpB,YAAY,CAACkB;QAC3BL,YAAYM,CAAAA,QAASA,MAAME,MAAM,CAACC,CAAAA,QAASA,MAAMJ,EAAE,KAAKA;IAC1D,GAAG,EAAE;IAEL,MAAMK,QAAQrB,QACZ,IAAO,CAAA;YAAEU;YAAUG;YAAaK;QAAQ,CAAA,GACxC;QAACR;QAAUG;QAAaK;KAAQ;IAElC,qBACE,KAACZ,aAAagB,QAAQ;QAACD,OAAOA;kBAAQZ;;AAE1C;AAEA;;;;CAIC,GACD,OAAO,MAAMc,YAAY,IAA0BxB,WAAWO,cAAc;AAE5E;;;;;;;;CAQC,GACD,OAAO,SAASkB;IACd,MAAMC,SAASF;IACf,MAAMG,IAAItB;IACV,MAAMuB,UAAUxB;IAEhB,IAAIsB,WAAWlB,WAAW,OAAO;IACjC,qBACE,KAACqB;QACCC,cAAYH,EAAE;QACdI,WAAU;kBAETL,OAAOf,QAAQ,CAACqB,GAAG,CAAC,CAAC,EAAEf,EAAE,EAAEF,OAAO,EAAE,iBACnC,MAACkB;gBAECC,MAAK;gBACLH,WAAU;;kCAEV,KAACI;wBAAEJ,WAAU;kCAAmCH,QAAQb;;kCACxD,KAACT;wBACC8B,MAAK;wBACLC,SAAQ;wBACRC,SAAS,IAAMZ,OAAOP,OAAO,CAACF;kCAE7BU,EAAE;;;eAVAV;;AAgBf"}
1
+ {"version":3,"sources":["../../../../src/ui/molecules/toast-region/toast-region.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport { useErrorMessage, useT } from '../../../i18n/index.js';\nimport { useErrorAction } from '../../actions/error-actions.js';\nimport { Alert, type AlertTone } from '../../atoms/alert/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { cn } from '../../utils/cn.js';\n\n/**\n * A failure as {@link useErrorMessage} renders it: a typed failure carries its\n * `code`, and anything else that reached the user keeps its `message`.\n */\nexport type ToastFailure = Parameters<ReturnType<typeof useErrorMessage>>[0];\n\n/**\n * What a toast reports\n * ([ADR 0028](../../../../../../../docs/adr/0028-states-an-organism-must-render.md)):\n * a write that worked, one that can still be taken back, something worth\n * knowing, or a failure. Only a failure interrupts.\n */\nexport type ToastTone = 'success' | 'undo' | 'info' | 'failure';\n\n/** The one thing a toast offers to do about what it reports. */\nexport interface ToastAction {\n /** The button's text, already in the reader's language. */\n readonly label: string;\n readonly onAction: () => void;\n}\n\n/**\n * A toast to show. A failure carries the failure itself, rendered through the\n * same {@link useErrorMessage} a form's error slot uses; every other tone\n * carries a sentence already in the reader's language. An `undo` toast without\n * its Undo would be a success toast, so the type asks for the action.\n */\nexport type ToastInput =\n | {\n readonly tone: 'success' | 'info';\n readonly message: string;\n readonly action?: ToastAction;\n }\n | {\n readonly tone: 'undo';\n readonly message: string;\n readonly action: ToastAction;\n }\n | {\n readonly tone: 'failure';\n readonly failure: ToastFailure;\n readonly action?: ToastAction;\n };\n\n/** One toast waiting in the region, under the id it was shown with. */\nexport type Toast = ToastInput & { readonly id: number };\n\nexport interface Toasts {\n /** The toasts shown, oldest first. */\n readonly toasts: readonly Toast[];\n /** Shows a toast, and answers its id. */\n readonly show: (toast: ToastInput) => number;\n readonly dismiss: (id: number) => void;\n}\n\n/**\n * How long a toast that is neither a failure nor offers an action stays, in\n * milliseconds. A failure stays until it is dismissed: it reports something\n * that already happened, often after the screen that started it was left, and\n * a message that times out is one a slow reader never gets. A toast with an\n * action stays too, for the same reader.\n */\nexport const TOAST_TIMEOUT_MS = 8000;\n\nconst ToastContext = createContext<Toasts | undefined>(undefined);\n\nexport interface ToastProviderProps {\n readonly children: ReactNode;\n /** How long a success, undo or info toast stays. */\n readonly timeoutMs?: number;\n}\n\n/**\n * Holds the toasts one host shows. Mounted once, above everything that can\n * fail asynchronously, with a {@link ToastRegion} somewhere inside it.\n */\nexport function ToastProvider({\n children,\n timeoutMs = TOAST_TIMEOUT_MS,\n}: ToastProviderProps) {\n const [toasts, setToasts] = useState<readonly Toast[]>([]);\n const next = useRef(0);\n const timers = useRef(new Map<number, ReturnType<typeof setTimeout>>());\n\n const dismiss = useCallback((id: number) => {\n clearTimeout(timers.current.get(id));\n timers.current.delete(id);\n setToasts(shown => shown.filter(toast => toast.id !== id));\n }, []);\n\n const show = useCallback(\n (toast: ToastInput) => {\n next.current += 1;\n const id = next.current;\n setToasts(shown => [...shown, { ...toast, id }]);\n // A failure stays until it is dismissed, and so does a toast that\n // offers an action: Undo on a timer is a time limit on the reader's\n // only way back (WCAG 2.2.1). The rest say their piece and go.\n if (toast.tone !== 'failure' && toast.action === undefined)\n timers.current.set(\n id,\n setTimeout(() => dismiss(id), timeoutMs),\n );\n return id;\n },\n [dismiss, timeoutMs],\n );\n\n useEffect(() => {\n const pending = timers.current;\n return () => {\n for (const timer of pending.values()) clearTimeout(timer);\n pending.clear();\n };\n }, []);\n\n const value = useMemo(\n () => ({ toasts, show, dismiss }),\n [toasts, show, dismiss],\n );\n return (\n <ToastContext.Provider value={value}>{children}</ToastContext.Provider>\n );\n}\n\n/**\n * The {@link ToastProvider} above, or `undefined` outside one: a caller with\n * no region to show a toast in keeps what it has to say where it can, rather\n * than having it vanish.\n */\nexport const useToasts = (): Toasts | undefined => useContext(ToastContext);\n\nconst ALERT_TONE: Readonly<Record<ToastTone, AlertTone>> = {\n success: 'success',\n undo: 'info',\n info: 'info',\n failure: 'danger',\n};\n\nfunction ToastItem({\n toast,\n onDismiss,\n}: {\n readonly toast: Toast;\n readonly onDismiss: () => void;\n}) {\n const t = useT();\n const message = useErrorMessage();\n const errorAction = useErrorAction();\n // A failure with no action of its own offers the host's next step for its\n // code, when there is one.\n const next =\n toast.tone === 'failure' ? errorAction(toast.failure) : undefined;\n const action =\n toast.action ??\n (next === undefined\n ? undefined\n : { label: next.label, onAction: next.onAction });\n return (\n <Alert\n tone={ALERT_TONE[toast.tone]}\n data-toast-tone={toast.tone}\n className=\"pointer-events-auto bg-surface shadow-overlay\"\n action={\n <div className=\"flex gap-3xs\">\n {action !== undefined && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={() => {\n action.onAction();\n onDismiss();\n }}\n >\n {action.label}\n </Button>\n )}\n <Button type=\"button\" variant=\"ghost\" onClick={onDismiss}>\n {t('toast.dismiss')}\n </Button>\n </div>\n }\n >\n {toast.tone === 'failure' ? message(toast.failure) : toast.message}\n </Alert>\n );\n}\n\n/**\n * Where a host's toasts render, each through {@link Alert}: a failure is\n * `role=\"alert\"` and every other tone `role=\"status\"`.\n *\n * ⚠️ **An empty region takes no room and no clicks.** It stays mounted, so a\n * toast lands in a region that already exists, but at zero size and with\n * `pointer-events: none` — a fixed box that kept its padding once covered two\n * focused links and swallowed clicks (WCAG 2.4.11). While toasts show, the\n * document's bottom scroll padding is the region's height, so a control\n * focused by keyboard scrolls clear of it. F6 moves focus into the region.\n */\nexport function ToastRegion() {\n const toasts = useToasts();\n const t = useT();\n const region = useRef<HTMLElement>(null);\n const shown = toasts?.toasts ?? [];\n const count = shown.length;\n\n useEffect(() => {\n const root = document.documentElement;\n if (count === 0) {\n root.style.removeProperty('scroll-padding-bottom');\n return;\n }\n // The region renders whenever a toast can: the ref is set by now.\n const node = region.current as HTMLElement;\n root.style.setProperty('scroll-padding-bottom', `${node.offsetHeight}px`);\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'F6') return;\n event.preventDefault();\n node.focus();\n };\n document.addEventListener('keydown', onKeyDown);\n return () => document.removeEventListener('keydown', onKeyDown);\n }, [count]);\n\n if (toasts === undefined) return null;\n return (\n <section\n ref={region}\n aria-label={t('toast.region')}\n tabIndex={-1}\n data-testid=\"toast-region\"\n className={cn(\n 'bottom-0 right-0 pointer-events-none fixed z-(--z-toast) flex flex-col gap-xs focus-ring',\n count === 0 ? 'size-0 overflow-hidden' : 'max-w-md w-full p-s',\n )}\n >\n {shown.map(toast => (\n <ToastItem\n key={toast.id}\n toast={toast}\n onDismiss={() => toasts.dismiss(toast.id)}\n />\n ))}\n </section>\n );\n}\n"],"names":["createContext","useCallback","useContext","useEffect","useMemo","useRef","useState","useErrorMessage","useT","useErrorAction","Alert","Button","cn","TOAST_TIMEOUT_MS","ToastContext","undefined","ToastProvider","children","timeoutMs","toasts","setToasts","next","timers","Map","dismiss","id","clearTimeout","current","get","delete","shown","filter","toast","show","tone","action","set","setTimeout","pending","timer","values","clear","value","Provider","useToasts","ALERT_TONE","success","undo","info","failure","ToastItem","onDismiss","t","message","errorAction","label","onAction","data-toast-tone","className","div","type","variant","onClick","ToastRegion","region","count","length","root","document","documentElement","style","removeProperty","node","setProperty","offsetHeight","onKeyDown","event","key","preventDefault","focus","addEventListener","removeEventListener","section","ref","aria-label","tabIndex","data-testid","map"],"mappings":"AAAA;;;AAEA,SACEA,aAAa,EAEbC,WAAW,EACXC,UAAU,EACVC,SAAS,EACTC,OAAO,EACPC,MAAM,EACNC,QAAQ,QACH,QAAQ;AAEf,SAASC,eAAe,EAAEC,IAAI,QAAQ,yBAAyB;AAC/D,SAASC,cAAc,QAAQ,iCAAiC;AAChE,SAASC,KAAK,QAAwB,6BAA6B;AACnE,SAASC,MAAM,QAAQ,8BAA8B;AACrD,SAASC,EAAE,QAAQ,oBAAoB;AAyDvC;;;;;;CAMC,GACD,OAAO,MAAMC,mBAAmB,KAAK;AAErC,MAAMC,6BAAed,cAAkCe;AAQvD;;;CAGC,GACD,OAAO,SAASC,cAAc,EAC5BC,QAAQ,EACRC,YAAYL,gBAAgB,EACT;IACnB,MAAM,CAACM,QAAQC,UAAU,GAAGd,SAA2B,EAAE;IACzD,MAAMe,OAAOhB,OAAO;IACpB,MAAMiB,SAASjB,OAAO,IAAIkB;IAE1B,MAAMC,UAAUvB,YAAY,CAACwB;QAC3BC,aAAaJ,OAAOK,OAAO,CAACC,GAAG,CAACH;QAChCH,OAAOK,OAAO,CAACE,MAAM,CAACJ;QACtBL,UAAUU,CAAAA,QAASA,MAAMC,MAAM,CAACC,CAAAA,QAASA,MAAMP,EAAE,KAAKA;IACxD,GAAG,EAAE;IAEL,MAAMQ,OAAOhC,YACX,CAAC+B;QACCX,KAAKM,OAAO,IAAI;QAChB,MAAMF,KAAKJ,KAAKM,OAAO;QACvBP,UAAUU,CAAAA,QAAS;mBAAIA;gBAAO,aAAKE;oBAAOP;;aAAK;QAC/C,kEAAkE;QAClE,oEAAoE;QACpE,+DAA+D;QAC/D,IAAIO,MAAME,IAAI,KAAK,aAAaF,MAAMG,MAAM,KAAKpB,WAC/CO,OAAOK,OAAO,CAACS,GAAG,CAChBX,IACAY,WAAW,IAAMb,QAAQC,KAAKP;QAElC,OAAOO;IACT,GACA;QAACD;QAASN;KAAU;IAGtBf,UAAU;QACR,MAAMmC,UAAUhB,OAAOK,OAAO;QAC9B,OAAO;YACL,KAAK,MAAMY,SAASD,QAAQE,MAAM,GAAId,aAAaa;YACnDD,QAAQG,KAAK;QACf;IACF,GAAG,EAAE;IAEL,MAAMC,QAAQtC,QACZ,IAAO,CAAA;YAAEe;YAAQc;YAAMT;QAAQ,CAAA,GAC/B;QAACL;QAAQc;QAAMT;KAAQ;IAEzB,qBACE,KAACV,aAAa6B,QAAQ;QAACD,OAAOA;kBAAQzB;;AAE1C;AAEA;;;;CAIC,GACD,OAAO,MAAM2B,YAAY,IAA0B1C,WAAWY,cAAc;AAE5E,MAAM+B,aAAqD;IACzDC,SAAS;IACTC,MAAM;IACNC,MAAM;IACNC,SAAS;AACX;AAEA,SAASC,UAAU,EACjBlB,KAAK,EACLmB,SAAS,EAIV;QASGnB;IARF,MAAMoB,IAAI5C;IACV,MAAM6C,UAAU9C;IAChB,MAAM+C,cAAc7C;IACpB,0EAA0E;IAC1E,2BAA2B;IAC3B,MAAMY,OACJW,MAAME,IAAI,KAAK,YAAYoB,YAAYtB,MAAMiB,OAAO,IAAIlC;IAC1D,MAAMoB,UACJH,gBAAAA,MAAMG,MAAM,YAAZH,gBACCX,SAASN,YACNA,YACA;QAAEwC,OAAOlC,KAAKkC,KAAK;QAAEC,UAAUnC,KAAKmC,QAAQ;IAAC;IACnD,qBACE,KAAC9C;QACCwB,MAAMW,UAAU,CAACb,MAAME,IAAI,CAAC;QAC5BuB,mBAAiBzB,MAAME,IAAI;QAC3BwB,WAAU;QACVvB,sBACE,MAACwB;YAAID,WAAU;;gBACZvB,WAAWpB,2BACV,KAACJ;oBACCiD,MAAK;oBACLC,SAAQ;oBACRC,SAAS;wBACP3B,OAAOqB,QAAQ;wBACfL;oBACF;8BAEChB,OAAOoB,KAAK;;8BAGjB,KAAC5C;oBAAOiD,MAAK;oBAASC,SAAQ;oBAAQC,SAASX;8BAC5CC,EAAE;;;;kBAKRpB,MAAME,IAAI,KAAK,YAAYmB,QAAQrB,MAAMiB,OAAO,IAAIjB,MAAMqB,OAAO;;AAGxE;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASU;;IACd,MAAM5C,SAASyB;IACf,MAAMQ,IAAI5C;IACV,MAAMwD,SAAS3D,OAAoB;IACnC,MAAMyB,gBAAQX,0BAAAA,OAAQA,MAAM,mBAAI,EAAE;IAClC,MAAM8C,QAAQnC,MAAMoC,MAAM;IAE1B/D,UAAU;QACR,MAAMgE,OAAOC,SAASC,eAAe;QACrC,IAAIJ,UAAU,GAAG;YACfE,KAAKG,KAAK,CAACC,cAAc,CAAC;YAC1B;QACF;QACA,kEAAkE;QAClE,MAAMC,OAAOR,OAAOrC,OAAO;QAC3BwC,KAAKG,KAAK,CAACG,WAAW,CAAC,yBAAyB,GAAGD,KAAKE,YAAY,CAAC,EAAE,CAAC;QACxE,MAAMC,YAAY,CAACC;YACjB,IAAIA,MAAMC,GAAG,KAAK,MAAM;YACxBD,MAAME,cAAc;YACpBN,KAAKO,KAAK;QACZ;QACAX,SAASY,gBAAgB,CAAC,WAAWL;QACrC,OAAO,IAAMP,SAASa,mBAAmB,CAAC,WAAWN;IACvD,GAAG;QAACV;KAAM;IAEV,IAAI9C,WAAWJ,WAAW,OAAO;IACjC,qBACE,KAACmE;QACCC,KAAKnB;QACLoB,cAAYhC,EAAE;QACdiC,UAAU,CAAC;QACXC,eAAY;QACZ5B,WAAW9C,GACT,4FACAqD,UAAU,IAAI,2BAA2B;kBAG1CnC,MAAMyD,GAAG,CAACvD,CAAAA,sBACT,KAACkB;gBAEClB,OAAOA;gBACPmB,WAAW,IAAMhC,OAAOK,OAAO,CAACQ,MAAMP,EAAE;eAFnCO,MAAMP,EAAE;;AAOvB"}
@@ -1 +1 @@
1
- {"version":3,"file":"command-palette.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/command-palette/command-palette.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,WAAW,EACjB,MAAM,eAAe,CAAC;AAevB,oFAAoF;AACpF,MAAM,WAAW,oBAAoB;IACnC,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7C,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,+DAA+D;IAC/D,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9B;;;;;;;OAOG;IACH,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,aAAa,EAAE,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC;;;OAGG;IACH,QAAQ,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,MAAM,EAAE,oBAAoB,CAAC;CAC9B;AA4GD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,OAAO,EACP,KAAK,EACL,KAAK,EACL,aAAa,EACb,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,MAAM,GACP,EAAE,mBAAmB,+BAyIrB"}
1
+ {"version":3,"file":"command-palette.d.ts","sourceRoot":"","sources":["../../../../src/ui/organisms/command-palette/command-palette.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,WAAW,EACjB,MAAM,eAAe,CAAC;AAevB,oFAAoF;AACpF,MAAM,WAAW,oBAAoB;IACnC,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7C,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,+DAA+D;IAC/D,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9B;;;;;;;OAOG;IACH,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,aAAa,EAAE,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC;;;OAGG;IACH,QAAQ,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,MAAM,EAAE,oBAAoB,CAAC;CAC9B;AA2GD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,OAAO,EACP,KAAK,EACL,KAAK,EACL,aAAa,EACb,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,MAAM,GACP,EAAE,mBAAmB,+BAqJrB"}
@@ -43,18 +43,14 @@ function Group({ group, labels }) {
43
43
  /*#__PURE__*/ _jsx(GroupHeading, {
44
44
  label: group.label
45
45
  }),
46
- group.isLoading && /*#__PURE__*/ _jsxs("div", {
46
+ group.isLoading && // The wait is said once, by the palette's status region: a listbox
47
+ // holds options, and text inside it is read as none of them.
48
+ /*#__PURE__*/ _jsx("div", {
47
49
  role: "presentation",
48
50
  className: "px-2xs py-3xs",
49
- children: [
50
- /*#__PURE__*/ _jsx("span", {
51
- className: "sr-only",
52
- children: labels.loading
53
- }),
54
- /*#__PURE__*/ _jsx(SkeletonText, {
55
- lines: 2
56
- })
57
- ]
51
+ children: /*#__PURE__*/ _jsx(SkeletonText, {
52
+ lines: 2
53
+ })
58
54
  }),
59
55
  group.options.map((option)=>/*#__PURE__*/ _jsx(CommandRow, {
60
56
  option: option
@@ -110,6 +106,7 @@ function Group({ group, labels }) {
110
106
  // that fires after the bug is reported.
111
107
  groups.forEach((group)=>group.options.forEach(commandEffectOf));
112
108
  const isEmpty = groups.every(groupIsEmpty);
109
+ const isAnswering = groups.some((group)=>group.isLoading);
113
110
  const pop = ()=>{
114
111
  if (stack.length === 0) {
115
112
  onClose();
@@ -185,9 +182,11 @@ function Group({ group, labels }) {
185
182
  children: [
186
183
  page.title !== undefined && /*#__PURE__*/ _jsx("button", {
187
184
  type: "button",
188
- "aria-label": labels.back,
185
+ // Its name holds the text it shows (WCAG 2.5.3): "Volver" is
186
+ // said, and "Nuevo" is both seen and said.
187
+ "aria-label": `${labels.back} ${page.title}`,
189
188
  onClick: pop,
190
- className: "shrink-0 rounded-md bg-surface px-2xs py-3xs text-step-xs text-content-muted focus-ring",
189
+ className: "inline-flex min-h-(--target-min) shrink-0 items-center rounded-md bg-surface px-2xs py-3xs text-step-xs text-content-muted focus-ring",
191
190
  children: page.title
192
191
  }),
193
192
  /*#__PURE__*/ _jsx(ComboboxInput, {
@@ -201,13 +200,21 @@ function Group({ group, labels }) {
201
200
  })
202
201
  ]
203
202
  }),
203
+ /*#__PURE__*/ _jsx("span", {
204
+ role: "status",
205
+ className: "sr-only",
206
+ children: isEmpty ? labels.empty : isAnswering ? labels.loading : ''
207
+ }),
204
208
  /*#__PURE__*/ _jsx(ComboboxOptions, {
205
209
  static: true,
210
+ // Still filling: a listbox with no options yet is not a broken one.
211
+ "aria-busy": isAnswering || undefined,
206
212
  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
213
  // belongs on the row inside it.
208
214
  'focus:outline-none'),
209
215
  children: isEmpty ? /*#__PURE__*/ _jsx("div", {
210
216
  role: "presentation",
217
+ "aria-hidden": "true",
211
218
  className: "px-2xs py-s text-step-sm text-content-muted",
212
219
  children: labels.empty
213
220
  }) : groups.filter((group)=>!groupIsEmpty(group)).map((group)=>/*#__PURE__*/ _jsx(Group, {