@eifi1/ui-kit 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/README.md +10 -10
  2. package/dist/components/amount-input.d.ts +23 -1
  3. package/dist/components/amount-input.js +78 -13
  4. package/dist/components/amount-input.js.map +1 -1
  5. package/dist/components/bulk-action-bar.d.ts +10 -2
  6. package/dist/components/bulk-action-bar.js.map +1 -1
  7. package/dist/components/button-group.d.ts +1 -1
  8. package/dist/components/calculator.d.ts +1 -1
  9. package/dist/components/chart.js +14 -8
  10. package/dist/components/chart.js.map +1 -1
  11. package/dist/components/chip.js +20 -15
  12. package/dist/components/chip.js.map +1 -1
  13. package/dist/components/combobox-core.d.ts +3 -1
  14. package/dist/components/combobox-core.js +16 -5
  15. package/dist/components/combobox-core.js.map +1 -1
  16. package/dist/components/combobox.js.map +1 -1
  17. package/dist/components/copy-button.d.ts +1 -1
  18. package/dist/components/data-table-filter-popover.js +8 -1
  19. package/dist/components/data-table-filter-popover.js.map +1 -1
  20. package/dist/components/data-table.js +28 -20
  21. package/dist/components/data-table.js.map +1 -1
  22. package/dist/components/entity-combobox.js +22 -17
  23. package/dist/components/entity-combobox.js.map +1 -1
  24. package/dist/components/error-boundary.d.ts +202 -13
  25. package/dist/components/error-boundary.js +225 -21
  26. package/dist/components/error-boundary.js.map +1 -1
  27. package/dist/components/field-sync.js +90 -83
  28. package/dist/components/field-sync.js.map +1 -1
  29. package/dist/components/file-button.d.ts +1 -1
  30. package/dist/components/file-dropzone.d.ts +1 -1
  31. package/dist/components/form-actions.d.ts +1 -1
  32. package/dist/components/full-bleed-dialog.js +4 -0
  33. package/dist/components/full-bleed-dialog.js.map +1 -1
  34. package/dist/components/hover-menu.js +64 -56
  35. package/dist/components/hover-menu.js.map +1 -1
  36. package/dist/components/list.d.ts +14 -2
  37. package/dist/components/list.js +3 -1
  38. package/dist/components/list.js.map +1 -1
  39. package/dist/components/modal.js.map +1 -1
  40. package/dist/components/multi-entity-combobox.js +23 -17
  41. package/dist/components/multi-entity-combobox.js.map +1 -1
  42. package/dist/components/number-field.d.ts +1 -1
  43. package/dist/components/number-input.d.ts +1 -1
  44. package/dist/components/number-input.js.map +1 -1
  45. package/dist/components/numpad-sheet.d.ts +10 -2
  46. package/dist/components/numpad-sheet.js +6 -4
  47. package/dist/components/numpad-sheet.js.map +1 -1
  48. package/dist/components/page-header.d.ts +35 -6
  49. package/dist/components/page-header.js +7 -1
  50. package/dist/components/page-header.js.map +1 -1
  51. package/dist/components/picker-sheet.js +3 -0
  52. package/dist/components/picker-sheet.js.map +1 -1
  53. package/dist/components/popover.js +3 -0
  54. package/dist/components/popover.js.map +1 -1
  55. package/dist/components/settings-fields.d.ts +1 -1
  56. package/dist/components/signed-amount.d.ts +2 -1
  57. package/dist/components/signed-amount.js +1 -1
  58. package/dist/components/signed-amount.js.map +1 -1
  59. package/dist/components/stat-tile.js +1 -1
  60. package/dist/components/stat-tile.js.map +1 -1
  61. package/dist/components/swipeable-row.js +4 -3
  62. package/dist/components/swipeable-row.js.map +1 -1
  63. package/dist/components/text-link.d.ts +73 -11
  64. package/dist/components/text-link.js +45 -10
  65. package/dist/components/text-link.js.map +1 -1
  66. package/dist/components/text.d.ts +26 -2
  67. package/dist/components/text.js +16 -2
  68. package/dist/components/text.js.map +1 -1
  69. package/dist/components/time-input.d.ts +1 -1
  70. package/dist/components/toggle-group.d.ts +22 -0
  71. package/dist/components/toggle-group.js +8 -8
  72. package/dist/components/toggle-group.js.map +1 -1
  73. package/dist/components/toggle-legend.d.ts +17 -0
  74. package/dist/components/toggle-legend.js +6 -2
  75. package/dist/components/toggle-legend.js.map +1 -1
  76. package/dist/components/tooltip.d.ts +51 -1
  77. package/dist/components/tooltip.js +59 -35
  78. package/dist/components/tooltip.js.map +1 -1
  79. package/dist/components/ui.d.ts +1 -1
  80. package/dist/components/ui.js +166 -28
  81. package/dist/components/ui.js.map +1 -1
  82. package/dist/feedback/feedback-attachment.js +4 -6
  83. package/dist/feedback/feedback-attachment.js.map +1 -1
  84. package/dist/feedback/feedback-dialog.js +5 -3
  85. package/dist/feedback/feedback-dialog.js.map +1 -1
  86. package/dist/feedback/feedback-inbox.js +86 -75
  87. package/dist/feedback/feedback-inbox.js.map +1 -1
  88. package/dist/feedback/feedback-thread.d.ts +43 -3
  89. package/dist/feedback/feedback-thread.js +53 -26
  90. package/dist/feedback/feedback-thread.js.map +1 -1
  91. package/dist/feedback.d.ts +1 -1
  92. package/dist/hooks/use-file-drop.d.ts +1 -1
  93. package/dist/i18n/defaults.d.ts +1 -1
  94. package/dist/i18n/kit-labels.d.ts +1 -1
  95. package/dist/i18n/kit-labels.js.map +1 -1
  96. package/dist/i18n/locales/de-CH-informal.d.ts +1 -1
  97. package/dist/i18n/locales/de-CH.d.ts +1 -1
  98. package/dist/i18n/locales/de-informal.d.ts +1 -1
  99. package/dist/i18n/locales/de.d.ts +1 -1
  100. package/dist/i18n/locales/de.js +9 -1
  101. package/dist/i18n/locales/de.js.map +1 -1
  102. package/dist/i18n/locales/es.d.ts +1 -1
  103. package/dist/i18n/locales/es.js +9 -1
  104. package/dist/i18n/locales/es.js.map +1 -1
  105. package/dist/i18n/locales/fr.d.ts +1 -1
  106. package/dist/i18n/locales/fr.js +9 -1
  107. package/dist/i18n/locales/fr.js.map +1 -1
  108. package/dist/i18n/locales/hu.d.ts +1 -1
  109. package/dist/i18n/locales/hu.js +9 -1
  110. package/dist/i18n/locales/hu.js.map +1 -1
  111. package/dist/i18n/locales/it.d.ts +1 -1
  112. package/dist/i18n/locales/it.js +9 -1
  113. package/dist/i18n/locales/it.js.map +1 -1
  114. package/dist/i18n/locales/zh.d.ts +1 -1
  115. package/dist/i18n/locales/zh.js +9 -1
  116. package/dist/i18n/locales/zh.js.map +1 -1
  117. package/dist/index.d.ts +6 -6
  118. package/dist/{kit-labels-v3biUF1L.d.ts → kit-labels-Mz8zZHIF.d.ts} +102 -3
  119. package/dist/lib/format.d.ts +23 -5
  120. package/dist/lib/format.js +4 -2
  121. package/dist/lib/format.js.map +1 -1
  122. package/dist/rhf/fields.d.ts +59 -15
  123. package/dist/rhf/fields.js +41 -11
  124. package/dist/rhf/fields.js.map +1 -1
  125. package/dist/rhf/form.d.ts +1 -1
  126. package/dist/rhf.d.ts +2 -2
  127. package/dist/rhf.js.map +1 -1
  128. package/dist/search/command-palette.js +20 -14
  129. package/dist/search/command-palette.js.map +1 -1
  130. package/dist/shell/app-shell.d.ts +1 -1
  131. package/dist/shell/app-shell.js +62 -54
  132. package/dist/shell/app-shell.js.map +1 -1
  133. package/dist/shell/top-bar-brand.d.ts +1 -1
  134. package/dist/shell/topbar-action-menu.d.ts +9 -3
  135. package/dist/shell/topbar-action-menu.js +2 -1
  136. package/dist/shell/topbar-action-menu.js.map +1 -1
  137. package/dist/shell.d.ts +1 -1
  138. package/dist/tour/tour.js +10 -4
  139. package/dist/tour/tour.js.map +1 -1
  140. package/dist/wizard/stepper-nav.d.ts +1 -1
  141. package/dist/wizard/use-wizard.js +4 -2
  142. package/dist/wizard/use-wizard.js.map +1 -1
  143. package/dist/wizard/wizard-step.d.ts +11 -1
  144. package/dist/wizard/wizard-step.js +24 -6
  145. package/dist/wizard/wizard-step.js.map +1 -1
  146. package/dist/wizard.d.ts +1 -1
  147. package/package.json +6 -6
  148. package/src/components/amount-input.tsx +165 -12
  149. package/src/components/bulk-action-bar.tsx +16 -3
  150. package/src/components/chart.tsx +19 -8
  151. package/src/components/chip.tsx +6 -0
  152. package/src/components/combobox-core.tsx +28 -9
  153. package/src/components/combobox.tsx +2 -0
  154. package/src/components/data-table-filter-popover.tsx +13 -5
  155. package/src/components/data-table.tsx +6 -0
  156. package/src/components/entity-combobox.tsx +7 -4
  157. package/src/components/error-boundary.tsx +470 -30
  158. package/src/components/field-sync.tsx +48 -40
  159. package/src/components/full-bleed-dialog.tsx +4 -0
  160. package/src/components/hover-menu.tsx +26 -17
  161. package/src/components/list.tsx +17 -3
  162. package/src/components/modal.tsx +4 -0
  163. package/src/components/multi-entity-combobox.tsx +8 -4
  164. package/src/components/number-input.tsx +1 -0
  165. package/src/components/numpad-sheet.tsx +17 -3
  166. package/src/components/page-header.tsx +42 -5
  167. package/src/components/picker-sheet.tsx +3 -0
  168. package/src/components/popover.tsx +3 -0
  169. package/src/components/signed-amount.tsx +12 -3
  170. package/src/components/stat-tile.tsx +3 -1
  171. package/src/components/swipeable-row.tsx +7 -7
  172. package/src/components/text-link.tsx +126 -18
  173. package/src/components/text.tsx +47 -2
  174. package/src/components/toggle-group.tsx +35 -10
  175. package/src/components/toggle-legend.tsx +22 -1
  176. package/src/components/tooltip.tsx +79 -8
  177. package/src/components/ui.tsx +307 -39
  178. package/src/feedback/feedback-attachment.tsx +10 -7
  179. package/src/feedback/feedback-dialog.tsx +7 -4
  180. package/src/feedback/feedback-inbox.tsx +7 -0
  181. package/src/feedback/feedback-thread.tsx +109 -33
  182. package/src/i18n/kit-labels.tsx +16 -0
  183. package/src/i18n/locales/de.ts +8 -0
  184. package/src/i18n/locales/es.ts +8 -0
  185. package/src/i18n/locales/fr.ts +8 -0
  186. package/src/i18n/locales/hu.ts +8 -0
  187. package/src/i18n/locales/it.ts +8 -0
  188. package/src/i18n/locales/zh.ts +8 -0
  189. package/src/lib/format.ts +32 -7
  190. package/src/rhf/fields.tsx +99 -16
  191. package/src/rhf.ts +1 -1
  192. package/src/search/command-palette.tsx +29 -19
  193. package/src/shell/app-shell.tsx +6 -1
  194. package/src/shell/topbar-action-menu.tsx +11 -4
  195. package/src/tour/tour.tsx +16 -5
  196. package/src/wizard/use-wizard.ts +8 -4
  197. package/src/wizard/wizard-step.tsx +38 -6
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { createContext, useContext, useId, useRef } from "react";
3
+ import { createContext, useContext, useId, useLayoutEffect, useRef, useState } from "react";
4
4
  import {
5
5
  Legend as RechartsLegend,
6
6
  ResponsiveContainer,
@@ -99,15 +99,21 @@ function ChartTooltipContent({
99
99
  const config = useChart();
100
100
  const tipRef = useRef(null);
101
101
  const items = includeHidden ? payload : payload?.filter((item) => item.hide !== true);
102
- if (!active || !items?.length) return null;
103
- let flip = false;
104
- const boundary = boundaryRef?.current;
105
- if (boundary && coordinate?.x != null) {
102
+ const visible = Boolean(active && items?.length);
103
+ const [flip, setFlip] = useState(false);
104
+ const x = coordinate?.x;
105
+ useLayoutEffect(() => {
106
+ const boundary = boundaryRef?.current;
107
+ if (!visible || !boundary || x == null) {
108
+ setFlip(false);
109
+ return;
110
+ }
106
111
  const offset = isRtl(boundary) ? boundary.scrollWidth - boundary.clientWidth + boundary.scrollLeft : boundary.scrollLeft;
107
- const visibleX = coordinate.x - offset;
112
+ const visibleX = x - offset;
108
113
  const tipWidth = tipRef.current?.offsetWidth ?? 160;
109
- flip = visibleX + tipWidth + 12 > boundary.clientWidth;
110
- }
114
+ setFlip(visibleX + tipWidth + 12 > boundary.clientWidth);
115
+ }, [visible, x, boundaryRef, payload, label]);
116
+ if (!visible || !items) return null;
111
117
  return /* @__PURE__ */ jsxs(
112
118
  "div",
113
119
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/chart.tsx"],"sourcesContent":["// shadcn/ui chart kit — vendored (MIT) and adapted for this app:\n// - Recharts 3 instead of 2\n// - the original keys its styling off shadcn theme tokens (--muted-foreground,\n// --border, bg-background …). This app has no shadcn token layer, so those\n// are mapped onto this kit's own tokens (--text-muted, --border, --bg-surface\n// …), which follow the active palette preset and flip with the theme by\n// themselves — hence no `dark:` variants anywhere below.\n// The value on show is the *structure*: config-driven colors injected as CSS\n// vars, muted axis/grid styling, and a polished tooltip/legend — the things\n// that make shadcn charts read as \"designed\" rather than \"default Recharts\".\nimport { createContext, useContext, useId, useRef } from \"react\";\nimport type { ComponentProps, CSSProperties, ReactNode, RefObject } from \"react\";\nimport {\n Legend as RechartsLegend,\n ResponsiveContainer,\n Tooltip as RechartsTooltip,\n} from \"recharts\";\nimport { cn } from \"../lib/cn\";\nimport { isRtl } from \"../lib/direction\";\n\nexport interface ChartSeriesConfig {\n label: ReactNode;\n color: string;\n}\n\nexport type ChartConfig = Record<string, ChartSeriesConfig>;\n\nconst ChartContext = createContext<ChartConfig | null>(null);\n\nexport function useChart(): ChartConfig {\n const ctx = useContext(ChartContext);\n if (!ctx) throw new Error(\"useChart must be used within a <ChartContainer />\");\n return ctx;\n}\n\ninterface ChartContainerProps extends ComponentProps<\"div\"> {\n config: ChartConfig;\n children: ComponentProps<typeof ResponsiveContainer>[\"children\"];\n}\n\n/**\n * The shell every chart in the kit is drawn inside.\n *\n * **The plot is not mirrored in RTL.** An x axis is a number line or a timeline, and\n * both run left-to-right in Hebrew and Arabic charts as much as in English ones; a\n * time axis flipped to run right-to-left is the surprise, not the courtesy. So no\n * `reversed` on any axis, and the SVG is pinned to `direction: ltr` — without that it\n * inherits the page's `rtl`, which flips what `text-anchor: end` means, and the tick\n * labels of a left-hand y axis are drawn INTO the plot instead of beside it. Text\n * inside the SVG still shapes by its own script; only the anchoring is physical.\n * The HTML around the plot — legend, tooltip, the zoom reset button — follows the\n * page's direction like any other markup. A consumer who does want a mirrored\n * category axis passes `reversed` on its own `XAxis`.\n */\nexport function ChartContainer({ id, className, children, config, ...props }: ChartContainerProps) {\n const uid = useId();\n // The id is pasted into an UNQUOTED `[data-chart=…]` selector below, so it has to be\n // a CSS identifier: one stray space or `#` and the browser discards the whole rule,\n // taking every series colour with it — silently, because an invalid rule is not an\n // error anywhere. `useId()` gives `:r0:`, and a consumer-supplied id gives whatever\n // they typed, so both are filtered rather than just the colons.\n const chartId = `chart-${(id ?? uid).replace(/[^A-Za-z0-9_-]/g, \"\")}`;\n return (\n <ChartContext.Provider value={config}>\n <div\n data-chart={chartId}\n className={cn(\n // touch-pan-y lets a vertical finger drag still scroll the page while\n // a horizontal drag is handed to recharts for tooltip scrubbing,\n // instead of the page scrolling underneath the gesture (feedback #292).\n \"flex w-full touch-pan-y justify-center text-xs\",\n // physical anchoring for the unmirrored plot (see the doc comment above)\n \"[&_.recharts-surface]:[direction:ltr]\",\n // muted axis labels + gridlines; one class each, since the tokens\n // already carry the light/dark values\n \"[&_.recharts-cartesian-axis-tick_text]:fill-[var(--text-muted)]\",\n \"[&_.recharts-cartesian-grid_line]:stroke-[var(--border)]/70\",\n // hairline hover cursor instead of the heavy default\n \"[&_.recharts-curve.recharts-tooltip-cursor]:stroke-[var(--border-strong)]\",\n \"[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-[var(--bg-hover)]\",\n // kill the focus outlines & white sector strokes Recharts adds\n \"[&_.recharts-sector]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent\",\n \"[&_.recharts-layer]:outline-none [&_.recharts-surface]:outline-none [&_.recharts-dot[stroke='#fff']]:stroke-transparent\",\n className,\n )}\n {...props}\n >\n <ChartStyle id={chartId} config={config} />\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n {children}\n </ResponsiveContainer>\n </div>\n </ChartContext.Provider>\n );\n}\n\n/**\n * A config key, which becomes the tail of a custom-property NAME.\n *\n * `dangerouslySetInnerHTML` means what it says: nothing below is escaped by React, and\n * a `ChartConfig` is not necessarily written by hand — kastlan builds one from category\n * rows in its database, so these keys are user data. A key containing `}` closes this\n * rule, and whatever follows it in that key becomes a NEW rule — one that applies to\n * the whole document rather than to this one chart.\n */\nconst CSS_IDENT = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\n/**\n * A config colour, which becomes a declaration VALUE.\n *\n * Every form a palette actually produces: both hex spellings, the functional notations\n * — with the argument list restricted to the characters those functions can legally\n * contain, so no `;`, no `}` and no nested `url(` — and `var(--token)`, which is what\n * this kit's own `paletteFor`/`CHART_COLORS` return. `var()`'s optional FALLBACK is\n * deliberately not accepted: its second argument is an arbitrary value, i.e. the same\n * hole in a shape that looks safe.\n */\nconst CSS_COLOR =\n /^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|(?:rgba?|hsla?)\\([0-9A-Za-z.,%/ -]*\\)|var\\(--[A-Za-z0-9_-]+\\))$/;\n\n/**\n * The colour an entry is painted with, or `undefined` when the style block refuses it.\n *\n * The one gate both sides go through: {@link ChartStyle} emits exactly these, and the\n * legend and tooltip swatches show exactly these. They used to read `config[key].color`\n * directly, so an entry the style block had skipped still showed its configured colour\n * in the key — a swatch promising a colour the series was never painted in.\n */\nfunction paintedColor(key: string, entry: ChartSeriesConfig | undefined): string | undefined {\n if (!entry?.color) return undefined;\n return CSS_IDENT.test(key) && CSS_COLOR.test(entry.color) ? entry.color : undefined;\n}\n\n// Inject each series colour as a `--color-<key>` custom property scoped to this\n// chart, so chart JSX can refer to `var(--color-total)` and the legend/tooltip\n// stay in sync from one source of truth.\nfunction ChartStyle({ id, config }: { id: string; config: ChartConfig }) {\n const entries = Object.entries(config).filter(([key, c]) => {\n if (!c.color) return false;\n // Skipping the entry costs that one series its `var(--color-…)` and leaves the rest\n // of the chart painted; emitting it unchecked costs the page. The warning is so the\n // consumer finds out from their console rather than from a colourless bar.\n if (!CSS_IDENT.test(key)) {\n console.warn(`[ui-kit] chart config key is not a CSS identifier, skipped: ${key}`);\n return false;\n }\n if (!CSS_COLOR.test(c.color)) {\n console.warn(`[ui-kit] chart config colour for \"${key}\" is not a colour, skipped: ${c.color}`);\n return false;\n }\n return true;\n });\n if (!entries.length) return null;\n const css = `[data-chart=${id}] {\\n${entries\n .map(([key, c]) => ` --color-${key}: ${c.color};`)\n .join(\"\\n\")}\\n}`;\n return <style dangerouslySetInnerHTML={{ __html: css }} />;\n}\n\n/**\n * recharts' `Tooltip`, with `filterNull` defaulting to **false**.\n *\n * recharts drops every payload entry whose value is `null`/`undefined` before the\n * content sees it, so the missing-value path {@link ChartTooltipContent} documents —\n * the em dash, `formatValue(undefined)` — never ran under the default: a series with a\n * gap at the cursor simply vanished from the tooltip, which reads as \"this series does\n * not exist here\" rather than \"it has no value here\". Kept, the entry prints \"—\".\n *\n * The other half of that filter — dropping series drawn with `hide` — is redone by\n * `ChartTooltipContent` itself (honouring `includeHidden`), so a legend toggle still\n * takes a series out of the tooltip. Pass `filterNull` to get recharts' behaviour back.\n */\nexport function ChartTooltip(props: ComponentProps<typeof RechartsTooltip>) {\n return <RechartsTooltip filterNull={false} {...props} />;\n}\nexport const ChartLegend = RechartsLegend;\n\ninterface TooltipPayloadItem {\n dataKey?: string | number;\n name?: string | number;\n value?: number | string;\n color?: string;\n /** A series drawn with `hide` — dropped here, see {@link ChartTooltip}. */\n hide?: boolean;\n payload?: Record<string, unknown>;\n}\n\n/**\n * What a series has no value for at this point: a gap in a history, a projection that\n * has not happened yet, a field that did not exist at an earlier capture.\n *\n * An em dash rather than an empty cell, so the row still lines up with its siblings and\n * the reader can see that the series WAS asked and had nothing, rather than wondering\n * whether it was dropped. It is a mark, not a word, so there is nothing here to\n * translate and no label prop to resolve; a caller who wants words passes\n * `formatValue`, which is handed the `undefined` and decides.\n */\nconst NO_VALUE = \"—\";\n\n/**\n * The number this payload item carries, or `undefined` when it carries none.\n *\n * This was `item.value ?? 0`, so every one of the absences above printed a confident\n * \"0\" that appears nowhere in the data — a fabricated figure in a tooltip, which is the\n * one place a reader goes to check an exact number. keksdose found it and wrapped the\n * component rather than trust it. `\"\"` and a non-numeric string are the same absence\n * spelled differently (recharts hands a string value straight through from the data),\n * and are answered the same way.\n */\nfunction tooltipValue(value: number | string | undefined): number | undefined {\n if (value == null || value === \"\") return undefined;\n const n = typeof value === \"string\" ? Number(value) : value;\n return Number.isFinite(n) ? n : undefined;\n}\n\ninterface ChartTooltipContentProps {\n active?: boolean;\n payload?: TooltipPayloadItem[];\n label?: string | number;\n indicator?: \"dot\" | \"line\";\n hideLabel?: boolean;\n labelFormatter?: (label: string | number) => ReactNode;\n /** Formats one series' value for display. NOT called when the series has no value at\n * this point — {@link ChartTooltipContentProps.formatValue} is the one that is. */\n valueFormatter?: (value: number) => ReactNode;\n /** Formats one series' value INCLUDING its absence: handed `undefined` for a gap, a\n * projection that has not happened yet, or a field that did not exist at an earlier\n * capture, and whatever it returns is rendered as-is. Only the caller knows whether\n * that absence reads as a dash, a \"not yet\" or a genuine zero, so this is how it\n * says. Supplying it wins over `valueFormatter` for every value.\n *\n * It sits NEXT to `valueFormatter` rather than widening it because that prop's\n * parameter is contextually typed at every call site there is — `(v) => money(v)` —\n * so widening it to `number | undefined` is a compile error in all three consuming\n * apps, which is a breaking change this module does not get to make by itself. */\n formatValue?: (value: number | undefined) => ReactNode;\n /** Cursor position within the chart, injected by recharts. Used with\n * {@link boundaryRef} to decide whether to flip the tooltip. */\n coordinate?: { x?: number; y?: number };\n /** When set, the tooltip flips to the LEFT of the cursor if it would otherwise\n * spill past the right edge of this (usually horizontally-scrolling) container\n * — e.g. a wide chart whose rightmost bars sat off-screen (feedback #77). */\n boundaryRef?: RefObject<HTMLElement | null>;\n /** Injected by recharts: keep series drawn with `hide` in the tooltip. */\n includeHidden?: boolean;\n}\n\nexport function ChartTooltipContent({\n active,\n payload,\n label,\n indicator = \"dot\",\n hideLabel = false,\n labelFormatter,\n valueFormatter,\n formatValue,\n coordinate,\n boundaryRef,\n includeHidden = false,\n}: ChartTooltipContentProps) {\n const config = useChart();\n const tipRef = useRef<HTMLDivElement>(null);\n // `ChartTooltip` keeps null values (so they can print \"—\"), which also keeps the\n // series a legend toggle switched off with `hide`; those go here instead.\n const items = includeHidden ? payload : payload?.filter((item) => item.hide !== true);\n if (!active || !items?.length) return null;\n // Recharts anchors the tooltip at coordinate.x inside the full (scrolled) chart\n // width; subtract the container's scrollLeft to get its on-screen x, then flip\n // left if the tooltip's own width would run past the visible right edge.\n //\n // Both the cursor and the flip are PHYSICAL (the plot is never mirrored, see\n // `ChartContainer`), but an RTL scroller counts `scrollLeft` from its right edge —\n // 0 at the start, negative going left — so there the content's left edge sits\n // `scrollWidth - clientWidth + scrollLeft` px to the left of the visible one.\n let flip = false;\n const boundary = boundaryRef?.current;\n if (boundary && coordinate?.x != null) {\n const offset = isRtl(boundary)\n ? boundary.scrollWidth - boundary.clientWidth + boundary.scrollLeft\n : boundary.scrollLeft;\n const visibleX = coordinate.x - offset;\n const tipWidth = tipRef.current?.offsetWidth ?? 160;\n flip = visibleX + tipWidth + 12 > boundary.clientWidth;\n }\n return (\n <div\n ref={tipRef}\n style={boundaryRef ? { transform: flip ? \"translateX(calc(-100% - 12px))\" : \"translateX(12px)\" } : undefined}\n className=\"min-w-[9rem] rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] px-2.5 py-2 text-xs shadow-xl\"\n >\n {!hideLabel && label != null && (\n <div className=\"mb-1.5 font-medium text-[var(--text-primary)]\">\n {labelFormatter ? labelFormatter(label) : label}\n </div>\n )}\n <div className=\"grid gap-1.5\">\n {items.map((item, i) => {\n // Bars/lines/areas identify a series by dataKey; pies/treemaps key off\n // `name` (the nameKey value). Prefer whichever the config knows.\n const dk = item.dataKey != null ? String(item.dataKey) : undefined;\n const nm = item.name != null ? String(item.name) : undefined;\n const cfgKey = dk && config[dk] ? dk : nm && config[nm] ? nm : (dk ?? nm ?? String(i));\n const series = config[cfgKey];\n const fill = typeof item.payload?.fill === \"string\" ? item.payload.fill : undefined;\n // The data point's own `fill` before recharts' `item.color`: a treemap tile,\n // or a bar with a per-cell colour, IS the colour the reader is pointing at,\n // while `item.color` is the series-wide one — every tile's swatch came out\n // `--chart-1` whatever colour the tile was.\n const color = paintedColor(cfgKey, series) ?? fill ?? item.color ?? \"#64748b\";\n const name = series?.label ?? nm ?? cfgKey;\n const raw = tooltipValue(item.value);\n let shown: ReactNode;\n if (formatValue) shown = formatValue(raw);\n else if (raw == null) shown = NO_VALUE;\n else shown = valueFormatter ? valueFormatter(raw) : raw;\n return (\n <div key={`${cfgKey}-${i}`} className=\"flex items-center gap-2\">\n <span\n className={cn(\n \"shrink-0 rounded-[2px]\",\n indicator === \"dot\" ? \"h-2.5 w-2.5 rounded-full\" : \"h-2.5 w-1\",\n )}\n style={{ backgroundColor: color }}\n />\n <span className=\"text-[var(--text-muted)]\">{name}</span>\n <span data-private className=\"ms-auto font-mono font-medium tabular-nums text-[var(--text-primary)]\">\n {shown}\n </span>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n\ninterface LegendPayloadItem {\n value?: string;\n dataKey?: string | number;\n color?: string;\n}\n\ninterface ChartLegendContentProps {\n payload?: LegendPayloadItem[];\n onItemClick?: (key: string) => void;\n /**\n * TOGGLE mode (recommended): the series currently switched off. Each entry is a\n * toggle button — pressed while its series is shown — and a hidden one is drawn\n * faded with its label struck through. Several can be off at once; pair with\n * `toggleHidden(hidden, key)` in `onItemClick` and `hide` on the chart's series.\n */\n hiddenKeys?: ReadonlySet<string> | readonly string[];\n /**\n * ISOLATE mode: the one series shown in full, every other entry dimmed. Kept for\n * the charts built on it; a legend where each entry switches its own series is\n * what readers expect from a legend, so prefer `hiddenKeys`.\n */\n activeKey?: string | null;\n}\n\nexport function ChartLegendContent({\n payload,\n onItemClick,\n hiddenKeys,\n activeKey,\n}: ChartLegendContentProps) {\n const hidden = hiddenKeys ? new Set(hiddenKeys) : null;\n const config = useChart();\n if (!payload?.length) return null;\n return (\n <div className=\"flex flex-wrap items-center justify-center gap-x-4 gap-y-1 pt-3\">\n {payload.map((item, i) => {\n const dk = item.dataKey != null ? String(item.dataKey) : undefined;\n const vv = item.value != null ? String(item.value) : undefined;\n const cfgKey = dk && config[dk] ? dk : vv && config[vv] ? vv : (dk ?? vv ?? String(i));\n const series = config[cfgKey];\n const color = paintedColor(cfgKey, series) ?? item.color ?? \"#64748b\";\n const label = series?.label ?? vv ?? cfgKey;\n const off = hidden?.has(cfgKey) ?? false;\n const dimmed = off || (activeKey != null && activeKey !== cfgKey);\n const className = cn(\n \"flex items-center gap-1.5 text-[var(--text-secondary)] transition-opacity\",\n onItemClick ? \"cursor-pointer hover:opacity-80\" : \"cursor-default\",\n dimmed && \"opacity-40\",\n );\n const swatch = (\n <span\n aria-hidden\n className=\"h-2.5 w-2.5 shrink-0 rounded-[3px]\"\n style={{ backgroundColor: color } as CSSProperties}\n />\n );\n // A legend with no `onItemClick` is a KEY, not a control — and that is every\n // legend in kastlan, where this rendered N focusable buttons per chart that did\n // nothing when activated. A keyboard user paid the tab stops for all of them,\n // and a screen reader announced each one as a button, promising an action there\n // was none of. The interactive branch is unchanged.\n return onItemClick ? (\n <button\n key={`${cfgKey}-${i}`}\n type=\"button\"\n onClick={() => onItemClick(cfgKey)}\n // Toggles report their state: in toggle mode pressed = the series is shown;\n // in isolate mode pressed = the isolated one. With neither supplied the\n // legend knows nothing about state, and a pressed=\"false\" it cannot vouch\n // for would lie — so it says nothing.\n aria-pressed={\n hidden ? !off : activeKey === undefined ? undefined : activeKey === cfgKey\n }\n className={className}\n >\n {swatch}\n <span className={cn(off && \"line-through\")}>{label}</span>\n </button>\n ) : (\n <span key={`${cfgKey}-${i}`} className={className}>\n {swatch}\n {label}\n </span>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/chart.tsx"],"sourcesContent":["// shadcn/ui chart kit — vendored (MIT) and adapted for this app:\n// - Recharts 3 instead of 2\n// - the original keys its styling off shadcn theme tokens (--muted-foreground,\n// --border, bg-background …). This app has no shadcn token layer, so those\n// are mapped onto this kit's own tokens (--text-muted, --border, --bg-surface\n// …), which follow the active palette preset and flip with the theme by\n// themselves — hence no `dark:` variants anywhere below.\n// The value on show is the *structure*: config-driven colors injected as CSS\n// vars, muted axis/grid styling, and a polished tooltip/legend — the things\n// that make shadcn charts read as \"designed\" rather than \"default Recharts\".\nimport { createContext, useContext, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentProps, CSSProperties, ReactNode, RefObject } from \"react\";\nimport {\n Legend as RechartsLegend,\n ResponsiveContainer,\n Tooltip as RechartsTooltip,\n} from \"recharts\";\nimport { cn } from \"../lib/cn\";\nimport { isRtl } from \"../lib/direction\";\n\nexport interface ChartSeriesConfig {\n label: ReactNode;\n color: string;\n}\n\nexport type ChartConfig = Record<string, ChartSeriesConfig>;\n\nconst ChartContext = createContext<ChartConfig | null>(null);\n\nexport function useChart(): ChartConfig {\n const ctx = useContext(ChartContext);\n if (!ctx) throw new Error(\"useChart must be used within a <ChartContainer />\");\n return ctx;\n}\n\ninterface ChartContainerProps extends ComponentProps<\"div\"> {\n config: ChartConfig;\n children: ComponentProps<typeof ResponsiveContainer>[\"children\"];\n}\n\n/**\n * The shell every chart in the kit is drawn inside.\n *\n * **The plot is not mirrored in RTL.** An x axis is a number line or a timeline, and\n * both run left-to-right in Hebrew and Arabic charts as much as in English ones; a\n * time axis flipped to run right-to-left is the surprise, not the courtesy. So no\n * `reversed` on any axis, and the SVG is pinned to `direction: ltr` — without that it\n * inherits the page's `rtl`, which flips what `text-anchor: end` means, and the tick\n * labels of a left-hand y axis are drawn INTO the plot instead of beside it. Text\n * inside the SVG still shapes by its own script; only the anchoring is physical.\n * The HTML around the plot — legend, tooltip, the zoom reset button — follows the\n * page's direction like any other markup. A consumer who does want a mirrored\n * category axis passes `reversed` on its own `XAxis`.\n */\nexport function ChartContainer({ id, className, children, config, ...props }: ChartContainerProps) {\n const uid = useId();\n // The id is pasted into an UNQUOTED `[data-chart=…]` selector below, so it has to be\n // a CSS identifier: one stray space or `#` and the browser discards the whole rule,\n // taking every series colour with it — silently, because an invalid rule is not an\n // error anywhere. `useId()` gives `:r0:`, and a consumer-supplied id gives whatever\n // they typed, so both are filtered rather than just the colons.\n const chartId = `chart-${(id ?? uid).replace(/[^A-Za-z0-9_-]/g, \"\")}`;\n return (\n <ChartContext.Provider value={config}>\n <div\n data-chart={chartId}\n className={cn(\n // touch-pan-y lets a vertical finger drag still scroll the page while\n // a horizontal drag is handed to recharts for tooltip scrubbing,\n // instead of the page scrolling underneath the gesture (feedback #292).\n \"flex w-full touch-pan-y justify-center text-xs\",\n // physical anchoring for the unmirrored plot (see the doc comment above)\n \"[&_.recharts-surface]:[direction:ltr]\",\n // muted axis labels + gridlines; one class each, since the tokens\n // already carry the light/dark values\n \"[&_.recharts-cartesian-axis-tick_text]:fill-[var(--text-muted)]\",\n \"[&_.recharts-cartesian-grid_line]:stroke-[var(--border)]/70\",\n // hairline hover cursor instead of the heavy default\n \"[&_.recharts-curve.recharts-tooltip-cursor]:stroke-[var(--border-strong)]\",\n \"[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-[var(--bg-hover)]\",\n // kill the focus outlines & white sector strokes Recharts adds\n \"[&_.recharts-sector]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent\",\n \"[&_.recharts-layer]:outline-none [&_.recharts-surface]:outline-none [&_.recharts-dot[stroke='#fff']]:stroke-transparent\",\n className,\n )}\n {...props}\n >\n <ChartStyle id={chartId} config={config} />\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n {children}\n </ResponsiveContainer>\n </div>\n </ChartContext.Provider>\n );\n}\n\n/**\n * A config key, which becomes the tail of a custom-property NAME.\n *\n * `dangerouslySetInnerHTML` means what it says: nothing below is escaped by React, and\n * a `ChartConfig` is not necessarily written by hand — kastlan builds one from category\n * rows in its database, so these keys are user data. A key containing `}` closes this\n * rule, and whatever follows it in that key becomes a NEW rule — one that applies to\n * the whole document rather than to this one chart.\n */\nconst CSS_IDENT = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\n/**\n * A config colour, which becomes a declaration VALUE.\n *\n * Every form a palette actually produces: both hex spellings, the functional notations\n * — with the argument list restricted to the characters those functions can legally\n * contain, so no `;`, no `}` and no nested `url(` — and `var(--token)`, which is what\n * this kit's own `paletteFor`/`CHART_COLORS` return. `var()`'s optional FALLBACK is\n * deliberately not accepted: its second argument is an arbitrary value, i.e. the same\n * hole in a shape that looks safe.\n */\nconst CSS_COLOR =\n /^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|(?:rgba?|hsla?)\\([0-9A-Za-z.,%/ -]*\\)|var\\(--[A-Za-z0-9_-]+\\))$/;\n\n/**\n * The colour an entry is painted with, or `undefined` when the style block refuses it.\n *\n * The one gate both sides go through: {@link ChartStyle} emits exactly these, and the\n * legend and tooltip swatches show exactly these. They used to read `config[key].color`\n * directly, so an entry the style block had skipped still showed its configured colour\n * in the key — a swatch promising a colour the series was never painted in.\n */\nfunction paintedColor(key: string, entry: ChartSeriesConfig | undefined): string | undefined {\n if (!entry?.color) return undefined;\n return CSS_IDENT.test(key) && CSS_COLOR.test(entry.color) ? entry.color : undefined;\n}\n\n// Inject each series colour as a `--color-<key>` custom property scoped to this\n// chart, so chart JSX can refer to `var(--color-total)` and the legend/tooltip\n// stay in sync from one source of truth.\nfunction ChartStyle({ id, config }: { id: string; config: ChartConfig }) {\n const entries = Object.entries(config).filter(([key, c]) => {\n if (!c.color) return false;\n // Skipping the entry costs that one series its `var(--color-…)` and leaves the rest\n // of the chart painted; emitting it unchecked costs the page. The warning is so the\n // consumer finds out from their console rather than from a colourless bar.\n if (!CSS_IDENT.test(key)) {\n console.warn(`[ui-kit] chart config key is not a CSS identifier, skipped: ${key}`);\n return false;\n }\n if (!CSS_COLOR.test(c.color)) {\n console.warn(`[ui-kit] chart config colour for \"${key}\" is not a colour, skipped: ${c.color}`);\n return false;\n }\n return true;\n });\n if (!entries.length) return null;\n const css = `[data-chart=${id}] {\\n${entries\n .map(([key, c]) => ` --color-${key}: ${c.color};`)\n .join(\"\\n\")}\\n}`;\n return <style dangerouslySetInnerHTML={{ __html: css }} />;\n}\n\n/**\n * recharts' `Tooltip`, with `filterNull` defaulting to **false**.\n *\n * recharts drops every payload entry whose value is `null`/`undefined` before the\n * content sees it, so the missing-value path {@link ChartTooltipContent} documents —\n * the em dash, `formatValue(undefined)` — never ran under the default: a series with a\n * gap at the cursor simply vanished from the tooltip, which reads as \"this series does\n * not exist here\" rather than \"it has no value here\". Kept, the entry prints \"—\".\n *\n * The other half of that filter — dropping series drawn with `hide` — is redone by\n * `ChartTooltipContent` itself (honouring `includeHidden`), so a legend toggle still\n * takes a series out of the tooltip. Pass `filterNull` to get recharts' behaviour back.\n */\nexport function ChartTooltip(props: ComponentProps<typeof RechartsTooltip>) {\n return <RechartsTooltip filterNull={false} {...props} />;\n}\nexport const ChartLegend = RechartsLegend;\n\ninterface TooltipPayloadItem {\n dataKey?: string | number;\n name?: string | number;\n value?: number | string;\n color?: string;\n /** A series drawn with `hide` — dropped here, see {@link ChartTooltip}. */\n hide?: boolean;\n payload?: Record<string, unknown>;\n}\n\n/**\n * What a series has no value for at this point: a gap in a history, a projection that\n * has not happened yet, a field that did not exist at an earlier capture.\n *\n * An em dash rather than an empty cell, so the row still lines up with its siblings and\n * the reader can see that the series WAS asked and had nothing, rather than wondering\n * whether it was dropped. It is a mark, not a word, so there is nothing here to\n * translate and no label prop to resolve; a caller who wants words passes\n * `formatValue`, which is handed the `undefined` and decides.\n */\nconst NO_VALUE = \"—\";\n\n/**\n * The number this payload item carries, or `undefined` when it carries none.\n *\n * This was `item.value ?? 0`, so every one of the absences above printed a confident\n * \"0\" that appears nowhere in the data — a fabricated figure in a tooltip, which is the\n * one place a reader goes to check an exact number. keksdose found it and wrapped the\n * component rather than trust it. `\"\"` and a non-numeric string are the same absence\n * spelled differently (recharts hands a string value straight through from the data),\n * and are answered the same way.\n */\nfunction tooltipValue(value: number | string | undefined): number | undefined {\n if (value == null || value === \"\") return undefined;\n const n = typeof value === \"string\" ? Number(value) : value;\n return Number.isFinite(n) ? n : undefined;\n}\n\ninterface ChartTooltipContentProps {\n active?: boolean;\n payload?: TooltipPayloadItem[];\n label?: string | number;\n indicator?: \"dot\" | \"line\";\n hideLabel?: boolean;\n labelFormatter?: (label: string | number) => ReactNode;\n /** Formats one series' value for display. NOT called when the series has no value at\n * this point — {@link ChartTooltipContentProps.formatValue} is the one that is. */\n valueFormatter?: (value: number) => ReactNode;\n /** Formats one series' value INCLUDING its absence: handed `undefined` for a gap, a\n * projection that has not happened yet, or a field that did not exist at an earlier\n * capture, and whatever it returns is rendered as-is. Only the caller knows whether\n * that absence reads as a dash, a \"not yet\" or a genuine zero, so this is how it\n * says. Supplying it wins over `valueFormatter` for every value.\n *\n * It sits NEXT to `valueFormatter` rather than widening it because that prop's\n * parameter is contextually typed at every call site there is — `(v) => money(v)` —\n * so widening it to `number | undefined` is a compile error in all three consuming\n * apps, which is a breaking change this module does not get to make by itself. */\n formatValue?: (value: number | undefined) => ReactNode;\n /** Cursor position within the chart, injected by recharts. Used with\n * {@link boundaryRef} to decide whether to flip the tooltip. */\n coordinate?: { x?: number; y?: number };\n /** When set, the tooltip flips to the LEFT of the cursor if it would otherwise\n * spill past the right edge of this (usually horizontally-scrolling) container\n * — e.g. a wide chart whose rightmost bars sat off-screen (feedback #77). */\n boundaryRef?: RefObject<HTMLElement | null>;\n /** Injected by recharts: keep series drawn with `hide` in the tooltip. */\n includeHidden?: boolean;\n}\n\nexport function ChartTooltipContent({\n active,\n payload,\n label,\n indicator = \"dot\",\n hideLabel = false,\n labelFormatter,\n valueFormatter,\n formatValue,\n coordinate,\n boundaryRef,\n includeHidden = false,\n}: ChartTooltipContentProps) {\n const config = useChart();\n const tipRef = useRef<HTMLDivElement>(null);\n // `ChartTooltip` keeps null values (so they can print \"—\"), which also keeps the\n // series a legend toggle switched off with `hide`; those go here instead.\n const items = includeHidden ? payload : payload?.filter((item) => item.hide !== true);\n const visible = Boolean(active && items?.length);\n // Recharts anchors the tooltip at coordinate.x inside the full (scrolled) chart\n // width; subtract the container's scrollLeft to get its on-screen x, then flip\n // left if the tooltip's own width would run past the visible right edge.\n //\n // Both the cursor and the flip are PHYSICAL (the plot is never mirrored, see\n // `ChartContainer`), but an RTL scroller counts `scrollLeft` from its right edge —\n // 0 at the start, negative going left — so there the content's left edge sits\n // `scrollWidth - clientWidth + scrollLeft` px to the left of the visible one.\n //\n // Measured in a layout effect (the DOM is not readable during render), so it commits\n // before paint. `payload` and `label` are deps because they change the tooltip's own\n // width, and recharts hands a fresh `payload` on every tooltip render, so a scroll\n // followed by any re-render is re-measured just as it was when this ran in render.\n const [flip, setFlip] = useState(false);\n const x = coordinate?.x;\n useLayoutEffect(() => {\n const boundary = boundaryRef?.current;\n if (!visible || !boundary || x == null) {\n setFlip(false);\n return;\n }\n const offset = isRtl(boundary)\n ? boundary.scrollWidth - boundary.clientWidth + boundary.scrollLeft\n : boundary.scrollLeft;\n const visibleX = x - offset;\n const tipWidth = tipRef.current?.offsetWidth ?? 160;\n setFlip(visibleX + tipWidth + 12 > boundary.clientWidth);\n }, [visible, x, boundaryRef, payload, label]);\n if (!visible || !items) return null;\n return (\n <div\n ref={tipRef}\n style={boundaryRef ? { transform: flip ? \"translateX(calc(-100% - 12px))\" : \"translateX(12px)\" } : undefined}\n className=\"min-w-[9rem] rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] px-2.5 py-2 text-xs shadow-xl\"\n >\n {!hideLabel && label != null && (\n <div className=\"mb-1.5 font-medium text-[var(--text-primary)]\">\n {labelFormatter ? labelFormatter(label) : label}\n </div>\n )}\n <div className=\"grid gap-1.5\">\n {items.map((item, i) => {\n // Bars/lines/areas identify a series by dataKey; pies/treemaps key off\n // `name` (the nameKey value). Prefer whichever the config knows.\n const dk = item.dataKey != null ? String(item.dataKey) : undefined;\n const nm = item.name != null ? String(item.name) : undefined;\n const cfgKey = dk && config[dk] ? dk : nm && config[nm] ? nm : (dk ?? nm ?? String(i));\n const series = config[cfgKey];\n const fill = typeof item.payload?.fill === \"string\" ? item.payload.fill : undefined;\n // The data point's own `fill` before recharts' `item.color`: a treemap tile,\n // or a bar with a per-cell colour, IS the colour the reader is pointing at,\n // while `item.color` is the series-wide one — every tile's swatch came out\n // `--chart-1` whatever colour the tile was.\n const color = paintedColor(cfgKey, series) ?? fill ?? item.color ?? \"#64748b\";\n const name = series?.label ?? nm ?? cfgKey;\n const raw = tooltipValue(item.value);\n let shown: ReactNode;\n if (formatValue) shown = formatValue(raw);\n else if (raw == null) shown = NO_VALUE;\n else shown = valueFormatter ? valueFormatter(raw) : raw;\n return (\n <div key={`${cfgKey}-${i}`} className=\"flex items-center gap-2\">\n <span\n className={cn(\n \"shrink-0 rounded-[2px]\",\n indicator === \"dot\" ? \"h-2.5 w-2.5 rounded-full\" : \"h-2.5 w-1\",\n )}\n style={{ backgroundColor: color }}\n />\n <span className=\"text-[var(--text-muted)]\">{name}</span>\n <span data-private className=\"ms-auto font-mono font-medium tabular-nums text-[var(--text-primary)]\">\n {shown}\n </span>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n\ninterface LegendPayloadItem {\n value?: string;\n dataKey?: string | number;\n color?: string;\n}\n\ninterface ChartLegendContentProps {\n payload?: LegendPayloadItem[];\n onItemClick?: (key: string) => void;\n /**\n * TOGGLE mode (recommended): the series currently switched off. Each entry is a\n * toggle button — pressed while its series is shown — and a hidden one is drawn\n * faded with its label struck through. Several can be off at once; pair with\n * `toggleHidden(hidden, key)` in `onItemClick` and `hide` on the chart's series.\n */\n hiddenKeys?: ReadonlySet<string> | readonly string[];\n /**\n * ISOLATE mode: the one series shown in full, every other entry dimmed. Kept for\n * the charts built on it; a legend where each entry switches its own series is\n * what readers expect from a legend, so prefer `hiddenKeys`.\n */\n activeKey?: string | null;\n}\n\nexport function ChartLegendContent({\n payload,\n onItemClick,\n hiddenKeys,\n activeKey,\n}: ChartLegendContentProps) {\n const hidden = hiddenKeys ? new Set(hiddenKeys) : null;\n const config = useChart();\n if (!payload?.length) return null;\n return (\n <div className=\"flex flex-wrap items-center justify-center gap-x-4 gap-y-1 pt-3\">\n {payload.map((item, i) => {\n const dk = item.dataKey != null ? String(item.dataKey) : undefined;\n const vv = item.value != null ? String(item.value) : undefined;\n const cfgKey = dk && config[dk] ? dk : vv && config[vv] ? vv : (dk ?? vv ?? String(i));\n const series = config[cfgKey];\n const color = paintedColor(cfgKey, series) ?? item.color ?? \"#64748b\";\n const label = series?.label ?? vv ?? cfgKey;\n const off = hidden?.has(cfgKey) ?? false;\n const dimmed = off || (activeKey != null && activeKey !== cfgKey);\n const className = cn(\n \"flex items-center gap-1.5 text-[var(--text-secondary)] transition-opacity\",\n onItemClick ? \"cursor-pointer hover:opacity-80\" : \"cursor-default\",\n dimmed && \"opacity-40\",\n );\n const swatch = (\n <span\n aria-hidden\n className=\"h-2.5 w-2.5 shrink-0 rounded-[3px]\"\n style={{ backgroundColor: color } as CSSProperties}\n />\n );\n // A legend with no `onItemClick` is a KEY, not a control — and that is every\n // legend in kastlan, where this rendered N focusable buttons per chart that did\n // nothing when activated. A keyboard user paid the tab stops for all of them,\n // and a screen reader announced each one as a button, promising an action there\n // was none of. The interactive branch is unchanged.\n return onItemClick ? (\n <button\n key={`${cfgKey}-${i}`}\n type=\"button\"\n onClick={() => onItemClick(cfgKey)}\n // Toggles report their state: in toggle mode pressed = the series is shown;\n // in isolate mode pressed = the isolated one. With neither supplied the\n // legend knows nothing about state, and a pressed=\"false\" it cannot vouch\n // for would lie — so it says nothing.\n aria-pressed={\n hidden ? !off : activeKey === undefined ? undefined : activeKey === cfgKey\n }\n className={className}\n >\n {swatch}\n <span className={cn(off && \"line-through\")}>{label}</span>\n </button>\n ) : (\n <span key={`${cfgKey}-${i}`} className={className}>\n {swatch}\n {label}\n </span>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -314,21 +314,26 @@ const Chip = forwardRef(function Chip2({
314
314
  );
315
315
  return remove ? pill(button) : button;
316
316
  }
317
- return /* @__PURE__ */ jsxs(
318
- "span",
319
- {
320
- ref,
321
- className: look,
322
- tabIndex,
323
- role,
324
- onKeyDown,
325
- onFocus,
326
- ...rest,
327
- children: [
328
- body,
329
- remove
330
- ]
331
- }
317
+ return (
318
+ /* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- the handlers are
319
+ the roving-focus hooks, live only when the container passes `role` and `tabIndex`
320
+ (ChipInput does); the role is a prop, so the rule cannot see it. */
321
+ /* @__PURE__ */ jsxs(
322
+ "span",
323
+ {
324
+ ref,
325
+ className: look,
326
+ tabIndex,
327
+ role,
328
+ onKeyDown,
329
+ onFocus,
330
+ ...rest,
331
+ children: [
332
+ body,
333
+ remove
334
+ ]
335
+ }
336
+ )
332
337
  );
333
338
  });
334
339
  function RenderedLink({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/chip.tsx"],"sourcesContent":["import { forwardRef, useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, 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 { horizontalStep } from \"../lib/direction\";\nimport { FIELD_INVALID } from \"./ui\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink, useKitLocale } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\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-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-secondary)]\",\n selected: \"border-[var(--border-strong)] bg-[var(--bg-active)] text-[var(--text-primary)]\",\n },\n brand: {\n idle: \"border-[var(--border)] bg-[var(--brand-bg)] text-[var(--brand-muted)]\",\n selected: \"border-[var(--brand)] bg-[var(--brand)] text-[var(--brand-contrast)]\",\n },\n danger: {\n idle: \"border-[var(--danger-border)] bg-[var(--danger-bg)] text-[var(--danger)]\",\n selected: \"border-[var(--danger)] bg-[var(--danger)] text-[var(--danger-contrast)]\",\n },\n warning: {\n idle: \"border-[var(--warning-border)] bg-[var(--warning-bg)] text-[var(--warning)]\",\n selected: \"border-[var(--warning)] bg-[var(--warning-bg)] text-[var(--warning)]\",\n },\n success: {\n idle: \"border-[var(--success-border)] bg-[var(--success-bg)] text-[var(--success)]\",\n selected: \"border-[var(--success)] bg-[var(--success-bg)] text-[var(--success)]\",\n },\n info: {\n idle: \"border-[var(--info-border)] bg-[var(--info-bg)] text-[var(--info)]\",\n selected: \"border-[var(--info)] bg-[var(--info-bg)] text-[var(--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-[var(--bg-surface-2)] text-[var(--money-income)]\",\n selected: \"border-current bg-[var(--bg-active)] text-[var(--money-income)]\",\n },\n expense: {\n idle: \"border-current/40 bg-[var(--bg-surface-2)] text-[var(--money-expense)]\",\n selected: \"border-current bg-[var(--bg-active)] text-[var(--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-[var(--hue-blue-border)] bg-[var(--hue-blue-bg)] text-[var(--hue-blue)]\",\n selected: \"border-[var(--hue-blue)] bg-[var(--hue-blue-bg)] text-[var(--hue-blue)]\",\n },\n indigo: {\n idle: \"border-[var(--hue-indigo-border)] bg-[var(--hue-indigo-bg)] text-[var(--hue-indigo)]\",\n selected: \"border-[var(--hue-indigo)] bg-[var(--hue-indigo-bg)] text-[var(--hue-indigo)]\",\n },\n purple: {\n idle: \"border-[var(--hue-purple-border)] bg-[var(--hue-purple-bg)] text-[var(--hue-purple)]\",\n selected: \"border-[var(--hue-purple)] bg-[var(--hue-purple-bg)] text-[var(--hue-purple)]\",\n },\n teal: {\n idle: \"border-[var(--hue-teal-border)] bg-[var(--hue-teal-bg)] text-[var(--hue-teal)]\",\n selected: \"border-[var(--hue-teal)] bg-[var(--hue-teal-bg)] text-[var(--hue-teal)]\",\n },\n orange: {\n idle: \"border-[var(--hue-orange-border)] bg-[var(--hue-orange-bg)] text-[var(--hue-orange)]\",\n selected: \"border-[var(--hue-orange)] bg-[var(--hue-orange-bg)] text-[var(--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-[var(--border-strong)] bg-transparent text-[var(--text-secondary)]\",\n brand: \"border-[var(--brand)] bg-transparent text-[var(--brand)]\",\n danger: \"border-[var(--danger-border)] bg-transparent text-[var(--danger)]\",\n warning: \"border-[var(--warning-border)] bg-transparent text-[var(--warning)]\",\n success: \"border-[var(--success-border)] bg-transparent text-[var(--success)]\",\n info: \"border-[var(--info-border)] bg-transparent text-[var(--info)]\",\n income: \"border-current/60 bg-transparent text-[var(--money-income)]\",\n expense: \"border-current/60 bg-transparent text-[var(--money-expense)]\",\n blue: \"border-[var(--hue-blue-border)] bg-transparent text-[var(--hue-blue)]\",\n indigo: \"border-[var(--hue-indigo-border)] bg-transparent text-[var(--hue-indigo)]\",\n purple: \"border-[var(--hue-purple-border)] bg-transparent text-[var(--hue-purple)]\",\n teal: \"border-[var(--hue-teal-border)] bg-transparent text-[var(--hue-teal)]\",\n orange: \"border-[var(--hue-orange-border)] bg-transparent text-[var(--hue-orange)]\",\n};\n\n// `solid`: the fill under its contrast pair. Only brand and danger have a declared\n// `-contrast` token; the other hues are 700s in the light theme and 300s in the dark\n// one, so `--text-inverse` (the surface colour) is the side of the pair that contrasts\n// with them in both — which is what the inverse token is for. The border goes\n// transparent rather than away, so a solid chip is exactly as tall as a soft one.\nconst SOLID: Record<ChipTone, string> = {\n neutral: \"border-transparent bg-[var(--bg-inverse)] text-[var(--text-inverse)]\",\n brand: \"border-transparent bg-[var(--brand)] text-[var(--brand-contrast)]\",\n danger: \"border-transparent bg-[var(--danger)] text-[var(--danger-contrast)]\",\n warning: \"border-transparent bg-[var(--warning)] text-[var(--text-inverse)]\",\n success: \"border-transparent bg-[var(--success)] text-[var(--text-inverse)]\",\n info: \"border-transparent bg-[var(--info)] text-[var(--text-inverse)]\",\n income: \"border-transparent bg-[var(--money-income)] text-[var(--text-inverse)]\",\n expense: \"border-transparent bg-[var(--money-expense)] text-[var(--text-inverse)]\",\n blue: \"border-transparent bg-[var(--hue-blue)] text-[var(--text-inverse)]\",\n indigo: \"border-transparent bg-[var(--hue-indigo)] text-[var(--text-inverse)]\",\n purple: \"border-transparent bg-[var(--hue-purple)] text-[var(--text-inverse)]\",\n teal: \"border-transparent bg-[var(--hue-teal)] text-[var(--text-inverse)]\",\n orange: \"border-transparent bg-[var(--hue-orange)] text-[var(--text-inverse)]\",\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-[var(--brand)]\",\n danger: \"bg-[var(--danger)]\",\n warning: \"bg-[var(--warning)]\",\n success: \"bg-[var(--success)]\",\n info: \"bg-[var(--info)]\",\n income: \"bg-[var(--money-income)]\",\n expense: \"bg-[var(--money-expense)]\",\n blue: \"bg-[var(--hue-blue)]\",\n indigo: \"bg-[var(--hue-indigo)]\",\n purple: \"bg-[var(--hue-purple)]\",\n teal: \"bg-[var(--hue-teal)]\",\n orange: \"bg-[var(--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-[10px] font-semibold uppercase tracking-wider\",\n sm: \"text-[10px] font-semibold uppercase tracking-wider\",\n md: \"text-[11px] 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-[var(--text-primary)]\"\n : \"border-transparent bg-transparent text-[var(--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-[var(--border-strong)] ring-offset-1 ring-offset-[var(--bg-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-[11px] leading-4\",\n split: \"gap-1 ps-1.5 pe-0.5 py-0 text-[11px] 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// `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 =\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--bg-surface)]\";\nconst CHIP_BASE = `${CHIP_PILL} ${CHIP_RING}`;\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 icon?: LucideIcon;\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\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 }\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 }\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 );\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 const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(\n {\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 ...rest\n },\n ref,\n) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS, { remove: removeLabel });\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 disabled && \"cursor-default opacity-50\",\n );\n const look = cn(\n CHIP_BASE,\n s.body,\n type,\n radius,\n surface,\n interactive && !disabled && \"cursor-pointer hover:brightness-[0.97] dark:hover:brightness-110\",\n // An inert dot chip is text in a column: no inset, so it aligns with the header.\n dot && !interactive && \"px-0\",\n className,\n );\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 && <Icon className={cn(s.icon, \"shrink-0\")} aria-hidden />}\n <span className=\"min-w-0 truncate\">{children}</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 onRemove();\n }}\n disabled={disabled || removeDisabled}\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 \"hover:bg-[var(--bg-active)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]\",\n (disabled || 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 className={cn(CHIP_PILL, s.tail, radius, surface, className)}>\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 && \"cursor-pointer hover:bg-current/10\",\n disabled && \"cursor-default\",\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 ...rest,\n ref: ref 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={ref as React.Ref<HTMLAnchorElement>}\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 button = (\n <button\n ref={ref as React.Ref<HTMLButtonElement>}\n type=\"button\"\n onClick={onButtonClick}\n disabled={disabled}\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 >\n {body}\n </button>\n );\n return remove ? pill(button) : button;\n }\n\n return (\n <span\n ref={ref as React.Ref<HTMLSpanElement>}\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\n/* ── ChipInput ────────────────────────────────────────────────────────────── */\n\nexport interface ChipInputLabels {\n /** Announced after a value is added. Receives the value. */\n added: (value: string) => string;\n /** Announced after a value is removed. Receives the value. */\n removed: (value: string) => string;\n /** Accessible name for each chip's dismiss button. */\n remove: string;\n /** Shown when `max` is reached. Receives the limit. */\n atLimit: (max: number) => string;\n /** Shown when a duplicate is rejected. Receives the value. */\n duplicate: (value: string) => string;\n}\n\nexport const DEFAULT_CHIP_INPUT_LABELS: ChipInputLabels = {\n added: (v) => `${v} added`,\n removed: (v) => `${v} removed`,\n remove: \"Remove\",\n atLimit: (max) => `Limit of ${max} reached`,\n duplicate: (v) => `${v} is already in the list`,\n};\n\nexport function resolveChipInputLabels(labels?: Partial<ChipInputLabels>): ChipInputLabels {\n return labels ? { ...DEFAULT_CHIP_INPUT_LABELS, ...labels } : DEFAULT_CHIP_INPUT_LABELS;\n}\n\n/**\n * `onChange` is the field's own — the whole LIST, not a DOM event — so the wrapper\n * div's `onChange` is omitted rather than shadowed. `placeholder` is likewise this\n * component's, forwarded to the inner `<input>`; the div has none of its own.\n */\nexport interface ChipInputProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"placeholder\" | \"children\"> {\n value: string[];\n onChange: (next: string[]) => void;\n placeholder?: string;\n /** Visible label, rendered above the field. Without one, name the field with\n * `aria-label`. */\n label?: string;\n /**\n * @deprecated Pass `aria-label` instead — the DOM spelling the rest of this kit\n * now uses. Still honoured, and still second to `aria-label` when both are given.\n */\n ariaLabel?: string;\n /** Reject or rewrite a value on commit. Return a string to reject WITH that message. */\n validate?: (raw: string) => string | null;\n /** Characters that commit the current text, besides Enter. Default: comma. */\n separators?: string[];\n allowDuplicates?: boolean;\n max?: number;\n tone?: ChipTone;\n size?: ChipSize;\n disabled?: boolean;\n invalid?: boolean;\n /** Error text. Wires `aria-describedby` and implies `invalid`. */\n error?: ReactNode;\n labels?: Partial<ChipInputLabels>;\n className?: string;\n}\n\n/**\n * A field whose value is a LIST, entered as chips.\n *\n * The keyboard model is the whole component; the pills are the easy part. Per the WAI\n * pattern for a \"tag\" input:\n *\n * Enter, or any separator commit the typed text\n * Backspace on empty text move focus to the last chip (does NOT delete it)\n * ← → while on a chip move between chips, in reading order (mirrored in RTL)\n * Backspace / Delete remove the focused chip, focus its neighbour\n * Escape clear what is typed but keep the committed chips\n * paste splits on the separators, so a pasted CSV becomes chips\n *\n * The Backspace-on-empty behaviour is the one worth arguing about. Deleting immediately\n * is more common and is a trap: it is a destructive action with no feedback on a key\n * people hit by reflex, and the value it destroys may have taken a while to type. Moving\n * focus first makes the second Backspace a deliberate one, and it is what lets a\n * keyboard user reach a chip at all.\n */\nexport function ChipInput({\n value,\n onChange,\n placeholder,\n label,\n ariaLabel,\n validate,\n separators = [\",\"],\n allowDuplicates = false,\n max,\n tone = \"neutral\",\n size = \"md\",\n disabled = false,\n invalid = false,\n error,\n labels,\n className,\n \"aria-label\": ariaLabelAttr,\n ...rest\n}: ChipInputProps) {\n // `labels` over the provider's `chipInput` over English — the same order\n // `resolveChipInputLabels` gives outside a provider.\n const text = useKitLabels(\"chipInput\", DEFAULT_CHIP_INPUT_LABELS, labels);\n // A batch of pasted values is announced as one list, joined the way the language\n // joins a list in a sentence (\"a, b and c added\") — hence the long conjunction,\n // where a trigger's summary would use the narrow one.\n const locale = useKitLocale();\n const [draft, setDraft] = useState(\"\");\n const [message, setMessage] = useState(\"\");\n // The last refusal, shown under the field until the next edit. The live region alone\n // told only a screen-reader user why Enter did nothing; everyone else saw the text\n // simply stay put.\n const [rejected, setRejected] = useState<string | null>(null);\n const [focusedChip, setFocusedChip] = useState<number | null>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const chipRefs = useRef<Array<HTMLElement | null>>([]);\n const id = useId();\n const listId = `${id}-list`;\n const errorId = `${id}-error`;\n const rejectedId = `${id}-rejected`;\n const isInvalid = invalid || !!error;\n const describedBy = [error ? errorId : null, rejected ? rejectedId : null]\n .filter(Boolean)\n .join(\" \");\n\n const focusChip = (index: number | null) => {\n setFocusedChip(index);\n if (index === null) inputRef.current?.focus();\n else chipRefs.current[index]?.focus();\n };\n\n /**\n * Try to add every candidate, as ONE change.\n *\n * Batched rather than looped for a reason that bites immediately: `commit` closes over\n * `value`, so calling it once per pasted item makes every call compute its result from\n * the list as it was BEFORE the paste, and each `onChange` overwrites the last. A\n * pasted \"one,two,three\" left only \"three\". Accumulating locally and emitting once is\n * also the correct behaviour for an undo stack — a paste is one action.\n */\n const commitMany = (raws: string[]): { added: string[]; rejection: string | null } => {\n const next = [...value];\n const added: string[] = [];\n let rejection: string | null = null;\n\n for (const raw of raws) {\n const candidate = raw.trim();\n if (!candidate) continue;\n if (max !== undefined && next.length >= max) {\n rejection ??= text.atLimit(max);\n break;\n }\n if (!allowDuplicates && next.includes(candidate)) {\n rejection ??= text.duplicate(candidate);\n continue;\n }\n const invalidReason = validate?.(candidate);\n if (invalidReason) {\n rejection ??= invalidReason;\n continue;\n }\n next.push(candidate);\n added.push(candidate);\n }\n\n if (added.length) onChange(next);\n // A rejection is the more useful thing to say: \"three added\" is obvious from the\n // chips appearing, whereas \"already in the list\" is the only sign of what did not.\n const addedList = new Intl.ListFormat(locale, { type: \"conjunction\", style: \"long\" });\n setMessage(rejection ?? (added.length ? text.added(addedList.format(added)) : \"\"));\n setRejected(rejection);\n return { added, rejection };\n };\n\n const commit = (raw: string): boolean => commitMany([raw]).added.length > 0;\n\n const removeAt = (index: number) => {\n const removed = value[index];\n onChange(value.filter((_, i) => i !== index));\n setMessage(text.removed(removed));\n setRejected(null);\n // Focus the neighbour that takes its place, or the input when the list empties —\n // dropping focus to <body> here is how a keyboard user loses the field entirely.\n const next = index >= value.length - 1 ? null : index;\n requestAnimationFrame(() => focusChip(value.length - 1 === 0 ? null : next));\n };\n\n const onInputKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\") {\n // Only when there is something to commit: an empty Enter belongs to the form.\n if (draft.trim()) {\n e.preventDefault();\n if (commit(draft)) setDraft(\"\");\n }\n return;\n }\n if (separators.includes(e.key)) {\n e.preventDefault();\n if (commit(draft)) setDraft(\"\");\n return;\n }\n if (e.key === \"Escape\" && draft) {\n e.preventDefault();\n setDraft(\"\");\n return;\n }\n if (e.key === \"Backspace\" && !draft && value.length) {\n e.preventDefault();\n focusChip(value.length - 1);\n }\n };\n\n const onChipKeyDown = (e: KeyboardEvent<HTMLElement>, index: number) => {\n if (e.key === \"Backspace\" || e.key === \"Delete\") {\n e.preventDefault();\n removeAt(index);\n return;\n }\n // ←/→ follow the reading direction, not DOM order: in RTL the previous chip is to\n // the RIGHT, and ArrowLeft moves on towards the input.\n const step = horizontalStep(e.key, e.currentTarget);\n if (step === -1 && index > 0) {\n e.preventDefault();\n focusChip(index - 1);\n return;\n }\n if (step === 1) {\n e.preventDefault();\n focusChip(index === value.length - 1 ? null : index + 1);\n return;\n }\n if (e.key === \"Escape\") {\n e.preventDefault();\n focusChip(null);\n }\n };\n\n return (\n // The rest lands on the OUTER box — the thing a `data-tour` step or a test id\n // wants to point at — while the accessible name below goes on the `<input>`,\n // because that is what a screen-reader user actually lands in. Spreading the two\n // to the same element would have named a decorative wrapper and left the field\n // anonymous.\n <div {...rest} className={cn(\"space-y-1\", className)}>\n {label && (\n <label htmlFor={`${id}-input`} className=\"block text-sm text-[var(--text-secondary)]\">\n {label}\n </label>\n )}\n <div\n // `relative` so the live region below can be `sr-only` without escaping to the\n // initial containing block and inflating the document height.\n className={cn(\n \"relative flex flex-wrap items-center gap-1.5 rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1.5 shadow-sm\",\n \"focus-within:border-[var(--brand)] focus-within:ring-1 focus-within:ring-[var(--brand)]\",\n disabled && \"cursor-default bg-[var(--bg-surface-2)]\",\n isInvalid && FIELD_INVALID,\n )}\n onClick={() => !disabled && inputRef.current?.focus()}\n >\n {/* `display: contents` on the wrappers so the chips participate in the field's\n own flex wrap rather than forming a nested row. The list semantics are\n restored with explicit roles, because `contents` removes the ul/li boxes from\n the accessibility tree in several engines. */}\n {/* eslint-disable-next-line jsx-a11y/no-redundant-roles -- NOT redundant here: the\n `contents` display below removes the ul and li boxes from the box tree, and\n several engines drop the list semantics with them. The explicit role is what\n puts \"list, 3 items\" back. */}\n <ul id={listId} role=\"list\" className=\"contents\">\n {value.map((v, i) => (\n <li key={`${v}-${i}`} role=\"presentation\" className=\"contents\">\n <Chip\n ref={(el: HTMLElement | null) => {\n chipRefs.current[i] = el;\n }}\n tone={tone}\n size={size}\n disabled={disabled}\n removeLabel={text.remove}\n onRemove={disabled ? undefined : () => removeAt(i)}\n // A ROVING tabIndex: only the focused chip is tabbable, so Tab leaves\n // the field instead of walking through every value in it — the reason a\n // list of twenty tags is otherwise a twenty-stop detour for a keyboard.\n tabIndex={focusedChip === i ? 0 : -1}\n role=\"listitem\"\n aria-label={v}\n onKeyDown={(e) => onChipKeyDown(e, i)}\n onFocus={() => setFocusedChip(i)}\n data-chip-index={i}\n >\n {v}\n </Chip>\n </li>\n ))}\n </ul>\n <input\n id={`${id}-input`}\n ref={inputRef}\n value={draft}\n onChange={(e) => {\n setDraft(e.target.value);\n setMessage(\"\");\n setRejected(null);\n }}\n onKeyDown={onInputKeyDown}\n onBlur={() => {\n // Commit on blur: a value typed and then clicked away from is a value the\n // user meant to add, and losing it silently is the commonest complaint\n // about this pattern.\n if (draft.trim() && commit(draft)) setDraft(\"\");\n }}\n onPaste={(e) => {\n const pasted = e.clipboardData.getData(\"text\");\n const pattern = new RegExp(`[${separators.map((s) => `\\\\${s}`).join(\"\")}\\n\\r]`);\n if (!pattern.test(pasted)) return;\n e.preventDefault();\n if (commitMany(pasted.split(pattern)).added.length) setDraft(\"\");\n }}\n placeholder={value.length ? undefined : placeholder}\n disabled={disabled}\n // A visible `label` is already wired through `htmlFor`, so adding a name\n // here would give the field two. Failing that, the DOM spelling wins and\n // the deprecated `ariaLabel` is the fallback.\n aria-label={label ? undefined : (ariaLabelAttr ?? ariaLabel)}\n aria-describedby={describedBy || undefined}\n aria-invalid={isInvalid || !!rejected || undefined}\n className=\"min-w-[6rem] flex-1 bg-transparent py-0.5 text-sm text-[var(--text-primary)] outline-none placeholder:text-[var(--text-placeholder)] disabled:cursor-default\"\n />\n {/* Additions, removals and rejections move no focus, so nothing would announce\n them. `sr-only-fixed` rather than `sr-only`: this sits inside a consumer's\n markup and must not extend the document height. */}\n <span role=\"status\" aria-live=\"polite\" aria-atomic className=\"sr-only-fixed\">\n {message}\n </span>\n </div>\n {error && (\n <p id={errorId} className=\"text-xs text-[var(--danger)]\">\n {error}\n </p>\n )}\n {rejected && (\n <p id={rejectedId} className=\"text-xs text-[var(--danger)]\" data-chip-input-rejected=\"\">\n {rejected}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["Chip","inner"]}
1
+ {"version":3,"sources":["../../src/components/chip.tsx"],"sourcesContent":["import { forwardRef, useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, 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 { horizontalStep } from \"../lib/direction\";\nimport { FIELD_INVALID } from \"./ui\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink, useKitLocale } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\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-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-secondary)]\",\n selected: \"border-[var(--border-strong)] bg-[var(--bg-active)] text-[var(--text-primary)]\",\n },\n brand: {\n idle: \"border-[var(--border)] bg-[var(--brand-bg)] text-[var(--brand-muted)]\",\n selected: \"border-[var(--brand)] bg-[var(--brand)] text-[var(--brand-contrast)]\",\n },\n danger: {\n idle: \"border-[var(--danger-border)] bg-[var(--danger-bg)] text-[var(--danger)]\",\n selected: \"border-[var(--danger)] bg-[var(--danger)] text-[var(--danger-contrast)]\",\n },\n warning: {\n idle: \"border-[var(--warning-border)] bg-[var(--warning-bg)] text-[var(--warning)]\",\n selected: \"border-[var(--warning)] bg-[var(--warning-bg)] text-[var(--warning)]\",\n },\n success: {\n idle: \"border-[var(--success-border)] bg-[var(--success-bg)] text-[var(--success)]\",\n selected: \"border-[var(--success)] bg-[var(--success-bg)] text-[var(--success)]\",\n },\n info: {\n idle: \"border-[var(--info-border)] bg-[var(--info-bg)] text-[var(--info)]\",\n selected: \"border-[var(--info)] bg-[var(--info-bg)] text-[var(--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-[var(--bg-surface-2)] text-[var(--money-income)]\",\n selected: \"border-current bg-[var(--bg-active)] text-[var(--money-income)]\",\n },\n expense: {\n idle: \"border-current/40 bg-[var(--bg-surface-2)] text-[var(--money-expense)]\",\n selected: \"border-current bg-[var(--bg-active)] text-[var(--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-[var(--hue-blue-border)] bg-[var(--hue-blue-bg)] text-[var(--hue-blue)]\",\n selected: \"border-[var(--hue-blue)] bg-[var(--hue-blue-bg)] text-[var(--hue-blue)]\",\n },\n indigo: {\n idle: \"border-[var(--hue-indigo-border)] bg-[var(--hue-indigo-bg)] text-[var(--hue-indigo)]\",\n selected: \"border-[var(--hue-indigo)] bg-[var(--hue-indigo-bg)] text-[var(--hue-indigo)]\",\n },\n purple: {\n idle: \"border-[var(--hue-purple-border)] bg-[var(--hue-purple-bg)] text-[var(--hue-purple)]\",\n selected: \"border-[var(--hue-purple)] bg-[var(--hue-purple-bg)] text-[var(--hue-purple)]\",\n },\n teal: {\n idle: \"border-[var(--hue-teal-border)] bg-[var(--hue-teal-bg)] text-[var(--hue-teal)]\",\n selected: \"border-[var(--hue-teal)] bg-[var(--hue-teal-bg)] text-[var(--hue-teal)]\",\n },\n orange: {\n idle: \"border-[var(--hue-orange-border)] bg-[var(--hue-orange-bg)] text-[var(--hue-orange)]\",\n selected: \"border-[var(--hue-orange)] bg-[var(--hue-orange-bg)] text-[var(--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-[var(--border-strong)] bg-transparent text-[var(--text-secondary)]\",\n brand: \"border-[var(--brand)] bg-transparent text-[var(--brand)]\",\n danger: \"border-[var(--danger-border)] bg-transparent text-[var(--danger)]\",\n warning: \"border-[var(--warning-border)] bg-transparent text-[var(--warning)]\",\n success: \"border-[var(--success-border)] bg-transparent text-[var(--success)]\",\n info: \"border-[var(--info-border)] bg-transparent text-[var(--info)]\",\n income: \"border-current/60 bg-transparent text-[var(--money-income)]\",\n expense: \"border-current/60 bg-transparent text-[var(--money-expense)]\",\n blue: \"border-[var(--hue-blue-border)] bg-transparent text-[var(--hue-blue)]\",\n indigo: \"border-[var(--hue-indigo-border)] bg-transparent text-[var(--hue-indigo)]\",\n purple: \"border-[var(--hue-purple-border)] bg-transparent text-[var(--hue-purple)]\",\n teal: \"border-[var(--hue-teal-border)] bg-transparent text-[var(--hue-teal)]\",\n orange: \"border-[var(--hue-orange-border)] bg-transparent text-[var(--hue-orange)]\",\n};\n\n// `solid`: the fill under its contrast pair. Only brand and danger have a declared\n// `-contrast` token; the other hues are 700s in the light theme and 300s in the dark\n// one, so `--text-inverse` (the surface colour) is the side of the pair that contrasts\n// with them in both — which is what the inverse token is for. The border goes\n// transparent rather than away, so a solid chip is exactly as tall as a soft one.\nconst SOLID: Record<ChipTone, string> = {\n neutral: \"border-transparent bg-[var(--bg-inverse)] text-[var(--text-inverse)]\",\n brand: \"border-transparent bg-[var(--brand)] text-[var(--brand-contrast)]\",\n danger: \"border-transparent bg-[var(--danger)] text-[var(--danger-contrast)]\",\n warning: \"border-transparent bg-[var(--warning)] text-[var(--text-inverse)]\",\n success: \"border-transparent bg-[var(--success)] text-[var(--text-inverse)]\",\n info: \"border-transparent bg-[var(--info)] text-[var(--text-inverse)]\",\n income: \"border-transparent bg-[var(--money-income)] text-[var(--text-inverse)]\",\n expense: \"border-transparent bg-[var(--money-expense)] text-[var(--text-inverse)]\",\n blue: \"border-transparent bg-[var(--hue-blue)] text-[var(--text-inverse)]\",\n indigo: \"border-transparent bg-[var(--hue-indigo)] text-[var(--text-inverse)]\",\n purple: \"border-transparent bg-[var(--hue-purple)] text-[var(--text-inverse)]\",\n teal: \"border-transparent bg-[var(--hue-teal)] text-[var(--text-inverse)]\",\n orange: \"border-transparent bg-[var(--hue-orange)] text-[var(--text-inverse)]\",\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-[var(--brand)]\",\n danger: \"bg-[var(--danger)]\",\n warning: \"bg-[var(--warning)]\",\n success: \"bg-[var(--success)]\",\n info: \"bg-[var(--info)]\",\n income: \"bg-[var(--money-income)]\",\n expense: \"bg-[var(--money-expense)]\",\n blue: \"bg-[var(--hue-blue)]\",\n indigo: \"bg-[var(--hue-indigo)]\",\n purple: \"bg-[var(--hue-purple)]\",\n teal: \"bg-[var(--hue-teal)]\",\n orange: \"bg-[var(--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-[10px] font-semibold uppercase tracking-wider\",\n sm: \"text-[10px] font-semibold uppercase tracking-wider\",\n md: \"text-[11px] 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-[var(--text-primary)]\"\n : \"border-transparent bg-transparent text-[var(--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-[var(--border-strong)] ring-offset-1 ring-offset-[var(--bg-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-[11px] leading-4\",\n split: \"gap-1 ps-1.5 pe-0.5 py-0 text-[11px] 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// `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 =\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--bg-surface)]\";\nconst CHIP_BASE = `${CHIP_PILL} ${CHIP_RING}`;\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 icon?: LucideIcon;\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\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 }\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 }\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 );\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 const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(\n {\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 ...rest\n },\n ref,\n) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS, { remove: removeLabel });\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 disabled && \"cursor-default opacity-50\",\n );\n const look = cn(\n CHIP_BASE,\n s.body,\n type,\n radius,\n surface,\n interactive && !disabled && \"cursor-pointer hover:brightness-[0.97] dark:hover:brightness-110\",\n // An inert dot chip is text in a column: no inset, so it aligns with the header.\n dot && !interactive && \"px-0\",\n className,\n );\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 && <Icon className={cn(s.icon, \"shrink-0\")} aria-hidden />}\n <span className=\"min-w-0 truncate\">{children}</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 onRemove();\n }}\n disabled={disabled || removeDisabled}\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 \"hover:bg-[var(--bg-active)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]\",\n (disabled || 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 className={cn(CHIP_PILL, s.tail, radius, surface, className)}>\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 && \"cursor-pointer hover:bg-current/10\",\n disabled && \"cursor-default\",\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 ...rest,\n ref: ref 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={ref as React.Ref<HTMLAnchorElement>}\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 button = (\n <button\n ref={ref as React.Ref<HTMLButtonElement>}\n type=\"button\"\n onClick={onButtonClick}\n disabled={disabled}\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 >\n {body}\n </button>\n );\n return remove ? pill(button) : button;\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={ref as React.Ref<HTMLSpanElement>}\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\n/* ── ChipInput ────────────────────────────────────────────────────────────── */\n\nexport interface ChipInputLabels {\n /** Announced after a value is added. Receives the value. */\n added: (value: string) => string;\n /** Announced after a value is removed. Receives the value. */\n removed: (value: string) => string;\n /** Accessible name for each chip's dismiss button. */\n remove: string;\n /** Shown when `max` is reached. Receives the limit. */\n atLimit: (max: number) => string;\n /** Shown when a duplicate is rejected. Receives the value. */\n duplicate: (value: string) => string;\n}\n\nexport const DEFAULT_CHIP_INPUT_LABELS: ChipInputLabels = {\n added: (v) => `${v} added`,\n removed: (v) => `${v} removed`,\n remove: \"Remove\",\n atLimit: (max) => `Limit of ${max} reached`,\n duplicate: (v) => `${v} is already in the list`,\n};\n\nexport function resolveChipInputLabels(labels?: Partial<ChipInputLabels>): ChipInputLabels {\n return labels ? { ...DEFAULT_CHIP_INPUT_LABELS, ...labels } : DEFAULT_CHIP_INPUT_LABELS;\n}\n\n/**\n * `onChange` is the field's own — the whole LIST, not a DOM event — so the wrapper\n * div's `onChange` is omitted rather than shadowed. `placeholder` is likewise this\n * component's, forwarded to the inner `<input>`; the div has none of its own.\n */\nexport interface ChipInputProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"placeholder\" | \"children\"> {\n value: string[];\n onChange: (next: string[]) => void;\n placeholder?: string;\n /** Visible label, rendered above the field. Without one, name the field with\n * `aria-label`. */\n label?: string;\n /**\n * @deprecated Pass `aria-label` instead — the DOM spelling the rest of this kit\n * now uses. Still honoured, and still second to `aria-label` when both are given.\n */\n ariaLabel?: string;\n /** Reject or rewrite a value on commit. Return a string to reject WITH that message. */\n validate?: (raw: string) => string | null;\n /** Characters that commit the current text, besides Enter. Default: comma. */\n separators?: string[];\n allowDuplicates?: boolean;\n max?: number;\n tone?: ChipTone;\n size?: ChipSize;\n disabled?: boolean;\n invalid?: boolean;\n /** Error text. Wires `aria-describedby` and implies `invalid`. */\n error?: ReactNode;\n labels?: Partial<ChipInputLabels>;\n className?: string;\n}\n\n/**\n * A field whose value is a LIST, entered as chips.\n *\n * The keyboard model is the whole component; the pills are the easy part. Per the WAI\n * pattern for a \"tag\" input:\n *\n * Enter, or any separator commit the typed text\n * Backspace on empty text move focus to the last chip (does NOT delete it)\n * ← → while on a chip move between chips, in reading order (mirrored in RTL)\n * Backspace / Delete remove the focused chip, focus its neighbour\n * Escape clear what is typed but keep the committed chips\n * paste splits on the separators, so a pasted CSV becomes chips\n *\n * The Backspace-on-empty behaviour is the one worth arguing about. Deleting immediately\n * is more common and is a trap: it is a destructive action with no feedback on a key\n * people hit by reflex, and the value it destroys may have taken a while to type. Moving\n * focus first makes the second Backspace a deliberate one, and it is what lets a\n * keyboard user reach a chip at all.\n */\nexport function ChipInput({\n value,\n onChange,\n placeholder,\n label,\n ariaLabel,\n validate,\n separators = [\",\"],\n allowDuplicates = false,\n max,\n tone = \"neutral\",\n size = \"md\",\n disabled = false,\n invalid = false,\n error,\n labels,\n className,\n \"aria-label\": ariaLabelAttr,\n ...rest\n}: ChipInputProps) {\n // `labels` over the provider's `chipInput` over English — the same order\n // `resolveChipInputLabels` gives outside a provider.\n const text = useKitLabels(\"chipInput\", DEFAULT_CHIP_INPUT_LABELS, labels);\n // A batch of pasted values is announced as one list, joined the way the language\n // joins a list in a sentence (\"a, b and c added\") — hence the long conjunction,\n // where a trigger's summary would use the narrow one.\n const locale = useKitLocale();\n const [draft, setDraft] = useState(\"\");\n const [message, setMessage] = useState(\"\");\n // The last refusal, shown under the field until the next edit. The live region alone\n // told only a screen-reader user why Enter did nothing; everyone else saw the text\n // simply stay put.\n const [rejected, setRejected] = useState<string | null>(null);\n const [focusedChip, setFocusedChip] = useState<number | null>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const chipRefs = useRef<Array<HTMLElement | null>>([]);\n const id = useId();\n const listId = `${id}-list`;\n const errorId = `${id}-error`;\n const rejectedId = `${id}-rejected`;\n const isInvalid = invalid || !!error;\n const describedBy = [error ? errorId : null, rejected ? rejectedId : null]\n .filter(Boolean)\n .join(\" \");\n\n const focusChip = (index: number | null) => {\n setFocusedChip(index);\n if (index === null) inputRef.current?.focus();\n else chipRefs.current[index]?.focus();\n };\n\n /**\n * Try to add every candidate, as ONE change.\n *\n * Batched rather than looped for a reason that bites immediately: `commit` closes over\n * `value`, so calling it once per pasted item makes every call compute its result from\n * the list as it was BEFORE the paste, and each `onChange` overwrites the last. A\n * pasted \"one,two,three\" left only \"three\". Accumulating locally and emitting once is\n * also the correct behaviour for an undo stack — a paste is one action.\n */\n const commitMany = (raws: string[]): { added: string[]; rejection: string | null } => {\n const next = [...value];\n const added: string[] = [];\n let rejection: string | null = null;\n\n for (const raw of raws) {\n const candidate = raw.trim();\n if (!candidate) continue;\n if (max !== undefined && next.length >= max) {\n rejection ??= text.atLimit(max);\n break;\n }\n if (!allowDuplicates && next.includes(candidate)) {\n rejection ??= text.duplicate(candidate);\n continue;\n }\n const invalidReason = validate?.(candidate);\n if (invalidReason) {\n rejection ??= invalidReason;\n continue;\n }\n next.push(candidate);\n added.push(candidate);\n }\n\n if (added.length) onChange(next);\n // A rejection is the more useful thing to say: \"three added\" is obvious from the\n // chips appearing, whereas \"already in the list\" is the only sign of what did not.\n const addedList = new Intl.ListFormat(locale, { type: \"conjunction\", style: \"long\" });\n setMessage(rejection ?? (added.length ? text.added(addedList.format(added)) : \"\"));\n setRejected(rejection);\n return { added, rejection };\n };\n\n const commit = (raw: string): boolean => commitMany([raw]).added.length > 0;\n\n const removeAt = (index: number) => {\n const removed = value[index];\n onChange(value.filter((_, i) => i !== index));\n setMessage(text.removed(removed));\n setRejected(null);\n // Focus the neighbour that takes its place, or the input when the list empties —\n // dropping focus to <body> here is how a keyboard user loses the field entirely.\n const next = index >= value.length - 1 ? null : index;\n requestAnimationFrame(() => focusChip(value.length - 1 === 0 ? null : next));\n };\n\n const onInputKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\") {\n // Only when there is something to commit: an empty Enter belongs to the form.\n if (draft.trim()) {\n e.preventDefault();\n if (commit(draft)) setDraft(\"\");\n }\n return;\n }\n if (separators.includes(e.key)) {\n e.preventDefault();\n if (commit(draft)) setDraft(\"\");\n return;\n }\n if (e.key === \"Escape\" && draft) {\n e.preventDefault();\n setDraft(\"\");\n return;\n }\n if (e.key === \"Backspace\" && !draft && value.length) {\n e.preventDefault();\n focusChip(value.length - 1);\n }\n };\n\n const onChipKeyDown = (e: KeyboardEvent<HTMLElement>, index: number) => {\n if (e.key === \"Backspace\" || e.key === \"Delete\") {\n e.preventDefault();\n removeAt(index);\n return;\n }\n // ←/→ follow the reading direction, not DOM order: in RTL the previous chip is to\n // the RIGHT, and ArrowLeft moves on towards the input.\n const step = horizontalStep(e.key, e.currentTarget);\n if (step === -1 && index > 0) {\n e.preventDefault();\n focusChip(index - 1);\n return;\n }\n if (step === 1) {\n e.preventDefault();\n focusChip(index === value.length - 1 ? null : index + 1);\n return;\n }\n if (e.key === \"Escape\") {\n e.preventDefault();\n focusChip(null);\n }\n };\n\n return (\n // The rest lands on the OUTER box — the thing a `data-tour` step or a test id\n // wants to point at — while the accessible name below goes on the `<input>`,\n // because that is what a screen-reader user actually lands in. Spreading the two\n // to the same element would have named a decorative wrapper and left the field\n // anonymous.\n <div {...rest} className={cn(\"space-y-1\", className)}>\n {label && (\n <label htmlFor={`${id}-input`} className=\"block text-sm text-[var(--text-secondary)]\">\n {label}\n </label>\n )}\n {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- a\n pointer convenience that widens the input's hit area to the whole field; the\n keyboard path is the <input> itself, which is in the tab order. */}\n <div\n // `relative` so the live region below can be `sr-only` without escaping to the\n // initial containing block and inflating the document height.\n className={cn(\n \"relative flex flex-wrap items-center gap-1.5 rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1.5 shadow-sm\",\n \"focus-within:border-[var(--brand)] focus-within:ring-1 focus-within:ring-[var(--brand)]\",\n disabled && \"cursor-default bg-[var(--bg-surface-2)]\",\n isInvalid && FIELD_INVALID,\n )}\n onClick={() => !disabled && inputRef.current?.focus()}\n >\n {/* `display: contents` on the wrappers so the chips participate in the field's\n own flex wrap rather than forming a nested row. The list semantics are\n restored with explicit roles, because `contents` removes the ul/li boxes from\n the accessibility tree in several engines. */}\n {/* eslint-disable-next-line jsx-a11y/no-redundant-roles -- NOT redundant here: the\n `contents` display below removes the ul and li boxes from the box tree, and\n several engines drop the list semantics with them. The explicit role is what\n puts \"list, 3 items\" back. */}\n <ul id={listId} role=\"list\" className=\"contents\">\n {value.map((v, i) => (\n <li key={`${v}-${i}`} role=\"presentation\" className=\"contents\">\n <Chip\n ref={(el: HTMLElement | null) => {\n chipRefs.current[i] = el;\n }}\n tone={tone}\n size={size}\n disabled={disabled}\n removeLabel={text.remove}\n onRemove={disabled ? undefined : () => removeAt(i)}\n // A ROVING tabIndex: only the focused chip is tabbable, so Tab leaves\n // the field instead of walking through every value in it — the reason a\n // list of twenty tags is otherwise a twenty-stop detour for a keyboard.\n tabIndex={focusedChip === i ? 0 : -1}\n role=\"listitem\"\n aria-label={v}\n onKeyDown={(e) => onChipKeyDown(e, i)}\n onFocus={() => setFocusedChip(i)}\n data-chip-index={i}\n >\n {v}\n </Chip>\n </li>\n ))}\n </ul>\n <input\n id={`${id}-input`}\n ref={inputRef}\n value={draft}\n onChange={(e) => {\n setDraft(e.target.value);\n setMessage(\"\");\n setRejected(null);\n }}\n onKeyDown={onInputKeyDown}\n onBlur={() => {\n // Commit on blur: a value typed and then clicked away from is a value the\n // user meant to add, and losing it silently is the commonest complaint\n // about this pattern.\n if (draft.trim() && commit(draft)) setDraft(\"\");\n }}\n onPaste={(e) => {\n const pasted = e.clipboardData.getData(\"text\");\n const pattern = new RegExp(`[${separators.map((s) => `\\\\${s}`).join(\"\")}\\n\\r]`);\n if (!pattern.test(pasted)) return;\n e.preventDefault();\n if (commitMany(pasted.split(pattern)).added.length) setDraft(\"\");\n }}\n placeholder={value.length ? undefined : placeholder}\n disabled={disabled}\n // A visible `label` is already wired through `htmlFor`, so adding a name\n // here would give the field two. Failing that, the DOM spelling wins and\n // the deprecated `ariaLabel` is the fallback.\n aria-label={label ? undefined : (ariaLabelAttr ?? ariaLabel)}\n aria-describedby={describedBy || undefined}\n aria-invalid={isInvalid || !!rejected || undefined}\n className=\"min-w-[6rem] flex-1 bg-transparent py-0.5 text-sm text-[var(--text-primary)] outline-none placeholder:text-[var(--text-placeholder)] disabled:cursor-default\"\n />\n {/* Additions, removals and rejections move no focus, so nothing would announce\n them. `sr-only-fixed` rather than `sr-only`: this sits inside a consumer's\n markup and must not extend the document height. */}\n <span role=\"status\" aria-live=\"polite\" aria-atomic className=\"sr-only-fixed\">\n {message}\n </span>\n </div>\n {error && (\n <p id={errorId} className=\"text-xs text-[var(--danger)]\">\n {error}\n </p>\n )}\n {rejected && (\n <p id={rejectedId} className=\"text-xs text-[var(--danger)]\" data-chip-input-rejected=\"\">\n {rejected}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["Chip","inner"]}
@@ -152,7 +152,9 @@ interface ComboboxCore<V extends string | number> {
152
152
  rect: AnchorRect | null;
153
153
  /** Where the dropdown goes, clamped to the visible viewport (feedback #135). */
154
154
  placement: AnchoredPanel;
155
- cacheRef: RefObject<Map<V, ComboOption<V>>>;
155
+ /** Put an option in the label cache, so {@link ComboboxCore.resolve} still knows a
156
+ * value the caller just chose after the result list has moved on. */
157
+ rememberOption: (o: ComboOption<V>) => void;
156
158
  resolve: (v: V) => ComboOption<V> | null;
157
159
  /** Close and hand focus back to the trigger — the keyboard paths only. See
158
160
  * {@link useDropdown} for why a pointer dismissal must not do this. */
@@ -111,9 +111,17 @@ function useComboboxCore({
111
111
  const isPhone = useMediaQuery(PHONE_QUERY, false);
112
112
  useOutsideClick([triggerRef, panelRef], close, open && !isPhone);
113
113
  useEscapeKey(closeToTrigger, open);
114
+ const [prevOpen, setPrevOpen] = useState(open);
115
+ const [prevQuery, setPrevQuery] = useState(query);
116
+ if (open !== prevOpen || query !== prevQuery) {
117
+ const opened = open && !prevOpen;
118
+ setPrevOpen(open);
119
+ setPrevQuery(opened ? "" : query);
120
+ if (opened) setQuery("");
121
+ setActive(0);
122
+ }
114
123
  useEffect(() => {
115
124
  if (!open) return;
116
- setQuery("");
117
125
  const id = requestAnimationFrame(() => inputRef.current?.focus());
118
126
  return () => cancelAnimationFrame(id);
119
127
  }, [open]);
@@ -130,9 +138,9 @@ function useComboboxCore({
130
138
  for (const o of results) cacheRef.current.set(o.value, o);
131
139
  }, [options, results]);
132
140
  const resolve = (v) => options?.find((o) => o.value === v) ?? cacheRef.current.get(v) ?? null;
133
- useEffect(() => {
134
- setActive(0);
135
- }, [query, open]);
141
+ const rememberOption = (o) => {
142
+ cacheRef.current.set(o.value, o);
143
+ };
136
144
  return {
137
145
  triggerRef,
138
146
  panelRef,
@@ -150,7 +158,7 @@ function useComboboxCore({
150
158
  minChars,
151
159
  rect,
152
160
  placement,
153
- cacheRef,
161
+ rememberOption,
154
162
  resolve,
155
163
  closeToTrigger
156
164
  };
@@ -400,6 +408,9 @@ function ComboboxPanel({
400
408
  if (!rect) return null;
401
409
  const inline = dir === "rtl" ? { right: (document.documentElement.clientWidth || window.innerWidth) - rect.right } : { left: rect.left };
402
410
  return createPortal(
411
+ /* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- never focused
412
+ itself: it catches the listbox keys bubbling from the search <input> inside it,
413
+ which holds focus while the panel is open. */
403
414
  /* @__PURE__ */ jsxs(
404
415
  "div",
405
416
  {