@eifi1/ui-kit 0.48.2 → 0.49.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 (292) hide show
  1. package/README.md +26 -12
  2. package/dist/admin/admin-action-confirm.js +1 -1
  3. package/dist/admin/admin-action-confirm.js.map +1 -1
  4. package/dist/admin/invitations-panel.js +5 -1
  5. package/dist/admin/invitations-panel.js.map +1 -1
  6. package/dist/admin/user-roster.d.ts +11 -5
  7. package/dist/admin/user-roster.js +15 -4
  8. package/dist/admin/user-roster.js.map +1 -1
  9. package/dist/auth/accept-invitation.js +2 -2
  10. package/dist/auth/accept-invitation.js.map +1 -1
  11. package/dist/auth/not-found-page.js +1 -1
  12. package/dist/auth/not-found-page.js.map +1 -1
  13. package/dist/auth/reset-password-form.js +2 -2
  14. package/dist/auth/reset-password-form.js.map +1 -1
  15. package/dist/auth/verify-email.js +2 -2
  16. package/dist/auth/verify-email.js.map +1 -1
  17. package/dist/billing/plan-card.js +2 -2
  18. package/dist/billing/plan-card.js.map +1 -1
  19. package/dist/components/breadcrumbs.d.ts +19 -1
  20. package/dist/components/breadcrumbs.js +11 -4
  21. package/dist/components/breadcrumbs.js.map +1 -1
  22. package/dist/components/button-look.d.ts +37 -7
  23. package/dist/components/button-look.js +33 -6
  24. package/dist/components/button-look.js.map +1 -1
  25. package/dist/components/button.d.ts +4 -1
  26. package/dist/components/button.js +2 -2
  27. package/dist/components/button.js.map +1 -1
  28. package/dist/components/calendar-heatmap.js +2 -1
  29. package/dist/components/calendar-heatmap.js.map +1 -1
  30. package/dist/components/chart-edge.d.ts +10 -0
  31. package/dist/components/chart-edge.js +57 -0
  32. package/dist/components/chart-edge.js.map +1 -0
  33. package/dist/components/chart.js +2 -1
  34. package/dist/components/chart.js.map +1 -1
  35. package/dist/components/chip.d.ts +8 -1
  36. package/dist/components/chip.js +4 -1
  37. package/dist/components/chip.js.map +1 -1
  38. package/dist/components/confirm-context.d.ts +5 -0
  39. package/dist/components/confirm-context.js +7 -0
  40. package/dist/components/confirm-context.js.map +1 -0
  41. package/dist/components/confirm-dialog.js +3 -3
  42. package/dist/components/confirm-dialog.js.map +1 -1
  43. package/dist/components/danger-confirm.d.ts +2 -2
  44. package/dist/components/danger-confirm.js +1 -1
  45. package/dist/components/danger-confirm.js.map +1 -1
  46. package/dist/components/data-table-fit.js +21 -3
  47. package/dist/components/data-table-fit.js.map +1 -1
  48. package/dist/components/data-table-mobile.d.ts +48 -2
  49. package/dist/components/data-table-mobile.js +87 -10
  50. package/dist/components/data-table-mobile.js.map +1 -1
  51. package/dist/components/data-table-row-actions.d.ts +2 -1
  52. package/dist/components/data-table-row-actions.js +7 -0
  53. package/dist/components/data-table-row-actions.js.map +1 -1
  54. package/dist/components/data-table-row.d.ts +1 -1
  55. package/dist/components/data-table-row.js.map +1 -1
  56. package/dist/components/data-table.d.ts +34 -1
  57. package/dist/components/data-table.js +34 -9
  58. package/dist/components/data-table.js.map +1 -1
  59. package/dist/components/error-boundary.js +2 -2
  60. package/dist/components/error-boundary.js.map +1 -1
  61. package/dist/components/floating-panel.d.ts +36 -13
  62. package/dist/components/floating-panel.js +27 -7
  63. package/dist/components/floating-panel.js.map +1 -1
  64. package/dist/components/form-actions.d.ts +1 -1
  65. package/dist/components/form-actions.js +1 -1
  66. package/dist/components/form-actions.js.map +1 -1
  67. package/dist/components/icon-button.d.ts +11 -0
  68. package/dist/components/icon-button.js +7 -5
  69. package/dist/components/icon-button.js.map +1 -1
  70. package/dist/components/nav-pills.d.ts +8 -1
  71. package/dist/components/nav-pills.js +61 -38
  72. package/dist/components/nav-pills.js.map +1 -1
  73. package/dist/components/page-header-context.d.ts +8 -0
  74. package/dist/components/page-header-context.js +7 -0
  75. package/dist/components/page-header-context.js.map +1 -0
  76. package/dist/components/page-header.d.ts +14 -2
  77. package/dist/components/page-header.js +10 -1
  78. package/dist/components/page-header.js.map +1 -1
  79. package/dist/components/pie-chart.js +2 -1
  80. package/dist/components/pie-chart.js.map +1 -1
  81. package/dist/components/progress-bar.js +5 -4
  82. package/dist/components/progress-bar.js.map +1 -1
  83. package/dist/components/reauth-dialog.js +1 -1
  84. package/dist/components/reauth-dialog.js.map +1 -1
  85. package/dist/components/row-actions-labels.js +3 -1
  86. package/dist/components/row-actions-labels.js.map +1 -1
  87. package/dist/components/row-actions.d.ts +43 -9
  88. package/dist/components/row-actions.js +78 -15
  89. package/dist/components/row-actions.js.map +1 -1
  90. package/dist/components/section-nav.d.ts +37 -0
  91. package/dist/components/section-nav.js +33 -0
  92. package/dist/components/section-nav.js.map +1 -0
  93. package/dist/components/series-chart.js +8 -3
  94. package/dist/components/series-chart.js.map +1 -1
  95. package/dist/components/share-card.js +1 -1
  96. package/dist/components/share-card.js.map +1 -1
  97. package/dist/components/signature-pad.js +1 -1
  98. package/dist/components/signature-pad.js.map +1 -1
  99. package/dist/components/sparkline.js +2 -1
  100. package/dist/components/sparkline.js.map +1 -1
  101. package/dist/components/stat-tile.js +14 -7
  102. package/dist/components/stat-tile.js.map +1 -1
  103. package/dist/components/status-dot.d.ts +5 -1
  104. package/dist/components/status-dot.js +4 -1
  105. package/dist/components/status-dot.js.map +1 -1
  106. package/dist/components/tooltip.js +1 -1
  107. package/dist/components/tooltip.js.map +1 -1
  108. package/dist/components/translation-review-editor.js +2 -2
  109. package/dist/components/translation-review-editor.js.map +1 -1
  110. package/dist/components/translation-review.js +2 -2
  111. package/dist/components/translation-review.js.map +1 -1
  112. package/dist/components/treemap.d.ts +5 -2
  113. package/dist/components/treemap.js +26 -6
  114. package/dist/components/treemap.js.map +1 -1
  115. package/dist/components/use-table-state.js +37 -6
  116. package/dist/components/use-table-state.js.map +1 -1
  117. package/dist/data-table.js.map +1 -1
  118. package/dist/demo/demo-banner.js +1 -1
  119. package/dist/demo/demo-banner.js.map +1 -1
  120. package/dist/demo/demo-ended.js +1 -1
  121. package/dist/demo/demo-ended.js.map +1 -1
  122. package/dist/demo/demo-start.js +1 -1
  123. package/dist/demo/demo-start.js.map +1 -1
  124. package/dist/eslint/index.js +1 -1
  125. package/dist/feedback/feedback-inbox.js +1 -1
  126. package/dist/feedback/feedback-inbox.js.map +1 -1
  127. package/dist/hooks/use-dismiss.d.ts +9 -1
  128. package/dist/hooks/use-dismiss.js +25 -5
  129. package/dist/hooks/use-dismiss.js.map +1 -1
  130. package/dist/hooks/use-document-title.d.ts +14 -0
  131. package/dist/hooks/use-document-title.js +22 -0
  132. package/dist/hooks/use-document-title.js.map +1 -0
  133. package/dist/i18n/german.js +6 -2
  134. package/dist/i18n/german.js.map +1 -1
  135. package/dist/i18n/kit-labels.d.ts +31 -1
  136. package/dist/i18n/kit-labels.js +43 -2
  137. package/dist/i18n/kit-labels.js.map +1 -1
  138. package/dist/i18n/locales/en.js +6 -2
  139. package/dist/i18n/locales/en.js.map +2 -2
  140. package/dist/i18n/locales/es.js +6 -2
  141. package/dist/i18n/locales/es.js.map +1 -1
  142. package/dist/i18n/locales/fr.js +6 -2
  143. package/dist/i18n/locales/fr.js.map +1 -1
  144. package/dist/i18n/locales/hu.js +6 -2
  145. package/dist/i18n/locales/hu.js.map +1 -1
  146. package/dist/i18n/locales/it.js +6 -2
  147. package/dist/i18n/locales/it.js.map +1 -1
  148. package/dist/i18n/locales/zh.js +6 -2
  149. package/dist/i18n/locales/zh.js.map +1 -1
  150. package/dist/i18n/review.js +2 -0
  151. package/dist/i18n/review.js.map +1 -1
  152. package/dist/i18n/shared-labels.js +1 -0
  153. package/dist/i18n/shared-labels.js.map +1 -1
  154. package/dist/index.d.ts +8 -1
  155. package/dist/index.js +11 -0
  156. package/dist/index.js.map +1 -1
  157. package/dist/landing/landing-actions.js +2 -2
  158. package/dist/landing/landing-actions.js.map +1 -1
  159. package/dist/landing/public-header.js +2 -2
  160. package/dist/landing/public-header.js.map +1 -1
  161. package/dist/lib/clipboard-minus.d.ts +27 -0
  162. package/dist/lib/clipboard-minus.js +63 -0
  163. package/dist/lib/clipboard-minus.js.map +1 -0
  164. package/dist/lib/document-title.d.ts +17 -0
  165. package/dist/lib/document-title.js +29 -0
  166. package/dist/lib/document-title.js.map +1 -0
  167. package/dist/lib/float-clearance.js +8 -2
  168. package/dist/lib/float-clearance.js.map +1 -1
  169. package/dist/lib/format.d.ts +35 -10
  170. package/dist/lib/format.js +55 -11
  171. package/dist/lib/format.js.map +1 -1
  172. package/dist/lib/minus.d.ts +22 -0
  173. package/dist/lib/minus.js +14 -0
  174. package/dist/lib/minus.js.map +1 -0
  175. package/dist/lib/status-tone.d.ts +30 -0
  176. package/dist/lib/status-tone.js +17 -0
  177. package/dist/lib/status-tone.js.map +1 -0
  178. package/dist/settings/settings-layout.js +3 -2
  179. package/dist/settings/settings-layout.js.map +1 -1
  180. package/dist/shell/account-menu-language.d.ts +1 -1
  181. package/dist/shell/account-menu-language.js +9 -1
  182. package/dist/shell/account-menu-language.js.map +1 -1
  183. package/dist/shell/app-shell-labels.js +2 -1
  184. package/dist/shell/app-shell-labels.js.map +1 -1
  185. package/dist/shell/app-shell.d.ts +58 -16
  186. package/dist/shell/app-shell.js +151 -43
  187. package/dist/shell/app-shell.js.map +1 -1
  188. package/dist/shell/language-code.d.ts +3 -0
  189. package/dist/shell/language-code.js +8 -0
  190. package/dist/shell/language-code.js.map +1 -0
  191. package/dist/shell/topbar-controls.d.ts +14 -6
  192. package/dist/shell/topbar-controls.js +14 -17
  193. package/dist/shell/topbar-controls.js.map +1 -1
  194. package/dist/testing/css-variables.generated.d.ts +1 -1
  195. package/dist/testing/css-variables.generated.js +1 -1
  196. package/dist/testing/css-variables.generated.js.map +1 -1
  197. package/dist/theme/chart-palette.d.ts +82 -15
  198. package/dist/theme/chart-palette.js +51 -24
  199. package/dist/theme/chart-palette.js.map +1 -1
  200. package/dist/theme/palette-presets.d.ts +9 -0
  201. package/dist/theme/palette-presets.js +77 -62
  202. package/dist/theme/palette-presets.js.map +1 -1
  203. package/dist/tour/tour.js +1 -1
  204. package/dist/tour/tour.js.map +1 -1
  205. package/dist/wizard/stepper-nav.d.ts +1 -1
  206. package/dist/wizard/stepper-nav.js +3 -3
  207. package/dist/wizard/stepper-nav.js.map +1 -1
  208. package/package.json +1 -1
  209. package/src/admin/admin-action-confirm.tsx +1 -1
  210. package/src/admin/invitations-panel.tsx +7 -3
  211. package/src/admin/user-roster.tsx +23 -6
  212. package/src/auth/accept-invitation.tsx +3 -3
  213. package/src/auth/not-found-page.tsx +1 -1
  214. package/src/auth/reset-password-form.tsx +3 -3
  215. package/src/auth/verify-email.tsx +3 -3
  216. package/src/billing/plan-card.tsx +2 -2
  217. package/src/components/breadcrumbs.tsx +32 -4
  218. package/src/components/button-look.ts +86 -15
  219. package/src/components/button.tsx +6 -3
  220. package/src/components/calendar-heatmap.tsx +2 -1
  221. package/src/components/chart-edge.ts +73 -0
  222. package/src/components/chart.tsx +3 -1
  223. package/src/components/chip.tsx +10 -1
  224. package/src/components/confirm-context.ts +7 -0
  225. package/src/components/confirm-dialog.tsx +3 -3
  226. package/src/components/danger-confirm.tsx +3 -3
  227. package/src/components/data-table-fit.ts +41 -6
  228. package/src/components/data-table-mobile.tsx +162 -11
  229. package/src/components/data-table-row-actions.tsx +9 -1
  230. package/src/components/data-table-row.tsx +2 -0
  231. package/src/components/data-table.tsx +82 -18
  232. package/src/components/error-boundary.tsx +2 -2
  233. package/src/components/floating-panel.tsx +67 -16
  234. package/src/components/form-actions.tsx +2 -2
  235. package/src/components/icon-button.tsx +21 -8
  236. package/src/components/nav-pills.tsx +29 -2
  237. package/src/components/page-header-context.ts +8 -0
  238. package/src/components/page-header.tsx +34 -2
  239. package/src/components/pie-chart.tsx +2 -1
  240. package/src/components/progress-bar.tsx +6 -3
  241. package/src/components/reauth-dialog.tsx +1 -1
  242. package/src/components/row-actions-labels.ts +2 -0
  243. package/src/components/row-actions.tsx +154 -33
  244. package/src/components/section-nav.tsx +65 -0
  245. package/src/components/series-chart.tsx +12 -3
  246. package/src/components/share-card.tsx +1 -1
  247. package/src/components/signature-pad.tsx +1 -1
  248. package/src/components/sparkline.tsx +2 -1
  249. package/src/components/stat-tile.tsx +19 -7
  250. package/src/components/status-dot.tsx +7 -1
  251. package/src/components/tooltip.tsx +3 -1
  252. package/src/components/translation-review-editor.tsx +2 -2
  253. package/src/components/translation-review.tsx +2 -2
  254. package/src/components/treemap.tsx +34 -8
  255. package/src/components/use-table-state.ts +63 -8
  256. package/src/data-table.ts +3 -3
  257. package/src/demo/demo-banner.tsx +1 -1
  258. package/src/demo/demo-ended.tsx +1 -1
  259. package/src/demo/demo-start.tsx +1 -1
  260. package/src/feedback/feedback-inbox.tsx +1 -1
  261. package/src/hooks/use-dismiss.ts +43 -4
  262. package/src/hooks/use-document-title.ts +31 -0
  263. package/src/i18n/german.ts +4 -0
  264. package/src/i18n/kit-labels.tsx +76 -1
  265. package/src/i18n/locales/es.ts +4 -0
  266. package/src/i18n/locales/fr.ts +4 -0
  267. package/src/i18n/locales/hu.ts +4 -0
  268. package/src/i18n/locales/it.ts +4 -0
  269. package/src/i18n/locales/zh.ts +4 -0
  270. package/src/i18n/review.ts +2 -0
  271. package/src/i18n/shared-labels.ts +1 -0
  272. package/src/index.ts +13 -0
  273. package/src/landing/landing-actions.tsx +2 -2
  274. package/src/landing/public-header.tsx +2 -2
  275. package/src/lib/clipboard-minus.ts +94 -0
  276. package/src/lib/document-title.ts +41 -0
  277. package/src/lib/float-clearance.ts +19 -3
  278. package/src/lib/format.ts +115 -30
  279. package/src/lib/minus.ts +40 -0
  280. package/src/lib/status-tone.ts +43 -0
  281. package/src/settings/settings-layout.tsx +3 -2
  282. package/src/shell/account-menu-language.tsx +14 -2
  283. package/src/shell/app-shell-labels.ts +1 -0
  284. package/src/shell/app-shell.tsx +291 -68
  285. package/src/shell/language-code.ts +5 -0
  286. package/src/shell/topbar-controls.tsx +35 -15
  287. package/src/testing/css-variables.generated.ts +6 -6
  288. package/src/theme/chart-palette.ts +102 -20
  289. package/src/theme/palette-presets.ts +121 -84
  290. package/src/tour/tour.tsx +1 -1
  291. package/src/wizard/stepper-nav.tsx +4 -4
  292. package/tokens.css +28 -18
@@ -3,6 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { isValidElement, useCallback, useId } from "react";
4
4
  import { Check, X } from "lucide-react";
5
5
  import { cn } from "../lib/cn.js";
6
+ import { statusTone } from "../lib/status-tone.js";
6
7
  import { TOUCH_TARGET_LARGE } from "./button-look.js";
7
8
  import { FOCUS_RING } from "./focus-ring.js";
8
9
  import { assignRef, LockedReason } from "./field-parts.js";
@@ -176,7 +177,8 @@ const REMOVE_HIT_AREA_LARGE = "relative large:after:absolute large:after:inset-[
176
177
  function Chip({
177
178
  ref,
178
179
  children,
179
- tone = "neutral",
180
+ tone: toneProp,
181
+ status,
180
182
  size = "md",
181
183
  variant = "soft",
182
184
  shape = "pill",
@@ -202,6 +204,7 @@ function Chip({
202
204
  commit,
203
205
  ...rest
204
206
  }) {
207
+ const tone = toneProp ?? (status ? statusTone(status) : "neutral");
205
208
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS, { remove: removeLabel });
206
209
  const reasonId = useId();
207
210
  const disabledReason = useCommitReason(commit, ownDisabledReason);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/chip.tsx"],"sourcesContent":["import { isValidElement, useCallback, useId } from \"react\";\nimport type { KeyboardEvent, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { Check, X } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { TOUCH_TARGET_LARGE } from \"./button-look\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { assignRef, LockedReason, type DisabledReasonDisplay } from \"./field-parts\";\nimport { hasContent } from \"../lib/presence\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\nimport { useCommitReason, type CommitScope } from \"./write-lock\";\nimport { useSnapEdges } from \"../lib/chip-edge-snap\";\n// The edge-snap engine lives in lib/chip-edge-snap.ts and ChipInput in chip-input.tsx\n// (MOD-9). `refreshChipEdges` is still this module's; of ChipInput only the types are,\n// since a runtime re-export would make chip ⇄ chip-input a cycle — the barrel exports\n// chip-input itself.\nexport { refreshChipEdges } from \"../lib/chip-edge-snap\";\nexport type { ChipInputLabels, ChipInputProps } from \"./chip-input\";\n\n/**\n * A chip: a compact pill carrying one value.\n *\n * Three jobs, deliberately one component. A chip can be inert (a tag on a row), a link\n * (a jump to a section), or a toggle (a filter that is on or off) — and the difference is\n * which prop you pass, not which component you pick. Keeping them together is what stops\n * an app growing three near-identical pills that drift apart; it is the shape this kit\n * already uses for `Button`, which is a link when it is given an href.\n *\n * A chip is not a `Button`. It is smaller, it is rounded to a pill, and it reads as a\n * VALUE rather than as an action — which matters because a row of eight buttons says\n * \"choose one of eight things to do\" and a row of eight chips says \"here are eight\n * things\". Using `Button` for both is the mistake this exists to prevent.\n */\n\nexport type ChipTone =\n | \"neutral\"\n | \"brand\"\n | \"danger\"\n | \"warning\"\n | \"success\"\n | \"info\"\n | \"income\"\n | \"expense\"\n | ChipHue;\n/**\n * Categorical hues (0.10.0): colour that says \"a different value\" and nothing more —\n * kastlan's status-badge.tsx paints ~15 enums in 9 Tailwind hues by hand. Four of those\n * nine are semantic and already tones (green = `success`, yellow = `warning`, red =\n * `danger`, gray = `neutral`); these five are the rest, each a `--hue-*` token triple\n * in tokens.css with a light and a dark value. Use a semantic tone when the value HAS a\n * meaning (overdue is `danger`), a hue when it only needs telling apart (sent vs\n * accepted vs invoiced).\n */\nexport type ChipHue = \"blue\" | \"indigo\" | \"purple\" | \"teal\" | \"orange\";\n/** `lg` is the 44px touch target (`min-h-11`) a phone surface wants; `md` is ~28px.\n * `xs` (0.10.0) is 11px type at `px-1.5` — keksdose's goal markers and budget-cell\n * badges, which sit inside a figure's line and must not make it taller. */\nexport type ChipSize = \"xs\" | \"sm\" | \"md\" | \"lg\";\n/**\n * How much of the tone the chip wears.\n *\n * - `soft` (default): the tinted surface every chip has had.\n * - `outline`: the tone's border and text on NO surface — keksdose's DEV badge in the\n * account menu and kastlan's 27 shadcn `variant=\"outline\"` badges, which sit on\n * coloured rows where a second tint would muddy the row.\n * - `solid`: the tone as a fill under contrasting text — the \"count\" pill (unread\n * messages, open tasks). It centres its content and sets figures tabular, so a\n * one-digit count is a round dot and \"9\" → \"10\" does not jiggle the row.\n * - `dot` (0.10.0): no pill at all — a dot in the tone's colour before plain text, for\n * a status in a dense list or a table column where eight tinted pills in a column\n * would outshout the figures beside them (kastlan's unit picker already draws its\n * UnitStatus this way, from a separate `dotColor` map).\n */\nexport type ChipVariant = \"soft\" | \"outline\" | \"solid\" | \"dot\";\n/** `pill` (default) is fully rounded; `square` has the small radius of a field or a\n * button — for a chip that sits in a table cell or beside square controls. */\nexport type ChipShape = \"pill\" | \"square\";\n\nconst TONE: Record<ChipTone, { idle: string; selected: string }> = {\n neutral: {\n idle: \"border-subtle bg-surface-2 text-secondary\",\n selected: \"border-strong bg-active text-primary\",\n },\n brand: {\n idle: \"border-subtle bg-brand-soft text-brand-muted\",\n selected: \"border-brand bg-brand text-brand-contrast\",\n },\n danger: {\n idle: \"border-danger bg-danger-soft text-danger\",\n selected: \"border-[var(--danger)] bg-danger text-danger-contrast\",\n },\n warning: {\n idle: \"border-warning bg-warning-soft text-warning\",\n selected: \"border-[var(--warning)] bg-warning-soft text-warning\",\n },\n success: {\n idle: \"border-success bg-success-soft text-success\",\n selected: \"border-[var(--success)] bg-success-soft text-success\",\n },\n info: {\n idle: \"border-info bg-info-soft text-info\",\n selected: \"border-[var(--info)] bg-info-soft text-info\",\n },\n // The money pair (Keksdose's direction toggle). Only the TEXT and the BORDER carry\n // the tint, on the neutral chip's own surfaces: the money tokens are chosen for text\n // contrast, and a filled amber pill beside an amber figure reads as one smear.\n // `border-current` takes its hue from the text, so the two cannot drift apart.\n income: {\n idle: \"border-current/40 bg-surface-2 text-money-income\",\n selected: \"border-current bg-active text-money-income\",\n },\n expense: {\n idle: \"border-current/40 bg-surface-2 text-money-expense\",\n selected: \"border-current bg-active text-money-expense\",\n },\n // The categorical hues: the `info` recipe with its own token triple, so a hue chip\n // and a status chip in one row carry the same weight.\n blue: {\n idle: \"border-hue-blue bg-hue-blue-soft text-hue-blue\",\n selected: \"border-[var(--hue-blue)] bg-hue-blue-soft text-hue-blue\",\n },\n indigo: {\n idle: \"border-hue-indigo bg-hue-indigo-soft text-hue-indigo\",\n selected: \"border-[var(--hue-indigo)] bg-hue-indigo-soft text-hue-indigo\",\n },\n purple: {\n idle: \"border-hue-purple bg-hue-purple-soft text-hue-purple\",\n selected: \"border-[var(--hue-purple)] bg-hue-purple-soft text-hue-purple\",\n },\n teal: {\n idle: \"border-hue-teal bg-hue-teal-soft text-hue-teal\",\n selected: \"border-[var(--hue-teal)] bg-hue-teal-soft text-hue-teal\",\n },\n orange: {\n idle: \"border-hue-orange bg-hue-orange-soft text-hue-orange\",\n selected: \"border-[var(--hue-orange)] bg-hue-orange-soft text-hue-orange\",\n },\n};\n\n// `outline` idle looks: the tone's border and text, the surface left to whatever the\n// chip sits on. Selected still takes the tone's `selected` look (below), because an\n// outline toggle that stays an outline when on has no visible state.\nconst OUTLINE: Record<ChipTone, string> = {\n neutral: \"border-strong bg-transparent text-secondary\",\n // The brand AS TEXT is `--brand-muted` (0.33): `--brand` itself is solved to 3:1, a\n // fill's boundary, and as text it fell to 3.0:1 in the derived dark presets.\n brand: \"border-brand bg-transparent text-brand-muted\",\n danger: \"border-danger bg-transparent text-danger\",\n warning: \"border-warning bg-transparent text-warning\",\n success: \"border-success bg-transparent text-success\",\n info: \"border-info bg-transparent text-info\",\n income: \"border-current/60 bg-transparent text-money-income\",\n expense: \"border-current/60 bg-transparent text-money-expense\",\n blue: \"border-hue-blue bg-transparent text-hue-blue\",\n indigo: \"border-hue-indigo bg-transparent text-hue-indigo\",\n purple: \"border-hue-purple bg-transparent text-hue-purple\",\n teal: \"border-hue-teal bg-transparent text-hue-teal\",\n orange: \"border-hue-orange bg-transparent text-hue-orange\",\n};\n\n// `solid`: the fill under its own foreground, `<fill>-contrast` (0.33, colour-roles\n// §5.1): white in light, the tone's 950 (the page, for money) on the dark theme's\n// pastels. Until 0.33 only brand and danger had one and the rest borrowed\n// `--text-inverse`, the surface colour — which left light warning at 4.24:1 and orange\n// at 4.38:1. Neutral keeps the inverse pair: a count pill, louder than the neutral\n// fill. The border goes transparent rather than away, so a solid chip is exactly as\n// tall as a soft one.\nconst SOLID: Record<ChipTone, string> = {\n neutral: \"border-transparent bg-inverse text-inverse\",\n brand: \"border-transparent bg-brand text-brand-contrast\",\n danger: \"border-transparent bg-danger text-danger-contrast\",\n warning: \"border-transparent bg-warning text-warning-contrast\",\n success: \"border-transparent bg-success text-success-contrast\",\n info: \"border-transparent bg-info text-info-contrast\",\n income: \"border-transparent bg-money-pos text-money-income-contrast\",\n expense: \"border-transparent bg-money-neg text-money-expense-contrast\",\n blue: \"border-transparent bg-hue-blue text-hue-blue-contrast\",\n indigo: \"border-transparent bg-hue-indigo text-hue-indigo-contrast\",\n purple: \"border-transparent bg-hue-purple text-hue-purple-contrast\",\n teal: \"border-transparent bg-hue-teal text-hue-teal-contrast\",\n orange: \"border-transparent bg-hue-orange text-hue-orange-contrast\",\n};\n\n/** `dot`: the fill of the dot itself — the tone's strongest colour, which is the one\n * chosen to read on the page surface in both themes. Neutral takes the muted ink: a\n * grey dot is \"no particular state\", not a darker black. */\nconst DOT: Record<ChipTone, string> = {\n neutral: \"bg-[var(--text-muted)]\",\n brand: \"bg-brand\",\n danger: \"bg-danger\",\n warning: \"bg-warning\",\n success: \"bg-success\",\n info: \"bg-info\",\n income: \"bg-money-income\",\n expense: \"bg-money-expense\",\n blue: \"bg-hue-blue\",\n indigo: \"bg-hue-indigo\",\n purple: \"bg-hue-purple\",\n teal: \"bg-hue-teal\",\n orange: \"bg-hue-orange\",\n};\n\n/** Per size: the dot's diameter. */\nconst DOT_SIZE: Record<ChipSize, string> = { xs: \"size-1.5\", sm: \"size-2\", md: \"size-2\", lg: \"size-2.5\" };\n\n/** The status-badge type (`caps`): a size step down, heavier and tracked, because\n * capitals at body size shout and capitals untracked run together. Per size, so it\n * replaces the size's own `text-*` through tailwind-merge. */\nconst CAPS: Record<ChipSize, string> = {\n xs: \"text-micro font-semibold uppercase tracking-wider\",\n sm: \"text-micro font-semibold uppercase tracking-wider\",\n md: \"text-caption font-semibold uppercase tracking-wider\",\n lg: \"text-xs font-semibold uppercase tracking-wider\",\n};\n\nfunction surfaceOf(tone: ChipTone, variant: ChipVariant, selected: boolean | undefined): string {\n const palette = TONE[tone];\n if (variant === \"dot\") {\n // No surface and no border colour: the dot carries the tone, the text stays ink.\n // The border goes transparent rather than away, so a dot chip lines up with a pill.\n return selected\n ? \"border-transparent bg-transparent font-medium text-primary\"\n : \"border-transparent bg-transparent text-secondary\";\n }\n if (variant === \"solid\") {\n // A solid chip is already the strongest look there is, so \"on\" is a ring round it.\n return cn(SOLID[tone], selected && \"ring-2 ring-strong ring-offset-1 ring-offset-surface\");\n }\n if (selected) return palette.selected;\n return variant === \"outline\" ? OUTLINE[tone] : palette.idle;\n}\n\n/** What {@link ChipProps.renderLink} is handed: everything the chip would have put on\n * its own `<a>`. Spread it onto your router's link, mapping `href` to what the link\n * calls it — `renderLink={({ href, ...p }) => <Link to={href} {...p} />}`. */\nexport interface ChipLinkProps {\n href: string;\n /** The chip's whole look — keep it, or the pill is a bare link. */\n className: string;\n children: ReactNode;\n /** The chip's forwarded ref. React 19 passes it to a function component as a prop. */\n ref?: Ref<HTMLAnchorElement>;\n \"aria-current\"?: \"true\";\n /**\n * The chip's `onClick`, when it was given one — spread it onto the link with the\n * rest. Carried here so a caller need not close over it twice: keksdose's admin pill\n * in the account menu closes the menu on the way to /admin (account-menu.tsx), and\n * with `onClick` on the Chip its `renderLink` stays the one-liner\n * `({ href, ...p }) => <Link to={href} {...p} />`.\n */\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n id?: string;\n title?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\n/**\n * `body` is the whole pill when the chip is ONE element. When a link or a button also has\n * a dismiss control, the pill is a wrapper holding two siblings (see the note on\n * {@link Chip}): `split` is then the interactive part's own padding — the full start\n * inset, a short end one before the ×; `tail` is the wrapper's end inset after the ×.\n */\nconst SIZE: Record<\n ChipSize,\n { body: string; split: string; tail: string; icon: string; remove: string }\n> = {\n xs: {\n body: \"gap-1 px-1.5 py-0 text-caption leading-4\",\n split: \"gap-1 ps-1.5 pe-0.5 py-0 text-caption leading-4\",\n tail: \"pe-1\",\n icon: \"size-3\",\n remove: \"size-2.5\",\n },\n sm: {\n body: \"gap-1 px-2 py-0.5 text-xs\",\n split: \"gap-1 ps-2 pe-1 py-0.5 text-xs\",\n tail: \"pe-1.5\",\n icon: \"size-3\",\n remove: \"size-3\",\n },\n md: {\n body: \"gap-1.5 px-2.5 py-1 text-sm\",\n split: \"gap-1.5 ps-2.5 pe-1.5 py-1 text-sm\",\n tail: \"pe-2\",\n icon: \"size-3.5\",\n remove: \"size-3.5\",\n },\n lg: {\n body: \"min-h-11 gap-2 px-4 py-2 text-sm\",\n split: \"min-h-11 gap-2 ps-4 pe-2 py-2 text-sm\",\n tail: \"min-h-11 pe-3.5\",\n icon: \"size-4\",\n remove: \"size-4\",\n },\n};\n\nconst CHIP_PILL = \"inline-flex max-w-full items-center rounded-full border transition-colors\";\n\n// `focus-visible`, not `focus`: a chip commonly receives focus programmatically (the\n// ChipInput moves focus onto one after a removal) and a ring that appears on a\n// pointer click reads as a stuck selection.\nconst CHIP_RING = `${FOCUS_RING} focus-visible:ring-offset-1 focus-visible:ring-offset-surface`;\nconst CHIP_BASE = `${CHIP_PILL} ${CHIP_RING}`;\n\n/**\n * A text chip's label at Large and Extra large (0.32.1, keksdose's 0.32 report;\n * docs/text-size-harmonization.md §4 \"nothing truncates\"): it wraps instead of ending in\n * \"…\". keksdose's \"Zur Prüfung zurücklegen\" read \"Zur Prüfung zu…\" on a phone at Extra\n * large, which is a different instruction. No size has a fixed height (padding and\n * `min-h-*` only), so the pill grows with its lines; the mark before the text stays\n * centred on them, as in a pill. A `large:` rule, so Normal keeps its one-line chip.\n *\n * 0.33 (§10.17): the kit's one `truncate-until-large` (tokens.css) in place of this\n * file's own `truncate` + `large:whitespace-normal large:break-words`, as StatusDot's\n * label took it. The utility breaks with `overflow-wrap: anywhere`, which lowers the\n * label's min-content width to one letter: a status chip in an auto-sized table column\n * wants the column given a width at Large, or it is squeezed to a letter per line\n * (StatusDot's label was, in the showcase's tone table at 360 px, Extra large).\n */\nconst CHIP_LABEL_TRUNCATE = \"truncate-until-large\";\n\n/**\n * An interactive chip's touch target at Large and Extra large (docs/text-size-harmonization.md\n * §4: \"touch targets at least 48 px … for buttons, inputs, chips\"): a link or toggle chip\n * grows its pill to 48 px, as Button `sm` does, and centres what is in it once the\n * minimum width is what sizes it. An inert chip (a tag on a row) is no target and keeps\n * its size; a disabled one is no target either.\n */\nconst CHIP_TOUCH_LARGE = `${TOUCH_TARGET_LARGE} large:justify-center`;\n\n/**\n * The ×'s 48 px hit area at Large (§4) — IconButton's `2xs` pattern: a box that cannot\n * grow without growing the chip around it keeps its size and gets an invisible\n * `::after`, 48 px square and centred on it, which takes the taps a finger lands round\n * the glyph. On an inert removable chip (ChipInput's values) the × is the only target.\n */\nconst REMOVE_HIT_AREA_LARGE =\n \"relative large:after:absolute large:after:inset-[calc((100%-48px)/2)] large:after:content-['']\";\n\ninterface ChipBaseProps {\n children: ReactNode;\n tone?: ChipTone;\n size?: ChipSize;\n /** See {@link ChipVariant}. Default `soft`. */\n variant?: ChipVariant;\n /** See {@link ChipShape}. Default `pill`. */\n shape?: ChipShape;\n /**\n * The uppercase, tracked \"status badge\" type — keksdose's ~25 PAID / DRAFT /\n * OVERDUE / ADMIN badges, each spelling `uppercase tracking-wide text-[10px]` by\n * hand. A flag rather than a fourth `variant` because it is TYPE, not surface: a\n * status badge is soft, outlined or solid as the row needs, and a `variant=\"tag\"`\n * would have had to pick one. Screen readers read the text as written, not as\n * drawn, so write it in normal case.\n */\n caps?: boolean;\n /**\n * The leading glyph: a lucide component (`icon={Check}`, drawn at the chip's size), or\n * — since 0.15.4 — a ready element (`icon={<Check className=\"text-success\" />}`)\n * for a glyph whose colour or stroke carries a state; it is sized to the same box\n * (keksdose live #358 built that slot by hand inside `children`).\n */\n icon?: LucideIcon | ReactElement;\n /**\n * Marks the chip as the current one — `aria-current` on a link, `aria-pressed` on a\n * toggle.\n *\n * On an ON/OFF toggle, keep the LABEL the same in both states and let `selected`\n * carry the state. A label that flips with it (\"Skip this month\" / \"Ask again this\n * month\") is announced together with \"pressed\", and then says the opposite of what\n * it does.\n *\n * A control that switches between two NAMED states (outflow ⇄ inflow) is not a\n * toggle but an ACTION button: leave `selected` out, let the visible label and the\n * tone follow the state (\"− Outflow\" / \"+ Inflow\"), and name the action in\n * `aria-label` (\"Direction: outflow — tap for inflow\"). Without `selected` the chip\n * reports no pressed state at all. A fixed label there contradicts the figure beside\n * it — \"+ Outflow\" in green next to an inflow (keksdose #417).\n */\n selected?: boolean;\n /**\n * Checkbox mode (0.10.0), for a toggle chip (`onClick` + `selected`): a small box\n * at the start that shows a tick when `selected`, and the button reports\n * `role=\"checkbox\"` + `aria-checked` instead of `aria-pressed`. For a set of\n * include/exclude choices where \"on\" must read as TICKED, not merely tinted —\n * keksdose's add-group-card (the \"which categories come along\" pills), which put a\n * real `<Checkbox>` inside a `<label>` pill by hand. The box is drawn in both\n * states so ticking one never shifts its label.\n */\n checkbox?: boolean;\n /** Renders a dismiss affordance. Works alongside `href`/`onClick` — see the note below. */\n onRemove?: () => void;\n /** Accessible name for the dismiss button. Default: `common.remove` from the\n * {@link UiKitProvider}, else \"Remove\". */\n removeLabel?: string;\n /** Disables the × ONLY — the chip itself stays a working link or toggle. For the\n * value that may not be removed right now (the last filter of a required set, a\n * tag the user may read but not edit) where `disabled` would also kill the chip's\n * own action. */\n removeDisabled?: boolean;\n disabled?: boolean;\n className?: string;\n /**\n * Roving focus, for a container that manages a row of chips (see {@link ChipInput}).\n * A chip with no `href`/`onClick` is inert and unfocusable by default; give it a\n * `tabIndex` and a `role` and it becomes the thing arrow keys move between.\n */\n tabIndex?: number;\n role?: string;\n onKeyDown?: (e: KeyboardEvent<HTMLElement>) => void;\n onFocus?: () => void;\n /** Accessible name, when the visible text is not the whole story. */\n \"aria-label\"?: string;\n /** Any other ARIA attribute — `aria-describedby`, `aria-expanded`, `aria-controls`,\n * `aria-haspopup` — reaches the chip's interactive element. */\n [key: `aria-${string}`]: string | boolean | number | undefined;\n /** A test id, a `data-tour` anchor, or any other data attribute. */\n [key: `data-${string}`]: unknown;\n id?: string;\n /** A native tooltip. For anything a user must read, prefer the kit's `Tooltip`. */\n title?: string;\n /**\n * Even hairlines on fractional-ratio displays (0.18.0). At a device-pixel ratio of\n * 1.25 — a 125 % Windows display, or 125 % zoom — Chrome draws a chip's two 1px\n * side borders on different sub-pixel phases, one crisp and one a half-strength\n * smear that reads as a thinner line (worst on the 60 %-alpha `danger` border).\n * `snapEdges` widens the chip by under 4px, from where it sits, so the two edges\n * rasterise as mirror images, and centres its content in the extra.\n *\n * Off by default: it reads the chip's position (batched for every snapped chip into\n * one pass a frame, through one shared observer), and the fix is only as fresh as the\n * last pass — it re-snaps on render, resize, zoom and ratio change, but not when\n * something else moves the chip; call {@link refreshChipEdges} then. At an integer\n * ratio, and at 1.5, it writes nothing. Turn it on for status chips that sit in a\n * column and are compared side by side; a chip with no visible border (`dot`) has\n * nothing to snap.\n */\n snapEdges?: boolean;\n /** The interactive element — the pill, or a split pill's body. A React 19 ref prop\n * since 0.34 (MOD-4, refactor audit 2026-10); it was `forwardRef`. */\n ref?: Ref<HTMLElement>;\n}\n\n/**\n * A chip is a link OR a button, never both — so the types say so. Before 0.7.0 both were\n * accepted and `onClick` was silently dropped whenever `href` was set. A LINK may still\n * run code on its way (close the menu it sits in): that is an `onClick` typed for the\n * anchor, on the link shape, and the pill stays a link — no `aria-pressed`, no button.\n * That shape wants a definite `href` string, so a bare `onClick` is always the\n * button's, with a button's event.\n */\nexport type ChipProps = ChipBaseProps &\n (\n | {\n /** Renders the chip as a link. */\n href?: string;\n onClick?: never;\n /**\n * Renders the link for `href` — pass your router's `<Link>` here, since a\n * plain `<a>` reloads a single-page app (keksdose's account-menu admin pill).\n * Default: the `<UiKitProvider linkComponent>`, then `<a>`. Ignored without\n * `href`.\n *\n * A render function rather than an `as={Link}`, for three reasons. It is the\n * API `StatTile` already has (`renderLink`), so a consumer learns it once.\n * Router links do not take `href` — react-router's takes `to` — so an `as`\n * would either have to pass props through untyped or teach the kit about one\n * router; here the caller maps `href` onto its own link in one line, fully\n * typed. And it keeps `href` as THE link prop, which is what the `href` /\n * `onClick` exclusion below is typed on: the pill is a link because it has an\n * href, whichever element draws it.\n */\n renderLink?: (props: ChipLinkProps) => ReactElement;\n commit?: never;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n }\n | {\n /** The link shape again, with an `onClick`. `href` is a required string here,\n * which is what lets TypeScript tell this shape from the button's: a bare\n * `onClick` can only be the button's, and keeps its button event. */\n href: string;\n /** Runs on the link's click, before the navigation — keksdose's admin pill\n * closes the account menu with it. Reaches a `renderLink` link as\n * {@link ChipLinkProps.onClick}. Not called on a disabled chip, which renders\n * no link. */\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n /** See the link shape above. */\n renderLink?: (props: ChipLinkProps) => ReactElement;\n commit?: never;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n }\n | {\n href?: never;\n renderLink?: never;\n /** Renders the chip as a toggle button. Mutually exclusive with `href`. Receives\n * the click, so a chip inside a clickable row can `stopPropagation()`. */\n onClick?: (event: MouseEvent<HTMLButtonElement>) => void;\n /**\n * Why the toggle cannot be pressed right now — {@link Button}'s\n * `disabledReason`, on the chip's button shape (keksdose K3: a filter or flag\n * chip that saves on press, under a read-only lock). The chip stays focusable\n * and `aria-disabled`, a press (and its `onRemove` ×) does nothing, and the\n * reason describes the chip — as a line under it at Large and on a touch\n * screen, in the kit {@link Tooltip} otherwise (see `disabledReasonDisplay`).\n * Wins over `disabled`. A link chip navigates rather than commits, so it takes\n * neither this nor `commit`; an inert chip has nothing to refuse.\n */\n disabledReason?: ReactNode;\n /**\n * Where `disabledReason` shows (0.32, docs/text-size-harmonization.md §4 \"No fact\n * only in a tooltip\") — {@link Button}'s `disabledReasonDisplay`: by default a line\n * under the chip at Large and Extra large and on a touch screen, where a bubble\n * cannot be hovered open, and the tooltip otherwise and inside\n * `CompactControls`. `\"line\"` / `\"tooltip\"` force one. See\n * {@link DisabledReasonDisplay}.\n */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /** This chip COMMITS — pressing it saves. 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?: CommitScope;\n }\n );\n\n/**\n * Note on `onRemove` with `href`/`onClick`: a button cannot be nested inside a button or\n * a link, so when both are present the chip renders a wrapper holding TWO siblings — the\n * interactive body and the dismiss control. The WRAPPER is the visual pill (border, tone,\n * `className`) and the two sit inside it, so the × reads as part of the chip; the body's\n * focus ring is drawn on the body alone, and `ref`, `id` and the ARIA props still reach\n * the body — the interactive element.\n */\nexport function Chip({\n ref,\n children,\n tone = \"neutral\",\n size = \"md\",\n variant = \"soft\",\n shape = \"pill\",\n caps = false,\n icon: Icon,\n selected,\n checkbox = false,\n href,\n renderLink,\n onClick,\n onRemove,\n removeLabel,\n removeDisabled = false,\n disabled = false,\n className,\n tabIndex,\n role,\n onKeyDown,\n onFocus,\n snapEdges = false,\n disabledReason: ownDisabledReason,\n disabledReasonDisplay,\n commit,\n ...rest\n}: ChipProps) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS, { remove: removeLabel });\n const reasonId = useId();\n const disabledReason = useCommitReason(commit, ownDisabledReason);\n // Only the BUTTON shape commits: a link navigates, an inert chip does nothing.\n const locked = Boolean(onClick) && !href && hasContent(disabledReason);\n const kitLink = useKitLink();\n const s = SIZE[size];\n const interactive = !!href || !!onClick;\n // One radius for every rounded piece — the pill, the body inside a split pill, the ×\n // — so a square chip has no round focus ring inside it.\n const radius =\n shape === \"square\" ? (size === \"sm\" || size === \"xs\" ? \"rounded\" : \"rounded-md\") : \"rounded-full\";\n const dot = variant === \"dot\";\n const type = cn(caps && CAPS[size], variant === \"solid\" && \"justify-center tabular-nums\");\n const surface = cn(\n surfaceOf(tone, variant, selected),\n // A reason wins over `disabled`: the focusable kind of disabled, with the cursor\n // that says \"refused\" rather than \"nothing here\".\n disabled && !locked && \"cursor-default opacity-50\",\n locked && \"cursor-not-allowed opacity-50\",\n );\n const snap = snapEdges && !dot;\n // A finger's target at Large (§4): what can be pressed, refused or not — see\n // CHIP_TOUCH_LARGE. A disabled link renders as the inert span below and is none.\n const target = interactive && (!disabled || locked);\n const snapRef = useSnapEdges(snap);\n // The visible pill carries the snap: the whole chip, or the wrapper of a split one\n // (whose body gets the forwarded ref instead).\n const ownRef = useCallback(\n (el: HTMLElement | null) => {\n snapRef(el);\n assignRef(ref, el);\n },\n [snapRef, ref],\n );\n const look = cn(\n CHIP_BASE,\n s.body,\n type,\n radius,\n surface,\n interactive && !disabled && !locked && \"cursor-pointer hover:brightness-[0.97] dark:hover:brightness-110\",\n // §4: a link or toggle is a 48 px target at Large — a locked one too, since it is\n // still focusable and still says why when pressed.\n target && CHIP_TOUCH_LARGE,\n // An inert dot chip is text in a column: no inset, so it aligns with the header.\n dot && !interactive && \"px-0\",\n // The under-4px a snap adds is split between the two sides, not stacked at the end.\n snap && \"justify-center\",\n className,\n );\n\n // The tone as data on the visible pill (0.34, refactor audit 2026-10, TST-9): a stable\n // hook for a test or a caller's selector, where the surface classes are the kit's to\n // change. On the pill wrapper of a split chip, else on the chip itself; before the\n // caller's props, so a `data-tone` of their own wins.\n const toneData = { \"data-tone\": onRemove ? undefined : tone } as Record<string, string | undefined>;\n\n // The checkbox box, only on the toggle shape it has meaning on.\n const isCheckbox = checkbox && !!onClick && !href;\n const body = (\n <>\n {isCheckbox && (\n <span\n aria-hidden\n data-chip-check=\"\"\n className={cn(\n \"flex shrink-0 items-center justify-center rounded-sm border\",\n s.icon,\n selected ? \"border-current bg-current/15\" : \"border-current/50\",\n )}\n >\n {selected && <Check className=\"size-full\" strokeWidth={3} />}\n </span>\n )}\n {dot && <span aria-hidden data-chip-dot=\"\" className={cn(\"shrink-0 rounded-full\", DOT_SIZE[size], DOT[tone])} />}\n {Icon &&\n (isValidElement(Icon) ? (\n <span aria-hidden className={cn(s.icon, \"inline-flex shrink-0 [&>svg]:size-full\")}>\n {Icon}\n </span>\n ) : (\n <Icon className={cn(s.icon, \"shrink-0\")} aria-hidden />\n ))}\n {/* Text truncates at Normal and wraps at Large (CHIP_LABEL_TRUNCATE). Mixed\n children (an icon and a word) sit in a row instead: as a plain span, the svg —\n a block under Tailwind's preflight — stacked above the text (keksdose live\n #358). */}\n <span\n className={cn(\n \"min-w-0\",\n typeof children === \"string\" || typeof children === \"number\"\n ? CHIP_LABEL_TRUNCATE\n : \"inline-flex items-center gap-1 [&>svg]:size-[1em] [&>svg]:shrink-0\",\n )}\n >\n {children}\n </span>\n </>\n );\n\n const remove = onRemove ? (\n <button\n type=\"button\"\n // The name carries WHAT is being removed, not just \"Remove\" — a screen-reader user\n // tabbing a row of eight dismiss buttons otherwise hears the same word eight times\n // with no way to tell which one they are on.\n // Composed through `common.fieldValue` because the punctuation between the two\n // halves is the language's, not ours. A chip whose content is not plain text\n // has no value to name, and gets the bare verb rather than a dangling colon.\n aria-label={\n typeof children === \"string\" && children\n ? common.fieldValue(common.remove, children)\n : common.remove\n }\n onClick={(e) => {\n // The chip may itself be a link; removing it must not also follow it.\n e.preventDefault();\n e.stopPropagation();\n // Removing a value from a chip that commits is a commit too.\n if (locked) return;\n onRemove();\n }}\n disabled={(disabled && !locked) || removeDisabled}\n aria-disabled={locked || undefined}\n aria-describedby={locked ? reasonId : undefined}\n className={cn(\n // Logical margins: the × sits at the END of the pill, which is the left in RTL.\n \"-me-0.5 ms-0.5 shrink-0 p-0.5 transition-colors\",\n shape === \"square\" ? \"rounded-sm\" : \"rounded-full\",\n FOCUS_RING,\n // §4: the × is a 48 px target at Large without growing the chip around it.\n REMOVE_HIT_AREA_LARGE,\n !locked && \"hover:bg-active\",\n locked && \"cursor-not-allowed\",\n ((disabled && !locked) || removeDisabled) && \"pointer-events-none\",\n // A disabled chip is already dimmed as a whole; dimming the × again on top\n // would take it to a quarter. `removeDisabled` alone dims just the ×.\n removeDisabled && !disabled && \"opacity-40\",\n )}\n >\n <X className={s.remove} aria-hidden />\n </button>\n ) : null;\n\n // ── The combined shape: one pill, two interactive siblings inside it.\n const pill = (inner: ReactNode) => (\n <span\n ref={snap ? snapRef : undefined}\n data-tone={tone}\n // The pill is the target's box here (§4): the body inside stretches to its height.\n className={cn(CHIP_PILL, s.tail, radius, surface, target && CHIP_TOUCH_LARGE, snap && \"justify-center\", className)}\n >\n {inner}\n {remove}\n </span>\n );\n // The body inside a pill: no border or surface of its own (the pill has them), a\n // hover wash in the text's own hue so it works on every tone.\n const inner = cn(\n \"inline-flex min-w-0 items-center self-stretch transition-colors\",\n radius,\n CHIP_RING,\n s.split,\n type,\n !disabled && !locked && \"cursor-pointer hover:bg-current/10\",\n disabled && !locked && \"cursor-default\",\n locked && \"cursor-not-allowed\",\n );\n\n // ── The three shapes.\n if (href && !disabled) {\n // On this shape `onClick` is the link shape's, typed for the anchor.\n const onLinkClick = onClick as ChipLinkProps[\"onClick\"];\n const linkProps: ChipLinkProps = {\n ...toneData,\n ...rest,\n ref: (remove ? ref : ownRef) as Ref<HTMLAnchorElement>,\n href,\n onClick: onLinkClick,\n \"aria-current\": selected ? \"true\" : undefined,\n className: remove ? inner : look,\n children: body,\n };\n const render = pickLinkRenderer(renderLink, kitLink, href);\n const link = render ? (\n // Through a component rather than called here, so the forwarded ref arrives as\n // an ordinary prop of an element and is never handed to a function mid-render.\n <RenderedLink render={render} {...linkProps} />\n ) : (\n <a\n ref={(remove ? ref : ownRef) as React.Ref<HTMLAnchorElement>}\n {...toneData}\n href={href}\n aria-current={selected ? \"true\" : undefined}\n onClick={onLinkClick}\n className={remove ? inner : look}\n {...rest}\n >\n {body}\n </a>\n );\n return remove ? pill(link) : link;\n }\n\n // `!href`: a DISABLED link renders no link, and its link-shaped `onClick` must not\n // turn it into a button instead — it falls through to the inert span, as before.\n if (onClick && !href) {\n const onButtonClick = onClick as (event: MouseEvent<HTMLButtonElement>) => void;\n const ownDescribedBy = rest[\"aria-describedby\"];\n const button = (\n <button\n ref={(remove ? ref : ownRef) as React.Ref<HTMLButtonElement>}\n {...toneData}\n type=\"button\"\n onClick={(e) => {\n // Locked: the press is swallowed — the chip saves on press, and a lock that\n // let it through would toggle a state that is never stored.\n if (locked) {\n e.preventDefault();\n return;\n }\n onButtonClick(e);\n }}\n disabled={disabled && !locked}\n // Only when `selected` is PASSED: a chip with `onClick` and no `selected` is an\n // action button (it does something), not a toggle (it is on or off), and\n // announcing it \"not pressed\" would claim a state it does not have.\n // A checkbox chip says \"checked\", not \"pressed\" — the same state in the words\n // of the control it looks like.\n role={isCheckbox ? \"checkbox\" : undefined}\n aria-pressed={isCheckbox ? undefined : selected}\n aria-checked={isCheckbox ? !!selected : undefined}\n className={remove ? inner : look}\n {...rest}\n // After the spread, merged with it: the lock's state and reason are not the\n // caller's to drop by passing an `aria-describedby` of their own.\n aria-disabled={locked || (rest[\"aria-disabled\"] as boolean | \"true\" | \"false\" | undefined)}\n aria-describedby={\n locked ? [ownDescribedBy, reasonId].filter(Boolean).join(\" \") : (ownDescribedBy as string | undefined)\n }\n >\n {body}\n </button>\n );\n const shaped = remove ? pill(button) : button;\n if (!locked) return shaped;\n return (\n <LockedReason lock={{ reason: disabledReason, reasonId }} display={disabledReasonDisplay}>\n {shaped}\n </LockedReason>\n );\n }\n\n return (\n /* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- the handlers are\n the roving-focus hooks, live only when the container passes `role` and `tabIndex`\n (ChipInput does); the role is a prop, so the rule cannot see it. */\n <span\n ref={ownRef as React.Ref<HTMLSpanElement>}\n {...toneData}\n className={look}\n tabIndex={tabIndex}\n role={role}\n onKeyDown={onKeyDown}\n onFocus={onFocus}\n {...rest}\n >\n {body}\n {remove}\n </span>\n );\n}\n\n/** Calls a chip's `renderLink` with the props it was given — see its call site. */\nfunction RenderedLink({\n render,\n ...props\n}: ChipLinkProps & { render: (props: ChipLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":"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","names":["inner"]}
1
+ {"version":3,"sources":["../../src/components/chip.tsx"],"sourcesContent":["import { isValidElement, useCallback, useId } from \"react\";\nimport type { KeyboardEvent, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { Check, X } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { statusTone, type StatusKind } from \"../lib/status-tone\";\nimport { TOUCH_TARGET_LARGE } from \"./button-look\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { assignRef, LockedReason, type DisabledReasonDisplay } from \"./field-parts\";\nimport { hasContent } from \"../lib/presence\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\nimport { useCommitReason, type CommitScope } from \"./write-lock\";\nimport { useSnapEdges } from \"../lib/chip-edge-snap\";\n// The edge-snap engine lives in lib/chip-edge-snap.ts and ChipInput in chip-input.tsx\n// (MOD-9). `refreshChipEdges` is still this module's; of ChipInput only the types are,\n// since a runtime re-export would make chip ⇄ chip-input a cycle — the barrel exports\n// chip-input itself.\nexport { refreshChipEdges } from \"../lib/chip-edge-snap\";\nexport type { ChipInputLabels, ChipInputProps } from \"./chip-input\";\n\n/**\n * A chip: a compact pill carrying one value.\n *\n * Three jobs, deliberately one component. A chip can be inert (a tag on a row), a link\n * (a jump to a section), or a toggle (a filter that is on or off) — and the difference is\n * which prop you pass, not which component you pick. Keeping them together is what stops\n * an app growing three near-identical pills that drift apart; it is the shape this kit\n * already uses for `Button`, which is a link when it is given an href.\n *\n * A chip is not a `Button`. It is smaller, it is rounded to a pill, and it reads as a\n * VALUE rather than as an action — which matters because a row of eight buttons says\n * \"choose one of eight things to do\" and a row of eight chips says \"here are eight\n * things\". Using `Button` for both is the mistake this exists to prevent.\n */\n\nexport type ChipTone =\n | \"neutral\"\n | \"brand\"\n | \"danger\"\n | \"warning\"\n | \"success\"\n | \"info\"\n | \"income\"\n | \"expense\"\n | ChipHue;\n/**\n * Categorical hues (0.10.0): colour that says \"a different value\" and nothing more —\n * kastlan's status-badge.tsx paints ~15 enums in 9 Tailwind hues by hand. Four of those\n * nine are semantic and already tones (green = `success`, yellow = `warning`, red =\n * `danger`, gray = `neutral`); these five are the rest, each a `--hue-*` token triple\n * in tokens.css with a light and a dark value. Use a semantic tone when the value HAS a\n * meaning (overdue is `danger`), a hue when it only needs telling apart (sent vs\n * accepted vs invoiced).\n */\nexport type ChipHue = \"blue\" | \"indigo\" | \"purple\" | \"teal\" | \"orange\";\n/** `lg` is the 44px touch target (`min-h-11`) a phone surface wants; `md` is ~28px.\n * `xs` (0.10.0) is 11px type at `px-1.5` — keksdose's goal markers and budget-cell\n * badges, which sit inside a figure's line and must not make it taller. */\nexport type ChipSize = \"xs\" | \"sm\" | \"md\" | \"lg\";\n/**\n * How much of the tone the chip wears.\n *\n * - `soft` (default): the tinted surface every chip has had.\n * - `outline`: the tone's border and text on NO surface — keksdose's DEV badge in the\n * account menu and kastlan's 27 shadcn `variant=\"outline\"` badges, which sit on\n * coloured rows where a second tint would muddy the row.\n * - `solid`: the tone as a fill under contrasting text — the \"count\" pill (unread\n * messages, open tasks). It centres its content and sets figures tabular, so a\n * one-digit count is a round dot and \"9\" → \"10\" does not jiggle the row.\n * - `dot` (0.10.0): no pill at all — a dot in the tone's colour before plain text, for\n * a status in a dense list or a table column where eight tinted pills in a column\n * would outshout the figures beside them (kastlan's unit picker already draws its\n * UnitStatus this way, from a separate `dotColor` map).\n */\nexport type ChipVariant = \"soft\" | \"outline\" | \"solid\" | \"dot\";\n/** `pill` (default) is fully rounded; `square` has the small radius of a field or a\n * button — for a chip that sits in a table cell or beside square controls. */\nexport type ChipShape = \"pill\" | \"square\";\n\nconst TONE: Record<ChipTone, { idle: string; selected: string }> = {\n neutral: {\n idle: \"border-subtle bg-surface-2 text-secondary\",\n selected: \"border-strong bg-active text-primary\",\n },\n brand: {\n idle: \"border-subtle bg-brand-soft text-brand-muted\",\n selected: \"border-brand bg-brand text-brand-contrast\",\n },\n danger: {\n idle: \"border-danger bg-danger-soft text-danger\",\n selected: \"border-[var(--danger)] bg-danger text-danger-contrast\",\n },\n warning: {\n idle: \"border-warning bg-warning-soft text-warning\",\n selected: \"border-[var(--warning)] bg-warning-soft text-warning\",\n },\n success: {\n idle: \"border-success bg-success-soft text-success\",\n selected: \"border-[var(--success)] bg-success-soft text-success\",\n },\n info: {\n idle: \"border-info bg-info-soft text-info\",\n selected: \"border-[var(--info)] bg-info-soft text-info\",\n },\n // The money pair (Keksdose's direction toggle). Only the TEXT and the BORDER carry\n // the tint, on the neutral chip's own surfaces: the money tokens are chosen for text\n // contrast, and a filled amber pill beside an amber figure reads as one smear.\n // `border-current` takes its hue from the text, so the two cannot drift apart.\n income: {\n idle: \"border-current/40 bg-surface-2 text-money-income\",\n selected: \"border-current bg-active text-money-income\",\n },\n expense: {\n idle: \"border-current/40 bg-surface-2 text-money-expense\",\n selected: \"border-current bg-active text-money-expense\",\n },\n // The categorical hues: the `info` recipe with its own token triple, so a hue chip\n // and a status chip in one row carry the same weight.\n blue: {\n idle: \"border-hue-blue bg-hue-blue-soft text-hue-blue\",\n selected: \"border-[var(--hue-blue)] bg-hue-blue-soft text-hue-blue\",\n },\n indigo: {\n idle: \"border-hue-indigo bg-hue-indigo-soft text-hue-indigo\",\n selected: \"border-[var(--hue-indigo)] bg-hue-indigo-soft text-hue-indigo\",\n },\n purple: {\n idle: \"border-hue-purple bg-hue-purple-soft text-hue-purple\",\n selected: \"border-[var(--hue-purple)] bg-hue-purple-soft text-hue-purple\",\n },\n teal: {\n idle: \"border-hue-teal bg-hue-teal-soft text-hue-teal\",\n selected: \"border-[var(--hue-teal)] bg-hue-teal-soft text-hue-teal\",\n },\n orange: {\n idle: \"border-hue-orange bg-hue-orange-soft text-hue-orange\",\n selected: \"border-[var(--hue-orange)] bg-hue-orange-soft text-hue-orange\",\n },\n};\n\n// `outline` idle looks: the tone's border and text, the surface left to whatever the\n// chip sits on. Selected still takes the tone's `selected` look (below), because an\n// outline toggle that stays an outline when on has no visible state.\nconst OUTLINE: Record<ChipTone, string> = {\n neutral: \"border-strong bg-transparent text-secondary\",\n // The brand AS TEXT is `--brand-muted` (0.33): `--brand` itself is solved to 3:1, a\n // fill's boundary, and as text it fell to 3.0:1 in the derived dark presets.\n brand: \"border-brand bg-transparent text-brand-muted\",\n danger: \"border-danger bg-transparent text-danger\",\n warning: \"border-warning bg-transparent text-warning\",\n success: \"border-success bg-transparent text-success\",\n info: \"border-info bg-transparent text-info\",\n income: \"border-current/60 bg-transparent text-money-income\",\n expense: \"border-current/60 bg-transparent text-money-expense\",\n blue: \"border-hue-blue bg-transparent text-hue-blue\",\n indigo: \"border-hue-indigo bg-transparent text-hue-indigo\",\n purple: \"border-hue-purple bg-transparent text-hue-purple\",\n teal: \"border-hue-teal bg-transparent text-hue-teal\",\n orange: \"border-hue-orange bg-transparent text-hue-orange\",\n};\n\n// `solid`: the fill under its own foreground, `<fill>-contrast` (0.33, colour-roles\n// §5.1): white in light, the tone's 950 (the page, for money) on the dark theme's\n// pastels. Until 0.33 only brand and danger had one and the rest borrowed\n// `--text-inverse`, the surface colour — which left light warning at 4.24:1 and orange\n// at 4.38:1. Neutral keeps the inverse pair: a count pill, louder than the neutral\n// fill. The border goes transparent rather than away, so a solid chip is exactly as\n// tall as a soft one.\nconst SOLID: Record<ChipTone, string> = {\n neutral: \"border-transparent bg-inverse text-inverse\",\n brand: \"border-transparent bg-brand text-brand-contrast\",\n danger: \"border-transparent bg-danger text-danger-contrast\",\n warning: \"border-transparent bg-warning text-warning-contrast\",\n success: \"border-transparent bg-success text-success-contrast\",\n info: \"border-transparent bg-info text-info-contrast\",\n income: \"border-transparent bg-money-pos text-money-income-contrast\",\n expense: \"border-transparent bg-money-neg text-money-expense-contrast\",\n blue: \"border-transparent bg-hue-blue text-hue-blue-contrast\",\n indigo: \"border-transparent bg-hue-indigo text-hue-indigo-contrast\",\n purple: \"border-transparent bg-hue-purple text-hue-purple-contrast\",\n teal: \"border-transparent bg-hue-teal text-hue-teal-contrast\",\n orange: \"border-transparent bg-hue-orange text-hue-orange-contrast\",\n};\n\n/** `dot`: the fill of the dot itself — the tone's strongest colour, which is the one\n * chosen to read on the page surface in both themes. Neutral takes the muted ink: a\n * grey dot is \"no particular state\", not a darker black. */\nconst DOT: Record<ChipTone, string> = {\n neutral: \"bg-[var(--text-muted)]\",\n brand: \"bg-brand\",\n danger: \"bg-danger\",\n warning: \"bg-warning\",\n success: \"bg-success\",\n info: \"bg-info\",\n income: \"bg-money-income\",\n expense: \"bg-money-expense\",\n blue: \"bg-hue-blue\",\n indigo: \"bg-hue-indigo\",\n purple: \"bg-hue-purple\",\n teal: \"bg-hue-teal\",\n orange: \"bg-hue-orange\",\n};\n\n/** Per size: the dot's diameter. */\nconst DOT_SIZE: Record<ChipSize, string> = { xs: \"size-1.5\", sm: \"size-2\", md: \"size-2\", lg: \"size-2.5\" };\n\n/** The status-badge type (`caps`): a size step down, heavier and tracked, because\n * capitals at body size shout and capitals untracked run together. Per size, so it\n * replaces the size's own `text-*` through tailwind-merge. */\nconst CAPS: Record<ChipSize, string> = {\n xs: \"text-micro font-semibold uppercase tracking-wider\",\n sm: \"text-micro font-semibold uppercase tracking-wider\",\n md: \"text-caption font-semibold uppercase tracking-wider\",\n lg: \"text-xs font-semibold uppercase tracking-wider\",\n};\n\nfunction surfaceOf(tone: ChipTone, variant: ChipVariant, selected: boolean | undefined): string {\n const palette = TONE[tone];\n if (variant === \"dot\") {\n // No surface and no border colour: the dot carries the tone, the text stays ink.\n // The border goes transparent rather than away, so a dot chip lines up with a pill.\n return selected\n ? \"border-transparent bg-transparent font-medium text-primary\"\n : \"border-transparent bg-transparent text-secondary\";\n }\n if (variant === \"solid\") {\n // A solid chip is already the strongest look there is, so \"on\" is a ring round it.\n return cn(SOLID[tone], selected && \"ring-2 ring-strong ring-offset-1 ring-offset-surface\");\n }\n if (selected) return palette.selected;\n return variant === \"outline\" ? OUTLINE[tone] : palette.idle;\n}\n\n/** What {@link ChipProps.renderLink} is handed: everything the chip would have put on\n * its own `<a>`. Spread it onto your router's link, mapping `href` to what the link\n * calls it — `renderLink={({ href, ...p }) => <Link to={href} {...p} />}`. */\nexport interface ChipLinkProps {\n href: string;\n /** The chip's whole look — keep it, or the pill is a bare link. */\n className: string;\n children: ReactNode;\n /** The chip's forwarded ref. React 19 passes it to a function component as a prop. */\n ref?: Ref<HTMLAnchorElement>;\n \"aria-current\"?: \"true\";\n /**\n * The chip's `onClick`, when it was given one — spread it onto the link with the\n * rest. Carried here so a caller need not close over it twice: keksdose's admin pill\n * in the account menu closes the menu on the way to /admin (account-menu.tsx), and\n * with `onClick` on the Chip its `renderLink` stays the one-liner\n * `({ href, ...p }) => <Link to={href} {...p} />`.\n */\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n id?: string;\n title?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\n/**\n * `body` is the whole pill when the chip is ONE element. When a link or a button also has\n * a dismiss control, the pill is a wrapper holding two siblings (see the note on\n * {@link Chip}): `split` is then the interactive part's own padding — the full start\n * inset, a short end one before the ×; `tail` is the wrapper's end inset after the ×.\n */\nconst SIZE: Record<\n ChipSize,\n { body: string; split: string; tail: string; icon: string; remove: string }\n> = {\n xs: {\n body: \"gap-1 px-1.5 py-0 text-caption leading-4\",\n split: \"gap-1 ps-1.5 pe-0.5 py-0 text-caption leading-4\",\n tail: \"pe-1\",\n icon: \"size-3\",\n remove: \"size-2.5\",\n },\n sm: {\n body: \"gap-1 px-2 py-0.5 text-xs\",\n split: \"gap-1 ps-2 pe-1 py-0.5 text-xs\",\n tail: \"pe-1.5\",\n icon: \"size-3\",\n remove: \"size-3\",\n },\n md: {\n body: \"gap-1.5 px-2.5 py-1 text-sm\",\n split: \"gap-1.5 ps-2.5 pe-1.5 py-1 text-sm\",\n tail: \"pe-2\",\n icon: \"size-3.5\",\n remove: \"size-3.5\",\n },\n lg: {\n body: \"min-h-11 gap-2 px-4 py-2 text-sm\",\n split: \"min-h-11 gap-2 ps-4 pe-2 py-2 text-sm\",\n tail: \"min-h-11 pe-3.5\",\n icon: \"size-4\",\n remove: \"size-4\",\n },\n};\n\nconst CHIP_PILL = \"inline-flex max-w-full items-center rounded-full border transition-colors\";\n\n// `focus-visible`, not `focus`: a chip commonly receives focus programmatically (the\n// ChipInput moves focus onto one after a removal) and a ring that appears on a\n// pointer click reads as a stuck selection.\nconst CHIP_RING = `${FOCUS_RING} focus-visible:ring-offset-1 focus-visible:ring-offset-surface`;\nconst CHIP_BASE = `${CHIP_PILL} ${CHIP_RING}`;\n\n/**\n * A text chip's label at Large and Extra large (0.32.1, keksdose's 0.32 report;\n * docs/text-size-harmonization.md §4 \"nothing truncates\"): it wraps instead of ending in\n * \"…\". keksdose's \"Zur Prüfung zurücklegen\" read \"Zur Prüfung zu…\" on a phone at Extra\n * large, which is a different instruction. No size has a fixed height (padding and\n * `min-h-*` only), so the pill grows with its lines; the mark before the text stays\n * centred on them, as in a pill. A `large:` rule, so Normal keeps its one-line chip.\n *\n * 0.33 (§10.17): the kit's one `truncate-until-large` (tokens.css) in place of this\n * file's own `truncate` + `large:whitespace-normal large:break-words`, as StatusDot's\n * label took it. The utility breaks with `overflow-wrap: anywhere`, which lowers the\n * label's min-content width to one letter: a status chip in an auto-sized table column\n * wants the column given a width at Large, or it is squeezed to a letter per line\n * (StatusDot's label was, in the showcase's tone table at 360 px, Extra large).\n */\nconst CHIP_LABEL_TRUNCATE = \"truncate-until-large\";\n\n/**\n * An interactive chip's touch target at Large and Extra large (docs/text-size-harmonization.md\n * §4: \"touch targets at least 48 px … for buttons, inputs, chips\"): a link or toggle chip\n * grows its pill to 48 px, as Button `sm` does, and centres what is in it once the\n * minimum width is what sizes it. An inert chip (a tag on a row) is no target and keeps\n * its size; a disabled one is no target either.\n */\nconst CHIP_TOUCH_LARGE = `${TOUCH_TARGET_LARGE} large:justify-center`;\n\n/**\n * The ×'s 48 px hit area at Large (§4) — IconButton's `2xs` pattern: a box that cannot\n * grow without growing the chip around it keeps its size and gets an invisible\n * `::after`, 48 px square and centred on it, which takes the taps a finger lands round\n * the glyph. On an inert removable chip (ChipInput's values) the × is the only target.\n */\nconst REMOVE_HIT_AREA_LARGE =\n \"relative large:after:absolute large:after:inset-[calc((100%-48px)/2)] large:after:content-['']\";\n\ninterface ChipBaseProps {\n children: ReactNode;\n tone?: ChipTone;\n /**\n * What the value means as a status (0.49 KIT-8), instead of a `tone`: `good` (success),\n * `attention` (warning), `problem` (danger), `active` (info), `inactive` (neutral) —\n * {@link statusTone}. An explicit `tone` wins. Never `brand` for a status.\n */\n status?: StatusKind;\n size?: ChipSize;\n /** See {@link ChipVariant}. Default `soft`. */\n variant?: ChipVariant;\n /** See {@link ChipShape}. Default `pill`. */\n shape?: ChipShape;\n /**\n * The uppercase, tracked \"status badge\" type — keksdose's ~25 PAID / DRAFT /\n * OVERDUE / ADMIN badges, each spelling `uppercase tracking-wide text-[10px]` by\n * hand. A flag rather than a fourth `variant` because it is TYPE, not surface: a\n * status badge is soft, outlined or solid as the row needs, and a `variant=\"tag\"`\n * would have had to pick one. Screen readers read the text as written, not as\n * drawn, so write it in normal case.\n */\n caps?: boolean;\n /**\n * The leading glyph: a lucide component (`icon={Check}`, drawn at the chip's size), or\n * — since 0.15.4 — a ready element (`icon={<Check className=\"text-success\" />}`)\n * for a glyph whose colour or stroke carries a state; it is sized to the same box\n * (keksdose live #358 built that slot by hand inside `children`).\n */\n icon?: LucideIcon | ReactElement;\n /**\n * Marks the chip as the current one — `aria-current` on a link, `aria-pressed` on a\n * toggle.\n *\n * On an ON/OFF toggle, keep the LABEL the same in both states and let `selected`\n * carry the state. A label that flips with it (\"Skip this month\" / \"Ask again this\n * month\") is announced together with \"pressed\", and then says the opposite of what\n * it does.\n *\n * A control that switches between two NAMED states (outflow ⇄ inflow) is not a\n * toggle but an ACTION button: leave `selected` out, let the visible label and the\n * tone follow the state (\"− Outflow\" / \"+ Inflow\"), and name the action in\n * `aria-label` (\"Direction: outflow — tap for inflow\"). Without `selected` the chip\n * reports no pressed state at all. A fixed label there contradicts the figure beside\n * it — \"+ Outflow\" in green next to an inflow (keksdose #417).\n */\n selected?: boolean;\n /**\n * Checkbox mode (0.10.0), for a toggle chip (`onClick` + `selected`): a small box\n * at the start that shows a tick when `selected`, and the button reports\n * `role=\"checkbox\"` + `aria-checked` instead of `aria-pressed`. For a set of\n * include/exclude choices where \"on\" must read as TICKED, not merely tinted —\n * keksdose's add-group-card (the \"which categories come along\" pills), which put a\n * real `<Checkbox>` inside a `<label>` pill by hand. The box is drawn in both\n * states so ticking one never shifts its label.\n */\n checkbox?: boolean;\n /** Renders a dismiss affordance. Works alongside `href`/`onClick` — see the note below. */\n onRemove?: () => void;\n /** Accessible name for the dismiss button. Default: `common.remove` from the\n * {@link UiKitProvider}, else \"Remove\". */\n removeLabel?: string;\n /** Disables the × ONLY — the chip itself stays a working link or toggle. For the\n * value that may not be removed right now (the last filter of a required set, a\n * tag the user may read but not edit) where `disabled` would also kill the chip's\n * own action. */\n removeDisabled?: boolean;\n disabled?: boolean;\n className?: string;\n /**\n * Roving focus, for a container that manages a row of chips (see {@link ChipInput}).\n * A chip with no `href`/`onClick` is inert and unfocusable by default; give it a\n * `tabIndex` and a `role` and it becomes the thing arrow keys move between.\n */\n tabIndex?: number;\n role?: string;\n onKeyDown?: (e: KeyboardEvent<HTMLElement>) => void;\n onFocus?: () => void;\n /** Accessible name, when the visible text is not the whole story. */\n \"aria-label\"?: string;\n /** Any other ARIA attribute — `aria-describedby`, `aria-expanded`, `aria-controls`,\n * `aria-haspopup` — reaches the chip's interactive element. */\n [key: `aria-${string}`]: string | boolean | number | undefined;\n /** A test id, a `data-tour` anchor, or any other data attribute. */\n [key: `data-${string}`]: unknown;\n id?: string;\n /** A native tooltip. For anything a user must read, prefer the kit's `Tooltip`. */\n title?: string;\n /**\n * Even hairlines on fractional-ratio displays (0.18.0). At a device-pixel ratio of\n * 1.25 — a 125 % Windows display, or 125 % zoom — Chrome draws a chip's two 1px\n * side borders on different sub-pixel phases, one crisp and one a half-strength\n * smear that reads as a thinner line (worst on the 60 %-alpha `danger` border).\n * `snapEdges` widens the chip by under 4px, from where it sits, so the two edges\n * rasterise as mirror images, and centres its content in the extra.\n *\n * Off by default: it reads the chip's position (batched for every snapped chip into\n * one pass a frame, through one shared observer), and the fix is only as fresh as the\n * last pass — it re-snaps on render, resize, zoom and ratio change, but not when\n * something else moves the chip; call {@link refreshChipEdges} then. At an integer\n * ratio, and at 1.5, it writes nothing. Turn it on for status chips that sit in a\n * column and are compared side by side; a chip with no visible border (`dot`) has\n * nothing to snap.\n */\n snapEdges?: boolean;\n /** The interactive element — the pill, or a split pill's body. A React 19 ref prop\n * since 0.34 (MOD-4, refactor audit 2026-10); it was `forwardRef`. */\n ref?: Ref<HTMLElement>;\n}\n\n/**\n * A chip is a link OR a button, never both — so the types say so. Before 0.7.0 both were\n * accepted and `onClick` was silently dropped whenever `href` was set. A LINK may still\n * run code on its way (close the menu it sits in): that is an `onClick` typed for the\n * anchor, on the link shape, and the pill stays a link — no `aria-pressed`, no button.\n * That shape wants a definite `href` string, so a bare `onClick` is always the\n * button's, with a button's event.\n */\nexport type ChipProps = ChipBaseProps &\n (\n | {\n /** Renders the chip as a link. */\n href?: string;\n onClick?: never;\n /**\n * Renders the link for `href` — pass your router's `<Link>` here, since a\n * plain `<a>` reloads a single-page app (keksdose's account-menu admin pill).\n * Default: the `<UiKitProvider linkComponent>`, then `<a>`. Ignored without\n * `href`.\n *\n * A render function rather than an `as={Link}`, for three reasons. It is the\n * API `StatTile` already has (`renderLink`), so a consumer learns it once.\n * Router links do not take `href` — react-router's takes `to` — so an `as`\n * would either have to pass props through untyped or teach the kit about one\n * router; here the caller maps `href` onto its own link in one line, fully\n * typed. And it keeps `href` as THE link prop, which is what the `href` /\n * `onClick` exclusion below is typed on: the pill is a link because it has an\n * href, whichever element draws it.\n */\n renderLink?: (props: ChipLinkProps) => ReactElement;\n commit?: never;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n }\n | {\n /** The link shape again, with an `onClick`. `href` is a required string here,\n * which is what lets TypeScript tell this shape from the button's: a bare\n * `onClick` can only be the button's, and keeps its button event. */\n href: string;\n /** Runs on the link's click, before the navigation — keksdose's admin pill\n * closes the account menu with it. Reaches a `renderLink` link as\n * {@link ChipLinkProps.onClick}. Not called on a disabled chip, which renders\n * no link. */\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n /** See the link shape above. */\n renderLink?: (props: ChipLinkProps) => ReactElement;\n commit?: never;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n }\n | {\n href?: never;\n renderLink?: never;\n /** Renders the chip as a toggle button. Mutually exclusive with `href`. Receives\n * the click, so a chip inside a clickable row can `stopPropagation()`. */\n onClick?: (event: MouseEvent<HTMLButtonElement>) => void;\n /**\n * Why the toggle cannot be pressed right now — {@link Button}'s\n * `disabledReason`, on the chip's button shape (keksdose K3: a filter or flag\n * chip that saves on press, under a read-only lock). The chip stays focusable\n * and `aria-disabled`, a press (and its `onRemove` ×) does nothing, and the\n * reason describes the chip — as a line under it at Large and on a touch\n * screen, in the kit {@link Tooltip} otherwise (see `disabledReasonDisplay`).\n * Wins over `disabled`. A link chip navigates rather than commits, so it takes\n * neither this nor `commit`; an inert chip has nothing to refuse.\n */\n disabledReason?: ReactNode;\n /**\n * Where `disabledReason` shows (0.32, docs/text-size-harmonization.md §4 \"No fact\n * only in a tooltip\") — {@link Button}'s `disabledReasonDisplay`: by default a line\n * under the chip at Large and Extra large and on a touch screen, where a bubble\n * cannot be hovered open, and the tooltip otherwise and inside\n * `CompactControls`. `\"line\"` / `\"tooltip\"` force one. See\n * {@link DisabledReasonDisplay}.\n */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /** This chip COMMITS — pressing it saves. 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?: CommitScope;\n }\n );\n\n/**\n * Note on `onRemove` with `href`/`onClick`: a button cannot be nested inside a button or\n * a link, so when both are present the chip renders a wrapper holding TWO siblings — the\n * interactive body and the dismiss control. The WRAPPER is the visual pill (border, tone,\n * `className`) and the two sit inside it, so the × reads as part of the chip; the body's\n * focus ring is drawn on the body alone, and `ref`, `id` and the ARIA props still reach\n * the body — the interactive element.\n */\nexport function Chip({\n ref,\n children,\n tone: toneProp,\n status,\n size = \"md\",\n variant = \"soft\",\n shape = \"pill\",\n caps = false,\n icon: Icon,\n selected,\n checkbox = false,\n href,\n renderLink,\n onClick,\n onRemove,\n removeLabel,\n removeDisabled = false,\n disabled = false,\n className,\n tabIndex,\n role,\n onKeyDown,\n onFocus,\n snapEdges = false,\n disabledReason: ownDisabledReason,\n disabledReasonDisplay,\n commit,\n ...rest\n}: ChipProps) {\n const tone: ChipTone = toneProp ?? (status ? statusTone(status) : \"neutral\");\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS, { remove: removeLabel });\n const reasonId = useId();\n const disabledReason = useCommitReason(commit, ownDisabledReason);\n // Only the BUTTON shape commits: a link navigates, an inert chip does nothing.\n const locked = Boolean(onClick) && !href && hasContent(disabledReason);\n const kitLink = useKitLink();\n const s = SIZE[size];\n const interactive = !!href || !!onClick;\n // One radius for every rounded piece — the pill, the body inside a split pill, the ×\n // — so a square chip has no round focus ring inside it.\n const radius =\n shape === \"square\" ? (size === \"sm\" || size === \"xs\" ? \"rounded\" : \"rounded-md\") : \"rounded-full\";\n const dot = variant === \"dot\";\n const type = cn(caps && CAPS[size], variant === \"solid\" && \"justify-center tabular-nums\");\n const surface = cn(\n surfaceOf(tone, variant, selected),\n // A reason wins over `disabled`: the focusable kind of disabled, with the cursor\n // that says \"refused\" rather than \"nothing here\".\n disabled && !locked && \"cursor-default opacity-50\",\n locked && \"cursor-not-allowed opacity-50\",\n );\n const snap = snapEdges && !dot;\n // A finger's target at Large (§4): what can be pressed, refused or not — see\n // CHIP_TOUCH_LARGE. A disabled link renders as the inert span below and is none.\n const target = interactive && (!disabled || locked);\n const snapRef = useSnapEdges(snap);\n // The visible pill carries the snap: the whole chip, or the wrapper of a split one\n // (whose body gets the forwarded ref instead).\n const ownRef = useCallback(\n (el: HTMLElement | null) => {\n snapRef(el);\n assignRef(ref, el);\n },\n [snapRef, ref],\n );\n const look = cn(\n CHIP_BASE,\n s.body,\n type,\n radius,\n surface,\n interactive && !disabled && !locked && \"cursor-pointer hover:brightness-[0.97] dark:hover:brightness-110\",\n // §4: a link or toggle is a 48 px target at Large — a locked one too, since it is\n // still focusable and still says why when pressed.\n target && CHIP_TOUCH_LARGE,\n // An inert dot chip is text in a column: no inset, so it aligns with the header.\n dot && !interactive && \"px-0\",\n // The under-4px a snap adds is split between the two sides, not stacked at the end.\n snap && \"justify-center\",\n className,\n );\n\n // The tone as data on the visible pill (0.34, refactor audit 2026-10, TST-9): a stable\n // hook for a test or a caller's selector, where the surface classes are the kit's to\n // change. On the pill wrapper of a split chip, else on the chip itself; before the\n // caller's props, so a `data-tone` of their own wins.\n const toneData = { \"data-tone\": onRemove ? undefined : tone } as Record<string, string | undefined>;\n\n // The checkbox box, only on the toggle shape it has meaning on.\n const isCheckbox = checkbox && !!onClick && !href;\n const body = (\n <>\n {isCheckbox && (\n <span\n aria-hidden\n data-chip-check=\"\"\n className={cn(\n \"flex shrink-0 items-center justify-center rounded-sm border\",\n s.icon,\n selected ? \"border-current bg-current/15\" : \"border-current/50\",\n )}\n >\n {selected && <Check className=\"size-full\" strokeWidth={3} />}\n </span>\n )}\n {dot && <span aria-hidden data-chip-dot=\"\" className={cn(\"shrink-0 rounded-full\", DOT_SIZE[size], DOT[tone])} />}\n {Icon &&\n (isValidElement(Icon) ? (\n <span aria-hidden className={cn(s.icon, \"inline-flex shrink-0 [&>svg]:size-full\")}>\n {Icon}\n </span>\n ) : (\n <Icon className={cn(s.icon, \"shrink-0\")} aria-hidden />\n ))}\n {/* Text truncates at Normal and wraps at Large (CHIP_LABEL_TRUNCATE). Mixed\n children (an icon and a word) sit in a row instead: as a plain span, the svg —\n a block under Tailwind's preflight — stacked above the text (keksdose live\n #358). */}\n <span\n className={cn(\n \"min-w-0\",\n typeof children === \"string\" || typeof children === \"number\"\n ? CHIP_LABEL_TRUNCATE\n : \"inline-flex items-center gap-1 [&>svg]:size-[1em] [&>svg]:shrink-0\",\n )}\n >\n {children}\n </span>\n </>\n );\n\n const remove = onRemove ? (\n <button\n type=\"button\"\n // The name carries WHAT is being removed, not just \"Remove\" — a screen-reader user\n // tabbing a row of eight dismiss buttons otherwise hears the same word eight times\n // with no way to tell which one they are on.\n // Composed through `common.fieldValue` because the punctuation between the two\n // halves is the language's, not ours. A chip whose content is not plain text\n // has no value to name, and gets the bare verb rather than a dangling colon.\n aria-label={\n typeof children === \"string\" && children\n ? common.fieldValue(common.remove, children)\n : common.remove\n }\n onClick={(e) => {\n // The chip may itself be a link; removing it must not also follow it.\n e.preventDefault();\n e.stopPropagation();\n // Removing a value from a chip that commits is a commit too.\n if (locked) return;\n onRemove();\n }}\n disabled={(disabled && !locked) || removeDisabled}\n aria-disabled={locked || undefined}\n aria-describedby={locked ? reasonId : undefined}\n className={cn(\n // Logical margins: the × sits at the END of the pill, which is the left in RTL.\n \"-me-0.5 ms-0.5 shrink-0 p-0.5 transition-colors\",\n shape === \"square\" ? \"rounded-sm\" : \"rounded-full\",\n FOCUS_RING,\n // §4: the × is a 48 px target at Large without growing the chip around it.\n REMOVE_HIT_AREA_LARGE,\n !locked && \"hover:bg-active\",\n locked && \"cursor-not-allowed\",\n ((disabled && !locked) || removeDisabled) && \"pointer-events-none\",\n // A disabled chip is already dimmed as a whole; dimming the × again on top\n // would take it to a quarter. `removeDisabled` alone dims just the ×.\n removeDisabled && !disabled && \"opacity-40\",\n )}\n >\n <X className={s.remove} aria-hidden />\n </button>\n ) : null;\n\n // ── The combined shape: one pill, two interactive siblings inside it.\n const pill = (inner: ReactNode) => (\n <span\n ref={snap ? snapRef : undefined}\n data-tone={tone}\n // The pill is the target's box here (§4): the body inside stretches to its height.\n className={cn(CHIP_PILL, s.tail, radius, surface, target && CHIP_TOUCH_LARGE, snap && \"justify-center\", className)}\n >\n {inner}\n {remove}\n </span>\n );\n // The body inside a pill: no border or surface of its own (the pill has them), a\n // hover wash in the text's own hue so it works on every tone.\n const inner = cn(\n \"inline-flex min-w-0 items-center self-stretch transition-colors\",\n radius,\n CHIP_RING,\n s.split,\n type,\n !disabled && !locked && \"cursor-pointer hover:bg-current/10\",\n disabled && !locked && \"cursor-default\",\n locked && \"cursor-not-allowed\",\n );\n\n // ── The three shapes.\n if (href && !disabled) {\n // On this shape `onClick` is the link shape's, typed for the anchor.\n const onLinkClick = onClick as ChipLinkProps[\"onClick\"];\n const linkProps: ChipLinkProps = {\n ...toneData,\n ...rest,\n ref: (remove ? ref : ownRef) as Ref<HTMLAnchorElement>,\n href,\n onClick: onLinkClick,\n \"aria-current\": selected ? \"true\" : undefined,\n className: remove ? inner : look,\n children: body,\n };\n const render = pickLinkRenderer(renderLink, kitLink, href);\n const link = render ? (\n // Through a component rather than called here, so the forwarded ref arrives as\n // an ordinary prop of an element and is never handed to a function mid-render.\n <RenderedLink render={render} {...linkProps} />\n ) : (\n <a\n ref={(remove ? ref : ownRef) as React.Ref<HTMLAnchorElement>}\n {...toneData}\n href={href}\n aria-current={selected ? \"true\" : undefined}\n onClick={onLinkClick}\n className={remove ? inner : look}\n {...rest}\n >\n {body}\n </a>\n );\n return remove ? pill(link) : link;\n }\n\n // `!href`: a DISABLED link renders no link, and its link-shaped `onClick` must not\n // turn it into a button instead — it falls through to the inert span, as before.\n if (onClick && !href) {\n const onButtonClick = onClick as (event: MouseEvent<HTMLButtonElement>) => void;\n const ownDescribedBy = rest[\"aria-describedby\"];\n const button = (\n <button\n ref={(remove ? ref : ownRef) as React.Ref<HTMLButtonElement>}\n {...toneData}\n type=\"button\"\n onClick={(e) => {\n // Locked: the press is swallowed — the chip saves on press, and a lock that\n // let it through would toggle a state that is never stored.\n if (locked) {\n e.preventDefault();\n return;\n }\n onButtonClick(e);\n }}\n disabled={disabled && !locked}\n // Only when `selected` is PASSED: a chip with `onClick` and no `selected` is an\n // action button (it does something), not a toggle (it is on or off), and\n // announcing it \"not pressed\" would claim a state it does not have.\n // A checkbox chip says \"checked\", not \"pressed\" — the same state in the words\n // of the control it looks like.\n role={isCheckbox ? \"checkbox\" : undefined}\n aria-pressed={isCheckbox ? undefined : selected}\n aria-checked={isCheckbox ? !!selected : undefined}\n className={remove ? inner : look}\n {...rest}\n // After the spread, merged with it: the lock's state and reason are not the\n // caller's to drop by passing an `aria-describedby` of their own.\n aria-disabled={locked || (rest[\"aria-disabled\"] as boolean | \"true\" | \"false\" | undefined)}\n aria-describedby={\n locked ? [ownDescribedBy, reasonId].filter(Boolean).join(\" \") : (ownDescribedBy as string | undefined)\n }\n >\n {body}\n </button>\n );\n const shaped = remove ? pill(button) : button;\n if (!locked) return shaped;\n return (\n <LockedReason lock={{ reason: disabledReason, reasonId }} display={disabledReasonDisplay}>\n {shaped}\n </LockedReason>\n );\n }\n\n return (\n /* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- the handlers are\n the roving-focus hooks, live only when the container passes `role` and `tabIndex`\n (ChipInput does); the role is a prop, so the rule cannot see it. */\n <span\n ref={ownRef as React.Ref<HTMLSpanElement>}\n {...toneData}\n className={look}\n tabIndex={tabIndex}\n role={role}\n onKeyDown={onKeyDown}\n onFocus={onFocus}\n {...rest}\n >\n {body}\n {remove}\n </span>\n );\n}\n\n/** Calls a chip's `renderLink` with the props it was given — see its call site. */\nfunction RenderedLink({\n render,\n ...props\n}: ChipLinkProps & { render: (props: ChipLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":";AA2nBI,mBAWmB,KAXnB;AA3nBJ,SAAS,gBAAgB,aAAa,aAAa;AAEnD,SAAS,OAAO,SAAS;AAEzB,SAAS,UAAU;AACnB,SAAS,kBAAmC;AAC5C,SAAS,0BAA0B;AACnC,SAAS,kBAAkB;AAC3B,SAAS,WAAW,oBAAgD;AACpE,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB,cAAc,kBAAkB;AAChE,SAAS,wBAAwB;AACjC,SAAS,uBAAyC;AAClD,SAAS,oBAAoB;AAK7B,SAAS,wBAAwB;AA8DjC,MAAM,OAA6D;AAAA,EACjE,SAAS;AAAA,IACP,MAAM;AAAA,IACN,UAAU;AAAA,EACZ;AAAA,EACA,OAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,EACZ;AAAA,EACA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,UAAU;AAAA,EACZ;AAAA,EACA,SAAS;AAAA,IACP,MAAM;AAAA,IACN,UAAU;AAAA,EACZ;AAAA,EACA,SAAS;AAAA,IACP,MAAM;AAAA,IACN,UAAU;AAAA,EACZ;AAAA,EACA,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,UAAU;AAAA,EACZ;AAAA;AAAA;AAAA;AAAA;AAAA,EAKA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,UAAU;AAAA,EACZ;AAAA,EACA,SAAS;AAAA,IACP,MAAM;AAAA,IACN,UAAU;AAAA,EACZ;AAAA;AAAA;AAAA,EAGA,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,UAAU;AAAA,EACZ;AAAA,EACA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,UAAU;AAAA,EACZ;AAAA,EACA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,UAAU;AAAA,EACZ;AAAA,EACA,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,UAAU;AAAA,EACZ;AAAA,EACA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,UAAU;AAAA,EACZ;AACF;AAKA,MAAM,UAAoC;AAAA,EACxC,SAAS;AAAA;AAAA;AAAA,EAGT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AACV;AASA,MAAM,QAAkC;AAAA,EACtC,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AACV;AAKA,MAAM,MAAgC;AAAA,EACpC,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AACV;AAGA,MAAM,WAAqC,EAAE,IAAI,YAAY,IAAI,UAAU,IAAI,UAAU,IAAI,WAAW;AAKxG,MAAM,OAAiC;AAAA,EACrC,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,SAAS,UAAU,MAAgB,SAAsB,UAAuC;AAC9F,QAAM,UAAU,KAAK,IAAI;AACzB,MAAI,YAAY,OAAO;AAGrB,WAAO,WACH,+DACA;AAAA,EACN;AACA,MAAI,YAAY,SAAS;AAEvB,WAAO,GAAG,MAAM,IAAI,GAAG,YAAY,sDAAsD;AAAA,EAC3F;AACA,MAAI,SAAU,QAAO,QAAQ;AAC7B,SAAO,YAAY,YAAY,QAAQ,IAAI,IAAI,QAAQ;AACzD;AAiCA,MAAM,OAGF;AAAA,EACF,IAAI;AAAA,IACF,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,EACV;AAAA,EACA,IAAI;AAAA,IACF,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,EACV;AAAA,EACA,IAAI;AAAA,IACF,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,EACV;AAAA,EACA,IAAI;AAAA,IACF,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,EACV;AACF;AAEA,MAAM,YAAY;AAKlB,MAAM,YAAY,GAAG,UAAU;AAC/B,MAAM,YAAY,GAAG,SAAS,IAAI,SAAS;AAiB3C,MAAM,sBAAsB;AAS5B,MAAM,mBAAmB,GAAG,kBAAkB;AAQ9C,MAAM,wBACJ;AA2MK,SAAS,KAAK;AAAA,EACnB;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA,OAAO;AAAA,EACP,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAc;AACZ,QAAM,OAAiB,aAAa,SAAS,WAAW,MAAM,IAAI;AAClE,QAAM,SAAS,aAAa,UAAU,uBAAuB,EAAE,QAAQ,YAAY,CAAC;AACpF,QAAM,WAAW,MAAM;AACvB,QAAM,iBAAiB,gBAAgB,QAAQ,iBAAiB;AAEhE,QAAM,SAAS,QAAQ,OAAO,KAAK,CAAC,QAAQ,WAAW,cAAc;AACrE,QAAM,UAAU,WAAW;AAC3B,QAAM,IAAI,KAAK,IAAI;AACnB,QAAM,cAAc,CAAC,CAAC,QAAQ,CAAC,CAAC;AAGhC,QAAM,SACJ,UAAU,WAAY,SAAS,QAAQ,SAAS,OAAO,YAAY,eAAgB;AACrF,QAAM,MAAM,YAAY;AACxB,QAAM,OAAO,GAAG,QAAQ,KAAK,IAAI,GAAG,YAAY,WAAW,6BAA6B;AACxF,QAAM,UAAU;AAAA,IACd,UAAU,MAAM,SAAS,QAAQ;AAAA;AAAA;AAAA,IAGjC,YAAY,CAAC,UAAU;AAAA,IACvB,UAAU;AAAA,EACZ;AACA,QAAM,OAAO,aAAa,CAAC;AAG3B,QAAM,SAAS,gBAAgB,CAAC,YAAY;AAC5C,QAAM,UAAU,aAAa,IAAI;AAGjC,QAAM,SAAS;AAAA,IACb,CAAC,OAA2B;AAC1B,cAAQ,EAAE;AACV,gBAAU,KAAK,EAAE;AAAA,IACnB;AAAA,IACA,CAAC,SAAS,GAAG;AAAA,EACf;AACA,QAAM,OAAO;AAAA,IACX;AAAA,IACA,EAAE;AAAA,IACF;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe,CAAC,YAAY,CAAC,UAAU;AAAA;AAAA;AAAA,IAGvC,UAAU;AAAA;AAAA,IAEV,OAAO,CAAC,eAAe;AAAA;AAAA,IAEvB,QAAQ;AAAA,IACR;AAAA,EACF;AAMA,QAAM,WAAW,EAAE,aAAa,WAAW,SAAY,KAAK;AAG5D,QAAM,aAAa,YAAY,CAAC,CAAC,WAAW,CAAC;AAC7C,QAAM,OACJ,iCACG;AAAA,kBACC;AAAA,MAAC;AAAA;AAAA,QACC,eAAW;AAAA,QACX,mBAAgB;AAAA,QAChB,WAAW;AAAA,UACT;AAAA,UACA,EAAE;AAAA,UACF,WAAW,iCAAiC;AAAA,QAC9C;AAAA,QAEC,sBAAY,oBAAC,SAAM,WAAU,aAAY,aAAa,GAAG;AAAA;AAAA,IAC5D;AAAA,IAED,OAAO,oBAAC,UAAK,eAAW,MAAC,iBAAc,IAAG,WAAW,GAAG,yBAAyB,SAAS,IAAI,GAAG,IAAI,IAAI,CAAC,GAAG;AAAA,IAC7G,SACE,eAAe,IAAI,IAClB,oBAAC,UAAK,eAAW,MAAC,WAAW,GAAG,EAAE,MAAM,wCAAwC,GAC7E,gBACH,IAEA,oBAAC,QAAK,WAAW,GAAG,EAAE,MAAM,UAAU,GAAG,eAAW,MAAC;AAAA,IAMzD;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA,OAAO,aAAa,YAAY,OAAO,aAAa,WAChD,sBACA;AAAA,QACN;AAAA,QAEC;AAAA;AAAA,IACH;AAAA,KACF;AAGF,QAAM,SAAS,WACb;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MAOL,cACE,OAAO,aAAa,YAAY,WAC5B,OAAO,WAAW,OAAO,QAAQ,QAAQ,IACzC,OAAO;AAAA,MAEb,SAAS,CAAC,MAAM;AAEd,UAAE,eAAe;AACjB,UAAE,gBAAgB;AAElB,YAAI,OAAQ;AACZ,iBAAS;AAAA,MACX;AAAA,MACA,UAAW,YAAY,CAAC,UAAW;AAAA,MACnC,iBAAe,UAAU;AAAA,MACzB,oBAAkB,SAAS,WAAW;AAAA,MACtC,WAAW;AAAA;AAAA,QAET;AAAA,QACA,UAAU,WAAW,eAAe;AAAA,QACpC;AAAA;AAAA,QAEA;AAAA,QACA,CAAC,UAAU;AAAA,QACX,UAAU;AAAA,SACR,YAAY,CAAC,UAAW,mBAAmB;AAAA;AAAA;AAAA,QAG7C,kBAAkB,CAAC,YAAY;AAAA,MACjC;AAAA,MAEA,8BAAC,KAAE,WAAW,EAAE,QAAQ,eAAW,MAAC;AAAA;AAAA,EACtC,IACE;AAGJ,QAAM,OAAO,CAACA,WACZ;AAAA,IAAC;AAAA;AAAA,MACC,KAAK,OAAO,UAAU;AAAA,MACtB,aAAW;AAAA,MAEX,WAAW,GAAG,WAAW,EAAE,MAAM,QAAQ,SAAS,UAAU,kBAAkB,QAAQ,kBAAkB,SAAS;AAAA,MAEhH;AAAA,QAAAA;AAAA,QACA;AAAA;AAAA;AAAA,EACH;AAIF,QAAM,QAAQ;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA,EAAE;AAAA,IACF;AAAA,IACA,CAAC,YAAY,CAAC,UAAU;AAAA,IACxB,YAAY,CAAC,UAAU;AAAA,IACvB,UAAU;AAAA,EACZ;AAGA,MAAI,QAAQ,CAAC,UAAU;AAErB,UAAM,cAAc;AACpB,UAAM,YAA2B;AAAA,MAC/B,GAAG;AAAA,MACH,GAAG;AAAA,MACH,KAAM,SAAS,MAAM;AAAA,MACrB;AAAA,MACA,SAAS;AAAA,MACT,gBAAgB,WAAW,SAAS;AAAA,MACpC,WAAW,SAAS,QAAQ;AAAA,MAC5B,UAAU;AAAA,IACZ;AACA,UAAM,SAAS,iBAAiB,YAAY,SAAS,IAAI;AACzD,UAAM,OAAO;AAAA;AAAA;AAAA,MAGX,oBAAC,gBAAa,QAAiB,GAAG,WAAW;AAAA,QAE7C;AAAA,MAAC;AAAA;AAAA,QACC,KAAM,SAAS,MAAM;AAAA,QACpB,GAAG;AAAA,QACJ;AAAA,QACA,gBAAc,WAAW,SAAS;AAAA,QAClC,SAAS;AAAA,QACT,WAAW,SAAS,QAAQ;AAAA,QAC3B,GAAG;AAAA,QAEH;AAAA;AAAA,IACH;AAEF,WAAO,SAAS,KAAK,IAAI,IAAI;AAAA,EAC/B;AAIA,MAAI,WAAW,CAAC,MAAM;AACpB,UAAM,gBAAgB;AACtB,UAAM,iBAAiB,KAAK,kBAAkB;AAC9C,UAAM,SACJ;AAAA,MAAC;AAAA;AAAA,QACC,KAAM,SAAS,MAAM;AAAA,QACpB,GAAG;AAAA,QACJ,MAAK;AAAA,QACL,SAAS,CAAC,MAAM;AAGd,cAAI,QAAQ;AACV,cAAE,eAAe;AACjB;AAAA,UACF;AACA,wBAAc,CAAC;AAAA,QACjB;AAAA,QACA,UAAU,YAAY,CAAC;AAAA,QAMvB,MAAM,aAAa,aAAa;AAAA,QAChC,gBAAc,aAAa,SAAY;AAAA,QACvC,gBAAc,aAAa,CAAC,CAAC,WAAW;AAAA,QACxC,WAAW,SAAS,QAAQ;AAAA,QAC3B,GAAG;AAAA,QAGJ,iBAAe,UAAW,KAAK,eAAe;AAAA,QAC9C,oBACE,SAAS,CAAC,gBAAgB,QAAQ,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,IAAK;AAAA,QAGlE;AAAA;AAAA,IACH;AAEF,UAAM,SAAS,SAAS,KAAK,MAAM,IAAI;AACvC,QAAI,CAAC,OAAQ,QAAO;AACpB,WACE,oBAAC,gBAAa,MAAM,EAAE,QAAQ,gBAAgB,SAAS,GAAG,SAAS,uBAChE,kBACH;AAAA,EAEJ;AAEA;AAAA;AAAA;AAAA;AAAA,IAIE;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACJ,GAAG;AAAA,QACJ,WAAW;AAAA,QACX;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACC,GAAG;AAAA,QAEH;AAAA;AAAA,UACA;AAAA;AAAA;AAAA,IACH;AAAA;AAEJ;AAGA,SAAS,aAAa;AAAA,EACpB;AAAA,EACA,GAAG;AACL,GAAuE;AACrE,SAAO,OAAO,KAAK;AACrB;","names":["inner"]}
@@ -0,0 +1,5 @@
1
+ import type { ConfirmFn } from "./confirm-dialog.js";
2
+ /** The nearest `ConfirmProvider`'s `confirm`, or `null`. Internal: its own module so a
3
+ * part that may sit outside a provider (`RowActions`, 0.49 KIT-12) can ask without
4
+ * `useConfirm()`'s throw, and without adding a name to the barrel. */
5
+ export declare const ConfirmContext: import("react").Context<ConfirmFn | null>;
@@ -0,0 +1,7 @@
1
+ "use client";
2
+ import { createContext } from "react";
3
+ const ConfirmContext = createContext(null);
4
+ export {
5
+ ConfirmContext
6
+ };
7
+ //# sourceMappingURL=confirm-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/confirm-context.ts"],"sourcesContent":["import { createContext } from \"react\";\nimport type { ConfirmFn } from \"./confirm-dialog\";\n\n/** The nearest `ConfirmProvider`'s `confirm`, or `null`. Internal: its own module so a\n * part that may sit outside a provider (`RowActions`, 0.49 KIT-12) can ask without\n * `useConfirm()`'s throw, and without adding a name to the barrel. */\nexport const ConfirmContext = createContext<ConfirmFn | null>(null);\n"],"mappings":";AAAA,SAAS,qBAAqB;AAMvB,MAAM,iBAAiB,cAAgC,IAAI;","names":[]}
@@ -1,13 +1,13 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
3
+ import { useCallback, useContext, useEffect, useRef, useState } from "react";
4
4
  import { useKitLabels } from "../i18n/kit-labels.js";
5
5
  import { TypedConfirmField, typedMatches } from "./danger-confirm.js";
6
6
  import { DialogFrame } from "./dialog-frame.js";
7
7
  import { Button } from "./button.js";
8
8
  import { useWriteLock, writeLockFor } from "./write-lock.js";
9
9
  import { DEFAULT_CONFIRM_DIALOG_LABELS } from "./confirm-dialog-labels.js";
10
- const ConfirmContext = createContext(null);
10
+ import { ConfirmContext } from "./confirm-context.js";
11
11
  function useConfirm() {
12
12
  const confirm = useContext(ConfirmContext);
13
13
  if (!confirm) {
@@ -87,7 +87,7 @@ function ConfirmDialogView({
87
87
  {
88
88
  ref: confirmRef,
89
89
  type: "button",
90
- variant: tone === "danger" ? "danger" : "primary",
90
+ variant: tone === "danger" ? "danger" : "default",
91
91
  disabled: !matches,
92
92
  disabledReason: request.lockReason,
93
93
  onClick: () => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/confirm-dialog.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { TypedConfirmField, typedMatches } from \"./danger-confirm\";\nimport type { TypedMatch } from \"./danger-confirm\";\nimport { DialogFrame } from \"./dialog-frame\";\nimport { Button } from \"./button\";\nimport { useWriteLock, writeLockFor, type CommitScope } from \"./write-lock\";\nimport { DEFAULT_CONFIRM_DIALOG_LABELS } from \"./confirm-dialog-labels\";\n\n/**\n * The `confirmDialog` namespace of `<UiKitProvider labels>`: the two buttons' fallback\n * text. A call that knows what it is confirming says so per call (`confirmLabel:\n * \"Delete budget\"`), which is what the dialog is for — these are for the rest.\n */\nexport interface ConfirmDialogLabels {\n confirm: string;\n cancel: string;\n /**\n * 0.18: label of the field a `requireTyped` confirm shows. A FUNCTION of the text to\n * type by default — where the address sits in the sentence moves with the language —\n * or a finished STRING. A call's own `typedLabel` wins over it.\n *\n * Required, like every key of a namespace: the shipped catalogues\n * (`@eifi1/ui-kit/i18n/<code>`) translate it, and an app's own full catalogue should\n * hear about the new string from the compiler rather than show it in English.\n */\n typed: string | ((text: string) => string);\n}\n\n// Defined in ./confirm-dialog-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_CONFIRM_DIALOG_LABELS };\n\nexport type ConfirmTone = \"danger\" | \"warning\" | \"neutral\";\n\nexport interface ConfirmOptions {\n /** The question, and the dialog's accessible name. */\n title: ReactNode;\n /** The smaller line under it — the consequence. Wired to `aria-describedby`. */\n body?: ReactNode;\n /** Default: `confirmDialog.confirm` from the provider, else \"Confirm\". */\n confirmLabel?: ReactNode;\n /** Default: `confirmDialog.cancel` from the provider, else \"Cancel\". */\n cancelLabel?: ReactNode;\n /**\n * `\"danger\"` for what cannot be undone (a delete): the confirm is the destructive\n * button and focus starts on CANCEL. `\"warning\"` and `\"neutral\"` (default) start on\n * the confirm. See {@link ConfirmProvider} for why the two differ.\n */\n tone?: ConfirmTone;\n /**\n * 0.18: make the user TYPE this before confirm is enabled — the target account's\n * e-mail address, a budget's name. For the action whose risk is not \"did you mean\n * it\" but \"is this the right row\": an admin who deactivates somebody has already\n * proven who THEY are (they passed the admin gate); typing the address is what\n * proves they read which account they are on. Both keksdose (`TYPE_EMAIL`) and\n * Kurvenschmiede (`TypedConfirm`) built this beside `useConfirm` because it had no\n * field.\n *\n * The dialog then shows one text field under the body and focuses it (whatever the\n * tone: a reflexive Enter cannot confirm a field that does not match yet). Confirm\n * stays disabled until the text matches; Enter in the field confirms once it does.\n * The answer is still `true` / `false` — the text matched what you passed, so send\n * that to the server if it asks for the address too, not the user's spelling.\n */\n requireTyped?: string;\n /** Label of that field. Default: `confirmDialog.typed` — `Type “a@b.c” to confirm`. */\n typedLabel?: string;\n /**\n * How the typed text is compared. Default `\"caseless\"`: surrounding spaces and case\n * ignored, the rule both apps applied to an address. `\"trim\"` keeps case, `\"exact\"`\n * keeps spaces too — for a phrase like \"DELETE\" whose contract is \"exactly this\".\n */\n typedMatch?: TypedMatch;\n /**\n * 0.31.1: the confirm button COMMITS, as `Button`'s `commit` does. Under a locked\n * `WriteLockProvider` — read where `useConfirm` was called, not where the one dialog\n * host sits — it is `aria-disabled` with the lock's reason, so a read-only page (a\n * demo, a viewer's budget) can't confirm a delete through a dialog. One prop for the\n * many confirm sites an app has (kastlan's 0.31 adoption counted ~23).\n *\n * 0.33: a {@link CommitScope} — `confirm({ commit: COMMIT_EXCEPT_BILLING })` for a\n * confirm a lapsed plan still allows (removing access, the account's own settings;\n * docs/billing-harmonization.md §12.36).\n */\n commit?: CommitScope;\n}\n\nexport type ConfirmFn = (options: ConfirmOptions) => Promise<boolean>;\n\nconst ConfirmContext = createContext<ConfirmFn | null>(null);\n\n/**\n * `const confirm = useConfirm()` → `if (!(await confirm({ title }))) return`.\n *\n * The returned function is STABLE for the life of the provider, so it can sit in an\n * effect's or a `useCallback`'s dependency list without re-running it.\n *\n * Throws outside a {@link ConfirmProvider}, rather than falling back to\n * `window.confirm`: a silent fallback is exactly the native dialog the three apps are\n * migrating away from, and it would look like the migration had worked.\n */\nexport function useConfirm(): ConfirmFn {\n const confirm = useContext(ConfirmContext);\n if (!confirm) {\n throw new Error(\"useConfirm() must be called below a <ConfirmProvider>.\");\n }\n // The lock of the CALLER's place: the dialog host usually sits above the app's\n // `WriteLockProvider`, so it could not read the lock itself. Each call's own `commit`\n // scope is applied to it when the call runs (`writeLockFor`, §12.36).\n const lock = useWriteLock();\n return useCallback(\n (options: ConfirmOptions) => {\n const scoped = writeLockFor(lock, options.commit);\n return confirm(scoped.locked ? ({ ...options, lockReason: scoped.reason } as ConfirmOptions) : options);\n },\n [confirm, lock],\n );\n}\n\ninterface Request extends ConfirmOptions {\n id: number;\n resolve: (answer: boolean) => void;\n /** Set by {@link useConfirm} for a `commit` confirm under a locked provider. */\n lockReason?: ReactNode;\n}\n\nexport interface ConfirmProviderProps {\n children: ReactNode;\n}\n\n/**\n * The host for {@link useConfirm}: ONE confirm dialog for the whole app, rendered on\n * {@link DialogFrame}, answering a promise.\n *\n * Asked for by all three apps at once — keksdose has 20+ `window.confirm` calls,\n * kastlan 7, lenkbank 1. The native dialog cannot be translated past its OK/Cancel\n * (those are the BROWSER's language, not the app's), cannot say which button is the\n * destructive one, and on an installed PWA it prints the origin over the question.\n * A promise is what lets each of those call sites change by one `await`:\n *\n * if (!window.confirm(t(\"budget.delete_confirm\"))) return;\n * // becomes\n * if (!(await confirm({ title: t(\"budget.delete_confirm\"), tone: \"danger\" }))) return;\n *\n * ## Its own provider, not part of `UiKitProvider`\n *\n * `UiKitProvider` renders nothing: it is strings and a locale, it is optional, and it\n * NESTS (an inner one re-labels one table). A dialog host inside it would mount a host\n * per nested provider, and a component under the inner one would open a second dialog\n * layer of its own. So the host is its own component, mounted once — inside the\n * `UiKitProvider` whose strings it should speak, which is also the order the labels\n * need.\n *\n * ## Focus\n *\n * On CANCEL for `tone: \"danger\"`, on CONFIRM otherwise. A confirm is often answered by\n * a reflexive Enter — the key that pressed the Delete button in the first place may\n * still be down — and for an action that cannot be undone that reflex must land on the\n * harmless answer; `DangerConfirm` makes the same call (\"never to the destructive\n * button itself\"). For everything else the confirm is the expected answer and Enter\n * accepting it is what `window.confirm` taught users, which the migrating call sites'\n * users already rely on. Focus returns to whatever opened it, through `Modal`'s trap.\n *\n * ## Escape, the backdrop, Back and Cancel all answer `false`\n *\n * Only the confirm button answers `true`. Anything that merely dismisses is a no.\n *\n * ## A second `confirm` while one is open QUEUES\n *\n * It is shown when the first is answered, and each call gets its own answer. The\n * alternative — answering the open one `false` to make room — would answer a question\n * the user never answered: a background prompt (\"your session is about to expire\")\n * would cancel the delete they were reading. `false` has to mean \"the user said no\".\n * A double-click cannot queue two of the same: the first click opens the modal\n * synchronously (a discrete event's update is flushed before the next one), so the\n * second lands on the open dialog rather than on the button — at worst on its\n * backdrop, which answers `false`, the safe way to be wrong.\n *\n * Unmounting the provider answers everything still pending `false`, so no caller's\n * `await` hangs forever.\n *\n * ## keksdose's synchronous call (transaction-swipe-plan.ts)\n *\n * `buildTxSwipeActions` is synchronous, but its `window.confirm` sits inside the\n * action THUNK (`confirmed(key, run)` returns `() => { if (confirm(…)) run() }`), and a\n * thunk may be async without its caller knowing. So the plan builder stays\n * synchronous; it takes the confirm as an argument and the thunk awaits it:\n *\n * // mobile-transaction-list.tsx\n * const confirm = useConfirm();\n * buildTxSwipeActions({ tx, …, confirm });\n *\n * // transaction-swipe-plan.ts\n * const confirmed = (messageKey: string, run: () => void) => () => {\n * void confirm({ title: t(messageKey), tone: \"danger\" }).then((ok) => ok && run());\n * };\n *\n * Its test then passes `confirm: vi.fn(async () => false)` instead of stubbing the\n * global, and asserts after an `await` (the mutation now runs a microtask later). The\n * row snaps back when the swipe commits, before the answer — which is right: the\n * dialog, not the half-swiped row, is where the question now is.\n */\nexport function ConfirmProvider({ children }: ConfirmProviderProps) {\n const [queue, setQueue] = useState<Request[]>([]);\n const nextId = useRef(0);\n // Every unanswered request, including the ones not yet in state — the unmount\n // cleanup below must reach a request made in the same tick it unmounts.\n const pending = useRef(new Set<Request>());\n\n const confirm = useCallback<ConfirmFn>(\n (options) =>\n new Promise<boolean>((resolve) => {\n const request: Request = { ...options, id: ++nextId.current, resolve };\n pending.current.add(request);\n setQueue((q) => [...q, request]);\n }),\n [],\n );\n\n useEffect(() => {\n const open = pending.current;\n return () => {\n for (const r of open) r.resolve(false);\n open.clear();\n };\n }, []);\n\n const answer = useCallback((request: Request, value: boolean) => {\n if (!pending.current.delete(request)) return;\n request.resolve(value);\n setQueue((q) => q.filter((r) => r !== request));\n }, []);\n\n const current = queue[0];\n return (\n <ConfirmContext.Provider value={confirm}>\n {children}\n {current && (\n // Keyed per request, so a queued one gets a fresh Modal: the first one's trap\n // hands focus back to the page, and the next one's captures that as its own\n // restore target instead of a button that has just been unmounted.\n <ConfirmDialogView key={current.id} request={current} onAnswer={answer} />\n )}\n </ConfirmContext.Provider>\n );\n}\n\nfunction ConfirmDialogView({\n request,\n onAnswer,\n}: {\n request: Request;\n onAnswer: (request: Request, value: boolean) => void;\n}) {\n const labels = useKitLabels(\"confirmDialog\", DEFAULT_CONFIRM_DIALOG_LABELS);\n const tone = request.tone ?? \"neutral\";\n const confirmRef = useRef<HTMLButtonElement>(null);\n const cancelRef = useRef<HTMLButtonElement>(null);\n // Set by the confirm button, read once the panel has finished lowering. Everything\n // else that ends in `onClose` — Escape, the backdrop, Back, Cancel — leaves it false.\n const accepted = useRef(false);\n const typedRef = useRef<HTMLInputElement>(null);\n const [typed, setTyped] = useState(\"\");\n const target = request.requireTyped;\n const matches = target === undefined || typedMatches(typed, target, request.typedMatch ?? \"caseless\");\n\n // In THIS component's effect, which runs after `Modal`'s own (a parent's effects run\n // after its children's). `Modal` engages its focus trap on the panel first — and\n // records the page's focused element as the restore target — and only then does\n // focus move to the button. Focusing it from inside the panel would run first and\n // make the button the restore target. A typed confirm starts in its field: typing is\n // the one thing the dialog is waiting for.\n useEffect(() => {\n (target !== undefined ? typedRef : tone === \"danger\" ? cancelRef : confirmRef).current?.focus();\n }, [tone, target]);\n\n const typedLabel =\n target === undefined\n ? \"\"\n : (request.typedLabel ??\n (typeof labels.typed === \"string\" ? labels.typed : labels.typed(target)));\n\n return (\n <DialogFrame\n title={request.title}\n description={request.body}\n onClose={() => onAnswer(request, accepted.current)}\n // A confirmation interrupts to ask a question that must be answered.\n role=\"alertdialog\"\n data-confirm-tone={tone}\n actions={(close) => (\n <>\n <Button ref={cancelRef} type=\"button\" variant=\"ghost\" onClick={close}>\n {request.cancelLabel ?? labels.cancel}\n </Button>\n <Button\n ref={confirmRef}\n type=\"button\"\n variant={tone === \"danger\" ? \"danger\" : \"primary\"}\n disabled={!matches}\n disabledReason={request.lockReason}\n onClick={() => {\n accepted.current = true;\n close();\n }}\n >\n {request.confirmLabel ?? labels.confirm}\n </Button>\n </>\n )}\n >\n {/* The field, when there is one, is the body; a plain confirm keeps having none. */}\n {target !== undefined && (\n <form\n noValidate\n onSubmit={(e) => {\n e.preventDefault();\n // Through the button, so Enter and a click are one path — and a disabled\n // (not yet matching) confirm ignores the click.\n confirmRef.current?.click();\n }}\n >\n {/* The kit's one typed-confirmation field (0.23), DangerConfirm's too: the\n keyboard told to leave the text alone, `data-matched` on the input. The\n label is this namespace's (`confirmDialog.typed`), and `placeholder=\"\"`\n keeps out the `dangerConfirm.phrasePlaceholder` a provider may set for\n the tile — this field never had one. */}\n <TypedConfirmField\n ref={typedRef}\n target={target}\n match={request.typedMatch ?? \"caseless\"}\n label={typedLabel}\n placeholder=\"\"\n value={typed}\n onValueChange={setTyped}\n />\n </form>\n )}\n </DialogFrame>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/confirm-dialog.tsx"],"sourcesContent":["import { useCallback, useContext, useEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { TypedConfirmField, typedMatches } from \"./danger-confirm\";\nimport type { TypedMatch } from \"./danger-confirm\";\nimport { DialogFrame } from \"./dialog-frame\";\nimport { Button } from \"./button\";\nimport { useWriteLock, writeLockFor, type CommitScope } from \"./write-lock\";\nimport { DEFAULT_CONFIRM_DIALOG_LABELS } from \"./confirm-dialog-labels\";\nimport { ConfirmContext } from \"./confirm-context\";\n\n/**\n * The `confirmDialog` namespace of `<UiKitProvider labels>`: the two buttons' fallback\n * text. A call that knows what it is confirming says so per call (`confirmLabel:\n * \"Delete budget\"`), which is what the dialog is for — these are for the rest.\n */\nexport interface ConfirmDialogLabels {\n confirm: string;\n cancel: string;\n /**\n * 0.18: label of the field a `requireTyped` confirm shows. A FUNCTION of the text to\n * type by default — where the address sits in the sentence moves with the language —\n * or a finished STRING. A call's own `typedLabel` wins over it.\n *\n * Required, like every key of a namespace: the shipped catalogues\n * (`@eifi1/ui-kit/i18n/<code>`) translate it, and an app's own full catalogue should\n * hear about the new string from the compiler rather than show it in English.\n */\n typed: string | ((text: string) => string);\n}\n\n// Defined in ./confirm-dialog-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_CONFIRM_DIALOG_LABELS };\n\nexport type ConfirmTone = \"danger\" | \"warning\" | \"neutral\";\n\nexport interface ConfirmOptions {\n /** The question, and the dialog's accessible name. */\n title: ReactNode;\n /** The smaller line under it — the consequence. Wired to `aria-describedby`. */\n body?: ReactNode;\n /** Default: `confirmDialog.confirm` from the provider, else \"Confirm\". */\n confirmLabel?: ReactNode;\n /** Default: `confirmDialog.cancel` from the provider, else \"Cancel\". */\n cancelLabel?: ReactNode;\n /**\n * `\"danger\"` for what cannot be undone (a delete): the confirm is the destructive\n * button and focus starts on CANCEL. `\"warning\"` and `\"neutral\"` (default) start on\n * the confirm. See {@link ConfirmProvider} for why the two differ.\n */\n tone?: ConfirmTone;\n /**\n * 0.18: make the user TYPE this before confirm is enabled — the target account's\n * e-mail address, a budget's name. For the action whose risk is not \"did you mean\n * it\" but \"is this the right row\": an admin who deactivates somebody has already\n * proven who THEY are (they passed the admin gate); typing the address is what\n * proves they read which account they are on. Both keksdose (`TYPE_EMAIL`) and\n * Kurvenschmiede (`TypedConfirm`) built this beside `useConfirm` because it had no\n * field.\n *\n * The dialog then shows one text field under the body and focuses it (whatever the\n * tone: a reflexive Enter cannot confirm a field that does not match yet). Confirm\n * stays disabled until the text matches; Enter in the field confirms once it does.\n * The answer is still `true` / `false` — the text matched what you passed, so send\n * that to the server if it asks for the address too, not the user's spelling.\n */\n requireTyped?: string;\n /** Label of that field. Default: `confirmDialog.typed` — `Type “a@b.c” to confirm`. */\n typedLabel?: string;\n /**\n * How the typed text is compared. Default `\"caseless\"`: surrounding spaces and case\n * ignored, the rule both apps applied to an address. `\"trim\"` keeps case, `\"exact\"`\n * keeps spaces too — for a phrase like \"DELETE\" whose contract is \"exactly this\".\n */\n typedMatch?: TypedMatch;\n /**\n * 0.31.1: the confirm button COMMITS, as `Button`'s `commit` does. Under a locked\n * `WriteLockProvider` — read where `useConfirm` was called, not where the one dialog\n * host sits — it is `aria-disabled` with the lock's reason, so a read-only page (a\n * demo, a viewer's budget) can't confirm a delete through a dialog. One prop for the\n * many confirm sites an app has (kastlan's 0.31 adoption counted ~23).\n *\n * 0.33: a {@link CommitScope} — `confirm({ commit: COMMIT_EXCEPT_BILLING })` for a\n * confirm a lapsed plan still allows (removing access, the account's own settings;\n * docs/billing-harmonization.md §12.36).\n */\n commit?: CommitScope;\n}\n\nexport type ConfirmFn = (options: ConfirmOptions) => Promise<boolean>;\n\n\n/**\n * `const confirm = useConfirm()` → `if (!(await confirm({ title }))) return`.\n *\n * The returned function is STABLE for the life of the provider, so it can sit in an\n * effect's or a `useCallback`'s dependency list without re-running it.\n *\n * Throws outside a {@link ConfirmProvider}, rather than falling back to\n * `window.confirm`: a silent fallback is exactly the native dialog the three apps are\n * migrating away from, and it would look like the migration had worked.\n */\nexport function useConfirm(): ConfirmFn {\n const confirm = useContext(ConfirmContext);\n if (!confirm) {\n throw new Error(\"useConfirm() must be called below a <ConfirmProvider>.\");\n }\n // The lock of the CALLER's place: the dialog host usually sits above the app's\n // `WriteLockProvider`, so it could not read the lock itself. Each call's own `commit`\n // scope is applied to it when the call runs (`writeLockFor`, §12.36).\n const lock = useWriteLock();\n return useCallback(\n (options: ConfirmOptions) => {\n const scoped = writeLockFor(lock, options.commit);\n return confirm(scoped.locked ? ({ ...options, lockReason: scoped.reason } as ConfirmOptions) : options);\n },\n [confirm, lock],\n );\n}\n\ninterface Request extends ConfirmOptions {\n id: number;\n resolve: (answer: boolean) => void;\n /** Set by {@link useConfirm} for a `commit` confirm under a locked provider. */\n lockReason?: ReactNode;\n}\n\nexport interface ConfirmProviderProps {\n children: ReactNode;\n}\n\n/**\n * The host for {@link useConfirm}: ONE confirm dialog for the whole app, rendered on\n * {@link DialogFrame}, answering a promise.\n *\n * Asked for by all three apps at once — keksdose has 20+ `window.confirm` calls,\n * kastlan 7, lenkbank 1. The native dialog cannot be translated past its OK/Cancel\n * (those are the BROWSER's language, not the app's), cannot say which button is the\n * destructive one, and on an installed PWA it prints the origin over the question.\n * A promise is what lets each of those call sites change by one `await`:\n *\n * if (!window.confirm(t(\"budget.delete_confirm\"))) return;\n * // becomes\n * if (!(await confirm({ title: t(\"budget.delete_confirm\"), tone: \"danger\" }))) return;\n *\n * ## Its own provider, not part of `UiKitProvider`\n *\n * `UiKitProvider` renders nothing: it is strings and a locale, it is optional, and it\n * NESTS (an inner one re-labels one table). A dialog host inside it would mount a host\n * per nested provider, and a component under the inner one would open a second dialog\n * layer of its own. So the host is its own component, mounted once — inside the\n * `UiKitProvider` whose strings it should speak, which is also the order the labels\n * need.\n *\n * ## Focus\n *\n * On CANCEL for `tone: \"danger\"`, on CONFIRM otherwise. A confirm is often answered by\n * a reflexive Enter — the key that pressed the Delete button in the first place may\n * still be down — and for an action that cannot be undone that reflex must land on the\n * harmless answer; `DangerConfirm` makes the same call (\"never to the destructive\n * button itself\"). For everything else the confirm is the expected answer and Enter\n * accepting it is what `window.confirm` taught users, which the migrating call sites'\n * users already rely on. Focus returns to whatever opened it, through `Modal`'s trap.\n *\n * ## Escape, the backdrop, Back and Cancel all answer `false`\n *\n * Only the confirm button answers `true`. Anything that merely dismisses is a no.\n *\n * ## A second `confirm` while one is open QUEUES\n *\n * It is shown when the first is answered, and each call gets its own answer. The\n * alternative — answering the open one `false` to make room — would answer a question\n * the user never answered: a background prompt (\"your session is about to expire\")\n * would cancel the delete they were reading. `false` has to mean \"the user said no\".\n * A double-click cannot queue two of the same: the first click opens the modal\n * synchronously (a discrete event's update is flushed before the next one), so the\n * second lands on the open dialog rather than on the button — at worst on its\n * backdrop, which answers `false`, the safe way to be wrong.\n *\n * Unmounting the provider answers everything still pending `false`, so no caller's\n * `await` hangs forever.\n *\n * ## keksdose's synchronous call (transaction-swipe-plan.ts)\n *\n * `buildTxSwipeActions` is synchronous, but its `window.confirm` sits inside the\n * action THUNK (`confirmed(key, run)` returns `() => { if (confirm(…)) run() }`), and a\n * thunk may be async without its caller knowing. So the plan builder stays\n * synchronous; it takes the confirm as an argument and the thunk awaits it:\n *\n * // mobile-transaction-list.tsx\n * const confirm = useConfirm();\n * buildTxSwipeActions({ tx, …, confirm });\n *\n * // transaction-swipe-plan.ts\n * const confirmed = (messageKey: string, run: () => void) => () => {\n * void confirm({ title: t(messageKey), tone: \"danger\" }).then((ok) => ok && run());\n * };\n *\n * Its test then passes `confirm: vi.fn(async () => false)` instead of stubbing the\n * global, and asserts after an `await` (the mutation now runs a microtask later). The\n * row snaps back when the swipe commits, before the answer — which is right: the\n * dialog, not the half-swiped row, is where the question now is.\n */\nexport function ConfirmProvider({ children }: ConfirmProviderProps) {\n const [queue, setQueue] = useState<Request[]>([]);\n const nextId = useRef(0);\n // Every unanswered request, including the ones not yet in state — the unmount\n // cleanup below must reach a request made in the same tick it unmounts.\n const pending = useRef(new Set<Request>());\n\n const confirm = useCallback<ConfirmFn>(\n (options) =>\n new Promise<boolean>((resolve) => {\n const request: Request = { ...options, id: ++nextId.current, resolve };\n pending.current.add(request);\n setQueue((q) => [...q, request]);\n }),\n [],\n );\n\n useEffect(() => {\n const open = pending.current;\n return () => {\n for (const r of open) r.resolve(false);\n open.clear();\n };\n }, []);\n\n const answer = useCallback((request: Request, value: boolean) => {\n if (!pending.current.delete(request)) return;\n request.resolve(value);\n setQueue((q) => q.filter((r) => r !== request));\n }, []);\n\n const current = queue[0];\n return (\n <ConfirmContext.Provider value={confirm}>\n {children}\n {current && (\n // Keyed per request, so a queued one gets a fresh Modal: the first one's trap\n // hands focus back to the page, and the next one's captures that as its own\n // restore target instead of a button that has just been unmounted.\n <ConfirmDialogView key={current.id} request={current} onAnswer={answer} />\n )}\n </ConfirmContext.Provider>\n );\n}\n\nfunction ConfirmDialogView({\n request,\n onAnswer,\n}: {\n request: Request;\n onAnswer: (request: Request, value: boolean) => void;\n}) {\n const labels = useKitLabels(\"confirmDialog\", DEFAULT_CONFIRM_DIALOG_LABELS);\n const tone = request.tone ?? \"neutral\";\n const confirmRef = useRef<HTMLButtonElement>(null);\n const cancelRef = useRef<HTMLButtonElement>(null);\n // Set by the confirm button, read once the panel has finished lowering. Everything\n // else that ends in `onClose` — Escape, the backdrop, Back, Cancel — leaves it false.\n const accepted = useRef(false);\n const typedRef = useRef<HTMLInputElement>(null);\n const [typed, setTyped] = useState(\"\");\n const target = request.requireTyped;\n const matches = target === undefined || typedMatches(typed, target, request.typedMatch ?? \"caseless\");\n\n // In THIS component's effect, which runs after `Modal`'s own (a parent's effects run\n // after its children's). `Modal` engages its focus trap on the panel first — and\n // records the page's focused element as the restore target — and only then does\n // focus move to the button. Focusing it from inside the panel would run first and\n // make the button the restore target. A typed confirm starts in its field: typing is\n // the one thing the dialog is waiting for.\n useEffect(() => {\n (target !== undefined ? typedRef : tone === \"danger\" ? cancelRef : confirmRef).current?.focus();\n }, [tone, target]);\n\n const typedLabel =\n target === undefined\n ? \"\"\n : (request.typedLabel ??\n (typeof labels.typed === \"string\" ? labels.typed : labels.typed(target)));\n\n return (\n <DialogFrame\n title={request.title}\n description={request.body}\n onClose={() => onAnswer(request, accepted.current)}\n // A confirmation interrupts to ask a question that must be answered.\n role=\"alertdialog\"\n data-confirm-tone={tone}\n actions={(close) => (\n <>\n <Button ref={cancelRef} type=\"button\" variant=\"ghost\" onClick={close}>\n {request.cancelLabel ?? labels.cancel}\n </Button>\n <Button\n ref={confirmRef}\n type=\"button\"\n variant={tone === \"danger\" ? \"danger\" : \"default\"}\n disabled={!matches}\n disabledReason={request.lockReason}\n onClick={() => {\n accepted.current = true;\n close();\n }}\n >\n {request.confirmLabel ?? labels.confirm}\n </Button>\n </>\n )}\n >\n {/* The field, when there is one, is the body; a plain confirm keeps having none. */}\n {target !== undefined && (\n <form\n noValidate\n onSubmit={(e) => {\n e.preventDefault();\n // Through the button, so Enter and a click are one path — and a disabled\n // (not yet matching) confirm ignores the click.\n confirmRef.current?.click();\n }}\n >\n {/* The kit's one typed-confirmation field (0.23), DangerConfirm's too: the\n keyboard told to leave the text alone, `data-matched` on the input. The\n label is this namespace's (`confirmDialog.typed`), and `placeholder=\"\"`\n keeps out the `dangerConfirm.phrasePlaceholder` a provider may set for\n the tile — this field never had one. */}\n <TypedConfirmField\n ref={typedRef}\n target={target}\n match={request.typedMatch ?? \"caseless\"}\n label={typedLabel}\n placeholder=\"\"\n value={typed}\n onValueChange={setTyped}\n />\n </form>\n )}\n </DialogFrame>\n );\n}\n"],"mappings":"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","names":[]}
@@ -298,14 +298,14 @@ export interface DangerConfirmProps extends Omit<ComponentPropsWithRef<"div">, "
298
298
  tone?: "danger" | "warning";
299
299
  /**
300
300
  * The confirm button's variant, apart from `tone` (0.24, keksdose). Default: what
301
- * `tone` picks, as before — `"danger"` for `"danger"`, `"primary"` for `"warning"`.
301
+ * `tone` picks, as before — `"danger"` for `"danger"`, `"default"` for `"warning"`.
302
302
  *
303
303
  * `tone` says how bad the outcome is; this says how loud the last press is, and the two
304
304
  * need not agree. keksdose's `UserActionConfirm` (an admin resets somebody else's
305
305
  * password) asks an AMBER question — what it guards against is mis-targeting, not data
306
306
  * loss, so the danger colour stays its one `severe` consequence's — and ends in a RED
307
307
  * Go, as its hand-built panel did before it moved onto this tile in 0.23; with only
308
- * `tone`, the amber question made Go the neutral primary fill. `tone="warning"
308
+ * `tone`, the amber question made Go the neutral `default` fill. `tone="warning"
309
309
  * confirmVariant="danger"` is that panel again.
310
310
  */
311
311
  confirmVariant?: ButtonVariant;
@@ -345,7 +345,7 @@ function DangerConfirm({
345
345
  {
346
346
  id: confirmId,
347
347
  type: "submit",
348
- variant: confirmVariant ?? (tone === "warning" ? "primary" : "danger"),
348
+ variant: confirmVariant ?? (tone === "warning" ? "default" : "danger"),
349
349
  disabled: !canConfirm,
350
350
  disabledReason: confirmReason,
351
351
  "aria-busy": busy || void 0,