@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
@@ -13,9 +13,9 @@ import { cn } from '../../utils/cn.js';
13
13
  * because the user walks away believing the work is done. So both counts are
14
14
  * always stated, and every failure is named with its own reason.
15
15
  *
16
- * `role="status"`, not `role="alert"`: a partial failure is an outcome to read,
17
- * not an interruption, and `alert` on a list this long is hostile. The region
18
- * is `tabIndex={-1}` so the caller can move focus to it once the run ends,
16
+ * `role="alert"` when anything failed, `role="status"` when nothing did
17
+ * (ADR 0028): rows the user believes were written and were not are news that
18
+ * cannot wait its turn. The region is `tabIndex={-1}` so the caller can move focus to it once the run ends,
19
19
  * which is the only reliable way a keyboard user learns the result arrived.
20
20
  */ export function BulkResult({ outcomes, labelFor, onRetryFailed, onDismiss, busy = false, className }) {
21
21
  const t = useT();
@@ -23,7 +23,7 @@ import { cn } from '../../utils/cn.js';
23
23
  const failed = outcomes.filter((outcome)=>!outcome.ok);
24
24
  const succeeded = succeededCount(outcomes);
25
25
  return /*#__PURE__*/ _jsxs("div", {
26
- role: "status",
26
+ role: failed.length > 0 ? 'alert' : 'status',
27
27
  tabIndex: -1,
28
28
  "aria-label": t('bulk.resultLabel'),
29
29
  "data-testid": "bulk-result",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/bulk-result/bulk-result.tsx"],"sourcesContent":["'use client';\n\nimport {\n type BulkOutcome,\n type EntityId,\n failedIds,\n succeededCount,\n} from '@entifix/core';\n\nimport { useT, useTranslateKey } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { cn } from '../../utils/cn.js';\n\nexport interface BulkResultProps {\n outcomes: readonly BulkOutcome[];\n /**\n * How a failed row is named to the user. An id is not an answer to \"which\n * ones failed\" — the caller holds the rows and can render the same label the\n * table shows. Falls back to the id when the row is no longer on the page.\n */\n labelFor?: (id: EntityId) => string | undefined;\n /** Re-runs the failures **only**. Absent hides the affordance. */\n onRetryFailed?: (ids: EntityId[]) => void;\n onDismiss: () => void;\n busy?: boolean;\n className?: string;\n}\n\n/**\n * What a bulk action actually did, per row.\n *\n * The requirement this exists for: 40 selected, 3 fail. A single notice lies\n * whichever way it is written — as a failure it hides the 37 rows that were\n * written, and as a success it hides the 3 that were not, which is worse\n * because the user walks away believing the work is done. So both counts are\n * always stated, and every failure is named with its own reason.\n *\n * `role=\"status\"`, not `role=\"alert\"`: a partial failure is an outcome to read,\n * not an interruption, and `alert` on a list this long is hostile. The region\n * is `tabIndex={-1}` so the caller can move focus to it once the run ends,\n * which is the only reliable way a keyboard user learns the result arrived.\n */\nexport function BulkResult({\n outcomes,\n labelFor,\n onRetryFailed,\n onDismiss,\n busy = false,\n className,\n}: BulkResultProps) {\n const t = useT();\n const translateKey = useTranslateKey();\n\n const failed = outcomes.filter(outcome => !outcome.ok);\n const succeeded = succeededCount(outcomes);\n\n return (\n <div\n role=\"status\"\n tabIndex={-1}\n aria-label={t('bulk.resultLabel')}\n data-testid=\"bulk-result\"\n className={cn(\n 'flex flex-col gap-2xs rounded-lg border border-border bg-surface-elevated px-s py-2xs',\n className,\n )}\n >\n <div className=\"flex flex-wrap items-center gap-2xs\">\n <span className=\"text-step-sm text-content\">\n {t('bulk.succeeded', { count: succeeded })}\n </span>\n {failed.length > 0 && (\n <span className=\"text-step-sm text-danger\">\n {t('bulk.failed', { count: failed.length })}\n </span>\n )}\n {failed.length > 0 && onRetryFailed && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n disabled={busy}\n onClick={() => onRetryFailed(failedIds(outcomes))}\n >\n {t('bulk.retryFailed')}\n </Button>\n )}\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={busy}\n onClick={onDismiss}\n >\n {t('bulk.dismiss')}\n </Button>\n </div>\n\n {failed.length > 0 && (\n <ul className=\"flex flex-col gap-3xs\">\n {failed.map(outcome => (\n <li\n key={String(outcome.id)}\n className=\"text-step-sm text-content-muted\"\n >\n <span className=\"text-content\">\n {labelFor?.(outcome.id) ?? String(outcome.id)}\n </span>\n {' — '}\n {/* The reason is a *code*, resolved through the shared `errors`\n catalog — the same vocabulary a service's `{ error, code }`\n body is rendered from, so a new failure reason is\n translatable rather than English, and a host's catalog\n check fails the build on one the catalog lacks. `unexpected` is the\n browser-side fallback the catalog already carries. */}\n {translateKey(`errors:${outcome.code ?? 'unexpected'}`)}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n"],"names":["failedIds","succeededCount","useT","useTranslateKey","Button","cn","BulkResult","outcomes","labelFor","onRetryFailed","onDismiss","busy","className","t","translateKey","failed","filter","outcome","ok","succeeded","div","role","tabIndex","aria-label","data-testid","span","count","length","type","variant","size","disabled","onClick","ul","map","li","id","String","code"],"mappings":"AAAA;;AAEA,SAGEA,SAAS,EACTC,cAAc,QACT,gBAAgB;AAEvB,SAASC,IAAI,EAAEC,eAAe,QAAQ,yBAAyB;AAC/D,SAASC,MAAM,QAAQ,8BAA8B;AACrD,SAASC,EAAE,QAAQ,oBAAoB;AAiBvC;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,WAAW,EACzBC,QAAQ,EACRC,QAAQ,EACRC,aAAa,EACbC,SAAS,EACTC,OAAO,KAAK,EACZC,SAAS,EACO;IAChB,MAAMC,IAAIX;IACV,MAAMY,eAAeX;IAErB,MAAMY,SAASR,SAASS,MAAM,CAACC,CAAAA,UAAW,CAACA,QAAQC,EAAE;IACrD,MAAMC,YAAYlB,eAAeM;IAEjC,qBACE,MAACa;QACCC,MAAK;QACLC,UAAU,CAAC;QACXC,cAAYV,EAAE;QACdW,eAAY;QACZZ,WAAWP,GACT,yFACAO;;0BAGF,MAACQ;gBAAIR,WAAU;;kCACb,KAACa;wBAAKb,WAAU;kCACbC,EAAE,kBAAkB;4BAAEa,OAAOP;wBAAU;;oBAEzCJ,OAAOY,MAAM,GAAG,mBACf,KAACF;wBAAKb,WAAU;kCACbC,EAAE,eAAe;4BAAEa,OAAOX,OAAOY,MAAM;wBAAC;;oBAG5CZ,OAAOY,MAAM,GAAG,KAAKlB,+BACpB,KAACL;wBACCwB,MAAK;wBACLC,SAAQ;wBACRC,MAAK;wBACLC,UAAUpB;wBACVqB,SAAS,IAAMvB,cAAcT,UAAUO;kCAEtCM,EAAE;;kCAGP,KAACT;wBACCwB,MAAK;wBACLC,SAAQ;wBACRC,MAAK;wBACLC,UAAUpB;wBACVqB,SAAStB;kCAERG,EAAE;;;;YAINE,OAAOY,MAAM,GAAG,mBACf,KAACM;gBAAGrB,WAAU;0BACXG,OAAOmB,GAAG,CAACjB,CAAAA;8BAegBA;yCAd1B,MAACkB;wBAECvB,WAAU;;0CAEV,KAACa;gCAAKb,WAAU;kDACbJ,4BAAAA,SAAWS,QAAQmB,EAAE,oBAAKC,OAAOpB,QAAQmB,EAAE;;4BAE7C;4BAOAtB,aAAa,CAAC,OAAO,GAAEG,gBAAAA,QAAQqB,IAAI,YAAZrB,gBAAgB,cAAc;;uBAbjDoB,OAAOpB,QAAQmB,EAAE;;;;;AAoBpC"}
1
+ {"version":3,"sources":["../../../../src/ui/molecules/bulk-result/bulk-result.tsx"],"sourcesContent":["'use client';\n\nimport {\n type BulkOutcome,\n type EntityId,\n failedIds,\n succeededCount,\n} from '@entifix/core';\n\nimport { useT, useTranslateKey } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { cn } from '../../utils/cn.js';\n\nexport interface BulkResultProps {\n outcomes: readonly BulkOutcome[];\n /**\n * How a failed row is named to the user. An id is not an answer to \"which\n * ones failed\" — the caller holds the rows and can render the same label the\n * table shows. Falls back to the id when the row is no longer on the page.\n */\n labelFor?: (id: EntityId) => string | undefined;\n /** Re-runs the failures **only**. Absent hides the affordance. */\n onRetryFailed?: (ids: EntityId[]) => void;\n onDismiss: () => void;\n busy?: boolean;\n className?: string;\n}\n\n/**\n * What a bulk action actually did, per row.\n *\n * The requirement this exists for: 40 selected, 3 fail. A single notice lies\n * whichever way it is written — as a failure it hides the 37 rows that were\n * written, and as a success it hides the 3 that were not, which is worse\n * because the user walks away believing the work is done. So both counts are\n * always stated, and every failure is named with its own reason.\n *\n * `role=\"alert\"` when anything failed, `role=\"status\"` when nothing did\n * (ADR 0028): rows the user believes were written and were not are news that\n * cannot wait its turn. The region is `tabIndex={-1}` so the caller can move focus to it once the run ends,\n * which is the only reliable way a keyboard user learns the result arrived.\n */\nexport function BulkResult({\n outcomes,\n labelFor,\n onRetryFailed,\n onDismiss,\n busy = false,\n className,\n}: BulkResultProps) {\n const t = useT();\n const translateKey = useTranslateKey();\n\n const failed = outcomes.filter(outcome => !outcome.ok);\n const succeeded = succeededCount(outcomes);\n\n return (\n <div\n role={failed.length > 0 ? 'alert' : 'status'}\n tabIndex={-1}\n aria-label={t('bulk.resultLabel')}\n data-testid=\"bulk-result\"\n className={cn(\n 'flex flex-col gap-2xs rounded-lg border border-border bg-surface-elevated px-s py-2xs',\n className,\n )}\n >\n <div className=\"flex flex-wrap items-center gap-2xs\">\n <span className=\"text-step-sm text-content\">\n {t('bulk.succeeded', { count: succeeded })}\n </span>\n {failed.length > 0 && (\n <span className=\"text-step-sm text-danger\">\n {t('bulk.failed', { count: failed.length })}\n </span>\n )}\n {failed.length > 0 && onRetryFailed && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n disabled={busy}\n onClick={() => onRetryFailed(failedIds(outcomes))}\n >\n {t('bulk.retryFailed')}\n </Button>\n )}\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={busy}\n onClick={onDismiss}\n >\n {t('bulk.dismiss')}\n </Button>\n </div>\n\n {failed.length > 0 && (\n <ul className=\"flex flex-col gap-3xs\">\n {failed.map(outcome => (\n <li\n key={String(outcome.id)}\n className=\"text-step-sm text-content-muted\"\n >\n <span className=\"text-content\">\n {labelFor?.(outcome.id) ?? String(outcome.id)}\n </span>\n {' — '}\n {/* The reason is a *code*, resolved through the shared `errors`\n catalog — the same vocabulary a service's `{ error, code }`\n body is rendered from, so a new failure reason is\n translatable rather than English, and a host's catalog\n check fails the build on one the catalog lacks. `unexpected` is the\n browser-side fallback the catalog already carries. */}\n {translateKey(`errors:${outcome.code ?? 'unexpected'}`)}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n"],"names":["failedIds","succeededCount","useT","useTranslateKey","Button","cn","BulkResult","outcomes","labelFor","onRetryFailed","onDismiss","busy","className","t","translateKey","failed","filter","outcome","ok","succeeded","div","role","length","tabIndex","aria-label","data-testid","span","count","type","variant","size","disabled","onClick","ul","map","li","id","String","code"],"mappings":"AAAA;;AAEA,SAGEA,SAAS,EACTC,cAAc,QACT,gBAAgB;AAEvB,SAASC,IAAI,EAAEC,eAAe,QAAQ,yBAAyB;AAC/D,SAASC,MAAM,QAAQ,8BAA8B;AACrD,SAASC,EAAE,QAAQ,oBAAoB;AAiBvC;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,WAAW,EACzBC,QAAQ,EACRC,QAAQ,EACRC,aAAa,EACbC,SAAS,EACTC,OAAO,KAAK,EACZC,SAAS,EACO;IAChB,MAAMC,IAAIX;IACV,MAAMY,eAAeX;IAErB,MAAMY,SAASR,SAASS,MAAM,CAACC,CAAAA,UAAW,CAACA,QAAQC,EAAE;IACrD,MAAMC,YAAYlB,eAAeM;IAEjC,qBACE,MAACa;QACCC,MAAMN,OAAOO,MAAM,GAAG,IAAI,UAAU;QACpCC,UAAU,CAAC;QACXC,cAAYX,EAAE;QACdY,eAAY;QACZb,WAAWP,GACT,yFACAO;;0BAGF,MAACQ;gBAAIR,WAAU;;kCACb,KAACc;wBAAKd,WAAU;kCACbC,EAAE,kBAAkB;4BAAEc,OAAOR;wBAAU;;oBAEzCJ,OAAOO,MAAM,GAAG,mBACf,KAACI;wBAAKd,WAAU;kCACbC,EAAE,eAAe;4BAAEc,OAAOZ,OAAOO,MAAM;wBAAC;;oBAG5CP,OAAOO,MAAM,GAAG,KAAKb,+BACpB,KAACL;wBACCwB,MAAK;wBACLC,SAAQ;wBACRC,MAAK;wBACLC,UAAUpB;wBACVqB,SAAS,IAAMvB,cAAcT,UAAUO;kCAEtCM,EAAE;;kCAGP,KAACT;wBACCwB,MAAK;wBACLC,SAAQ;wBACRC,MAAK;wBACLC,UAAUpB;wBACVqB,SAAStB;kCAERG,EAAE;;;;YAINE,OAAOO,MAAM,GAAG,mBACf,KAACW;gBAAGrB,WAAU;0BACXG,OAAOmB,GAAG,CAACjB,CAAAA;8BAegBA;yCAd1B,MAACkB;wBAECvB,WAAU;;0CAEV,KAACc;gCAAKd,WAAU;kDACbJ,4BAAAA,SAAWS,QAAQmB,EAAE,oBAAKC,OAAOpB,QAAQmB,EAAE;;4BAE7C;4BAOAtB,aAAa,CAAC,OAAO,GAAEG,gBAAAA,QAAQqB,IAAI,YAAZrB,gBAAgB,cAAc;;uBAbjDoB,OAAOpB,QAAQmB,EAAE;;;;;AAoBpC"}
@@ -1,31 +1,46 @@
1
1
  export type ConfirmTone = 'destructive' | 'neutral';
2
2
  export interface ConfirmDialogProps {
3
3
  open: boolean;
4
- /** Already-resolved copy — this control renders text, it does not translate. */
4
+ /**
5
+ * Already-resolved copy — this control renders text, it does not translate.
6
+ * A question naming what is affected and how many: "Delete Acme?",
7
+ * "Archive 12 invoices?".
8
+ */
5
9
  title: string;
10
+ /** The consequence, in one or two sentences. */
6
11
  message: string;
7
12
  /** `destructive` paints the confirm button in the danger tokens. */
8
13
  tone?: ConfirmTone;
14
+ /**
15
+ * The confirming button. Repeat the verb, with the count when there is more
16
+ * than one — "Delete 12" — so the reader does not go back to the title to
17
+ * learn what they are agreeing to. Defaults to "Confirm".
18
+ */
9
19
  confirmLabel?: string;
10
20
  cancelLabel?: string;
21
+ /**
22
+ * Asks the reader to type this — the record's name, or the count — before
23
+ * the confirming button enables: for a destruction too large to be one
24
+ * misplaced click.
25
+ */
26
+ typed?: string;
11
27
  busy?: boolean;
12
28
  onConfirm: () => void;
13
29
  onCancel: () => void;
14
30
  }
15
31
  /**
16
- * Asks before an irreversible act.
32
+ * Asks before an irreversible act
33
+ * ([ADR 0026](../../../../../../../docs/adr/0026-actions-hierarchy-overflow-and-destructive-verbs.md)).
17
34
  *
18
- * It exists because a use-case descriptor may carry
19
- * `confirm: { tone, messageKey }`, and a surface that renders such a descriptor
20
- * without asking would fire the act on the first click — `revoke-sessions` ends
21
- * every session a user holds. The descriptor names the tone and the message key;
22
- * resolving that key is the caller's job, so this control stays copy-free apart
23
- * from its own two buttons.
35
+ * An `alertdialog`, not a `dialog`: it interrupts to ask a question that must
36
+ * be answered. Its message is wired as the dialog's description, so it is read
37
+ * with the title on opening, and **Cancel takes the initial focus** — Enter on
38
+ * a dialog that opened under the reader's hand keeps what they have.
24
39
  *
25
40
  * The dialog element fills the viewport rather than wrapping a `fixed` child in
26
41
  * a `relative` box, for the reason `EntityLinkPicker` records: a zero-sized
27
42
  * dialog is invisible to anything that measures it — assistive tech and
28
43
  * Playwright alike — even with a painted panel inside.
29
44
  */
30
- export declare function ConfirmDialog({ open, title, message, tone, confirmLabel, cancelLabel, busy, onConfirm, onCancel, }: ConfirmDialogProps): import("react").JSX.Element;
45
+ export declare function ConfirmDialog({ open, title, message, tone, confirmLabel, cancelLabel, typed, busy, onConfirm, onCancel, }: ConfirmDialogProps): import("react").JSX.Element;
31
46
  //# sourceMappingURL=confirm-dialog.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"confirm-dialog.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/confirm-dialog/confirm-dialog.tsx"],"names":[],"mappings":"AAOA,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,SAAS,CAAC;AAEpD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,OAAO,EACP,IAAgB,EAChB,YAAY,EACZ,WAAW,EACX,IAAY,EACZ,SAAS,EACT,QAAQ,GACT,EAAE,kBAAkB,+BAuCpB"}
1
+ {"version":3,"file":"confirm-dialog.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/confirm-dialog/confirm-dialog.tsx"],"names":[],"mappings":"AAcA,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,SAAS,CAAC;AAEpD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,CAAC;IACd;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,OAAO,EACP,IAAgB,EAChB,YAAY,EACZ,WAAW,EACX,KAAK,EACL,IAAY,EACZ,SAAS,EACT,QAAQ,GACT,EAAE,kBAAkB,+BA4DpB"}
@@ -1,26 +1,31 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Dialog, DialogPanel, DialogTitle } from '@headlessui/react';
3
+ import { Dialog, DialogDescription, DialogPanel, DialogTitle } from '@headlessui/react';
4
+ import { useId, useState } from 'react';
4
5
  import { useT } from '../../../i18n/index.js';
5
6
  import { Button } from '../../atoms/button/index.js';
7
+ import { TextInput } from '../../atoms/field/index.js';
6
8
  /**
7
- * Asks before an irreversible act.
9
+ * Asks before an irreversible act
10
+ * ([ADR 0026](../../../../../../../docs/adr/0026-actions-hierarchy-overflow-and-destructive-verbs.md)).
8
11
  *
9
- * It exists because a use-case descriptor may carry
10
- * `confirm: { tone, messageKey }`, and a surface that renders such a descriptor
11
- * without asking would fire the act on the first click — `revoke-sessions` ends
12
- * every session a user holds. The descriptor names the tone and the message key;
13
- * resolving that key is the caller's job, so this control stays copy-free apart
14
- * from its own two buttons.
12
+ * An `alertdialog`, not a `dialog`: it interrupts to ask a question that must
13
+ * be answered. Its message is wired as the dialog's description, so it is read
14
+ * with the title on opening, and **Cancel takes the initial focus** — Enter on
15
+ * a dialog that opened under the reader's hand keeps what they have.
15
16
  *
16
17
  * The dialog element fills the viewport rather than wrapping a `fixed` child in
17
18
  * a `relative` box, for the reason `EntityLinkPicker` records: a zero-sized
18
19
  * dialog is invisible to anything that measures it — assistive tech and
19
20
  * Playwright alike — even with a painted panel inside.
20
- */ export function ConfirmDialog({ open, title, message, tone = 'neutral', confirmLabel, cancelLabel, busy = false, onConfirm, onCancel }) {
21
+ */ export function ConfirmDialog({ open, title, message, tone = 'neutral', confirmLabel, cancelLabel, typed, busy = false, onConfirm, onCancel }) {
21
22
  const t = useT();
23
+ const inputId = useId();
24
+ const [entered, setEntered] = useState('');
25
+ const matches = typed === undefined || entered.trim() === typed;
22
26
  return /*#__PURE__*/ _jsxs(Dialog, {
23
27
  open: open,
28
+ role: "alertdialog",
24
29
  onClose: onCancel,
25
30
  className: "inset-0 fixed z-(--z-overlay) flex items-center justify-center overflow-y-auto p-s",
26
31
  children: [
@@ -36,16 +41,36 @@ import { Button } from '../../atoms/button/index.js';
36
41
  className: "text-step-1 font-semibold text-content",
37
42
  children: title
38
43
  }),
39
- /*#__PURE__*/ _jsx("p", {
44
+ /*#__PURE__*/ _jsx(DialogDescription, {
40
45
  className: "mt-2xs text-step-sm text-content-muted",
41
46
  children: message
42
47
  }),
48
+ typed !== undefined && /*#__PURE__*/ _jsxs("div", {
49
+ className: "mt-s flex flex-col gap-3xs",
50
+ children: [
51
+ /*#__PURE__*/ _jsx("label", {
52
+ htmlFor: inputId,
53
+ className: "text-step-sm text-content",
54
+ children: t('confirm.typeToConfirm', {
55
+ expected: typed
56
+ })
57
+ }),
58
+ /*#__PURE__*/ _jsx(TextInput, {
59
+ id: inputId,
60
+ autoComplete: "off",
61
+ spellCheck: false,
62
+ value: entered,
63
+ onChange: (event)=>setEntered(event.currentTarget.value)
64
+ })
65
+ ]
66
+ }),
43
67
  /*#__PURE__*/ _jsxs("div", {
44
68
  className: "mt-s flex justify-end gap-xs",
45
69
  children: [
46
70
  /*#__PURE__*/ _jsx(Button, {
47
71
  type: "button",
48
72
  variant: "ghost",
73
+ "data-autofocus": true,
49
74
  onClick: onCancel,
50
75
  disabled: busy,
51
76
  children: cancelLabel != null ? cancelLabel : t('confirm.cancel')
@@ -54,7 +79,7 @@ import { Button } from '../../atoms/button/index.js';
54
79
  type: "button",
55
80
  variant: tone === 'destructive' ? 'destructive' : 'primary',
56
81
  onClick: onConfirm,
57
- disabled: busy,
82
+ disabled: busy || !matches,
58
83
  children: confirmLabel != null ? confirmLabel : t('confirm.confirm')
59
84
  })
60
85
  ]
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/confirm-dialog/confirm-dialog.tsx"],"sourcesContent":["'use client';\n\nimport { Dialog, DialogPanel, DialogTitle } from '@headlessui/react';\n\nimport { useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\n\nexport type ConfirmTone = 'destructive' | 'neutral';\n\nexport interface ConfirmDialogProps {\n open: boolean;\n /** Already-resolved copy — this control renders text, it does not translate. */\n title: string;\n message: string;\n /** `destructive` paints the confirm button in the danger tokens. */\n tone?: ConfirmTone;\n confirmLabel?: string;\n cancelLabel?: string;\n busy?: boolean;\n onConfirm: () => void;\n onCancel: () => void;\n}\n\n/**\n * Asks before an irreversible act.\n *\n * It exists because a use-case descriptor may carry\n * `confirm: { tone, messageKey }`, and a surface that renders such a descriptor\n * without asking would fire the act on the first click — `revoke-sessions` ends\n * every session a user holds. The descriptor names the tone and the message key;\n * resolving that key is the caller's job, so this control stays copy-free apart\n * from its own two buttons.\n *\n * The dialog element fills the viewport rather than wrapping a `fixed` child in\n * a `relative` box, for the reason `EntityLinkPicker` records: a zero-sized\n * dialog is invisible to anything that measures it — assistive tech and\n * Playwright alike — even with a painted panel inside.\n */\nexport function ConfirmDialog({\n open,\n title,\n message,\n tone = 'neutral',\n confirmLabel,\n cancelLabel,\n busy = false,\n onConfirm,\n onCancel,\n}: ConfirmDialogProps) {\n const t = useT();\n\n return (\n <Dialog\n open={open}\n onClose={onCancel}\n className=\"inset-0 fixed z-(--z-overlay) flex items-center justify-center overflow-y-auto p-s\"\n >\n <div aria-hidden className=\"inset-0 fixed bg-scrim\" />\n <DialogPanel\n data-testid=\"confirm-dialog\"\n className=\"max-w-md relative w-full rounded-lg border border-border bg-surface p-s shadow-overlay\"\n >\n <DialogTitle className=\"text-step-1 font-semibold text-content\">\n {title}\n </DialogTitle>\n <p className=\"mt-2xs text-step-sm text-content-muted\">{message}</p>\n <div className=\"mt-s flex justify-end gap-xs\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={onCancel}\n disabled={busy}\n >\n {cancelLabel ?? t('confirm.cancel')}\n </Button>\n <Button\n type=\"button\"\n variant={tone === 'destructive' ? 'destructive' : 'primary'}\n onClick={onConfirm}\n disabled={busy}\n >\n {confirmLabel ?? t('confirm.confirm')}\n </Button>\n </div>\n </DialogPanel>\n </Dialog>\n );\n}\n"],"names":["Dialog","DialogPanel","DialogTitle","useT","Button","ConfirmDialog","open","title","message","tone","confirmLabel","cancelLabel","busy","onConfirm","onCancel","t","onClose","className","div","aria-hidden","data-testid","p","type","variant","onClick","disabled"],"mappings":"AAAA;;AAEA,SAASA,MAAM,EAAEC,WAAW,EAAEC,WAAW,QAAQ,oBAAoB;AAErE,SAASC,IAAI,QAAQ,yBAAyB;AAC9C,SAASC,MAAM,QAAQ,8BAA8B;AAkBrD;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,cAAc,EAC5BC,IAAI,EACJC,KAAK,EACLC,OAAO,EACPC,OAAO,SAAS,EAChBC,YAAY,EACZC,WAAW,EACXC,OAAO,KAAK,EACZC,SAAS,EACTC,QAAQ,EACW;IACnB,MAAMC,IAAIZ;IAEV,qBACE,MAACH;QACCM,MAAMA;QACNU,SAASF;QACTG,WAAU;;0BAEV,KAACC;gBAAIC,aAAW;gBAACF,WAAU;;0BAC3B,MAAChB;gBACCmB,eAAY;gBACZH,WAAU;;kCAEV,KAACf;wBAAYe,WAAU;kCACpBV;;kCAEH,KAACc;wBAAEJ,WAAU;kCAA0CT;;kCACvD,MAACU;wBAAID,WAAU;;0CACb,KAACb;gCACCkB,MAAK;gCACLC,SAAQ;gCACRC,SAASV;gCACTW,UAAUb;0CAETD,sBAAAA,cAAeI,EAAE;;0CAEpB,KAACX;gCACCkB,MAAK;gCACLC,SAASd,SAAS,gBAAgB,gBAAgB;gCAClDe,SAASX;gCACTY,UAAUb;0CAETF,uBAAAA,eAAgBK,EAAE;;;;;;;;AAM/B"}
1
+ {"version":3,"sources":["../../../../src/ui/molecules/confirm-dialog/confirm-dialog.tsx"],"sourcesContent":["'use client';\n\nimport {\n Dialog,\n DialogDescription,\n DialogPanel,\n DialogTitle,\n} from '@headlessui/react';\nimport { useId, useState } from 'react';\n\nimport { useT } from '../../../i18n/index.js';\nimport { Button } from '../../atoms/button/index.js';\nimport { TextInput } from '../../atoms/field/index.js';\n\nexport type ConfirmTone = 'destructive' | 'neutral';\n\nexport interface ConfirmDialogProps {\n open: boolean;\n /**\n * Already-resolved copy — this control renders text, it does not translate.\n * A question naming what is affected and how many: \"Delete Acme?\",\n * \"Archive 12 invoices?\".\n */\n title: string;\n /** The consequence, in one or two sentences. */\n message: string;\n /** `destructive` paints the confirm button in the danger tokens. */\n tone?: ConfirmTone;\n /**\n * The confirming button. Repeat the verb, with the count when there is more\n * than one — \"Delete 12\" — so the reader does not go back to the title to\n * learn what they are agreeing to. Defaults to \"Confirm\".\n */\n confirmLabel?: string;\n cancelLabel?: string;\n /**\n * Asks the reader to type this — the record's name, or the count — before\n * the confirming button enables: for a destruction too large to be one\n * misplaced click.\n */\n typed?: string;\n busy?: boolean;\n onConfirm: () => void;\n onCancel: () => void;\n}\n\n/**\n * Asks before an irreversible act\n * ([ADR 0026](../../../../../../../docs/adr/0026-actions-hierarchy-overflow-and-destructive-verbs.md)).\n *\n * An `alertdialog`, not a `dialog`: it interrupts to ask a question that must\n * be answered. Its message is wired as the dialog's description, so it is read\n * with the title on opening, and **Cancel takes the initial focus** — Enter on\n * a dialog that opened under the reader's hand keeps what they have.\n *\n * The dialog element fills the viewport rather than wrapping a `fixed` child in\n * a `relative` box, for the reason `EntityLinkPicker` records: a zero-sized\n * dialog is invisible to anything that measures it — assistive tech and\n * Playwright alike — even with a painted panel inside.\n */\nexport function ConfirmDialog({\n open,\n title,\n message,\n tone = 'neutral',\n confirmLabel,\n cancelLabel,\n typed,\n busy = false,\n onConfirm,\n onCancel,\n}: ConfirmDialogProps) {\n const t = useT();\n const inputId = useId();\n const [entered, setEntered] = useState('');\n const matches = typed === undefined || entered.trim() === typed;\n\n return (\n <Dialog\n open={open}\n role=\"alertdialog\"\n onClose={onCancel}\n className=\"inset-0 fixed z-(--z-overlay) flex items-center justify-center overflow-y-auto p-s\"\n >\n <div aria-hidden className=\"inset-0 fixed bg-scrim\" />\n <DialogPanel\n data-testid=\"confirm-dialog\"\n className=\"max-w-md relative w-full rounded-lg border border-border bg-surface p-s shadow-overlay\"\n >\n <DialogTitle className=\"text-step-1 font-semibold text-content\">\n {title}\n </DialogTitle>\n <DialogDescription className=\"mt-2xs text-step-sm text-content-muted\">\n {message}\n </DialogDescription>\n {typed !== undefined && (\n <div className=\"mt-s flex flex-col gap-3xs\">\n <label htmlFor={inputId} className=\"text-step-sm text-content\">\n {t('confirm.typeToConfirm', { expected: typed })}\n </label>\n <TextInput\n id={inputId}\n autoComplete=\"off\"\n spellCheck={false}\n value={entered}\n onChange={event => setEntered(event.currentTarget.value)}\n />\n </div>\n )}\n <div className=\"mt-s flex justify-end gap-xs\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n data-autofocus\n onClick={onCancel}\n disabled={busy}\n >\n {cancelLabel ?? t('confirm.cancel')}\n </Button>\n <Button\n type=\"button\"\n variant={tone === 'destructive' ? 'destructive' : 'primary'}\n onClick={onConfirm}\n disabled={busy || !matches}\n >\n {confirmLabel ?? t('confirm.confirm')}\n </Button>\n </div>\n </DialogPanel>\n </Dialog>\n );\n}\n"],"names":["Dialog","DialogDescription","DialogPanel","DialogTitle","useId","useState","useT","Button","TextInput","ConfirmDialog","open","title","message","tone","confirmLabel","cancelLabel","typed","busy","onConfirm","onCancel","t","inputId","entered","setEntered","matches","undefined","trim","role","onClose","className","div","aria-hidden","data-testid","label","htmlFor","expected","id","autoComplete","spellCheck","value","onChange","event","currentTarget","type","variant","data-autofocus","onClick","disabled"],"mappings":"AAAA;;AAEA,SACEA,MAAM,EACNC,iBAAiB,EACjBC,WAAW,EACXC,WAAW,QACN,oBAAoB;AAC3B,SAASC,KAAK,EAAEC,QAAQ,QAAQ,QAAQ;AAExC,SAASC,IAAI,QAAQ,yBAAyB;AAC9C,SAASC,MAAM,QAAQ,8BAA8B;AACrD,SAASC,SAAS,QAAQ,6BAA6B;AAkCvD;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,cAAc,EAC5BC,IAAI,EACJC,KAAK,EACLC,OAAO,EACPC,OAAO,SAAS,EAChBC,YAAY,EACZC,WAAW,EACXC,KAAK,EACLC,OAAO,KAAK,EACZC,SAAS,EACTC,QAAQ,EACW;IACnB,MAAMC,IAAId;IACV,MAAMe,UAAUjB;IAChB,MAAM,CAACkB,SAASC,WAAW,GAAGlB,SAAS;IACvC,MAAMmB,UAAUR,UAAUS,aAAaH,QAAQI,IAAI,OAAOV;IAE1D,qBACE,MAAChB;QACCU,MAAMA;QACNiB,MAAK;QACLC,SAAST;QACTU,WAAU;;0BAEV,KAACC;gBAAIC,aAAW;gBAACF,WAAU;;0BAC3B,MAAC3B;gBACC8B,eAAY;gBACZH,WAAU;;kCAEV,KAAC1B;wBAAY0B,WAAU;kCACpBlB;;kCAEH,KAACV;wBAAkB4B,WAAU;kCAC1BjB;;oBAEFI,UAAUS,2BACT,MAACK;wBAAID,WAAU;;0CACb,KAACI;gCAAMC,SAASb;gCAASQ,WAAU;0CAChCT,EAAE,yBAAyB;oCAAEe,UAAUnB;gCAAM;;0CAEhD,KAACR;gCACC4B,IAAIf;gCACJgB,cAAa;gCACbC,YAAY;gCACZC,OAAOjB;gCACPkB,UAAUC,CAAAA,QAASlB,WAAWkB,MAAMC,aAAa,CAACH,KAAK;;;;kCAI7D,MAACT;wBAAID,WAAU;;0CACb,KAACtB;gCACCoC,MAAK;gCACLC,SAAQ;gCACRC,gBAAc;gCACdC,SAAS3B;gCACT4B,UAAU9B;0CAETF,sBAAAA,cAAeK,EAAE;;0CAEpB,KAACb;gCACCoC,MAAK;gCACLC,SAAS/B,SAAS,gBAAgB,gBAAgB;gCAClDiC,SAAS5B;gCACT6B,UAAU9B,QAAQ,CAACO;0CAElBV,uBAAAA,eAAgBM,EAAE;;;;;;;;AAM/B"}
@@ -1,30 +1,43 @@
1
- import type { ReactNode } from 'react';
2
- export interface EntityRecordCardColumn {
3
- name: string;
4
- label: string;
1
+ import { type ReactNode } from 'react';
2
+ /** One line of a card: a member's label and its value. */
3
+ export interface EntityRecordCardMeta {
4
+ key: string;
5
+ label: ReactNode;
6
+ value: ReactNode;
5
7
  }
6
8
  export interface EntityRecordCardProps {
7
- columns: readonly EntityRecordCardColumn[];
8
- /** Renders one column's cell. Supplied by the caller so a card and a table
9
- * row stay in agreement about custom renderers. */
10
- renderCell: (column: EntityRecordCardColumn) => ReactNode;
11
- /** Optional trailing actions (a link to the record, …). */
12
- actions?: ReactNode;
13
9
  /**
14
- * Rendered above the fields — the card's equivalent of a leading column.
15
- *
16
- * A selection box goes here rather than beside the actions because it is not
17
- * an action on the record: it decides whether the record is *in the set* the
18
- * actions will run over, and putting the two in one row reads as a third
19
- * button.
10
+ * The record's heading — what it is read as, usually a link to it. A link
11
+ * here is stretched over the whole card, so the card is one target.
12
+ */
13
+ title: ReactNode;
14
+ /** A line under the heading. */
15
+ subtitle?: ReactNode;
16
+ /** Short values beside the heading — a status. */
17
+ badges?: readonly {
18
+ key: string;
19
+ value: ReactNode;
20
+ }[];
21
+ /** The record's other members, as a description list. */
22
+ meta: readonly EntityRecordCardMeta[];
23
+ /**
24
+ * Above the heading: a selection box. It is not an action on the record —
25
+ * it decides whether the record is *in the set* the actions run over — so it
26
+ * does not sit with them.
20
27
  */
21
28
  leading?: ReactNode;
29
+ /** Above the heading, at the end: the record's menu. */
30
+ actions?: ReactNode;
22
31
  className?: string;
23
32
  }
24
33
  /**
25
- * A record pivoted from a row into a label/value stack. This is the narrow
26
- * viewport form of a table row: at phone widths a horizontal scroll hides most
27
- * columns, whereas stacking keeps every visible column readable.
34
+ * A record pivoted from a row into a card: the narrow form of a table row,
35
+ * where a horizontal scroll would hide most columns.
36
+ *
37
+ * An `article` named by its `h3`, so a reader can move card by card with the
38
+ * heading keys and hears what each record is. The title's link is stretched
39
+ * over the card; the selection box and the menu sit above it, so they stay
40
+ * their own targets.
28
41
  */
29
- export declare function EntityRecordCard({ columns, renderCell, actions, leading, className, }: EntityRecordCardProps): import("react").JSX.Element;
42
+ export declare function EntityRecordCard({ title, subtitle, badges, meta, leading, actions, className, }: EntityRecordCardProps): import("react").JSX.Element;
30
43
  //# sourceMappingURL=entity-record-card.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"entity-record-card.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/entity-record-card/entity-record-card.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC3C;wDACoD;IACpD,UAAU,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,SAAS,CAAC;IAC1D,2DAA2D;IAC3D,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,UAAU,EACV,OAAO,EACP,OAAO,EACP,SAAS,GACV,EAAE,qBAAqB,+BAyBvB"}
1
+ {"version":3,"file":"entity-record-card.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/entity-record-card/entity-record-card.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAS,MAAM,OAAO,CAAC;AAI9C,0DAA0D;AAC1D,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,KAAK,EAAE,SAAS,CAAC;IACjB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,kDAAkD;IAClD,MAAM,CAAC,EAAE,SAAS;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,EAAE,CAAC;IACtD,yDAAyD;IACzD,IAAI,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACtC;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,wDAAwD;IACxD,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,QAAQ,EACR,MAAW,EACX,IAAI,EACJ,OAAO,EACP,OAAO,EACP,SAAS,GACV,EAAE,qBAAqB,+BAyDvB"}
@@ -1,36 +1,69 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from 'react';
2
3
  import { cn } from '../../utils/cn.js';
3
4
  /**
4
- * A record pivoted from a row into a label/value stack. This is the narrow
5
- * viewport form of a table row: at phone widths a horizontal scroll hides most
6
- * columns, whereas stacking keeps every visible column readable.
7
- */ export function EntityRecordCard({ columns, renderCell, actions, leading, className }) {
8
- return /*#__PURE__*/ _jsxs("div", {
9
- className: cn('rounded-xl border border-border bg-surface-elevated p-s text-content shadow-edge', className),
5
+ * A record pivoted from a row into a card: the narrow form of a table row,
6
+ * where a horizontal scroll would hide most columns.
7
+ *
8
+ * An `article` named by its `h3`, so a reader can move card by card with the
9
+ * heading keys and hears what each record is. The title's link is stretched
10
+ * over the card; the selection box and the menu sit above it, so they stay
11
+ * their own targets.
12
+ */ export function EntityRecordCard({ title, subtitle, badges = [], meta, leading, actions, className }) {
13
+ const titleId = useId();
14
+ return /*#__PURE__*/ _jsxs("article", {
15
+ "aria-labelledby": titleId,
16
+ className: cn('relative rounded-xl border border-border bg-surface-elevated p-s text-content shadow-edge focus-within:border-primary', className),
10
17
  children: [
11
- leading && /*#__PURE__*/ _jsx("div", {
12
- className: "mb-2xs flex",
13
- children: leading
18
+ (leading !== undefined || actions !== undefined) && /*#__PURE__*/ _jsxs("div", {
19
+ className: "relative z-1 mb-2xs flex items-center justify-between gap-2xs",
20
+ children: [
21
+ /*#__PURE__*/ _jsx("div", {
22
+ className: "flex",
23
+ children: leading
24
+ }),
25
+ /*#__PURE__*/ _jsx("div", {
26
+ className: "flex",
27
+ children: actions
28
+ })
29
+ ]
14
30
  }),
15
- /*#__PURE__*/ _jsx("dl", {
16
- className: "flex flex-col gap-2xs",
17
- children: columns.map((column)=>/*#__PURE__*/ _jsxs("div", {
31
+ /*#__PURE__*/ _jsxs("div", {
32
+ className: "flex flex-wrap items-baseline justify-between gap-2xs",
33
+ children: [
34
+ /*#__PURE__*/ _jsx("h3", {
35
+ id: titleId,
36
+ className: "m-0 [&_a]:after:inset-0 text-step-0 font-semibold [&_a]:after:absolute [&_a]:after:content-['']",
37
+ children: title
38
+ }),
39
+ badges.length > 0 && /*#__PURE__*/ _jsx("div", {
40
+ className: "flex flex-wrap gap-3xs",
41
+ children: badges.map((badge)=>/*#__PURE__*/ _jsx("span", {
42
+ className: "rounded-full border border-border px-2xs text-step-sm",
43
+ children: badge.value
44
+ }, badge.key))
45
+ })
46
+ ]
47
+ }),
48
+ subtitle !== undefined && /*#__PURE__*/ _jsx("p", {
49
+ className: "m-0 text-step-sm text-content-muted",
50
+ children: subtitle
51
+ }),
52
+ meta.length > 0 && /*#__PURE__*/ _jsx("dl", {
53
+ className: "mt-2xs flex flex-col gap-2xs",
54
+ children: meta.map((line)=>/*#__PURE__*/ _jsxs("div", {
18
55
  className: "flex items-baseline justify-between gap-2xs",
19
56
  children: [
20
57
  /*#__PURE__*/ _jsx("dt", {
21
58
  className: "tracking-wide text-step-xs text-content-muted uppercase",
22
- children: column.label
59
+ children: line.label
23
60
  }),
24
61
  /*#__PURE__*/ _jsx("dd", {
25
- className: "text-right text-step-sm",
26
- children: renderCell(column)
62
+ className: "m-0 text-right text-step-sm",
63
+ children: line.value
27
64
  })
28
65
  ]
29
- }, column.name))
30
- }),
31
- actions && /*#__PURE__*/ _jsx("div", {
32
- className: "mt-2xs flex justify-end",
33
- children: actions
66
+ }, line.key))
34
67
  })
35
68
  ]
36
69
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/molecules/entity-record-card/entity-record-card.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\n\nimport { cn } from '../../utils/cn.js';\n\nexport interface EntityRecordCardColumn {\n name: string;\n label: string;\n}\n\nexport interface EntityRecordCardProps {\n columns: readonly EntityRecordCardColumn[];\n /** Renders one column's cell. Supplied by the caller so a card and a table\n * row stay in agreement about custom renderers. */\n renderCell: (column: EntityRecordCardColumn) => ReactNode;\n /** Optional trailing actions (a link to the record, …). */\n actions?: ReactNode;\n /**\n * Rendered above the fields — the card's equivalent of a leading column.\n *\n * A selection box goes here rather than beside the actions because it is not\n * an action on the record: it decides whether the record is *in the set* the\n * actions will run over, and putting the two in one row reads as a third\n * button.\n */\n leading?: ReactNode;\n className?: string;\n}\n\n/**\n * A record pivoted from a row into a label/value stack. This is the narrow\n * viewport form of a table row: at phone widths a horizontal scroll hides most\n * columns, whereas stacking keeps every visible column readable.\n */\nexport function EntityRecordCard({\n columns,\n renderCell,\n actions,\n leading,\n className,\n}: EntityRecordCardProps) {\n return (\n <div\n className={cn(\n 'rounded-xl border border-border bg-surface-elevated p-s text-content shadow-edge',\n className,\n )}\n >\n {leading && <div className=\"mb-2xs flex\">{leading}</div>}\n <dl className=\"flex flex-col gap-2xs\">\n {columns.map(column => (\n <div\n key={column.name}\n className=\"flex items-baseline justify-between gap-2xs\"\n >\n <dt className=\"tracking-wide text-step-xs text-content-muted uppercase\">\n {column.label}\n </dt>\n <dd className=\"text-right text-step-sm\">{renderCell(column)}</dd>\n </div>\n ))}\n </dl>\n {actions && <div className=\"mt-2xs flex justify-end\">{actions}</div>}\n </div>\n );\n}\n"],"names":["cn","EntityRecordCard","columns","renderCell","actions","leading","className","div","dl","map","column","dt","label","dd","name"],"mappings":";AAEA,SAASA,EAAE,QAAQ,oBAAoB;AA0BvC;;;;CAIC,GACD,OAAO,SAASC,iBAAiB,EAC/BC,OAAO,EACPC,UAAU,EACVC,OAAO,EACPC,OAAO,EACPC,SAAS,EACa;IACtB,qBACE,MAACC;QACCD,WAAWN,GACT,oFACAM;;YAGDD,yBAAW,KAACE;gBAAID,WAAU;0BAAeD;;0BAC1C,KAACG;gBAAGF,WAAU;0BACXJ,QAAQO,GAAG,CAACC,CAAAA,uBACX,MAACH;wBAECD,WAAU;;0CAEV,KAACK;gCAAGL,WAAU;0CACXI,OAAOE,KAAK;;0CAEf,KAACC;gCAAGP,WAAU;0CAA2BH,WAAWO;;;uBAN/CA,OAAOI,IAAI;;YAUrBV,yBAAW,KAACG;gBAAID,WAAU;0BAA2BF;;;;AAG5D"}
1
+ {"version":3,"sources":["../../../../src/ui/molecules/entity-record-card/entity-record-card.tsx"],"sourcesContent":["import { type ReactNode, useId } from 'react';\n\nimport { cn } from '../../utils/cn.js';\n\n/** One line of a card: a member's label and its value. */\nexport interface EntityRecordCardMeta {\n key: string;\n label: ReactNode;\n value: ReactNode;\n}\n\nexport interface EntityRecordCardProps {\n /**\n * The record's heading — what it is read as, usually a link to it. A link\n * here is stretched over the whole card, so the card is one target.\n */\n title: ReactNode;\n /** A line under the heading. */\n subtitle?: ReactNode;\n /** Short values beside the heading — a status. */\n badges?: readonly { key: string; value: ReactNode }[];\n /** The record's other members, as a description list. */\n meta: readonly EntityRecordCardMeta[];\n /**\n * Above the heading: a selection box. It is not an action on the record —\n * it decides whether the record is *in the set* the actions run over — so it\n * does not sit with them.\n */\n leading?: ReactNode;\n /** Above the heading, at the end: the record's menu. */\n actions?: ReactNode;\n className?: string;\n}\n\n/**\n * A record pivoted from a row into a card: the narrow form of a table row,\n * where a horizontal scroll would hide most columns.\n *\n * An `article` named by its `h3`, so a reader can move card by card with the\n * heading keys and hears what each record is. The title's link is stretched\n * over the card; the selection box and the menu sit above it, so they stay\n * their own targets.\n */\nexport function EntityRecordCard({\n title,\n subtitle,\n badges = [],\n meta,\n leading,\n actions,\n className,\n}: EntityRecordCardProps) {\n const titleId = useId();\n\n return (\n <article\n aria-labelledby={titleId}\n className={cn(\n 'relative rounded-xl border border-border bg-surface-elevated p-s text-content shadow-edge focus-within:border-primary',\n className,\n )}\n >\n {(leading !== undefined || actions !== undefined) && (\n <div className=\"relative z-1 mb-2xs flex items-center justify-between gap-2xs\">\n <div className=\"flex\">{leading}</div>\n <div className=\"flex\">{actions}</div>\n </div>\n )}\n <div className=\"flex flex-wrap items-baseline justify-between gap-2xs\">\n <h3\n id={titleId}\n className=\"m-0 [&_a]:after:inset-0 text-step-0 font-semibold [&_a]:after:absolute [&_a]:after:content-['']\"\n >\n {title}\n </h3>\n {badges.length > 0 && (\n <div className=\"flex flex-wrap gap-3xs\">\n {badges.map(badge => (\n <span\n key={badge.key}\n className=\"rounded-full border border-border px-2xs text-step-sm\"\n >\n {badge.value}\n </span>\n ))}\n </div>\n )}\n </div>\n {subtitle !== undefined && (\n <p className=\"m-0 text-step-sm text-content-muted\">{subtitle}</p>\n )}\n {meta.length > 0 && (\n <dl className=\"mt-2xs flex flex-col gap-2xs\">\n {meta.map(line => (\n <div\n key={line.key}\n className=\"flex items-baseline justify-between gap-2xs\"\n >\n <dt className=\"tracking-wide text-step-xs text-content-muted uppercase\">\n {line.label}\n </dt>\n <dd className=\"m-0 text-right text-step-sm\">{line.value}</dd>\n </div>\n ))}\n </dl>\n )}\n </article>\n );\n}\n"],"names":["useId","cn","EntityRecordCard","title","subtitle","badges","meta","leading","actions","className","titleId","article","aria-labelledby","undefined","div","h3","id","length","map","badge","span","value","key","p","dl","line","dt","label","dd"],"mappings":";AAAA,SAAyBA,KAAK,QAAQ,QAAQ;AAE9C,SAASC,EAAE,QAAQ,oBAAoB;AAgCvC;;;;;;;;CAQC,GACD,OAAO,SAASC,iBAAiB,EAC/BC,KAAK,EACLC,QAAQ,EACRC,SAAS,EAAE,EACXC,IAAI,EACJC,OAAO,EACPC,OAAO,EACPC,SAAS,EACa;IACtB,MAAMC,UAAUV;IAEhB,qBACE,MAACW;QACCC,mBAAiBF;QACjBD,WAAWR,GACT,yHACAQ;;YAGAF,CAAAA,YAAYM,aAAaL,YAAYK,SAAQ,mBAC7C,MAACC;gBAAIL,WAAU;;kCACb,KAACK;wBAAIL,WAAU;kCAAQF;;kCACvB,KAACO;wBAAIL,WAAU;kCAAQD;;;;0BAG3B,MAACM;gBAAIL,WAAU;;kCACb,KAACM;wBACCC,IAAIN;wBACJD,WAAU;kCAETN;;oBAEFE,OAAOY,MAAM,GAAG,mBACf,KAACH;wBAAIL,WAAU;kCACZJ,OAAOa,GAAG,CAACC,CAAAA,sBACV,KAACC;gCAECX,WAAU;0CAETU,MAAME,KAAK;+BAHPF,MAAMG,GAAG;;;;YASvBlB,aAAaS,2BACZ,KAACU;gBAAEd,WAAU;0BAAuCL;;YAErDE,KAAKW,MAAM,GAAG,mBACb,KAACO;gBAAGf,WAAU;0BACXH,KAAKY,GAAG,CAACO,CAAAA,qBACR,MAACX;wBAECL,WAAU;;0CAEV,KAACiB;gCAAGjB,WAAU;0CACXgB,KAAKE,KAAK;;0CAEb,KAACC;gCAAGnB,WAAU;0CAA+BgB,KAAKJ,KAAK;;;uBANlDI,KAAKH,GAAG;;;;AAa3B"}
@@ -1,10 +1,16 @@
1
- import { type Entity, type EntityFieldDescriptor, type FilterGroup } from '@entifix/core';
1
+ import { type Entity, type EntityFacetCounts, type EntityFieldDescriptor, type FilterGroup } from '@entifix/core';
2
2
  export interface FilterBuilderProps<TEntity extends Entity> {
3
3
  /** Filterable members, already resolved from the entity metadata. */
4
4
  descriptors: readonly EntityFieldDescriptor[];
5
5
  /** The filtering currently applied, used to seed the rows. */
6
6
  value?: FilterGroup<TEntity>;
7
7
  onChange: (filtering: FilterGroup<TEntity>) => void;
8
+ /**
9
+ * How many records hold each value, by member — shown beside an enum's or a
10
+ * boolean's choices, so a reader sees what a choice would keep before
11
+ * applying it ([ADR 0012](../../../../../../../docs/adr/0012-facet-counts-are-a-port-of-their-own.md)).
12
+ */
13
+ facets?: EntityFacetCounts;
8
14
  }
9
15
  /**
10
16
  * Builds an `EntityFiltering` value from entity metadata: the member list, the
@@ -15,5 +21,5 @@ export interface FilterBuilderProps<TEntity extends Entity> {
15
21
  * feeds a load request: emitting per keystroke would put one HTTP request on
16
22
  * the wire per character typed.
17
23
  */
18
- export declare function FilterBuilder<TEntity extends Entity>({ descriptors, value: applied, onChange, }: FilterBuilderProps<TEntity>): import("react").JSX.Element;
24
+ export declare function FilterBuilder<TEntity extends Entity>({ descriptors, value: applied, onChange, facets, }: FilterBuilderProps<TEntity>): import("react").JSX.Element;
19
25
  //# sourceMappingURL=filter-builder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"filter-builder.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/filter-builder/filter-builder.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,MAAM,EACX,KAAK,qBAAqB,EAE1B,KAAK,WAAW,EAMjB,MAAM,eAAe,CAAC;AA+IvB,MAAM,WAAW,kBAAkB,CAAC,OAAO,SAAS,MAAM;IACxD,qEAAqE;IACrE,WAAW,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC9C,8DAA8D;IAC9D,KAAK,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,CAAC,SAAS,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;CACrD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,SAAS,MAAM,EAAE,EACpD,WAAW,EACX,KAAK,EAAE,OAAO,EACd,QAAQ,GACT,EAAE,kBAAkB,CAAC,OAAO,CAAC,+BA+L7B"}
1
+ {"version":3,"file":"filter-builder.d.ts","sourceRoot":"","sources":["../../../../src/ui/molecules/filter-builder/filter-builder.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,MAAM,EACX,KAAK,iBAAiB,EAEtB,KAAK,qBAAqB,EAE1B,KAAK,WAAW,EAMjB,MAAM,eAAe,CAAC;AA+IvB,MAAM,WAAW,kBAAkB,CAAC,OAAO,SAAS,MAAM;IACxD,qEAAqE;IACrE,WAAW,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC9C,8DAA8D;IAC9D,KAAK,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,CAAC,SAAS,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;IACpD;;;;OAIG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,SAAS,MAAM,EAAE,EACpD,WAAW,EACX,KAAK,EAAE,OAAO,EACd,QAAQ,EACR,MAAM,GACP,EAAE,kBAAkB,CAAC,OAAO,CAAC,+BAgM7B"}
@@ -98,7 +98,7 @@ function coerce(raw, type) {
98
98
  * Editing is a draft. `onChange` fires only on **Apply**, because the value
99
99
  * feeds a load request: emitting per keystroke would put one HTTP request on
100
100
  * the wire per character typed.
101
- */ export function FilterBuilder({ descriptors, value: applied, onChange }) {
101
+ */ export function FilterBuilder({ descriptors, value: applied, onChange, facets }) {
102
102
  var _ref;
103
103
  const t = useT();
104
104
  const idPrefix = useId();
@@ -218,6 +218,7 @@ function coerce(raw, type) {
218
218
  }),
219
219
  arity !== 'none' && /*#__PURE__*/ _jsx(FilterValueInput, {
220
220
  descriptor: descriptor,
221
+ counts: facets == null ? void 0 : facets[descriptor.name],
221
222
  arity: arity,
222
223
  value: draft.value,
223
224
  end: draft.end,
@@ -276,9 +277,17 @@ function coerce(raw, type) {
276
277
  ]
277
278
  });
278
279
  }
279
- /** The value control the member's type calls for. */ function FilterValueInput({ descriptor, arity, value, end, onValueChange, onEndChange }) {
280
+ /** The value control the member's type calls for. */ function FilterValueInput({ descriptor, counts, arity, value, end, onValueChange, onEndChange }) {
280
281
  const t = useT();
281
282
  const enumLabel = useEnumLabel();
283
+ /** A choice's label, with how many records hold it when that is known. */ const counted = (label, option)=>{
284
+ var _counts_find;
285
+ const count = counts == null ? void 0 : (_counts_find = counts.find((entry)=>String(entry.value) === option)) == null ? void 0 : _counts_find.count;
286
+ return count === undefined ? label : t('filter.withCount', {
287
+ label,
288
+ count
289
+ });
290
+ };
282
291
  if (descriptor.type === 'boolean') {
283
292
  return /*#__PURE__*/ _jsxs(Select, {
284
293
  "aria-label": t('filter.value'),
@@ -291,11 +300,11 @@ function coerce(raw, type) {
291
300
  }),
292
301
  /*#__PURE__*/ _jsx("option", {
293
302
  value: "true",
294
- children: t('value.yes')
303
+ children: counted(t('value.yes'), 'true')
295
304
  }),
296
305
  /*#__PURE__*/ _jsx("option", {
297
306
  value: "false",
298
- children: t('value.no')
307
+ children: counted(t('value.no'), 'false')
299
308
  })
300
309
  ]
301
310
  });
@@ -312,7 +321,7 @@ function coerce(raw, type) {
312
321
  }),
313
322
  descriptor.enumValues.map((option)=>/*#__PURE__*/ _jsx("option", {
314
323
  value: option,
315
- children: enumLabel(descriptor, option)
324
+ children: counted(enumLabel(descriptor, option), option)
316
325
  }, option))
317
326
  ]
318
327
  });