@eifi1/ui-kit 0.22.0 → 0.24.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 (208) hide show
  1. package/README.md +42 -25
  2. package/dist/chart.d.ts +8 -7
  3. package/dist/components/amount-input.d.ts +34 -7
  4. package/dist/components/amount-input.js +14 -10
  5. package/dist/components/amount-input.js.map +1 -1
  6. package/dist/components/autocomplete.d.ts +26 -0
  7. package/dist/components/autocomplete.js +164 -137
  8. package/dist/components/autocomplete.js.map +1 -1
  9. package/dist/components/button-group.d.ts +8 -7
  10. package/dist/components/calculator.d.ts +8 -7
  11. package/dist/components/column-mapper.d.ts +235 -0
  12. package/dist/components/column-mapper.js +411 -0
  13. package/dist/components/column-mapper.js.map +1 -0
  14. package/dist/components/combobox-core.d.ts +39 -2
  15. package/dist/components/combobox-core.js +29 -6
  16. package/dist/components/combobox-core.js.map +1 -1
  17. package/dist/components/combobox.d.ts +113 -3
  18. package/dist/components/combobox.js +456 -313
  19. package/dist/components/combobox.js.map +1 -1
  20. package/dist/components/confirm-dialog.d.ts +8 -7
  21. package/dist/components/confirm-dialog.js +7 -8
  22. package/dist/components/confirm-dialog.js.map +1 -1
  23. package/dist/components/copy-button.d.ts +5 -4
  24. package/dist/components/country-select.d.ts +50 -7
  25. package/dist/components/country-select.js +65 -9
  26. package/dist/components/country-select.js.map +1 -1
  27. package/dist/components/danger-confirm.d.ts +8 -7
  28. package/dist/components/danger-confirm.js +64 -7
  29. package/dist/components/danger-confirm.js.map +1 -1
  30. package/dist/components/date-picker.d.ts +41 -0
  31. package/dist/components/date-picker.js +269 -8
  32. package/dist/components/date-picker.js.map +1 -1
  33. package/dist/components/entity-combobox.d.ts +48 -2
  34. package/dist/components/entity-combobox.js +159 -114
  35. package/dist/components/entity-combobox.js.map +1 -1
  36. package/dist/components/facing-pair.d.ts +8 -7
  37. package/dist/components/field-parts.d.ts +116 -6
  38. package/dist/components/field-parts.js +84 -0
  39. package/dist/components/field-parts.js.map +1 -1
  40. package/dist/components/field-strip.d.ts +105 -0
  41. package/dist/components/field-strip.js +46 -0
  42. package/dist/components/field-strip.js.map +1 -0
  43. package/dist/components/file-button.d.ts +8 -7
  44. package/dist/components/file-dropzone.d.ts +8 -7
  45. package/dist/components/form-actions.d.ts +8 -7
  46. package/dist/components/form-actions.js +23 -3
  47. package/dist/components/form-actions.js.map +1 -1
  48. package/dist/components/iban-input.d.ts +8 -7
  49. package/dist/components/icon-picker.d.ts +10 -1
  50. package/dist/components/icon-picker.js +12 -4
  51. package/dist/components/icon-picker.js.map +1 -1
  52. package/dist/components/language-select.d.ts +8 -7
  53. package/dist/components/money-field.d.ts +11 -7
  54. package/dist/components/money-field.js.map +1 -1
  55. package/dist/components/month-picker.js +2 -1
  56. package/dist/components/month-picker.js.map +1 -1
  57. package/dist/components/multi-entity-combobox.d.ts +31 -2
  58. package/dist/components/multi-entity-combobox.js +152 -103
  59. package/dist/components/multi-entity-combobox.js.map +1 -1
  60. package/dist/components/number-field.d.ts +8 -7
  61. package/dist/components/number-input.d.ts +8 -7
  62. package/dist/components/numpad-sheet.d.ts +8 -7
  63. package/dist/components/phone-input.d.ts +8 -7
  64. package/dist/components/reauth-dialog.d.ts +2 -2
  65. package/dist/components/reauth-dialog.js +6 -8
  66. package/dist/components/reauth-dialog.js.map +1 -1
  67. package/dist/components/series-chart.d.ts +8 -7
  68. package/dist/components/settings-fields.d.ts +8 -7
  69. package/dist/components/share-card.d.ts +8 -7
  70. package/dist/components/swatch-picker.d.ts +17 -1
  71. package/dist/components/swatch-picker.js +10 -4
  72. package/dist/components/swatch-picker.js.map +1 -1
  73. package/dist/components/text-link.d.ts +8 -7
  74. package/dist/components/tile-radio.d.ts +20 -0
  75. package/dist/components/tile-radio.js +25 -8
  76. package/dist/components/tile-radio.js.map +1 -1
  77. package/dist/components/time-input.d.ts +8 -7
  78. package/dist/components/toggle-group.js +2 -2
  79. package/dist/components/toggle-group.js.map +1 -1
  80. package/dist/components/trigger-aria.d.ts +3 -0
  81. package/dist/components/trigger-aria.js +3 -1
  82. package/dist/components/trigger-aria.js.map +1 -1
  83. package/dist/components/ui.d.ts +5 -4
  84. package/dist/components/ui.js +60 -28
  85. package/dist/components/ui.js.map +1 -1
  86. package/dist/feedback/feedback-attachment.d.ts +66 -1
  87. package/dist/feedback/feedback-attachment.js +185 -65
  88. package/dist/feedback/feedback-attachment.js.map +1 -1
  89. package/dist/feedback/feedback-dialog.d.ts +66 -1
  90. package/dist/feedback/feedback-dialog.js.map +1 -1
  91. package/dist/feedback/feedback-inbox.d.ts +67 -252
  92. package/dist/feedback/feedback-inbox.js.map +1 -1
  93. package/dist/feedback/feedback-thread.d.ts +66 -287
  94. package/dist/feedback/feedback-thread.js +16 -3
  95. package/dist/feedback/feedback-thread.js.map +1 -1
  96. package/dist/{kit-labels-R8mIAc9N.d.ts → feedback-DOwPu-Il.d.ts} +1273 -30
  97. package/dist/feedback.d.ts +65 -3
  98. package/dist/hooks/use-file-drop.d.ts +8 -7
  99. package/dist/i18n/defaults.d.ts +8 -7
  100. package/dist/i18n/defaults.js +3 -1
  101. package/dist/i18n/defaults.js.map +1 -1
  102. package/dist/i18n/german.d.ts +8 -7
  103. package/dist/i18n/german.js +36 -2
  104. package/dist/i18n/german.js.map +1 -1
  105. package/dist/i18n/kit-labels.d.ts +5 -4
  106. package/dist/i18n/kit-labels.js.map +1 -1
  107. package/dist/i18n/languages.d.ts +8 -7
  108. package/dist/i18n/locales/de-CH.d.ts +8 -7
  109. package/dist/i18n/locales/en.d.ts +8 -7
  110. package/dist/i18n/locales/en.js +7 -0
  111. package/dist/i18n/locales/en.js.map +1 -1
  112. package/dist/i18n/locales/es.d.ts +8 -7
  113. package/dist/i18n/locales/es.js +37 -2
  114. package/dist/i18n/locales/es.js.map +1 -1
  115. package/dist/i18n/locales/fr.d.ts +8 -7
  116. package/dist/i18n/locales/fr.js +36 -2
  117. package/dist/i18n/locales/fr.js.map +1 -1
  118. package/dist/i18n/locales/hu.d.ts +8 -7
  119. package/dist/i18n/locales/hu.js +38 -2
  120. package/dist/i18n/locales/hu.js.map +1 -1
  121. package/dist/i18n/locales/it.d.ts +8 -7
  122. package/dist/i18n/locales/it.js +37 -2
  123. package/dist/i18n/locales/it.js.map +1 -1
  124. package/dist/i18n/locales/zh.d.ts +8 -7
  125. package/dist/i18n/locales/zh.js +34 -2
  126. package/dist/i18n/locales/zh.js.map +1 -1
  127. package/dist/i18n/review.d.ts +8 -7
  128. package/dist/i18n/review.js +20 -1
  129. package/dist/i18n/review.js.map +1 -1
  130. package/dist/index.d.ts +6 -4
  131. package/dist/index.js +16 -0
  132. package/dist/index.js.map +1 -1
  133. package/dist/lib/column-mapping.d.ts +81 -0
  134. package/dist/lib/column-mapping.js +108 -0
  135. package/dist/lib/column-mapping.js.map +1 -0
  136. package/dist/lib/strip-fade.d.ts +2 -2
  137. package/dist/lib/strip-fade.js.map +1 -1
  138. package/dist/lib/table-text.d.ts +109 -1
  139. package/dist/lib/table-text.js +122 -1
  140. package/dist/lib/table-text.js.map +1 -1
  141. package/dist/rhf/fields.d.ts +142 -13
  142. package/dist/rhf/fields.js +185 -3
  143. package/dist/rhf/fields.js.map +1 -1
  144. package/dist/rhf/form.d.ts +8 -7
  145. package/dist/rhf.d.ts +9 -8
  146. package/dist/rhf.js.map +1 -1
  147. package/dist/shell/app-shell.d.ts +8 -7
  148. package/dist/shell/top-bar-brand.d.ts +8 -7
  149. package/dist/shell/topbar-action-menu.d.ts +36 -3
  150. package/dist/shell/topbar-action-menu.js +74 -33
  151. package/dist/shell/topbar-action-menu.js.map +1 -1
  152. package/dist/shell.d.ts +8 -7
  153. package/dist/table-text.d.ts +1 -1
  154. package/dist/wizard/stepper-nav.d.ts +38 -9
  155. package/dist/wizard/stepper-nav.js +4 -0
  156. package/dist/wizard/stepper-nav.js.map +1 -1
  157. package/dist/wizard.d.ts +8 -7
  158. package/package.json +1 -1
  159. package/src/components/amount-input.tsx +50 -14
  160. package/src/components/autocomplete.tsx +188 -116
  161. package/src/components/column-mapper.tsx +768 -0
  162. package/src/components/combobox-core.tsx +82 -9
  163. package/src/components/combobox.tsx +655 -335
  164. package/src/components/confirm-dialog.tsx +12 -8
  165. package/src/components/country-select.tsx +135 -22
  166. package/src/components/danger-confirm.tsx +274 -24
  167. package/src/components/date-picker.tsx +423 -10
  168. package/src/components/entity-combobox.tsx +243 -129
  169. package/src/components/field-parts.tsx +224 -5
  170. package/src/components/field-strip.tsx +149 -0
  171. package/src/components/form-actions.tsx +52 -5
  172. package/src/components/icon-picker.tsx +23 -4
  173. package/src/components/money-field.tsx +3 -0
  174. package/src/components/month-picker.tsx +2 -1
  175. package/src/components/multi-entity-combobox.tsx +221 -121
  176. package/src/components/reauth-dialog.tsx +17 -18
  177. package/src/components/swatch-picker.tsx +29 -5
  178. package/src/components/tile-radio.tsx +74 -13
  179. package/src/components/toggle-group.tsx +2 -2
  180. package/src/components/trigger-aria.ts +5 -0
  181. package/src/components/ui.tsx +114 -33
  182. package/src/feedback/feedback-attachment.tsx +372 -72
  183. package/src/feedback/feedback-dialog.tsx +7 -3
  184. package/src/feedback/feedback-inbox.tsx +3 -2
  185. package/src/feedback/feedback-thread.tsx +112 -9
  186. package/src/i18n/defaults.ts +2 -0
  187. package/src/i18n/german.ts +42 -0
  188. package/src/i18n/kit-labels.tsx +4 -0
  189. package/src/i18n/locales/en.ts +27 -5
  190. package/src/i18n/locales/es.ts +41 -0
  191. package/src/i18n/locales/fr.ts +42 -0
  192. package/src/i18n/locales/hu.ts +37 -0
  193. package/src/i18n/locales/it.ts +41 -0
  194. package/src/i18n/locales/zh.ts +32 -0
  195. package/src/i18n/review.ts +19 -0
  196. package/src/index.ts +18 -0
  197. package/src/lib/column-mapping.ts +234 -0
  198. package/src/lib/strip-fade.ts +2 -2
  199. package/src/lib/table-text.ts +271 -0
  200. package/src/rhf/fields.tsx +421 -6
  201. package/src/rhf.ts +2 -1
  202. package/src/shell/topbar-action-menu.tsx +134 -38
  203. package/src/wizard/stepper-nav.tsx +35 -1
  204. package/dist/components/field-anatomy.d.ts +0 -95
  205. package/dist/components/field-anatomy.js +0 -84
  206. package/dist/components/field-anatomy.js.map +0 -1
  207. package/dist/feedback-attachment-NhUm7Zga.d.ts +0 -367
  208. package/src/components/field-anatomy.tsx +0 -190
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/reauth-dialog.tsx"],"sourcesContent":["import { useContext, useEffect, useId, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\n\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { usePromisePending } from \"./danger-confirm\";\nimport { DialogFrame } from \"./dialog-frame\";\nimport { ModalCloseContext } from \"./modal\";\nimport { Button, Input, Spinner } from \"./ui\";\n\n/**\n * Every string the dialog renders — the `reauthDialog` namespace of\n * `<UiKitProvider labels>`, and overridable per instance through `labels`. The title,\n * description and submit button are usually worded per action (\"Add a passkey\"),\n * which is what the `title` / `description` / `submitLabel` props are for; these are\n * the fallbacks.\n */\nexport interface ReauthDialogLabels {\n title: string;\n description: string;\n /** Label of the password field. */\n password: string;\n submit: string;\n cancel: string;\n}\n\nexport const DEFAULT_REAUTH_DIALOG_LABELS: ReauthDialogLabels = {\n title: \"Confirm it’s you\",\n description: \"Enter your current password to continue.\",\n password: \"Current password\",\n submit: \"Continue\",\n cancel: \"Cancel\",\n};\n\nexport interface ReauthDialogProps {\n /**\n * Verifies the password — the app's call, e.g. the request the password guards.\n *\n * Return a promise and the dialog manages itself: busy until it settles, closes\n * (`onClose(true)`) when it resolves, STAYS OPEN with the password kept and selected\n * when it rejects — a wrong password is a typo to correct, not a sign-out. Pass the\n * reason as `error`. Return nothing and the app owns the rest: `busy` for the\n * pending state, unmounting the dialog once it is done.\n */\n onSubmit: (password: string) => void | Promise<unknown>;\n /**\n * The dialog has closed: `true` after a resolved `onSubmit`, `false` for Cancel,\n * Escape, the backdrop and Back. A dismissal while `onSubmit` is still running also\n * answers `false`, and that call's late result is then ignored.\n */\n onClose: (confirmed: boolean) => void;\n /**\n * Why the last attempt failed (\"Wrong password\"), shown under the field, which turns\n * invalid (`aria-invalid`, `--danger-border-strong`). The app decides the wording —\n * a 400 is a wrong password, a 500 is not. Hidden again as soon as the user edits\n * the field (the message is about the password they had typed), and shown again by\n * the next failed submit.\n */\n error?: ReactNode;\n /** The check is running, for a caller that tracks it itself (a mutation's\n * `isPending`); a promise returned from `onSubmit` does the same on its own. */\n busy?: boolean;\n /** Default: `labels.title` — \"Confirm it’s you\". */\n title?: ReactNode;\n /** The line under the title. Default: `labels.description`. */\n description?: ReactNode;\n /** Text of the submit button. Default: `labels.submit` — \"Continue\". */\n submitLabel?: ReactNode;\n /** `\"danger\"` when what the password unlocks cannot be undone: the submit is then the\n * destructive button. Default `\"neutral\"`. */\n tone?: \"neutral\" | \"danger\";\n /** Extra body above the field — what is about to happen, a warning. */\n children?: ReactNode;\n /** Kept-mounted mode, as on `Modal`: left out, the dialog is open while mounted. A\n * flip to `false` animates it out and wipes the password. */\n open?: boolean;\n /** User-facing strings; see {@link ReauthDialogLabels}. */\n labels?: Partial<ReauthDialogLabels>;\n}\n\n/**\n * \"Enter your current password to continue\" — the step-up check in front of an\n * action that changes how an account signs in (adding a passkey) or what it can\n * reach. Kurvenschmiede asked for the password before registering a passkey in a\n * dialog of its own; keksdose asks for it inline in `DangerConfirm`'s\n * `requirePassword` tile. Both are the same three facts — the field is the CURRENT\n * password (the password manager fills it, never generates one), a wrong one stays in\n * the dialog with its error, and the APP verifies it — so the promise handling is\n * `DangerConfirm`'s own, shared, the field is its `CurrentPasswordInput` spelled out\n * with an `error`, and this is the modal form of them.\n *\n * A component rather than a `useReauth()` promise: the step that follows the check\n * (Kurvenschmiede's WebAuthn ceremony) needs what the server answered, and the error\n * wording needs the app's own status codes — both already live in the app's mutation,\n * which this dialog only has to be handed (`onSubmit={(pw) => begin.mutateAsync(pw)}`,\n * `error={…}`).\n *\n * The field is a `<form>`, so Enter submits once something is typed. Focus starts in\n * the field and returns to the trigger through `Modal`'s trap. While a check runs,\n * the field is read-only, Cancel is disabled and Escape is held: a request that has\n * left cannot be cancelled by closing the dialog, and the dialog should not pretend it\n * can.\n */\nexport function ReauthDialog({\n onSubmit,\n onClose,\n error,\n busy: busyProp,\n title,\n description,\n submitLabel,\n tone = \"neutral\",\n children,\n open,\n labels: labelsProp,\n}: ReauthDialogProps) {\n const labels = useKitLabels(\"reauthDialog\", DEFAULT_REAUTH_DIALOG_LABELS, labelsProp);\n const [password, setPassword] = useState(\"\");\n // The error describes the password that was submitted; once the user edits it, it\n // describes nothing on screen. Cleared by the next submit, so a second failure with\n // the same message shows again.\n const [edited, setEdited] = useState(false);\n const { pending, run } = usePromisePending();\n const busy = Boolean(busyProp) || pending;\n const inputRef = useRef<HTMLInputElement>(null);\n // `onSubmit` resolved: the actions row lowers the panel (state, so it re-renders),\n // and `onClose` answers true (a ref, because `Modal` may call the `onClose` it saw\n // BEFORE that re-render — its latest-callback ref is refreshed in an effect that runs\n // after the actions row's).\n const [done, setDone] = useState(false);\n const confirmed = useRef(false);\n const formId = useId();\n const isOpen = open ?? true;\n\n // A close (kept-mounted) wipes the password: it must not sit in a hidden dialog.\n // During render, like DangerConfirm's disarm, so no frame holds it.\n const [wasOpen, setWasOpen] = useState(isOpen);\n if (wasOpen !== isOpen) {\n setWasOpen(isOpen);\n if (!isOpen) {\n setPassword(\"\");\n setEdited(false);\n setDone(false);\n }\n }\n\n // After `Modal`'s own effect (a parent's effects run after its children's), so the\n // trap has recorded the trigger as its restore target before focus moves in.\n useEffect(() => {\n if (!isOpen) return;\n confirmed.current = false;\n inputRef.current?.focus();\n }, [isOpen]);\n\n const submit = () => {\n if (busy || password === \"\") return;\n setEdited(false);\n run(onSubmit(password), () => {\n confirmed.current = true;\n setDone(true);\n });\n };\n\n // A rejection leaves focus where it was (the field, or the submit button) — select\n // the password there so the retry replaces it rather than appending to it.\n const wasPending = useRef(false);\n useEffect(() => {\n if (wasPending.current && !pending && !done) inputRef.current?.select();\n wasPending.current = pending;\n }, [pending, done]);\n\n const shownError = edited ? undefined : error;\n\n return (\n <DialogFrame\n open={open}\n title={title ?? labels.title}\n description={description ?? labels.description}\n onClose={() => onClose(confirmed.current)}\n onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {\n if (busy && e.key === \"Escape\") e.preventDefault();\n }}\n aria-busy={busy || undefined}\n actions={\n <ReauthActions\n formId={formId}\n done={done}\n busy={busy}\n canSubmit={password !== \"\"}\n danger={tone === \"danger\"}\n cancelLabel={labels.cancel}\n submitLabel={submitLabel ?? labels.submit}\n />\n }\n >\n {children}\n <form\n id={formId}\n noValidate\n onSubmit={(e) => {\n e.preventDefault();\n submit();\n }}\n >\n {/* CurrentPasswordInput's field — the password manager fills it with the CURRENT\n password and never offers to generate one; `readOnly`, not `disabled`, while\n busy, because disabling the field that has focus drops focus to <body> —\n plus `Input`'s own `error`, which that helper does not take. The message\n paints the field, sits under it and is its `aria-describedby`; passing the\n prop on every render keeps the field's box steady, so the message going away\n on the first keystroke of a retry no longer remounts the <input> under the\n typist (it did, which is why this used to draw its own message). */}\n <Input\n ref={inputRef}\n type=\"password\"\n autoComplete=\"current-password\"\n label={labels.password}\n value={password}\n readOnly={busy}\n error={shownError}\n onChange={(e) => {\n setPassword(e.target.value);\n setEdited(true);\n }}\n />\n </form>\n </DialogFrame>\n );\n}\n\n/**\n * The actions row. A component of its own because it renders INSIDE the `Modal`, where\n * the panel's animated close is in context: a resolved `onSubmit` lowers the panel\n * through it, the way Cancel and Escape do, and `Modal` then calls `onClose`.\n */\nfunction ReauthActions({\n formId,\n done,\n busy,\n canSubmit,\n danger,\n cancelLabel,\n submitLabel,\n}: {\n formId: string;\n done: boolean;\n busy: boolean;\n canSubmit: boolean;\n danger: boolean;\n cancelLabel: ReactNode;\n submitLabel: ReactNode;\n}) {\n const close = useContext(ModalCloseContext);\n useEffect(() => {\n if (done) close?.();\n }, [done, close]);\n return (\n <>\n <Button type=\"button\" variant=\"ghost\" disabled={busy} onClick={() => close?.()}>\n {cancelLabel}\n </Button>\n <Button\n type=\"submit\"\n form={formId}\n variant={danger ? \"danger\" : \"primary\"}\n disabled={!canSubmit || busy}\n aria-busy={busy || undefined}\n >\n {busy && <Spinner label={null} className=\"h-4 w-4\" />}\n {submitLabel}\n </Button>\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/reauth-dialog.tsx"],"sourcesContent":["import { useContext, useEffect, useId, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\n\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { CurrentPasswordInput, usePromisePending } from \"./danger-confirm\";\nimport { DialogFrame } from \"./dialog-frame\";\nimport { ModalCloseContext } from \"./modal\";\nimport { Button, Spinner } from \"./ui\";\n\n/**\n * Every string the dialog renders — the `reauthDialog` namespace of\n * `<UiKitProvider labels>`, and overridable per instance through `labels`. The title,\n * description and submit button are usually worded per action (\"Add a passkey\"),\n * which is what the `title` / `description` / `submitLabel` props are for; these are\n * the fallbacks.\n */\nexport interface ReauthDialogLabels {\n title: string;\n description: string;\n /** Label of the password field. */\n password: string;\n submit: string;\n cancel: string;\n}\n\nexport const DEFAULT_REAUTH_DIALOG_LABELS: ReauthDialogLabels = {\n title: \"Confirm it’s you\",\n description: \"Enter your current password to continue.\",\n password: \"Current password\",\n submit: \"Continue\",\n cancel: \"Cancel\",\n};\n\nexport interface ReauthDialogProps {\n /**\n * Verifies the password — the app's call, e.g. the request the password guards.\n *\n * Return a promise and the dialog manages itself: busy until it settles, closes\n * (`onClose(true)`) when it resolves, STAYS OPEN with the password kept and selected\n * when it rejects — a wrong password is a typo to correct, not a sign-out. Pass the\n * reason as `error`. Return nothing and the app owns the rest: `busy` for the\n * pending state, unmounting the dialog once it is done.\n */\n onSubmit: (password: string) => void | Promise<unknown>;\n /**\n * The dialog has closed: `true` after a resolved `onSubmit`, `false` for Cancel,\n * Escape, the backdrop and Back. A dismissal while `onSubmit` is still running also\n * answers `false`, and that call's late result is then ignored.\n */\n onClose: (confirmed: boolean) => void;\n /**\n * Why the last attempt failed (\"Wrong password\"), shown under the field, which turns\n * invalid (`aria-invalid`, `--danger-border-strong`). The app decides the wording —\n * a 400 is a wrong password, a 500 is not. Hidden again as soon as the user edits\n * the field (the message is about the password they had typed), and shown again by\n * the next failed submit.\n */\n error?: ReactNode;\n /** The check is running, for a caller that tracks it itself (a mutation's\n * `isPending`); a promise returned from `onSubmit` does the same on its own. */\n busy?: boolean;\n /** Default: `labels.title` — \"Confirm it’s you\". */\n title?: ReactNode;\n /** The line under the title. Default: `labels.description`. */\n description?: ReactNode;\n /** Text of the submit button. Default: `labels.submit` — \"Continue\". */\n submitLabel?: ReactNode;\n /** `\"danger\"` when what the password unlocks cannot be undone: the submit is then the\n * destructive button. Default `\"neutral\"`. */\n tone?: \"neutral\" | \"danger\";\n /** Extra body above the field — what is about to happen, a warning. */\n children?: ReactNode;\n /** Kept-mounted mode, as on `Modal`: left out, the dialog is open while mounted. A\n * flip to `false` animates it out and wipes the password. */\n open?: boolean;\n /** User-facing strings; see {@link ReauthDialogLabels}. */\n labels?: Partial<ReauthDialogLabels>;\n}\n\n/**\n * \"Enter your current password to continue\" — the step-up check in front of an\n * action that changes how an account signs in (adding a passkey) or what it can\n * reach. Kurvenschmiede asked for the password before registering a passkey in a\n * dialog of its own; keksdose asks for it inline in `DangerConfirm`'s\n * `requirePassword` tile. Both are the same three facts — the field is the CURRENT\n * password (the password manager fills it, never generates one), a wrong one stays in\n * the dialog with its error, and the APP verifies it — so the promise handling is\n * `DangerConfirm`'s own, shared, the field is its `CurrentPasswordInput` with an\n * `error`, and this is the modal form of them.\n *\n * A component rather than a `useReauth()` promise: the step that follows the check\n * (Kurvenschmiede's WebAuthn ceremony) needs what the server answered, and the error\n * wording needs the app's own status codes — both already live in the app's mutation,\n * which this dialog only has to be handed (`onSubmit={(pw) => begin.mutateAsync(pw)}`,\n * `error={…}`).\n *\n * The field is a `<form>`, so Enter submits once something is typed. Focus starts in\n * the field and returns to the trigger through `Modal`'s trap. While a check runs,\n * the field is read-only, Cancel is disabled and Escape is held: a request that has\n * left cannot be cancelled by closing the dialog, and the dialog should not pretend it\n * can.\n */\nexport function ReauthDialog({\n onSubmit,\n onClose,\n error,\n busy: busyProp,\n title,\n description,\n submitLabel,\n tone = \"neutral\",\n children,\n open,\n labels: labelsProp,\n}: ReauthDialogProps) {\n const labels = useKitLabels(\"reauthDialog\", DEFAULT_REAUTH_DIALOG_LABELS, labelsProp);\n const [password, setPassword] = useState(\"\");\n // The error describes the password that was submitted; once the user edits it, it\n // describes nothing on screen. Cleared by the next submit, so a second failure with\n // the same message shows again.\n const [edited, setEdited] = useState(false);\n const { pending, run } = usePromisePending();\n const busy = Boolean(busyProp) || pending;\n const inputRef = useRef<HTMLInputElement>(null);\n // `onSubmit` resolved: the actions row lowers the panel (state, so it re-renders),\n // and `onClose` answers true (a ref, because `Modal` may call the `onClose` it saw\n // BEFORE that re-render — its latest-callback ref is refreshed in an effect that runs\n // after the actions row's).\n const [done, setDone] = useState(false);\n const confirmed = useRef(false);\n const formId = useId();\n const isOpen = open ?? true;\n\n // A close (kept-mounted) wipes the password: it must not sit in a hidden dialog.\n // During render, like DangerConfirm's disarm, so no frame holds it.\n const [wasOpen, setWasOpen] = useState(isOpen);\n if (wasOpen !== isOpen) {\n setWasOpen(isOpen);\n if (!isOpen) {\n setPassword(\"\");\n setEdited(false);\n setDone(false);\n }\n }\n\n // After `Modal`'s own effect (a parent's effects run after its children's), so the\n // trap has recorded the trigger as its restore target before focus moves in.\n useEffect(() => {\n if (!isOpen) return;\n confirmed.current = false;\n inputRef.current?.focus();\n }, [isOpen]);\n\n const submit = () => {\n if (busy || password === \"\") return;\n setEdited(false);\n run(onSubmit(password), () => {\n confirmed.current = true;\n setDone(true);\n });\n };\n\n // A rejection leaves focus where it was (the field, or the submit button) — select\n // the password there so the retry replaces it rather than appending to it.\n const wasPending = useRef(false);\n useEffect(() => {\n if (wasPending.current && !pending && !done) inputRef.current?.select();\n wasPending.current = pending;\n }, [pending, done]);\n\n const shownError = edited ? undefined : error;\n\n return (\n <DialogFrame\n open={open}\n title={title ?? labels.title}\n description={description ?? labels.description}\n onClose={() => onClose(confirmed.current)}\n onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {\n if (busy && e.key === \"Escape\") e.preventDefault();\n }}\n aria-busy={busy || undefined}\n actions={\n <ReauthActions\n formId={formId}\n done={done}\n busy={busy}\n canSubmit={password !== \"\"}\n danger={tone === \"danger\"}\n cancelLabel={labels.cancel}\n submitLabel={submitLabel ?? labels.submit}\n />\n }\n >\n {children}\n <form\n id={formId}\n noValidate\n onSubmit={(e) => {\n e.preventDefault();\n submit();\n }}\n >\n {/* The kit's CurrentPasswordInput (0.23; it drew the same field by hand until\n then): the password manager fills it with the CURRENT password and never\n offers to generate one; `busy` makes it `readOnly`, not `disabled`, because\n disabling the field that has focus drops focus to <body>. Labelled by this\n namespace's `password` (\"Current password\"), not `dangerConfirm.password`.\n The `error` is Input's own: it paints the field, sits under it and is its\n `aria-describedby`; passing the prop on every render keeps the field's box\n steady, so the message going away on the first keystroke of a retry does not\n remount the <input> under the typist. */}\n <CurrentPasswordInput\n ref={inputRef}\n label={labels.password}\n value={password}\n busy={busy}\n error={shownError}\n onValueChange={(next) => {\n setPassword(next);\n setEdited(true);\n }}\n />\n </form>\n </DialogFrame>\n );\n}\n\n/**\n * The actions row. A component of its own because it renders INSIDE the `Modal`, where\n * the panel's animated close is in context: a resolved `onSubmit` lowers the panel\n * through it, the way Cancel and Escape do, and `Modal` then calls `onClose`.\n */\nfunction ReauthActions({\n formId,\n done,\n busy,\n canSubmit,\n danger,\n cancelLabel,\n submitLabel,\n}: {\n formId: string;\n done: boolean;\n busy: boolean;\n canSubmit: boolean;\n danger: boolean;\n cancelLabel: ReactNode;\n submitLabel: ReactNode;\n}) {\n const close = useContext(ModalCloseContext);\n useEffect(() => {\n if (done) close?.();\n }, [done, close]);\n return (\n <>\n <Button type=\"button\" variant=\"ghost\" disabled={busy} onClick={() => close?.()}>\n {cancelLabel}\n </Button>\n <Button\n type=\"submit\"\n form={formId}\n variant={danger ? \"danger\" : \"primary\"}\n disabled={!canSubmit || busy}\n aria-busy={busy || undefined}\n >\n {busy && <Spinner label={null} className=\"h-4 w-4\" />}\n {submitLabel}\n </Button>\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -5,8 +5,11 @@ import { LegendEntry } from './toggle-legend.js';
5
5
  import { SeriesChartLabels } from './series-chart-labels.js';
6
6
  import { TimeTickUnit } from './series-chart-ticks.js';
7
7
  import { SeriesChartAxisBudget } from './series-chart-budget.js';
8
- import { y as ChartTooltipPlacement } from '../kit-labels-R8mIAc9N.js';
8
+ import { y as ChartTooltipPlacement } from '../feedback-DOwPu-Il.js';
9
9
  import 'recharts';
10
+ import './tooltip.js';
11
+ import '../hooks/use-anchored-rect.js';
12
+ import '../lib/clipping.js';
10
13
  import '../data-table-labels-B7OdnM0S.js';
11
14
  import './data-table-sort.js';
12
15
  import './swipeable-row.js';
@@ -16,9 +19,6 @@ import './popover.js';
16
19
  import './chip.js';
17
20
  import 'lucide-react';
18
21
  import './field-sync.js';
19
- import './tooltip.js';
20
- import '../hooks/use-anchored-rect.js';
21
- import '../lib/clipping.js';
22
22
  import '../wizard/types.js';
23
23
  import '../tour/tour.js';
24
24
  import '../search/command-palette.js';
@@ -38,9 +38,6 @@ import './dialog-frame.js';
38
38
  import './modal.js';
39
39
  import '../hooks/use-search-param-state.js';
40
40
  import './measured-grid.js';
41
- import '../feedback-attachment-NhUm7Zga.js';
42
- import '../feedback/feedback-thread.js';
43
- import '../feedback/feedback-inbox.js';
44
41
  import './account-settings-labels.js';
45
42
  import './floating-panel.js';
46
43
  import './bulk-action-bar.js';
@@ -70,6 +67,10 @@ import './inline-edit-field.js';
70
67
  import '../lib/iban.js';
71
68
  import '../lib/phone.js';
72
69
  import './sign-chip.js';
70
+ import './column-mapper.js';
71
+ import '../lib/column-mapping.js';
72
+ import '../lib/table-text.js';
73
+ import './field-parts.js';
73
74
 
74
75
  interface SeriesChartSeries {
75
76
  /** The row key this line reads — and the `--color-<key>` it is painted with, so it
@@ -1,7 +1,10 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { bg as SelectProps } from '../kit-labels-R8mIAc9N.js';
3
+ import { c9 as SelectProps } from '../feedback-DOwPu-Il.js';
4
4
  import { ThemePreference } from '../theme/theme-store.js';
5
+ import './tooltip.js';
6
+ import '../hooks/use-anchored-rect.js';
7
+ import '../lib/clipping.js';
5
8
  import '../data-table-labels-B7OdnM0S.js';
6
9
  import './data-table-sort.js';
7
10
  import './swipeable-row.js';
@@ -11,9 +14,6 @@ import './popover.js';
11
14
  import './chip.js';
12
15
  import 'lucide-react';
13
16
  import './field-sync.js';
14
- import './tooltip.js';
15
- import '../hooks/use-anchored-rect.js';
16
- import '../lib/clipping.js';
17
17
  import '../wizard/types.js';
18
18
  import '../tour/tour.js';
19
19
  import '../search/command-palette.js';
@@ -34,9 +34,6 @@ import './dialog-frame.js';
34
34
  import './modal.js';
35
35
  import '../hooks/use-search-param-state.js';
36
36
  import './measured-grid.js';
37
- import '../feedback-attachment-NhUm7Zga.js';
38
- import '../feedback/feedback-thread.js';
39
- import '../feedback/feedback-inbox.js';
40
37
  import './account-settings-labels.js';
41
38
  import './floating-panel.js';
42
39
  import './bulk-action-bar.js';
@@ -66,6 +63,10 @@ import './inline-edit-field.js';
66
63
  import '../lib/iban.js';
67
64
  import '../lib/phone.js';
68
65
  import './sign-chip.js';
66
+ import './column-mapper.js';
67
+ import '../lib/column-mapping.js';
68
+ import '../lib/table-text.js';
69
+ import './field-parts.js';
69
70
  import 'zustand/middleware';
70
71
  import 'zustand';
71
72
 
@@ -1,7 +1,7 @@
1
1
  import 'react';
2
2
  import './account-chips.js';
3
3
  import './dialog-frame.js';
4
- export { a7 as DEFAULT_SHARE_CARD_LABELS, bh as ShareAddRequest, bi as ShareCandidate, bj as ShareCard, bk as ShareCardLabels, bl as ShareCardProps, bm as ShareDialog, bn as ShareDialogProps, bo as ShareGrantee, bp as SharePanel, bq as SharePanelProps, br as SharePendingGrant, bs as ShareRole } from '../kit-labels-R8mIAc9N.js';
4
+ export { aj as DEFAULT_SHARE_CARD_LABELS, ca as ShareAddRequest, cb as ShareCandidate, cc as ShareCard, cd as ShareCardLabels, ce as ShareCardProps, cf as ShareDialog, cg as ShareDialogProps, ch as ShareGrantee, ci as SharePanel, cj as SharePanelProps, ck as SharePendingGrant, cl as ShareRole } from '../feedback-DOwPu-Il.js';
5
5
  import 'lucide-react';
6
6
  import '../lib/format.js';
7
7
  import './chip.js';
@@ -10,13 +10,13 @@ import './data-table-sort.js';
10
10
  import './swipeable-row.js';
11
11
  import './modal.js';
12
12
  import '../hooks/use-search-param-state.js';
13
+ import './tooltip.js';
14
+ import '../hooks/use-anchored-rect.js';
15
+ import '../lib/clipping.js';
13
16
  import './mini-calendar.js';
14
17
  import './calendar-heatmap.js';
15
18
  import './popover.js';
16
19
  import './field-sync.js';
17
- import './tooltip.js';
18
- import '../hooks/use-anchored-rect.js';
19
- import '../lib/clipping.js';
20
20
  import '../wizard/types.js';
21
21
  import '../tour/tour.js';
22
22
  import '../search/command-palette.js';
@@ -34,9 +34,6 @@ import './swatch-picker.js';
34
34
  import './tile-radio.js';
35
35
  import './icon-picker.js';
36
36
  import './measured-grid.js';
37
- import '../feedback-attachment-NhUm7Zga.js';
38
- import '../feedback/feedback-thread.js';
39
- import '../feedback/feedback-inbox.js';
40
37
  import './account-settings-labels.js';
41
38
  import './floating-panel.js';
42
39
  import './bulk-action-bar.js';
@@ -64,3 +61,7 @@ import './inline-edit-field.js';
64
61
  import '../lib/iban.js';
65
62
  import '../lib/phone.js';
66
63
  import './sign-chip.js';
64
+ import './column-mapper.js';
65
+ import '../lib/column-mapping.js';
66
+ import '../lib/table-text.js';
67
+ import './field-parts.js';
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ComponentPropsWithoutRef, ReactNode } from 'react';
2
+ import { ReactNode, ComponentPropsWithoutRef } from 'react';
3
3
  import { TileSize } from './tile-radio.js';
4
4
 
5
5
  interface SwatchPickerLabels {
@@ -27,7 +27,23 @@ interface SwatchOption<T extends string> {
27
27
  /** A remark read after the name and shown under it in the bubble ("used by
28
28
  * Groceries"). The tile wears a dot while it has one. */
29
29
  note?: string;
30
+ /** Out of the choice and out of reach — skipped by the arrow keys and Tab. With a
31
+ * `disabledReason` the tile stays reachable instead and says why. */
30
32
  disabled?: boolean;
33
+ /**
34
+ * Why THIS colour cannot be chosen right now — the whole picker's `disabledReason`,
35
+ * one tile at a time (the kit's later list after keksdose K3: a colour another
36
+ * category already wears, a flag only an owner may set). {@link Button}'s rule: a tile
37
+ * that is merely `disabled` is a dead square a keyboard never lands on, so nobody
38
+ * learns why it is dimmed.
39
+ *
40
+ * The tile looks disabled but is `aria-disabled`, not `disabled`: Tab and the arrow
41
+ * keys still reach it, its bubble shows the reason under its name, and the reason
42
+ * describes it. Choosing it — a click, Space or Enter, or an arrow under automatic
43
+ * activation — does nothing: the focus moves onto it and the checked colour stays
44
+ * where it was, as under the whole-picker lock. Wins over `disabled`.
45
+ */
46
+ disabledReason?: ReactNode;
31
47
  }
32
48
  /**
33
49
  * `onChange` is the picker's own — the chosen VALUE, not a DOM event — so the div's
@@ -6,7 +6,7 @@ import { useKitLabels } from "../i18n/kit-labels.js";
6
6
  import { TILE_SIZE, TileRadioGroup } from "./tile-radio.js";
7
7
  import { Tooltip } from "./tooltip.js";
8
8
  import { useCommitReason } from "./write-lock.js";
9
- import { hasContent, LabelStrip } from "./field-anatomy.js";
9
+ import { hasContent, LabelStrip } from "./field-parts.js";
10
10
  const DEFAULT_SWATCH_PICKER_LABELS = {
11
11
  none: "No colour",
12
12
  mixed: "Mixed: the selected items have different colours"
@@ -39,7 +39,13 @@ function SwatchPicker({
39
39
  const byValue = useMemo(() => new Map(options.map((o) => [o.value, o])), [options]);
40
40
  const items = [
41
41
  ...allowNone ? [{ key: null, label: text.none }] : [],
42
- ...options.map((o) => ({ key: o.value, label: o.label, note: o.note, disabled: o.disabled }))
42
+ ...options.map((o) => ({
43
+ key: o.value,
44
+ label: o.label,
45
+ note: o.note,
46
+ disabled: o.disabled,
47
+ disabledReason: o.disabledReason
48
+ }))
43
49
  ];
44
50
  const tiles = /* @__PURE__ */ jsx(
45
51
  TileRadioGroup,
@@ -47,7 +53,7 @@ function SwatchPicker({
47
53
  items,
48
54
  checked: mixed ? void 0 : value,
49
55
  onSelect: (next) => {
50
- if (!locked) onChange(next);
56
+ if (!locked && !hasContent(next === null ? void 0 : byValue.get(next)?.disabledReason)) onChange(next);
51
57
  },
52
58
  activation,
53
59
  disabled: disabled && !locked,
@@ -110,7 +116,7 @@ function SwatchPicker({
110
116
  }
111
117
  );
112
118
  if (!labelled) return group;
113
- return /* @__PURE__ */ jsx(LabelStrip, { labelId, label, hint, disabled: disabled || locked, pad: "tiles", className, children: group });
119
+ return /* @__PURE__ */ jsx(LabelStrip, { labelId, label, hint, disabled: disabled || locked, pad: "clear", className, children: group });
114
120
  }
115
121
  export {
116
122
  DEFAULT_SWATCH_PICKER_LABELS,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/swatch-picker.tsx"],"sourcesContent":["import { useId, useMemo } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { TILE_SIZE, TileRadioGroup } from \"./tile-radio\";\nimport type { TileItem, TileSize } from \"./tile-radio\";\nimport { Tooltip } from \"./tooltip\";\nimport { useCommitReason } from \"./write-lock\";\nimport { hasContent, LabelStrip } from \"./field-anatomy\";\n\nexport interface SwatchPickerLabels {\n /** The \"no colour\" tile's name, when `allowNone` is set. */\n none: string;\n /** Describes the group while `mixed` is set — a bulk edit over rows that disagree. */\n mixed: string;\n}\n\nexport const DEFAULT_SWATCH_PICKER_LABELS: SwatchPickerLabels = {\n none: \"No colour\",\n mixed: \"Mixed: the selected items have different colours\",\n};\n\nexport interface SwatchOption<T extends string> {\n value: T;\n /** Any CSS colour — a token (`var(--chart-3)`), a hex, an `oklch()`. Painted as an\n * inline background, so it does not have to be a class the kit's CSS knows.\n * Leave it out when `swatchClassName` paints the dot instead. For a colour that\n * must match a `StatusDot` / `Chip` hue elsewhere, `statusDotColor(\"teal\")` rather\n * than `\"var(--hue-teal)\"` written out — one table, so the two cannot drift. */\n color?: string;\n /** The colour's name, in the user's language. Shown in the bubble and read out;\n * required, because a swatch with no name is a colour only some people can read. */\n label: string;\n /** Classes for the swatch dot, for a palette that lives in classes rather than\n * values (a light/dark pair). An inline `color` would win over a class, so pass\n * one or the other. */\n swatchClassName?: string;\n /** A remark read after the name and shown under it in the bubble (\"used by\n * Groceries\"). The tile wears a dot while it has one. */\n note?: string;\n disabled?: boolean;\n}\n\n/**\n * `onChange` is the picker's own — the chosen VALUE, not a DOM event — so the div's\n * is omitted rather than shadowed, as on {@link ToggleGroup}.\n */\nexport interface SwatchPickerProps<T extends string>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"children\" | \"defaultValue\"> {\n options: SwatchOption<T>[];\n /** The chosen colour, or `null` for none. */\n value: T | null;\n onChange: (value: T | null) => void;\n /** Lead with a \"no colour\" tile whose value is `null`. Clearing a colour has to be\n * as reachable as setting one, and a radio cannot be unchecked by clicking it. */\n allowNone?: boolean;\n /**\n * \"Some of each\" — a bulk edit over rows whose colours differ. Nothing is checked\n * (not even \"none\", whatever `value` says), and the group is described as mixed.\n * Distinct from `value={null}`, which is an answer: \"no colour\".\n */\n mixed?: boolean;\n /** See {@link IconPickerProps.activation}. Default \"automatic\". */\n activation?: \"automatic\" | \"manual\";\n /** 28 / 32 / 44px tiles. Default \"md\"; \"lg\" is the touch-target size. */\n size?: TileSize;\n /**\n * Classes for every tile's frame, merged after the kit's own — so a caller's\n * `size-*` beats `size`'s. For a tile that has to follow a container query or a\n * breakpoint (keksdose grows its tiles to 40px in a narrow card:\n * `tileClassName=\"@max-md:size-10\"`) without reaching into the kit's markup with an\n * `[&_[role=radio]]` selector. The swatch dot / glyph inside keeps `size`'s\n * dimensions; the tile grows round it.\n */\n tileClassName?: string;\n disabled?: boolean;\n labels?: Partial<SwatchPickerLabels>;\n /**\n * An 11px static label in a strip over the swatches, named by it (keksdose K2): the\n * transaction row's flag picker stands between labelled fields and hand-builds this\n * (`FieldLabel` + `pt-5`, transaction-flags.tsx:155) because the picker had no label.\n * The label sits on the same line as a field's own (`top-1`, `inset-x-3`); the\n * swatches start 20px down, below its whole line box — see `LABEL_STRIP_PAD`. With a\n * label, `className` styles the wrapper, as on a labelled {@link ToggleGroup}; an\n * explicit `aria-label` / `aria-labelledby` still names the group instead.\n */\n label?: ReactNode;\n /** A {@link FieldHint} beside the label. Only with `label`. */\n hint?: ReactNode;\n /**\n * Why the colour cannot be changed right now — {@link Button}'s `disabledReason`, for\n * a picker that saves itself on change (keksdose K3, the flag picker in a row a\n * read-only viewer may see). The swatches stay reachable and the arrow keys still\n * walk them, but nothing reaches `onChange`; the reason is shown in the kit\n * {@link Tooltip} under the row (each swatch's own name bubble sits above it) and\n * describes the group. Wins over `disabled`.\n */\n disabledReason?: ReactNode;\n /** This picker COMMITS — choosing a colour saves it. Under a locked\n * {@link WriteLockProvider} it takes the `disabledReason` path with the lock's\n * reason, as {@link Button}'s `commit` does. */\n commit?: boolean;\n}\n\n/**\n * A row of colour swatches that is one radio group.\n *\n * Name the group with `aria-label` or `aria-labelledby` — usually the visible heading\n * above it. Each swatch is named by its `label` and shows it in a bubble on hover and\n * focus, so the name is not the screen reader's alone.\n *\n * Keyboard (the APG radio group): Tab reaches the checked swatch, or the first one\n * while nothing is checked; the arrow keys move between swatches and, by default,\n * choose as they go; Home and End jump to the ends. Left and right follow the page's\n * direction.\n */\nexport function SwatchPicker<T extends string>({\n options,\n value,\n onChange,\n allowNone = false,\n mixed = false,\n activation = \"automatic\",\n size = \"md\",\n tileClassName,\n disabled = false,\n labels,\n label,\n hint,\n disabledReason: ownDisabledReason,\n commit,\n className,\n ...rest\n}: SwatchPickerProps<T>) {\n const text = useKitLabels(\"swatchPicker\", DEFAULT_SWATCH_PICKER_LABELS, labels);\n const mixedId = useId();\n const labelId = useId();\n const reasonId = useId();\n const disabledReason = useCommitReason(commit, ownDisabledReason);\n const locked = hasContent(disabledReason);\n const labelled = hasContent(label);\n const byValue = useMemo(() => new Map(options.map((o) => [o.value, o])), [options]);\n const items: TileItem<T>[] = [\n ...(allowNone ? [{ key: null, label: text.none }] : []),\n ...options.map((o) => ({ key: o.value, label: o.label, note: o.note, disabled: o.disabled })),\n ];\n const tiles = (\n <TileRadioGroup\n items={items}\n checked={mixed ? undefined : value}\n // A locked picker saves on change, so the change is swallowed — the arrow keys\n // still move the focus (and, automatic, would choose), and nothing is chosen.\n onSelect={(next) => {\n if (!locked) onChange(next);\n }}\n activation={activation}\n // A reason wins over `disabled`: the tiles stay in the tab order.\n disabled={disabled && !locked}\n size={size}\n tileClassName={\n tileClassName || locked\n ? (_item, selected) =>\n cn(\n // The disabled tile's look, without its `disabled`: dimmed, a\n // not-allowed cursor, and no hover offer on a tile that cannot be\n // chosen. Not on the selected tile, whose frame IS its state.\n locked && \"cursor-not-allowed opacity-50\",\n locked && !selected && \"hover:border-[var(--border)] hover:bg-[var(--bg-surface)]\",\n tileClassName,\n )\n : undefined\n }\n renderTile={(item) => {\n const opt = item.key === null ? undefined : byValue.get(item.key);\n return (\n <span\n aria-hidden\n // A hairline round the dot, drawn over whatever colour it is: a pale\n // yellow swatch on a white surface is otherwise a hole in the row.\n className={cn(\n \"rounded-full ring-1 ring-inset ring-black/10 dark:ring-white/15\",\n TILE_SIZE[size].swatch,\n opt?.swatchClassName,\n )}\n style={{ background: opt?.color }}\n />\n );\n }}\n />\n );\n const group = (\n <div\n {...rest}\n role=\"radiogroup\"\n aria-labelledby={\n labelled && rest[\"aria-label\"] === undefined && rest[\"aria-labelledby\"] === undefined\n ? labelId\n : rest[\"aria-labelledby\"]\n }\n aria-disabled={disabled || locked || undefined}\n aria-describedby={\n [rest[\"aria-describedby\"], mixed && mixedId, locked && reasonId].filter(Boolean).join(\" \") ||\n undefined\n }\n className={cn(\"flex flex-wrap items-center gap-1.5\", !labelled && className)}\n >\n {locked ? (\n // Inside the group, round the tiles — not round the group, whose box is the\n // caller's row item. This row inherits the group's gap, wrap and alignment, so\n // the tiles sit exactly where they sit unlocked. Below the row: each tile's own\n // bubble (its colour's name) is above it.\n <Tooltip\n label={disabledReason}\n side=\"bottom\"\n className=\"flex min-w-0 flex-1 gap-[inherit] [align-items:inherit] [flex-wrap:inherit] [justify-content:inherit]\"\n >\n <>\n {tiles}\n <span id={reasonId} hidden>\n {disabledReason}\n </span>\n </>\n </Tooltip>\n ) : (\n tiles\n )}\n {/* `hidden`, not `sr-only`: a description is read from hidden text, and this\n way it takes no room in the row. */}\n {mixed && (\n <span id={mixedId} hidden>\n {text.mixed}\n </span>\n )}\n </div>\n );\n if (!labelled) return group;\n return (\n <LabelStrip labelId={labelId} label={label} hint={hint} disabled={disabled || locked} pad=\"tiles\" className={className}>\n {group}\n </LabelStrip>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/swatch-picker.tsx"],"sourcesContent":["import { useId, useMemo } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { TILE_SIZE, TileRadioGroup } from \"./tile-radio\";\nimport type { TileItem, TileSize } from \"./tile-radio\";\nimport { Tooltip } from \"./tooltip\";\nimport { useCommitReason } from \"./write-lock\";\nimport { hasContent, LabelStrip } from \"./field-parts\";\n\nexport interface SwatchPickerLabels {\n /** The \"no colour\" tile's name, when `allowNone` is set. */\n none: string;\n /** Describes the group while `mixed` is set — a bulk edit over rows that disagree. */\n mixed: string;\n}\n\nexport const DEFAULT_SWATCH_PICKER_LABELS: SwatchPickerLabels = {\n none: \"No colour\",\n mixed: \"Mixed: the selected items have different colours\",\n};\n\nexport interface SwatchOption<T extends string> {\n value: T;\n /** Any CSS colour — a token (`var(--chart-3)`), a hex, an `oklch()`. Painted as an\n * inline background, so it does not have to be a class the kit's CSS knows.\n * Leave it out when `swatchClassName` paints the dot instead. For a colour that\n * must match a `StatusDot` / `Chip` hue elsewhere, `statusDotColor(\"teal\")` rather\n * than `\"var(--hue-teal)\"` written out — one table, so the two cannot drift. */\n color?: string;\n /** The colour's name, in the user's language. Shown in the bubble and read out;\n * required, because a swatch with no name is a colour only some people can read. */\n label: string;\n /** Classes for the swatch dot, for a palette that lives in classes rather than\n * values (a light/dark pair). An inline `color` would win over a class, so pass\n * one or the other. */\n swatchClassName?: string;\n /** A remark read after the name and shown under it in the bubble (\"used by\n * Groceries\"). The tile wears a dot while it has one. */\n note?: string;\n /** Out of the choice and out of reach — skipped by the arrow keys and Tab. With a\n * `disabledReason` the tile stays reachable instead and says why. */\n disabled?: boolean;\n /**\n * Why THIS colour cannot be chosen right now — the whole picker's `disabledReason`,\n * one tile at a time (the kit's later list after keksdose K3: a colour another\n * category already wears, a flag only an owner may set). {@link Button}'s rule: a tile\n * that is merely `disabled` is a dead square a keyboard never lands on, so nobody\n * learns why it is dimmed.\n *\n * The tile looks disabled but is `aria-disabled`, not `disabled`: Tab and the arrow\n * keys still reach it, its bubble shows the reason under its name, and the reason\n * describes it. Choosing it — a click, Space or Enter, or an arrow under automatic\n * activation — does nothing: the focus moves onto it and the checked colour stays\n * where it was, as under the whole-picker lock. Wins over `disabled`.\n */\n disabledReason?: ReactNode;\n}\n\n/**\n * `onChange` is the picker's own — the chosen VALUE, not a DOM event — so the div's\n * is omitted rather than shadowed, as on {@link ToggleGroup}.\n */\nexport interface SwatchPickerProps<T extends string>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"children\" | \"defaultValue\"> {\n options: SwatchOption<T>[];\n /** The chosen colour, or `null` for none. */\n value: T | null;\n onChange: (value: T | null) => void;\n /** Lead with a \"no colour\" tile whose value is `null`. Clearing a colour has to be\n * as reachable as setting one, and a radio cannot be unchecked by clicking it. */\n allowNone?: boolean;\n /**\n * \"Some of each\" — a bulk edit over rows whose colours differ. Nothing is checked\n * (not even \"none\", whatever `value` says), and the group is described as mixed.\n * Distinct from `value={null}`, which is an answer: \"no colour\".\n */\n mixed?: boolean;\n /** See {@link IconPickerProps.activation}. Default \"automatic\". */\n activation?: \"automatic\" | \"manual\";\n /** 28 / 32 / 44px tiles. Default \"md\"; \"lg\" is the touch-target size. */\n size?: TileSize;\n /**\n * Classes for every tile's frame, merged after the kit's own — so a caller's\n * `size-*` beats `size`'s. For a tile that has to follow a container query or a\n * breakpoint (keksdose grows its tiles to 40px in a narrow card:\n * `tileClassName=\"@max-md:size-10\"`) without reaching into the kit's markup with an\n * `[&_[role=radio]]` selector. The swatch dot / glyph inside keeps `size`'s\n * dimensions; the tile grows round it.\n */\n tileClassName?: string;\n disabled?: boolean;\n labels?: Partial<SwatchPickerLabels>;\n /**\n * An 11px static label in a strip over the swatches, named by it (keksdose K2): the\n * transaction row's flag picker stands between labelled fields and hand-builds this\n * (`FieldLabel` + `pt-5`, transaction-flags.tsx:155) because the picker had no label.\n * The label sits on the same line as a field's own (`top-1`, `inset-x-3`); the\n * swatches start 20px down, below its whole line box — see `LABEL_STRIP_PAD`. With a\n * label, `className` styles the wrapper, as on a labelled {@link ToggleGroup}; an\n * explicit `aria-label` / `aria-labelledby` still names the group instead.\n */\n label?: ReactNode;\n /** A {@link FieldHint} beside the label. Only with `label`. */\n hint?: ReactNode;\n /**\n * Why the colour cannot be changed right now — {@link Button}'s `disabledReason`, for\n * a picker that saves itself on change (keksdose K3, the flag picker in a row a\n * read-only viewer may see). The swatches stay reachable and the arrow keys still\n * walk them, but nothing reaches `onChange`; the reason is shown in the kit\n * {@link Tooltip} under the row (each swatch's own name bubble sits above it) and\n * describes the group. Wins over `disabled`.\n */\n disabledReason?: ReactNode;\n /** This picker COMMITS — choosing a colour saves it. Under a locked\n * {@link WriteLockProvider} it takes the `disabledReason` path with the lock's\n * reason, as {@link Button}'s `commit` does. */\n commit?: boolean;\n}\n\n/**\n * A row of colour swatches that is one radio group.\n *\n * Name the group with `aria-label` or `aria-labelledby` — usually the visible heading\n * above it. Each swatch is named by its `label` and shows it in a bubble on hover and\n * focus, so the name is not the screen reader's alone.\n *\n * Keyboard (the APG radio group): Tab reaches the checked swatch, or the first one\n * while nothing is checked; the arrow keys move between swatches and, by default,\n * choose as they go; Home and End jump to the ends. Left and right follow the page's\n * direction.\n */\nexport function SwatchPicker<T extends string>({\n options,\n value,\n onChange,\n allowNone = false,\n mixed = false,\n activation = \"automatic\",\n size = \"md\",\n tileClassName,\n disabled = false,\n labels,\n label,\n hint,\n disabledReason: ownDisabledReason,\n commit,\n className,\n ...rest\n}: SwatchPickerProps<T>) {\n const text = useKitLabels(\"swatchPicker\", DEFAULT_SWATCH_PICKER_LABELS, labels);\n const mixedId = useId();\n const labelId = useId();\n const reasonId = useId();\n const disabledReason = useCommitReason(commit, ownDisabledReason);\n const locked = hasContent(disabledReason);\n const labelled = hasContent(label);\n const byValue = useMemo(() => new Map(options.map((o) => [o.value, o])), [options]);\n const items: TileItem<T>[] = [\n ...(allowNone ? [{ key: null, label: text.none }] : []),\n ...options.map((o) => ({\n key: o.value,\n label: o.label,\n note: o.note,\n disabled: o.disabled,\n disabledReason: o.disabledReason,\n })),\n ];\n const tiles = (\n <TileRadioGroup\n items={items}\n checked={mixed ? undefined : value}\n // A locked picker saves on change, so the change is swallowed — the arrow keys\n // still move the focus (and, automatic, would choose), and nothing is chosen. A\n // locked TILE is refused by the tiles themselves; asked again here so a reason\n // can never become a change, whatever the tiles do.\n onSelect={(next) => {\n if (!locked && !hasContent(next === null ? undefined : byValue.get(next)?.disabledReason)) onChange(next);\n }}\n activation={activation}\n // A reason wins over `disabled`: the tiles stay in the tab order.\n disabled={disabled && !locked}\n size={size}\n tileClassName={\n tileClassName || locked\n ? (_item, selected) =>\n cn(\n // The disabled tile's look, without its `disabled`: dimmed, a\n // not-allowed cursor, and no hover offer on a tile that cannot be\n // chosen. Not on the selected tile, whose frame IS its state.\n locked && \"cursor-not-allowed opacity-50\",\n locked && !selected && \"hover:border-[var(--border)] hover:bg-[var(--bg-surface)]\",\n tileClassName,\n )\n : undefined\n }\n renderTile={(item) => {\n const opt = item.key === null ? undefined : byValue.get(item.key);\n return (\n <span\n aria-hidden\n // A hairline round the dot, drawn over whatever colour it is: a pale\n // yellow swatch on a white surface is otherwise a hole in the row.\n className={cn(\n \"rounded-full ring-1 ring-inset ring-black/10 dark:ring-white/15\",\n TILE_SIZE[size].swatch,\n opt?.swatchClassName,\n )}\n style={{ background: opt?.color }}\n />\n );\n }}\n />\n );\n const group = (\n <div\n {...rest}\n role=\"radiogroup\"\n aria-labelledby={\n labelled && rest[\"aria-label\"] === undefined && rest[\"aria-labelledby\"] === undefined\n ? labelId\n : rest[\"aria-labelledby\"]\n }\n aria-disabled={disabled || locked || undefined}\n aria-describedby={\n [rest[\"aria-describedby\"], mixed && mixedId, locked && reasonId].filter(Boolean).join(\" \") ||\n undefined\n }\n className={cn(\"flex flex-wrap items-center gap-1.5\", !labelled && className)}\n >\n {locked ? (\n // Inside the group, round the tiles — not round the group, whose box is the\n // caller's row item. This row inherits the group's gap, wrap and alignment, so\n // the tiles sit exactly where they sit unlocked. Below the row: each tile's own\n // bubble (its colour's name) is above it.\n <Tooltip\n label={disabledReason}\n side=\"bottom\"\n className=\"flex min-w-0 flex-1 gap-[inherit] [align-items:inherit] [flex-wrap:inherit] [justify-content:inherit]\"\n >\n <>\n {tiles}\n <span id={reasonId} hidden>\n {disabledReason}\n </span>\n </>\n </Tooltip>\n ) : (\n tiles\n )}\n {/* `hidden`, not `sr-only`: a description is read from hidden text, and this\n way it takes no room in the row. */}\n {mixed && (\n <span id={mixedId} hidden>\n {text.mixed}\n </span>\n )}\n </div>\n );\n if (!labelled) return group;\n return (\n <LabelStrip labelId={labelId} label={label} hint={hint} disabled={disabled || locked} pad=\"clear\" className={className}>\n {group}\n </LabelStrip>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,7 +1,10 @@
1
1
  import * as react from 'react';
2
2
  import { ReactElement, ComponentPropsWithoutRef, ReactNode, Ref, MouseEvent, KeyboardEvent } from 'react';
3
3
  import { LucideIcon } from 'lucide-react';
4
- import { b2 as KitLinkComponent } from '../kit-labels-R8mIAc9N.js';
4
+ import { bX as KitLinkComponent } from '../feedback-DOwPu-Il.js';
5
+ import './tooltip.js';
6
+ import '../hooks/use-anchored-rect.js';
7
+ import '../lib/clipping.js';
5
8
  import '../data-table-labels-B7OdnM0S.js';
6
9
  import './data-table-sort.js';
7
10
  import './swipeable-row.js';
@@ -10,9 +13,6 @@ import './calendar-heatmap.js';
10
13
  import './popover.js';
11
14
  import './chip.js';
12
15
  import './field-sync.js';
13
- import './tooltip.js';
14
- import '../hooks/use-anchored-rect.js';
15
- import '../lib/clipping.js';
16
16
  import '../wizard/types.js';
17
17
  import '../tour/tour.js';
18
18
  import '../search/command-palette.js';
@@ -33,9 +33,6 @@ import './dialog-frame.js';
33
33
  import './modal.js';
34
34
  import '../hooks/use-search-param-state.js';
35
35
  import './measured-grid.js';
36
- import '../feedback-attachment-NhUm7Zga.js';
37
- import '../feedback/feedback-thread.js';
38
- import '../feedback/feedback-inbox.js';
39
36
  import './account-settings-labels.js';
40
37
  import './floating-panel.js';
41
38
  import './bulk-action-bar.js';
@@ -65,6 +62,10 @@ import './inline-edit-field.js';
65
62
  import '../lib/iban.js';
66
63
  import '../lib/phone.js';
67
64
  import './sign-chip.js';
65
+ import './column-mapper.js';
66
+ import '../lib/column-mapping.js';
67
+ import '../lib/table-text.js';
68
+ import './field-parts.js';
68
69
 
69
70
  /**
70
71
  * An `href` that leaves the app: a scheme (`https:`, `mailto:`, `tel:`) or a
@@ -47,6 +47,14 @@ import { ReactNode } from 'react';
47
47
  * where a tile would hide the text behind a bubble and repeat it.
48
48
  *
49
49
  * No strings of its own: every name is an item's `label`, the "none" tile's included.
50
+ *
51
+ * ## A tile that cannot be chosen
52
+ *
53
+ * `disabled` takes a tile out of the choice AND out of reach: the arrow keys skip it
54
+ * and Tab never lands on it, so a dimmed square says nothing about why. Give it a
55
+ * `disabledReason` instead and it stays reachable, `aria-disabled`, with the reason in
56
+ * its bubble and its description — and choosing it does nothing (0.23, the kit's later
57
+ * list after keksdose K3; {@link Button}'s `disabledReason`, one tile at a time).
50
58
  */
51
59
  /** A tile's size: 28 / 32 / 44px — see {@link TILE_SIZE}. */
52
60
  type TileSize = "sm" | "md" | "lg";
@@ -66,7 +74,19 @@ interface TileItem<T extends string> {
66
74
  /** Said after the name, and shown in the bubble under it. Marks the tile with a
67
75
  * dot, so the note is visible without hovering every tile to find it. */
68
76
  note?: string;
77
+ /** Out of the choice and out of reach: natively disabled, skipped by the arrow keys,
78
+ * never the tab stop. */
69
79
  disabled?: boolean;
80
+ /**
81
+ * Why this tile cannot be chosen right now — a colour another category already wears,
82
+ * a symbol only an owner may set. The tile LOOKS disabled but is `aria-disabled`, not
83
+ * `disabled`: Tab and the arrow keys still reach it, the reason shows in its bubble
84
+ * under its name (and its `note`) and describes it, and a click, Space / Enter or an
85
+ * arrow under automatic activation chooses nothing — the focus moves, the checked
86
+ * tile stays. Wins over `disabled`, as on {@link Button}; the whole group's
87
+ * `disabled` still wins over it.
88
+ */
89
+ disabledReason?: ReactNode;
70
90
  }
71
91
  interface TileRadioGroupProps<T extends string> {
72
92
  /** The tiles, in reading order. Render them inside a `role="radiogroup"` element
@@ -13,6 +13,15 @@ const TILE_SIZE = {
13
13
  const TILE_BASE = "relative inline-flex shrink-0 items-center justify-center rounded-md border transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-surface)] disabled:cursor-not-allowed disabled:opacity-50";
14
14
  const TILE_IDLE = "border-[var(--border)] bg-[var(--bg-surface)] hover:border-[var(--border-strong)] hover:bg-[var(--bg-hover)]";
15
15
  const TILE_SELECTED = "border-[var(--text-primary)] bg-[var(--bg-surface)] ring-1 ring-[var(--text-primary)]";
16
+ const TILE_LOCKED = "cursor-not-allowed opacity-50";
17
+ const TILE_LOCKED_IDLE = "hover:border-[var(--border)] hover:bg-[var(--bg-surface)]";
18
+ function isLocked(item) {
19
+ const r = item.disabledReason;
20
+ return r !== void 0 && r !== null && r !== false && r !== "";
21
+ }
22
+ function isOff(item) {
23
+ return Boolean(item.disabled) && !isLocked(item);
24
+ }
16
25
  function TileRadioGroup({
17
26
  items,
18
27
  checked,
@@ -25,9 +34,10 @@ function TileRadioGroup({
25
34
  }) {
26
35
  const refs = useRef([]);
27
36
  const noteId = useId();
37
+ const reasonId = useId();
28
38
  const s = TILE_SIZE[size];
29
39
  const checkedIndex = checked === void 0 ? -1 : items.findIndex((it) => it.key === checked);
30
- const tabStop = checkedIndex >= 0 && !items[checkedIndex].disabled ? checkedIndex : items.findIndex((it) => !it.disabled);
40
+ const tabStop = checkedIndex >= 0 && !isOff(items[checkedIndex]) ? checkedIndex : items.findIndex((it) => !isOff(it));
31
41
  const onKeyDown = (e, index) => {
32
42
  const horizontal = horizontalStep(e.key, e.currentTarget);
33
43
  let step;
@@ -51,7 +61,7 @@ function TileRadioGroup({
51
61
  return;
52
62
  }
53
63
  e.preventDefault();
54
- const enabled = items.map((it, i) => it.disabled ? -1 : i).filter((i) => i >= 0);
64
+ const enabled = items.map((it, i) => isOff(it) ? -1 : i).filter((i) => i >= 0);
55
65
  if (!enabled.length) return;
56
66
  const at = enabled.indexOf(index);
57
67
  let target;
@@ -59,10 +69,12 @@ function TileRadioGroup({
59
69
  else if (step === "last") target = enabled[enabled.length - 1];
60
70
  else target = enabled[(at + step + enabled.length) % enabled.length];
61
71
  refs.current[target]?.focus();
62
- if (activation === "automatic") onSelect(items[target].key);
72
+ if (activation === "automatic" && !isLocked(items[target])) onSelect(items[target].key);
63
73
  };
64
74
  return /* @__PURE__ */ jsx(Fragment, { children: items.map((item, i) => {
65
75
  const selected = i === checkedIndex;
76
+ const locked = isLocked(item);
77
+ const describedBy = [item.note && `${noteId}-${i}`, locked && `${reasonId}-${i}`].filter(Boolean).join(" ");
66
78
  const button = /* @__PURE__ */ jsxs(
67
79
  "button",
68
80
  {
@@ -73,15 +85,18 @@ function TileRadioGroup({
73
85
  role: "radio",
74
86
  "aria-checked": selected,
75
87
  "aria-label": item.label,
76
- "aria-describedby": item.note ? `${noteId}-${i}` : void 0,
88
+ "aria-describedby": describedBy || void 0,
77
89
  tabIndex: i === tabStop ? 0 : -1,
78
- disabled: disabled || item.disabled,
79
- onClick: () => !selected && onSelect(item.key),
90
+ disabled: disabled || isOff(item),
91
+ "aria-disabled": locked && !disabled || void 0,
92
+ onClick: () => !selected && !locked && onSelect(item.key),
80
93
  onKeyDown: (e) => onKeyDown(e, i),
81
94
  className: cn(
82
95
  TILE_BASE,
83
96
  s.tile,
84
97
  selected ? TILE_SELECTED : TILE_IDLE,
98
+ locked && TILE_LOCKED,
99
+ locked && !selected && TILE_LOCKED_IDLE,
85
100
  tileClassName?.(item, selected)
86
101
  ),
87
102
  children: [
@@ -96,6 +111,7 @@ function TileRadioGroup({
96
111
  ),
97
112
  /* @__PURE__ */ jsx("span", { id: `${noteId}-${i}`, hidden: true, children: item.note })
98
113
  ] }),
114
+ locked && /* @__PURE__ */ jsx("span", { id: `${reasonId}-${i}`, hidden: true, children: item.disabledReason }),
99
115
  selected && /* @__PURE__ */ jsx(SelectedTick, {})
100
116
  ]
101
117
  }
@@ -109,9 +125,10 @@ function TileRadioGroup({
109
125
  /* @__PURE__ */ jsx(
110
126
  Tooltip,
111
127
  {
112
- label: item.note ? /* @__PURE__ */ jsxs(Fragment, { children: [
128
+ label: item.note || locked ? /* @__PURE__ */ jsxs(Fragment, { children: [
113
129
  /* @__PURE__ */ jsx("span", { className: "block", children: item.label }),
114
- /* @__PURE__ */ jsx("span", { className: "block font-normal text-[var(--text-muted)]", children: item.note })
130
+ item.note && /* @__PURE__ */ jsx("span", { className: "block font-normal text-[var(--text-muted)]", children: item.note }),
131
+ locked && /* @__PURE__ */ jsx("span", { className: "block font-normal", children: item.disabledReason })
115
132
  ] }) : item.label,
116
133
  portal: true,
117
134
  children: /* @__PURE__ */ jsx("span", { className: "inline-flex", children: button })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/tile-radio.tsx"],"sourcesContent":["import { useId, useRef } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { Ban, Check } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { Tooltip } from \"./tooltip\";\n\n/**\n * A row of square tiles that behaves as ONE radio group — the machinery\n * {@link SwatchPicker} and {@link IconPicker} are built on, public since 0.22.0 for the\n * one-of-n pickers those two do not cover (kastlan's input audit: a choice whose tiles\n * show something of the app's own — a mark, a pattern, a symbol set the kit does not\n * ship).\n *\n * Keksdose had three of these (the category colours, the category symbols, the\n * transaction flags) and all three were rows of `aria-pressed` buttons: a separate\n * tab stop per tile, and \"pressed\" on a choice that is really one-of-n. The APG radio\n * group is the shape a screen reader expects for that (\"Red, radio, 2 of 7, checked\")\n * and the one a keyboard expects too — a single tab stop, arrows to move.\n *\n * ## The container is yours\n *\n * It renders the TILES only — no wrapper — so it can sit in a layout the caller owns\n * (a wrapping row, a grid, a row beside a search box). That element must carry the\n * group's semantics, as SwatchPicker's does:\n *\n * ```tsx\n * <div role=\"radiogroup\" aria-labelledby={headingId} className=\"flex flex-wrap gap-1.5\">\n * <TileRadioGroup\n * items={[{ key: \"dots\", label: \"Dots\" }, { key: \"lines\", label: \"Lines\" }]}\n * checked={pattern}\n * onSelect={(key) => key && setPattern(key)}\n * renderTile={(item) => <PatternGlyph pattern={item.key} className={TILE_SIZE.md.glyph} />}\n * />\n * </div>\n * ```\n *\n * `role=\"radiogroup\"` and a name (`aria-label` or `aria-labelledby`) — without them a\n * screen reader hears radios that belong to nothing, and no \"2 of 7\". Set\n * `aria-disabled` on it as well when the whole group is `disabled`.\n *\n * ## What a tile is\n *\n * A square (28 / 32 / 44px, {@link TILE_SIZE}) holding `renderTile`'s content, NAMED by\n * the item's `label` (`aria-label`, so what is drawn inside is decoration) and showing\n * that label in a bubble on hover and focus — a row of unlabelled squares is the\n * complaint that put the bubble there. It is for choices that are SEEN: a colour, a\n * glyph, a pattern. A choice that has to be READ — a plan with a price and three lines\n * of features, an option with a sentence of explanation — is a {@link ChoiceCardGroup}\n * (`type=\"radio\"`): a card names its option by its title and describes it by its text,\n * where a tile would hide the text behind a bubble and repeat it.\n *\n * No strings of its own: every name is an item's `label`, the \"none\" tile's included.\n */\n\n/** A tile's size: 28 / 32 / 44px — see {@link TILE_SIZE}. */\nexport type TileSize = \"sm\" | \"md\" | \"lg\";\n\n/** 28 / 32 / 44px. `lg` is the touch target a phone surface wants (`min-h-11`). `glyph`\n * and `swatch` are the content sizes the kit's own pickers draw inside each — use them\n * in `renderTile` so a tile of the app's own matches theirs. */\nexport const TILE_SIZE: Record<TileSize, { tile: string; glyph: string; swatch: string }> = {\n sm: { tile: \"size-7\", glyph: \"size-3.5\", swatch: \"size-4\" },\n md: { tile: \"size-8\", glyph: \"size-4\", swatch: \"size-5\" },\n lg: { tile: \"size-11\", glyph: \"size-5\", swatch: \"size-6\" },\n};\n\n/** One tile, whatever it shows. `key` is `null` for the \"none\" entry — a tile of its\n * own that leads back to nothing, since clicking a checked tile does not uncheck it. */\nexport interface TileItem<T extends string> {\n key: T | null;\n label: string;\n /** Said after the name, and shown in the bubble under it. Marks the tile with a\n * dot, so the note is visible without hovering every tile to find it. */\n note?: string;\n disabled?: boolean;\n}\n\nexport interface TileRadioGroupProps<T extends string> {\n /** The tiles, in reading order. Render them inside a `role=\"radiogroup\"` element\n * with a name — see the module note. */\n items: TileItem<T>[];\n /** The checked key, or `undefined` for \"nothing is checked\" (the mixed state, or\n * a value that is not among the options). */\n checked: T | null | undefined;\n onSelect: (key: T | null) => void;\n /** \"automatic\" (default): an arrow key moves AND checks, the APG default. \"manual\":\n * arrows move focus only, Space or Enter checks — for a picker whose change is\n * expensive (a bulk edit that saves on every change). */\n activation?: \"automatic\" | \"manual\";\n /** Every tile disabled. Mark the container `aria-disabled` too. Default `false`. */\n disabled?: boolean;\n /** 28 / 32 / 44px tiles ({@link TILE_SIZE}). Default \"md\"; \"lg\" is the touch-target\n * size a phone surface wants. */\n size?: TileSize;\n /** What goes inside a tile — `aria-hidden` decoration, since the tile is named by\n * `label`. Not called for the `null` (\"none\") item, which shows the kit's \"no\"\n * glyph. The frame, the tick and the note's dot are drawn around it. */\n renderTile: (item: TileItem<T>, selected: boolean) => ReactNode;\n /** Extra classes for a tile, per item, merged after the kit's — so a `size-*` here\n * beats `size`'s (a tile that grows in a narrow card). */\n tileClassName?: (item: TileItem<T>, selected: boolean) => string | undefined;\n}\n\n/**\n * The tile's frame. Selection is a thicker, darker frame AND a tick in the corner —\n * never a colour change alone,\n * because the thing inside a swatch tile IS a colour, and a red frame round a red dot\n * says nothing to a reader who cannot see red.\n *\n * `focus-visible` with an offset in the surface colour, as the checkbox does: the ring\n * must not be painted in the colour of the frame it touches.\n */\nconst TILE_BASE =\n \"relative inline-flex shrink-0 items-center justify-center rounded-md border transition-colors \" +\n \"focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-surface)] \" +\n \"disabled:cursor-not-allowed disabled:opacity-50\";\nconst TILE_IDLE =\n \"border-[var(--border)] bg-[var(--bg-surface)] hover:border-[var(--border-strong)] hover:bg-[var(--bg-hover)]\";\nconst TILE_SELECTED =\n \"border-[var(--text-primary)] bg-[var(--bg-surface)] ring-1 ring-[var(--text-primary)]\";\n\n/**\n * The tiles of one radio group, without the group element — render them inside a\n * `role=\"radiogroup\"` with a name (the module note above has the example). One tab stop\n * (the checked tile, else the first that can be chosen), arrows to move — left and\n * right follow the page's direction — Home and End to the ends, and by default the\n * arrows choose as they go.\n */\nexport function TileRadioGroup<T extends string>({\n items,\n checked,\n onSelect,\n activation = \"automatic\",\n disabled = false,\n size = \"md\",\n renderTile,\n tileClassName,\n}: TileRadioGroupProps<T>) {\n const refs = useRef<Array<HTMLButtonElement | null>>([]);\n const noteId = useId();\n const s = TILE_SIZE[size];\n const checkedIndex =\n checked === undefined ? -1 : items.findIndex((it) => it.key === checked);\n // The ONE tab stop: the checked tile, else the first that can be chosen — the APG\n // rule, so Tab into a group with nothing checked still lands somewhere.\n const tabStop =\n checkedIndex >= 0 && !items[checkedIndex].disabled\n ? checkedIndex\n : items.findIndex((it) => !it.disabled);\n\n const onKeyDown = (e: KeyboardEvent<HTMLButtonElement>, index: number) => {\n // Left and right are VISUAL directions: in a right-to-left page the row runs the\n // other way. Up and down follow reading order either way — the APG's radio group\n // treats them as previous/next, and the tiles wrap, so there is no column to keep.\n const horizontal = horizontalStep(e.key, e.currentTarget);\n let step: number | \"first\" | \"last\";\n switch (horizontal ? \"horizontal\" : e.key) {\n case \"horizontal\":\n step = horizontal;\n break;\n case \"ArrowDown\":\n step = 1;\n break;\n case \"ArrowUp\":\n step = -1;\n break;\n case \"Home\":\n step = \"first\";\n break;\n case \"End\":\n step = \"last\";\n break;\n default:\n return;\n }\n e.preventDefault();\n const enabled = items.map((it, i) => (it.disabled ? -1 : i)).filter((i) => i >= 0);\n if (!enabled.length) return;\n const at = enabled.indexOf(index);\n let target: number;\n if (step === \"first\") target = enabled[0];\n else if (step === \"last\") target = enabled[enabled.length - 1];\n // Wrapping, as a native radio group does: the last tile's \"next\" is the first.\n else target = enabled[(at + step + enabled.length) % enabled.length];\n refs.current[target]?.focus();\n if (activation === \"automatic\") onSelect(items[target].key);\n };\n\n return (\n <>\n {items.map((item, i) => {\n const selected = i === checkedIndex;\n const button = (\n <button\n ref={(el) => {\n refs.current[i] = el;\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n aria-label={item.label}\n // The note is a DESCRIPTION, read after the name and the state, not glued\n // into the name — the punctuation between the two would be English's.\n aria-describedby={item.note ? `${noteId}-${i}` : undefined}\n tabIndex={i === tabStop ? 0 : -1}\n disabled={disabled || item.disabled}\n // Clicking the checked tile again does nothing: a radio is not a toggle.\n // \"None\" is the way back to nothing, and it is a tile of its own.\n onClick={() => !selected && onSelect(item.key)}\n onKeyDown={(e) => onKeyDown(e, i)}\n className={cn(\n TILE_BASE,\n s.tile,\n selected ? TILE_SELECTED : TILE_IDLE,\n tileClassName?.(item, selected),\n )}\n >\n {item.key === null ? (\n <Ban aria-hidden className={cn(s.glyph, \"text-[var(--text-muted)]\")} />\n ) : (\n renderTile(item, selected)\n )}\n {item.note && (\n <>\n {/* A dot rather than a dimmed tile: dimming reads as \"disabled\", and a\n tile with a note is entirely available. */}\n <span\n aria-hidden\n className=\"absolute end-0.5 top-0.5 size-1.5 rounded-full bg-[var(--text-muted)]\"\n />\n {/* `hidden` still serves as a description: aria-describedby reads\n hidden text, which is what keeps it out of the layout. */}\n <span id={`${noteId}-${i}`} hidden>\n {item.note}\n </span>\n </>\n )}\n {selected && <SelectedTick />}\n </button>\n );\n return (\n // The bubble is the SIGHTED user's name for the tile — eight unlabelled\n // squares are the complaint that put a bubble on Keksdose's (live #289).\n // It wraps a span rather than the button, because a Tooltip hands its\n // child an `aria-describedby`, and a tile named \"Red\" and described as\n // \"Red\" is read out twice.\n <Tooltip\n key={item.key ?? \"\\u0000none\"}\n label={\n item.note ? (\n <>\n <span className=\"block\">{item.label}</span>\n <span className=\"block font-normal text-[var(--text-muted)]\">{item.note}</span>\n </>\n ) : (\n item.label\n )\n }\n portal\n >\n <span className=\"inline-flex\">{button}</span>\n </Tooltip>\n );\n })}\n </>\n );\n}\n\n/** The tick on a selected tile — a small badge in the corner, in the text colour on\n * the surface colour, so it reads the same over every swatch colour. */\nfunction SelectedTick() {\n return (\n <span\n aria-hidden\n className=\"absolute -bottom-1 -end-1 inline-flex size-3.5 items-center justify-center rounded-full bg-[var(--text-primary)] text-[var(--bg-surface)]\"\n >\n <Check strokeWidth={3} className=\"size-2.5\" />\n </span>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/tile-radio.tsx"],"sourcesContent":["import { useId, useRef } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { Ban, Check } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { Tooltip } from \"./tooltip\";\n\n/**\n * A row of square tiles that behaves as ONE radio group — the machinery\n * {@link SwatchPicker} and {@link IconPicker} are built on, public since 0.22.0 for the\n * one-of-n pickers those two do not cover (kastlan's input audit: a choice whose tiles\n * show something of the app's own — a mark, a pattern, a symbol set the kit does not\n * ship).\n *\n * Keksdose had three of these (the category colours, the category symbols, the\n * transaction flags) and all three were rows of `aria-pressed` buttons: a separate\n * tab stop per tile, and \"pressed\" on a choice that is really one-of-n. The APG radio\n * group is the shape a screen reader expects for that (\"Red, radio, 2 of 7, checked\")\n * and the one a keyboard expects too — a single tab stop, arrows to move.\n *\n * ## The container is yours\n *\n * It renders the TILES only — no wrapper — so it can sit in a layout the caller owns\n * (a wrapping row, a grid, a row beside a search box). That element must carry the\n * group's semantics, as SwatchPicker's does:\n *\n * ```tsx\n * <div role=\"radiogroup\" aria-labelledby={headingId} className=\"flex flex-wrap gap-1.5\">\n * <TileRadioGroup\n * items={[{ key: \"dots\", label: \"Dots\" }, { key: \"lines\", label: \"Lines\" }]}\n * checked={pattern}\n * onSelect={(key) => key && setPattern(key)}\n * renderTile={(item) => <PatternGlyph pattern={item.key} className={TILE_SIZE.md.glyph} />}\n * />\n * </div>\n * ```\n *\n * `role=\"radiogroup\"` and a name (`aria-label` or `aria-labelledby`) — without them a\n * screen reader hears radios that belong to nothing, and no \"2 of 7\". Set\n * `aria-disabled` on it as well when the whole group is `disabled`.\n *\n * ## What a tile is\n *\n * A square (28 / 32 / 44px, {@link TILE_SIZE}) holding `renderTile`'s content, NAMED by\n * the item's `label` (`aria-label`, so what is drawn inside is decoration) and showing\n * that label in a bubble on hover and focus — a row of unlabelled squares is the\n * complaint that put the bubble there. It is for choices that are SEEN: a colour, a\n * glyph, a pattern. A choice that has to be READ — a plan with a price and three lines\n * of features, an option with a sentence of explanation — is a {@link ChoiceCardGroup}\n * (`type=\"radio\"`): a card names its option by its title and describes it by its text,\n * where a tile would hide the text behind a bubble and repeat it.\n *\n * No strings of its own: every name is an item's `label`, the \"none\" tile's included.\n *\n * ## A tile that cannot be chosen\n *\n * `disabled` takes a tile out of the choice AND out of reach: the arrow keys skip it\n * and Tab never lands on it, so a dimmed square says nothing about why. Give it a\n * `disabledReason` instead and it stays reachable, `aria-disabled`, with the reason in\n * its bubble and its description — and choosing it does nothing (0.23, the kit's later\n * list after keksdose K3; {@link Button}'s `disabledReason`, one tile at a time).\n */\n\n/** A tile's size: 28 / 32 / 44px — see {@link TILE_SIZE}. */\nexport type TileSize = \"sm\" | \"md\" | \"lg\";\n\n/** 28 / 32 / 44px. `lg` is the touch target a phone surface wants (`min-h-11`). `glyph`\n * and `swatch` are the content sizes the kit's own pickers draw inside each — use them\n * in `renderTile` so a tile of the app's own matches theirs. */\nexport const TILE_SIZE: Record<TileSize, { tile: string; glyph: string; swatch: string }> = {\n sm: { tile: \"size-7\", glyph: \"size-3.5\", swatch: \"size-4\" },\n md: { tile: \"size-8\", glyph: \"size-4\", swatch: \"size-5\" },\n lg: { tile: \"size-11\", glyph: \"size-5\", swatch: \"size-6\" },\n};\n\n/** One tile, whatever it shows. `key` is `null` for the \"none\" entry — a tile of its\n * own that leads back to nothing, since clicking a checked tile does not uncheck it. */\nexport interface TileItem<T extends string> {\n key: T | null;\n label: string;\n /** Said after the name, and shown in the bubble under it. Marks the tile with a\n * dot, so the note is visible without hovering every tile to find it. */\n note?: string;\n /** Out of the choice and out of reach: natively disabled, skipped by the arrow keys,\n * never the tab stop. */\n disabled?: boolean;\n /**\n * Why this tile cannot be chosen right now — a colour another category already wears,\n * a symbol only an owner may set. The tile LOOKS disabled but is `aria-disabled`, not\n * `disabled`: Tab and the arrow keys still reach it, the reason shows in its bubble\n * under its name (and its `note`) and describes it, and a click, Space / Enter or an\n * arrow under automatic activation chooses nothing — the focus moves, the checked\n * tile stays. Wins over `disabled`, as on {@link Button}; the whole group's\n * `disabled` still wins over it.\n */\n disabledReason?: ReactNode;\n}\n\nexport interface TileRadioGroupProps<T extends string> {\n /** The tiles, in reading order. Render them inside a `role=\"radiogroup\"` element\n * with a name — see the module note. */\n items: TileItem<T>[];\n /** The checked key, or `undefined` for \"nothing is checked\" (the mixed state, or\n * a value that is not among the options). */\n checked: T | null | undefined;\n onSelect: (key: T | null) => void;\n /** \"automatic\" (default): an arrow key moves AND checks, the APG default. \"manual\":\n * arrows move focus only, Space or Enter checks — for a picker whose change is\n * expensive (a bulk edit that saves on every change). */\n activation?: \"automatic\" | \"manual\";\n /** Every tile disabled. Mark the container `aria-disabled` too. Default `false`. */\n disabled?: boolean;\n /** 28 / 32 / 44px tiles ({@link TILE_SIZE}). Default \"md\"; \"lg\" is the touch-target\n * size a phone surface wants. */\n size?: TileSize;\n /** What goes inside a tile — `aria-hidden` decoration, since the tile is named by\n * `label`. Not called for the `null` (\"none\") item, which shows the kit's \"no\"\n * glyph. The frame, the tick and the note's dot are drawn around it. */\n renderTile: (item: TileItem<T>, selected: boolean) => ReactNode;\n /** Extra classes for a tile, per item, merged after the kit's — so a `size-*` here\n * beats `size`'s (a tile that grows in a narrow card). */\n tileClassName?: (item: TileItem<T>, selected: boolean) => string | undefined;\n}\n\n/**\n * The tile's frame. Selection is a thicker, darker frame AND a tick in the corner —\n * never a colour change alone,\n * because the thing inside a swatch tile IS a colour, and a red frame round a red dot\n * says nothing to a reader who cannot see red.\n *\n * `focus-visible` with an offset in the surface colour, as the checkbox does: the ring\n * must not be painted in the colour of the frame it touches.\n */\nconst TILE_BASE =\n \"relative inline-flex shrink-0 items-center justify-center rounded-md border transition-colors \" +\n \"focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-surface)] \" +\n \"disabled:cursor-not-allowed disabled:opacity-50\";\nconst TILE_IDLE =\n \"border-[var(--border)] bg-[var(--bg-surface)] hover:border-[var(--border-strong)] hover:bg-[var(--bg-hover)]\";\nconst TILE_SELECTED =\n \"border-[var(--text-primary)] bg-[var(--bg-surface)] ring-1 ring-[var(--text-primary)]\";\n/** A locked tile: the disabled look without `disabled` — dimmed, a not-allowed cursor. */\nconst TILE_LOCKED = \"cursor-not-allowed opacity-50\";\n/** …and no hover offer on an idle one, which cannot be chosen. Not on the selected\n * tile, whose frame IS its state. */\nconst TILE_LOCKED_IDLE = \"hover:border-[var(--border)] hover:bg-[var(--bg-surface)]\";\n\n/** `undefined`, `null`, `false` and `\"\"` are no reason — what `cond && \"…\"` gives. */\nfunction isLocked(item: TileItem<string>): boolean {\n const r = item.disabledReason;\n return r !== undefined && r !== null && r !== false && r !== \"\";\n}\n\n/** Out of reach: natively disabled. A reason keeps a `disabled` tile reachable. */\nfunction isOff(item: TileItem<string>): boolean {\n return Boolean(item.disabled) && !isLocked(item);\n}\n\n/**\n * The tiles of one radio group, without the group element — render them inside a\n * `role=\"radiogroup\"` with a name (the module note above has the example). One tab stop\n * (the checked tile, else the first that can be chosen), arrows to move — left and\n * right follow the page's direction — Home and End to the ends, and by default the\n * arrows choose as they go.\n */\nexport function TileRadioGroup<T extends string>({\n items,\n checked,\n onSelect,\n activation = \"automatic\",\n disabled = false,\n size = \"md\",\n renderTile,\n tileClassName,\n}: TileRadioGroupProps<T>) {\n const refs = useRef<Array<HTMLButtonElement | null>>([]);\n const noteId = useId();\n const reasonId = useId();\n const s = TILE_SIZE[size];\n const checkedIndex =\n checked === undefined ? -1 : items.findIndex((it) => it.key === checked);\n // The ONE tab stop: the checked tile, else the first that can be reached — the APG\n // rule, so Tab into a group with nothing checked still lands somewhere. A locked\n // tile is reachable: landing on it is how its reason is read.\n const tabStop =\n checkedIndex >= 0 && !isOff(items[checkedIndex])\n ? checkedIndex\n : items.findIndex((it) => !isOff(it));\n\n const onKeyDown = (e: KeyboardEvent<HTMLButtonElement>, index: number) => {\n // Left and right are VISUAL directions: in a right-to-left page the row runs the\n // other way. Up and down follow reading order either way — the APG's radio group\n // treats them as previous/next, and the tiles wrap, so there is no column to keep.\n const horizontal = horizontalStep(e.key, e.currentTarget);\n let step: number | \"first\" | \"last\";\n switch (horizontal ? \"horizontal\" : e.key) {\n case \"horizontal\":\n step = horizontal;\n break;\n case \"ArrowDown\":\n step = 1;\n break;\n case \"ArrowUp\":\n step = -1;\n break;\n case \"Home\":\n step = \"first\";\n break;\n case \"End\":\n step = \"last\";\n break;\n default:\n return;\n }\n e.preventDefault();\n // Locked tiles are in the walk; natively disabled ones are not.\n const enabled = items.map((it, i) => (isOff(it) ? -1 : i)).filter((i) => i >= 0);\n if (!enabled.length) return;\n const at = enabled.indexOf(index);\n let target: number;\n if (step === \"first\") target = enabled[0];\n else if (step === \"last\") target = enabled[enabled.length - 1];\n // Wrapping, as a native radio group does: the last tile's \"next\" is the first.\n else target = enabled[(at + step + enabled.length) % enabled.length];\n refs.current[target]?.focus();\n // Onto a locked tile the focus moves and the choice does not: the checked tile\n // stays checked, and the next arrow walks on from here.\n if (activation === \"automatic\" && !isLocked(items[target])) onSelect(items[target].key);\n };\n\n return (\n <>\n {items.map((item, i) => {\n const selected = i === checkedIndex;\n const locked = isLocked(item);\n const describedBy = [item.note && `${noteId}-${i}`, locked && `${reasonId}-${i}`].filter(Boolean).join(\" \");\n const button = (\n <button\n ref={(el) => {\n refs.current[i] = el;\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n aria-label={item.label}\n // The note is a DESCRIPTION, read after the name and the state, not glued\n // into the name — the punctuation between the two would be English's. So\n // is a lock's reason, after the note.\n aria-describedby={describedBy || undefined}\n tabIndex={i === tabStop ? 0 : -1}\n // A reason wins over the tile's own `disabled` (as on Button): reachable,\n // and refusing. The whole group's `disabled` still wins over both.\n disabled={disabled || isOff(item)}\n aria-disabled={(locked && !disabled) || undefined}\n // Clicking the checked tile again does nothing: a radio is not a toggle.\n // \"None\" is the way back to nothing, and it is a tile of its own. A locked\n // tile is never chosen.\n onClick={() => !selected && !locked && onSelect(item.key)}\n onKeyDown={(e) => onKeyDown(e, i)}\n className={cn(\n TILE_BASE,\n s.tile,\n selected ? TILE_SELECTED : TILE_IDLE,\n locked && TILE_LOCKED,\n locked && !selected && TILE_LOCKED_IDLE,\n tileClassName?.(item, selected),\n )}\n >\n {item.key === null ? (\n <Ban aria-hidden className={cn(s.glyph, \"text-[var(--text-muted)]\")} />\n ) : (\n renderTile(item, selected)\n )}\n {item.note && (\n <>\n {/* A dot rather than a dimmed tile: dimming reads as \"disabled\", and a\n tile with a note is entirely available. */}\n <span\n aria-hidden\n className=\"absolute end-0.5 top-0.5 size-1.5 rounded-full bg-[var(--text-muted)]\"\n />\n {/* `hidden` still serves as a description: aria-describedby reads\n hidden text, which is what keeps it out of the layout. */}\n <span id={`${noteId}-${i}`} hidden>\n {item.note}\n </span>\n </>\n )}\n {/* The reason's description is a `hidden` copy, as on a locked Button: the\n bubble exists only while hovered or focused once portalled, and a\n description that comes and goes is read inconsistently. */}\n {locked && (\n <span id={`${reasonId}-${i}`} hidden>\n {item.disabledReason}\n </span>\n )}\n {selected && <SelectedTick />}\n </button>\n );\n return (\n // The bubble is the SIGHTED user's name for the tile — eight unlabelled\n // squares are the complaint that put a bubble on Keksdose's (live #289).\n // It wraps a span rather than the button, because a Tooltip hands its\n // child an `aria-describedby`, and a tile named \"Red\" and described as\n // \"Red\" is read out twice.\n <Tooltip\n key={item.key ?? \"\\u0000none\"}\n label={\n item.note || locked ? (\n <>\n <span className=\"block\">{item.label}</span>\n {item.note && <span className=\"block font-normal text-[var(--text-muted)]\">{item.note}</span>}\n {/* The reason in the body colour: it is the news, the note is\n standing context. */}\n {locked && <span className=\"block font-normal\">{item.disabledReason}</span>}\n </>\n ) : (\n item.label\n )\n }\n portal\n >\n <span className=\"inline-flex\">{button}</span>\n </Tooltip>\n );\n })}\n </>\n );\n}\n\n/** The tick on a selected tile — a small badge in the corner, in the text colour on\n * the surface colour, so it reads the same over every swatch colour. */\nfunction SelectedTick() {\n return (\n <span\n aria-hidden\n className=\"absolute -bottom-1 -end-1 inline-flex size-3.5 items-center justify-center rounded-full bg-[var(--text-primary)] text-[var(--bg-surface)]\"\n >\n <Check strokeWidth={3} className=\"size-2.5\" />\n </span>\n );\n}\n"],"mappings":"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","names":[]}