@eifi1/ui-kit 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/README.md +69 -33
  2. package/dist/chart.d.ts +1 -0
  3. package/dist/components/account-settings.js +3 -2
  4. package/dist/components/account-settings.js.map +1 -1
  5. package/dist/components/amount-input.d.ts +3 -0
  6. package/dist/components/amount-input.js +6 -4
  7. package/dist/components/amount-input.js.map +1 -1
  8. package/dist/components/autocomplete.d.ts +36 -7
  9. package/dist/components/autocomplete.js +68 -22
  10. package/dist/components/autocomplete.js.map +1 -1
  11. package/dist/components/calculator.d.ts +3 -0
  12. package/dist/components/calculator.js +2 -1
  13. package/dist/components/calculator.js.map +1 -1
  14. package/dist/components/chart-zoom.js +2 -1
  15. package/dist/components/chart-zoom.js.map +1 -1
  16. package/dist/components/chart.d.ts +33 -2
  17. package/dist/components/chart.js +20 -8
  18. package/dist/components/chart.js.map +1 -1
  19. package/dist/components/checkbox.d.ts +9 -0
  20. package/dist/components/checkbox.js +7 -2
  21. package/dist/components/checkbox.js.map +1 -1
  22. package/dist/components/chip.d.ts +34 -14
  23. package/dist/components/chip.js +63 -28
  24. package/dist/components/chip.js.map +1 -1
  25. package/dist/components/choice-card.d.ts +18 -1
  26. package/dist/components/choice-card.js +21 -8
  27. package/dist/components/choice-card.js.map +1 -1
  28. package/dist/components/combobox-core.d.ts +61 -2
  29. package/dist/components/combobox-core.js +68 -17
  30. package/dist/components/combobox-core.js.map +1 -1
  31. package/dist/components/combobox.d.ts +11 -7
  32. package/dist/components/combobox.js +51 -28
  33. package/dist/components/combobox.js.map +1 -1
  34. package/dist/components/currency-select.js +3 -3
  35. package/dist/components/currency-select.js.map +1 -1
  36. package/dist/components/danger-confirm.d.ts +30 -6
  37. package/dist/components/danger-confirm.js +51 -32
  38. package/dist/components/danger-confirm.js.map +1 -1
  39. package/dist/components/data-table-filter-popover.d.ts +1 -1
  40. package/dist/components/data-table-filters.d.ts +1 -1
  41. package/dist/components/data-table-pagination.d.ts +6 -3
  42. package/dist/components/data-table-pagination.js +6 -5
  43. package/dist/components/data-table-pagination.js.map +1 -1
  44. package/dist/components/data-table.d.ts +1 -1
  45. package/dist/components/data-table.js +332 -248
  46. package/dist/components/data-table.js.map +1 -1
  47. package/dist/components/date-picker.js +17 -7
  48. package/dist/components/date-picker.js.map +1 -1
  49. package/dist/components/dialog-frame.d.ts +28 -3
  50. package/dist/components/dialog-frame.js +47 -11
  51. package/dist/components/dialog-frame.js.map +1 -1
  52. package/dist/components/disclosure.d.ts +22 -2
  53. package/dist/components/disclosure.js +41 -8
  54. package/dist/components/disclosure.js.map +1 -1
  55. package/dist/components/dropdown.d.ts +7 -3
  56. package/dist/components/dropdown.js +6 -2
  57. package/dist/components/dropdown.js.map +1 -1
  58. package/dist/components/entity-combobox.d.ts +17 -8
  59. package/dist/components/entity-combobox.js +7 -5
  60. package/dist/components/entity-combobox.js.map +1 -1
  61. package/dist/components/file-button.d.ts +36 -5
  62. package/dist/components/file-button.js +42 -3
  63. package/dist/components/file-button.js.map +1 -1
  64. package/dist/components/file-dropzone.d.ts +9 -5
  65. package/dist/components/file-dropzone.js +14 -19
  66. package/dist/components/file-dropzone.js.map +1 -1
  67. package/dist/components/grouped-picker.js +2 -2
  68. package/dist/components/grouped-picker.js.map +1 -1
  69. package/dist/components/hover-menu.d.ts +8 -3
  70. package/dist/components/hover-menu.js +88 -4
  71. package/dist/components/hover-menu.js.map +1 -1
  72. package/dist/components/icon-picker.d.ts +10 -1
  73. package/dist/components/icon-picker.js +5 -1
  74. package/dist/components/icon-picker.js.map +1 -1
  75. package/dist/components/measured-grid.d.ts +165 -0
  76. package/dist/components/measured-grid.js +559 -0
  77. package/dist/components/measured-grid.js.map +1 -0
  78. package/dist/components/mini-calendar.js +7 -8
  79. package/dist/components/mini-calendar.js.map +1 -1
  80. package/dist/components/month-picker.js +13 -9
  81. package/dist/components/month-picker.js.map +1 -1
  82. package/dist/components/multi-entity-combobox.js +2 -2
  83. package/dist/components/multi-entity-combobox.js.map +1 -1
  84. package/dist/components/multi-select.d.ts +9 -1
  85. package/dist/components/multi-select.js +22 -8
  86. package/dist/components/multi-select.js.map +1 -1
  87. package/dist/components/number-field.d.ts +3 -0
  88. package/dist/components/number-input.d.ts +3 -0
  89. package/dist/components/number-input.js +3 -3
  90. package/dist/components/number-input.js.map +1 -1
  91. package/dist/components/numpad-sheet.d.ts +3 -0
  92. package/dist/components/numpad-sheet.js +3 -3
  93. package/dist/components/numpad-sheet.js.map +1 -1
  94. package/dist/components/picker-sheet.js +1 -1
  95. package/dist/components/picker-sheet.js.map +1 -1
  96. package/dist/components/popover.js +11 -2
  97. package/dist/components/popover.js.map +1 -1
  98. package/dist/components/series-chart-ticks.d.ts +13 -0
  99. package/dist/components/series-chart-ticks.js +29 -0
  100. package/dist/components/series-chart-ticks.js.map +1 -0
  101. package/dist/components/series-chart.js +33 -27
  102. package/dist/components/series-chart.js.map +1 -1
  103. package/dist/components/swatch-picker.d.ts +10 -1
  104. package/dist/components/swatch-picker.js +2 -0
  105. package/dist/components/swatch-picker.js.map +1 -1
  106. package/dist/components/swipeable-row.d.ts +11 -2
  107. package/dist/components/swipeable-row.js +5 -3
  108. package/dist/components/swipeable-row.js.map +1 -1
  109. package/dist/components/switch.d.ts +9 -0
  110. package/dist/components/switch.js +7 -2
  111. package/dist/components/switch.js.map +1 -1
  112. package/dist/components/tile-radio.js +5 -7
  113. package/dist/components/tile-radio.js.map +1 -1
  114. package/dist/components/toggle-group.d.ts +13 -3
  115. package/dist/components/toggle-group.js +21 -1
  116. package/dist/components/toggle-group.js.map +1 -1
  117. package/dist/components/toggle-legend.js +1 -2
  118. package/dist/components/toggle-legend.js.map +1 -1
  119. package/dist/components/treemap.d.ts +6 -1
  120. package/dist/components/treemap.js +16 -6
  121. package/dist/components/treemap.js.map +1 -1
  122. package/dist/components/ui.d.ts +31 -7
  123. package/dist/components/ui.js +26 -18
  124. package/dist/components/ui.js.map +1 -1
  125. package/dist/components/use-anchor-dir.d.ts +15 -0
  126. package/dist/components/use-anchor-dir.js +14 -0
  127. package/dist/components/use-anchor-dir.js.map +1 -0
  128. package/dist/components/use-mobile-reveal.d.ts +8 -1
  129. package/dist/components/use-mobile-reveal.js +3 -2
  130. package/dist/components/use-mobile-reveal.js.map +1 -1
  131. package/dist/components/use-table-state.d.ts +4 -4
  132. package/dist/components/use-table-state.js +12 -5
  133. package/dist/components/use-table-state.js.map +1 -1
  134. package/dist/{data-table-filters-DM1auUfH.d.ts → data-table-filters-noy0Abvi.d.ts} +40 -7
  135. package/dist/data-table.d.ts +1 -1
  136. package/dist/data-table.js.map +1 -1
  137. package/dist/feedback/feedback-attachment.d.ts +22 -3
  138. package/dist/feedback/feedback-attachment.js +15 -7
  139. package/dist/feedback/feedback-attachment.js.map +1 -1
  140. package/dist/feedback/feedback-dialog.d.ts +3 -2
  141. package/dist/feedback/feedback-dialog.js.map +1 -1
  142. package/dist/feedback.d.ts +1 -1
  143. package/dist/hooks/use-windowed-rows.d.ts +29 -0
  144. package/dist/hooks/use-windowed-rows.js +31 -0
  145. package/dist/hooks/use-windowed-rows.js.map +1 -0
  146. package/dist/i18n/defaults.d.ts +3 -0
  147. package/dist/i18n/defaults.js +5 -1
  148. package/dist/i18n/defaults.js.map +1 -1
  149. package/dist/i18n/kit-labels.d.ts +5 -0
  150. package/dist/i18n/kit-labels.js.map +1 -1
  151. package/dist/i18n/locales/de-CH.d.ts +43 -0
  152. package/dist/i18n/locales/de-CH.js +21 -0
  153. package/dist/i18n/locales/de-CH.js.map +1 -0
  154. package/dist/i18n/locales/de.d.ts +43 -0
  155. package/dist/i18n/locales/de.js +344 -0
  156. package/dist/i18n/locales/de.js.map +1 -0
  157. package/dist/i18n/locales/es.d.ts +43 -0
  158. package/dist/i18n/locales/es.js +339 -0
  159. package/dist/i18n/locales/es.js.map +1 -0
  160. package/dist/i18n/locales/fr.d.ts +43 -0
  161. package/dist/i18n/locales/fr.js +344 -0
  162. package/dist/i18n/locales/fr.js.map +1 -0
  163. package/dist/i18n/locales/hu.d.ts +43 -0
  164. package/dist/i18n/locales/hu.js +343 -0
  165. package/dist/i18n/locales/hu.js.map +1 -0
  166. package/dist/i18n/locales/it.d.ts +43 -0
  167. package/dist/i18n/locales/it.js +342 -0
  168. package/dist/i18n/locales/it.js.map +1 -0
  169. package/dist/i18n/locales/zh.d.ts +43 -0
  170. package/dist/i18n/locales/zh.js +338 -0
  171. package/dist/i18n/locales/zh.js.map +1 -0
  172. package/dist/index.d.ts +6 -3
  173. package/dist/index.js +4 -1
  174. package/dist/index.js.map +1 -1
  175. package/dist/lib/direction.d.ts +24 -0
  176. package/dist/lib/direction.js +19 -0
  177. package/dist/lib/direction.js.map +1 -0
  178. package/dist/rhf/form.js.map +1 -1
  179. package/dist/search/command-palette.d.ts +5 -0
  180. package/dist/search/command-palette.js +80 -50
  181. package/dist/search/command-palette.js.map +1 -1
  182. package/dist/shell/app-shell.d.ts +13 -1
  183. package/dist/shell/app-shell.js +133 -84
  184. package/dist/shell/app-shell.js.map +1 -1
  185. package/dist/shell/topbar-action-menu.js +1 -1
  186. package/dist/shell/topbar-action-menu.js.map +1 -1
  187. package/dist/shell/topbar-controls.d.ts +7 -3
  188. package/dist/shell/topbar-controls.js +38 -31
  189. package/dist/shell/topbar-controls.js.map +1 -1
  190. package/dist/theme/chart-palette.d.ts +7 -1
  191. package/dist/theme/chart-palette.js +2 -1
  192. package/dist/theme/chart-palette.js.map +1 -1
  193. package/dist/tour/tour.d.ts +11 -5
  194. package/dist/tour/tour.js +124 -58
  195. package/dist/tour/tour.js.map +1 -1
  196. package/dist/wizard/use-wizard.js +30 -13
  197. package/dist/wizard/use-wizard.js.map +1 -1
  198. package/dist/wizard/wizard-summary.js +1 -1
  199. package/dist/wizard/wizard-summary.js.map +1 -1
  200. package/package.json +12 -2
  201. package/src/components/account-settings.tsx +6 -2
  202. package/src/components/amount-input.tsx +10 -8
  203. package/src/components/autocomplete.tsx +123 -33
  204. package/src/components/calculator.tsx +5 -1
  205. package/src/components/chart-zoom.tsx +8 -1
  206. package/src/components/chart.tsx +68 -7
  207. package/src/components/checkbox.tsx +16 -0
  208. package/src/components/chip.tsx +131 -49
  209. package/src/components/choice-card.tsx +77 -36
  210. package/src/components/combobox-core.tsx +147 -20
  211. package/src/components/combobox.tsx +100 -44
  212. package/src/components/currency-select.tsx +6 -6
  213. package/src/components/danger-confirm.tsx +90 -36
  214. package/src/components/data-table-pagination.tsx +32 -19
  215. package/src/components/data-table.tsx +198 -36
  216. package/src/components/date-picker.tsx +24 -7
  217. package/src/components/dialog-frame.tsx +97 -22
  218. package/src/components/disclosure.tsx +75 -13
  219. package/src/components/dropdown.tsx +17 -5
  220. package/src/components/entity-combobox.tsx +26 -13
  221. package/src/components/file-button.tsx +102 -6
  222. package/src/components/file-dropzone.tsx +44 -35
  223. package/src/components/grouped-picker.tsx +2 -2
  224. package/src/components/hover-menu.tsx +139 -10
  225. package/src/components/icon-picker.tsx +14 -1
  226. package/src/components/measured-grid.tsx +852 -0
  227. package/src/components/mini-calendar.tsx +11 -8
  228. package/src/components/month-picker.tsx +18 -11
  229. package/src/components/multi-entity-combobox.tsx +2 -2
  230. package/src/components/multi-select.tsx +34 -7
  231. package/src/components/number-input.tsx +4 -4
  232. package/src/components/numpad-sheet.tsx +5 -3
  233. package/src/components/picker-sheet.tsx +1 -1
  234. package/src/components/popover.tsx +18 -3
  235. package/src/components/series-chart-ticks.ts +52 -0
  236. package/src/components/series-chart.tsx +39 -31
  237. package/src/components/swatch-picker.tsx +11 -0
  238. package/src/components/swipeable-row.tsx +21 -6
  239. package/src/components/switch.tsx +16 -0
  240. package/src/components/tile-radio.tsx +5 -7
  241. package/src/components/toggle-group.tsx +39 -3
  242. package/src/components/toggle-legend.tsx +5 -2
  243. package/src/components/treemap.tsx +28 -5
  244. package/src/components/ui.tsx +74 -30
  245. package/src/components/use-anchor-dir.ts +20 -0
  246. package/src/components/use-mobile-reveal.ts +10 -2
  247. package/src/components/use-table-state.ts +44 -7
  248. package/src/data-table.ts +1 -0
  249. package/src/feedback/feedback-attachment.tsx +36 -9
  250. package/src/feedback/feedback-dialog.tsx +3 -2
  251. package/src/hooks/use-windowed-rows.ts +69 -0
  252. package/src/i18n/defaults.ts +4 -0
  253. package/src/i18n/kit-labels.tsx +4 -0
  254. package/src/i18n/locales/de-CH.ts +30 -0
  255. package/src/i18n/locales/de.ts +365 -0
  256. package/src/i18n/locales/es.ts +358 -0
  257. package/src/i18n/locales/fr.ts +371 -0
  258. package/src/i18n/locales/hu.ts +359 -0
  259. package/src/i18n/locales/it.ts +360 -0
  260. package/src/i18n/locales/zh.ts +348 -0
  261. package/src/index.ts +10 -1
  262. package/src/lib/direction.ts +37 -0
  263. package/src/rhf/form.tsx +21 -0
  264. package/src/search/command-palette.tsx +77 -19
  265. package/src/shell/app-shell.tsx +200 -129
  266. package/src/shell/topbar-action-menu.tsx +1 -1
  267. package/src/shell/topbar-controls.tsx +19 -8
  268. package/src/theme/chart-palette.ts +9 -2
  269. package/src/tour/tour.tsx +119 -18
  270. package/src/wizard/use-wizard.ts +55 -17
  271. package/src/wizard/wizard-summary.tsx +1 -1
@@ -8,9 +8,14 @@ import type {
8
8
  } from "react";
9
9
  import { createPortal } from "react-dom";
10
10
  import { cn } from "../lib/cn";
11
- import { FieldLabel, FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID, Spinner } from "./ui";
11
+ import { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID, Spinner } from "./ui";
12
12
  import { useAnchoredPanel } from "../hooks/use-anchored-panel";
13
+ import { useAnchorDir } from "./use-anchor-dir";
13
14
  import {
15
+ ComboboxFieldLabel,
16
+ DISABLED_ROW_CLASS,
17
+ isOptionEnabled,
18
+ stepEnabled,
14
19
  useActiveOptionScroll,
15
20
  useComboboxFieldError,
16
21
  useOptionSource,
@@ -26,7 +31,7 @@ import { DEFAULT_COMBOBOX_LABELS, useKitLabels } from "../i18n/kit-labels";
26
31
  export interface AutocompleteProps<V extends string | number = string>
27
32
  extends Omit<
28
33
  ComponentPropsWithoutRef<"input">,
29
- "value" | "defaultValue" | "onChange" | "onSelect" | "children" | "type"
34
+ "value" | "defaultValue" | "onChange" | "onSelect" | "children" | "type" | "size"
30
35
  > {
31
36
  /** What is in the field. Controlled, and never reset by the component: opening,
32
37
  * closing and a failed lookup all leave it exactly as typed. */
@@ -34,7 +39,8 @@ export interface AutocompleteProps<V extends string | number = string>
34
39
  /** Every keystroke — and, with `fillOnSelect`, the label of a taken suggestion. */
35
40
  onChange: (text: string) => void;
36
41
  /** Suggestions the caller already has (e.g. from its own query hook). Narrowed by
37
- * the text unless `filter={false}`. */
42
+ * the text unless `filter={false}`. An option with `disabled: true` is listed but
43
+ * cannot be taken — see {@link ComboOption.disabled}. */
38
44
  options?: ComboOption<V>[];
39
45
  /** Or: fetch them. Debounced (`debounceMs`), race-safe, only at `minChars` and up,
40
46
  * never while the field is idle; a rejection shows `loadErrorLabel` and empties
@@ -52,6 +58,30 @@ export interface AutocompleteProps<V extends string | number = string>
52
58
  * inter-key interval is 150–250 ms, so the pickers' 150 fires on nearly every
53
59
  * letter, which a rate-limited geocoder cannot afford. */
54
60
  debounceMs?: number;
61
+ /**
62
+ * Whether the list MAY show — controlled. Omit (or pass `undefined`) and the field
63
+ * decides: open while focused, until Escape, Tab or a taken row. `false` keeps it
64
+ * shut whatever the user does, and stops `loadOptions` asking — keksdose's address
65
+ * search, with its confirm map up, where it used to pass `minChars={Infinity}`.
66
+ * `true` shows it even unfocused.
67
+ *
68
+ * Even open, the list only APPEARS when it has something to show (rows, or a
69
+ * status line); `aria-expanded` follows what appears, not this flag.
70
+ */
71
+ open?: boolean;
72
+ /** The field wants the list open or shut — focus, typing, ↓/↑ open it; blur,
73
+ * Escape, Tab and taking a row shut it. Called on every change, controlled or not. */
74
+ onOpenChange?: (open: boolean) => void;
75
+ /**
76
+ * `"sm"`: the 28px, 12px-type field of {@link Select}'s `size="sm"`, with the icon,
77
+ * spinner and list rows scaled to match — for a card or toolbar whose buttons are
78
+ * small. `"md"` (default) is the form field. Unlabelled only, as on `Select`: a
79
+ * floating label needs the tall box, so a labelled field ignores `"sm"`.
80
+ *
81
+ * A NUMBER is still the native `size` attribute (the width in characters), passed
82
+ * straight through.
83
+ */
84
+ size?: "sm" | "md" | number;
55
85
  /** A suggestion was taken (click, or Enter on the highlighted row). */
56
86
  onSelect?: (option: ComboOption<V>) => void;
57
87
  /**
@@ -61,11 +91,12 @@ export interface AutocompleteProps<V extends string | number = string>
61
91
  * and the field is only the search that found it; the text then stays as typed.
62
92
  */
63
93
  fillOnSelect?: boolean;
64
- /** A floating label, as on {@link Combobox}. Names the field (`aria-labelledby`)
65
- * unless an `aria-label` is given. */
94
+ /** A floating label, as on {@link Combobox} — a real `<label for>` on the input.
95
+ * Names the field (`aria-labelledby`) unless an `aria-label` is given. */
66
96
  label?: ReactNode;
67
97
  /** Leading decoration inside the field (a pin, a magnifier). Decorative: the
68
- * label names the field. */
98
+ * label names the field. An `<svg>` child is sized by the field — 16px, or 14px
99
+ * at `size="sm"` — so a lucide icon needs no class of its own. */
69
100
  icon?: ReactNode;
70
101
  /** Required and unanswered — {@link FIELD_INVALID}. */
71
102
  invalid?: boolean;
@@ -107,6 +138,9 @@ function AutocompleteInner<V extends string | number = string>(
107
138
  status,
108
139
  emptyLabel,
109
140
  loadErrorLabel,
141
+ open: openProp,
142
+ onOpenChange,
143
+ size,
110
144
  className,
111
145
  inputClassName,
112
146
  id,
@@ -136,7 +170,15 @@ function AutocompleteInner<V extends string | number = string>(
136
170
 
137
171
  /** The user's intent: the field is focused and the list has not been dismissed.
138
172
  * Whether anything SHOWS is decided below, from what there is to show. */
139
- const [open, setOpen] = useState(false);
173
+ const [ownOpen, setOwnOpen] = useState(false);
174
+ // Controlled when the caller passes a boolean. The own state is kept up to date
175
+ // either way, so a caller that controls only SOMETIMES (`open={confirming ? false :
176
+ // undefined}`) hands back a field that is exactly as open as focus says it is.
177
+ const open = openProp ?? ownOpen;
178
+ const setOpen = (next: boolean) => {
179
+ setOwnOpen(next);
180
+ if (next !== open) onOpenChange?.(next);
181
+ };
140
182
  const [active, setActive] = useState(-1);
141
183
  const live = open && !disabled;
142
184
  const { results, busy, failed, tooShort } = useOptionSource<V>({
@@ -165,22 +207,29 @@ function AutocompleteInner<V extends string | number = string>(
165
207
  ? labels.noResults
166
208
  : null;
167
209
  const expanded = live && (results.length > 0 || statusLine !== null);
168
- const activeId =
169
- expanded && active >= 0 && active < results.length ? optionId(active) : undefined;
210
+ // A disabled row is never the keyboard's — not even when a list that changed under
211
+ // the highlight put one there.
212
+ const activeId = expanded && isOptionEnabled(results[active]) ? optionId(active) : undefined;
170
213
  useActiveOptionScroll(activeId);
171
214
  const { rect, top, maxHeight } = useAnchoredPanel(fieldRef, expanded, { preferredHeight: 256 });
215
+ const dir = useAnchorDir(fieldRef, expanded);
172
216
 
173
217
  const close = () => {
174
218
  setOpen(false);
175
219
  setActive(-1);
176
220
  };
177
221
  const take = (o: ComboOption<V>) => {
222
+ if (o.disabled) return;
178
223
  if (fillOnSelect) onChange(o.label);
179
224
  onSelect?.(o);
180
225
  close();
181
226
  };
182
227
 
183
228
  const hasLabel = label !== undefined;
229
+ // `size="sm"` is for an UNLABELLED field, as on Select: the floating label needs the
230
+ // tall box. A number is the native attribute.
231
+ const small = size === "sm" && !hasLabel;
232
+ const nativeSize = typeof size === "number" ? size : undefined;
184
233
  // The caller's name wins; else the floating label, by reference so a non-string
185
234
  // label still names the field.
186
235
  const labelledBy =
@@ -190,12 +239,21 @@ function AutocompleteInner<V extends string | number = string>(
190
239
  // `relative`: the floating label and the live region's `sr-only` both need a
191
240
  // local containing block.
192
241
  <div className={cn("relative", className)}>
193
- {hasLabel && <FieldLabel id={labelId}>{label}</FieldLabel>}
242
+ {/* A real <label for> (lenkbank), and still `aria-labelledby`'s target, so the
243
+ name — and the listbox's, which borrows it — is exactly what it was. */}
244
+ {hasLabel && (
245
+ <ComboboxFieldLabel id={labelId} htmlFor={fieldId}>
246
+ {label}
247
+ </ComboboxFieldLabel>
248
+ )}
194
249
  <div ref={fieldRef} className="relative">
195
250
  {icon && (
196
251
  <span
197
252
  aria-hidden
198
- className="pointer-events-none absolute start-2.5 top-1/2 flex -translate-y-1/2 text-[var(--text-muted)] [&>svg]:size-4"
253
+ className={cn(
254
+ "pointer-events-none absolute top-1/2 flex -translate-y-1/2 text-[var(--text-muted)]",
255
+ small ? "start-2 [&>svg]:size-3.5" : "start-2.5 [&>svg]:size-4",
256
+ )}
199
257
  >
200
258
  {icon}
201
259
  </span>
@@ -204,6 +262,7 @@ function AutocompleteInner<V extends string | number = string>(
204
262
  {...rest}
205
263
  ref={ref}
206
264
  id={fieldId}
265
+ size={nativeSize}
207
266
  type="text"
208
267
  value={value}
209
268
  disabled={disabled}
@@ -234,18 +293,28 @@ function AutocompleteInner<V extends string | number = string>(
234
293
  setActive(-1);
235
294
  }}
236
295
  onKeyDown={(e) => {
296
+ // An Escape that closes the OPEN list is the list's, and is consumed before
297
+ // the caller sees it. A caller whose Escape means "close the panel" (keksdose's
298
+ // address search) otherwise closed the whole panel when the user only meant
299
+ // to dismiss the suggestions — the opposite of what the docs promised.
300
+ if (e.key === "Escape" && expanded) {
301
+ e.preventDefault();
302
+ e.stopPropagation();
303
+ close();
304
+ return;
305
+ }
237
306
  onKeyDown?.(e);
238
307
  if (e.defaultPrevented) return;
239
- const last = results.length - 1;
308
+ // Disabled rows are passed over; at either end the highlight stays put.
240
309
  if (e.key === "ArrowDown") {
241
310
  e.preventDefault();
242
311
  if (!open) setOpen(true);
243
- else setActive((i) => Math.min(i + 1, last));
312
+ else setActive((i) => stepEnabled(results, i, 1));
244
313
  } else if (e.key === "ArrowUp") {
245
314
  e.preventDefault();
246
315
  if (!open) setOpen(true);
247
316
  // From "nothing highlighted", Up goes to the bottom, as the APG has it.
248
- else setActive((i) => (i < 0 ? last : Math.max(i - 1, 0)));
317
+ else setActive((i) => stepEnabled(results, i < 0 ? results.length : i, -1));
249
318
  } else if (e.key === "Enter") {
250
319
  // No row highlighted: the text is the answer, and a form's own submit
251
320
  // is left alone.
@@ -253,14 +322,6 @@ function AutocompleteInner<V extends string | number = string>(
253
322
  e.preventDefault();
254
323
  take(results[active]);
255
324
  }
256
- } else if (e.key === "Escape") {
257
- // Only when there is a list to close — otherwise the Escape belongs to
258
- // whatever this field sits in (a dialog), and must reach it.
259
- if (expanded) {
260
- e.preventDefault();
261
- e.stopPropagation();
262
- close();
263
- }
264
325
  } else if (e.key === "Tab") {
265
326
  close();
266
327
  }
@@ -268,16 +329,24 @@ function AutocompleteInner<V extends string | number = string>(
268
329
  className={cn(
269
330
  FIELD_BASE,
270
331
  hasLabel && FIELD_FLOATING_PAD,
271
- icon ? "ps-8" : undefined,
272
- busy && "pe-9",
332
+ // Select's `SELECT_SM` box: a fixed 28px, so the caller's line height
333
+ // cannot grow it.
334
+ small && "h-7 py-0 pe-2 ps-2 text-xs",
335
+ icon ? (small ? "ps-7" : "ps-8") : undefined,
336
+ busy && (small ? "pe-7" : "pe-9"),
273
337
  field.isInvalid && FIELD_INVALID,
274
338
  inputClassName,
275
339
  )}
276
340
  />
277
341
  {busy && live && (
278
342
  // Decorative: the live region below already says "Loading…".
279
- <span className="pointer-events-none absolute end-2.5 top-1/2 flex -translate-y-1/2">
280
- <Spinner label={null} className="h-4 w-4" />
343
+ <span
344
+ className={cn(
345
+ "pointer-events-none absolute top-1/2 flex -translate-y-1/2",
346
+ small ? "end-2" : "end-2.5",
347
+ )}
348
+ >
349
+ <Spinner label={null} className={small ? "h-3.5 w-3.5" : "h-4 w-4"} />
281
350
  </span>
282
351
  )}
283
352
  </div>
@@ -303,6 +372,9 @@ function AutocompleteInner<V extends string | number = string>(
303
372
  // Every press inside the list keeps focus in the field — rows, the status
304
373
  // line, the scrollbar — or the input's blur would close the list first.
305
374
  role="presentation"
375
+ // Portalled out of the form's `dir`; the field's is put back. The list is
376
+ // exactly the field's width, so `left` places it in either direction.
377
+ dir={dir}
306
378
  onMouseDown={(e) => e.preventDefault()}
307
379
  className="fixed z-50 flex flex-col overflow-hidden rounded-md border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg"
308
380
  style={{ top, left: rect.left, width: rect.width, maxHeight }}
@@ -338,12 +410,22 @@ function AutocompleteInner<V extends string | number = string>(
338
410
  // field holds no value, so the only row that can be chosen
339
411
  // is the one whose label the text already is.
340
412
  aria-selected={fillOnSelect && o.label === value}
413
+ // Listed, announced as unavailable, never taken: `take`
414
+ // refuses it and the arrows pass it over.
415
+ aria-disabled={o.disabled || undefined}
341
416
  tabIndex={-1}
342
417
  onClick={() => take(o)}
343
- onMouseEnter={() => setActive(i)}
418
+ onMouseEnter={() => {
419
+ if (!o.disabled) setActive(i);
420
+ }}
344
421
  className={cn(
345
- "flex w-full items-center gap-2 px-3 py-1.5 text-start text-sm",
346
- i === active ? "bg-[var(--bg-active)]" : "hover:bg-[var(--bg-hover)]",
422
+ "flex w-full items-center gap-2 px-3 text-start",
423
+ small ? "py-1 text-xs" : "py-1.5 text-sm",
424
+ o.disabled
425
+ ? DISABLED_ROW_CLASS
426
+ : i === active
427
+ ? "bg-[var(--bg-active)]"
428
+ : "hover:bg-[var(--bg-hover)]",
347
429
  o.group != null && "ps-6",
348
430
  )}
349
431
  >
@@ -357,7 +439,12 @@ function AutocompleteInner<V extends string | number = string>(
357
439
  {o.label}
358
440
  </span>
359
441
  {o.sublabel && (
360
- <span className="block truncate text-xs text-[var(--text-placeholder)]">
442
+ <span
443
+ className={cn(
444
+ "block truncate text-[var(--text-placeholder)]",
445
+ small ? "text-[11px]" : "text-xs",
446
+ )}
447
+ >
361
448
  {o.sublabel}
362
449
  </span>
363
450
  )}
@@ -374,7 +461,7 @@ function AutocompleteInner<V extends string | number = string>(
374
461
  <div
375
462
  aria-hidden
376
463
  className={cn(
377
- "px-3 py-2 text-sm",
464
+ small ? "px-3 py-1.5 text-xs" : "px-3 py-2 text-sm",
378
465
  failed && !hasStatus ? "text-[var(--danger)]" : "text-[var(--text-muted)]",
379
466
  results.length > 0 && "border-t border-[var(--border)] text-xs",
380
467
  )}
@@ -409,9 +496,12 @@ AutocompleteBase.displayName = "Autocomplete";
409
496
  *
410
497
  * Focus never leaves the `<input>`: `aria-activedescendant` names the highlighted
411
498
  * row, `aria-controls` the list, and a polite live region says what the list now
412
- * holds. Keyboard: ↓/↑ move (↓ opens), Enter takes the highlighted row (with none,
499
+ * holds. Keyboard: ↓/↑ move (↓ opens) over every row but a `disabled` one, Enter
500
+ * takes the highlighted row (with none,
413
501
  * Enter is left to the form), Escape closes, Tab closes and moves on; Home/End stay
414
- * with the caret. A caller's `onKeyDown` runs first and may `preventDefault()` to
502
+ * with the caret. An Escape that closes an open list is consumed and never reaches the
503
+ * caller; with the list closed it does. For every other key a caller's `onKeyDown`
504
+ * runs first and may `preventDefault()` to
415
505
  * keep a key for itself.
416
506
  *
417
507
  * The list is portalled and anchored under the field on every screen size: the
@@ -147,9 +147,13 @@ function CalculatorPanel({
147
147
  equals();
148
148
  }
149
149
  }}
150
+ // Arithmetic reads left to right in every script, and a calculator display
151
+ // is right-aligned in every script too — so both stay physical, pinned
152
+ // with `dir="ltr"` rather than mirrored in a right-to-left form.
153
+ dir="ltr"
150
154
  className="block w-full rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1.5 text-right font-mono text-sm text-[var(--text-primary)] focus:border-[var(--border-strong)] focus:ring-[var(--border-strong)]"
151
155
  />
152
- <div className="h-4 pr-1 text-right font-mono text-xs text-[var(--text-placeholder)]">
156
+ <div dir="ltr" className="h-4 pr-1 text-right font-mono text-xs text-[var(--text-placeholder)]">
153
157
  {result !== null && formatResult(result) !== text.trim() ? `= ${formatResult(result)}` : ""}
154
158
  </div>
155
159
  <div className="grid grid-cols-4 gap-1.5">
@@ -337,6 +337,13 @@ export function withChartZoom<P extends ZoomTarget>(Chart: ComponentType<P>) {
337
337
  };
338
338
  const state: ZoomState = { x: xWindow, y: ownY };
339
339
  const zoomed = xWindow !== undefined || ownY !== null;
340
+ // A chart handed no rows or no series draws its empty state, not a plot — and a
341
+ // "Reset zoom" floating over "No data" (every line toggled off while zoomed) offers
342
+ // to undo something the reader cannot see. The zoom itself is KEPT: it is where they
343
+ // were looking, and it applies again the moment a line comes back. Only an array
344
+ // that is there and empty counts; a chart that does not pass its data is not known
345
+ // to be empty.
346
+ const empty = props.rows?.length === 0 || props.series?.length === 0;
340
347
 
341
348
  const clear = () => {
342
349
  setXWindow(undefined);
@@ -362,7 +369,7 @@ export function withChartZoom<P extends ZoomTarget>(Chart: ComponentType<P>) {
362
369
  return (
363
370
  <div className="relative">
364
371
  <Chart {...props} zoom={binding} />
365
- {zoomed && (
372
+ {zoomed && !empty && (
366
373
  <button
367
374
  type="button"
368
375
  onClick={clear}
@@ -16,6 +16,7 @@ import {
16
16
  Tooltip as RechartsTooltip,
17
17
  } from "recharts";
18
18
  import { cn } from "../lib/cn";
19
+ import { isRtl } from "../lib/direction";
19
20
 
20
21
  export interface ChartSeriesConfig {
21
22
  label: ReactNode;
@@ -37,6 +38,20 @@ interface ChartContainerProps extends ComponentProps<"div"> {
37
38
  children: ComponentProps<typeof ResponsiveContainer>["children"];
38
39
  }
39
40
 
41
+ /**
42
+ * The shell every chart in the kit is drawn inside.
43
+ *
44
+ * **The plot is not mirrored in RTL.** An x axis is a number line or a timeline, and
45
+ * both run left-to-right in Hebrew and Arabic charts as much as in English ones; a
46
+ * time axis flipped to run right-to-left is the surprise, not the courtesy. So no
47
+ * `reversed` on any axis, and the SVG is pinned to `direction: ltr` — without that it
48
+ * inherits the page's `rtl`, which flips what `text-anchor: end` means, and the tick
49
+ * labels of a left-hand y axis are drawn INTO the plot instead of beside it. Text
50
+ * inside the SVG still shapes by its own script; only the anchoring is physical.
51
+ * The HTML around the plot — legend, tooltip, the zoom reset button — follows the
52
+ * page's direction like any other markup. A consumer who does want a mirrored
53
+ * category axis passes `reversed` on its own `XAxis`.
54
+ */
40
55
  export function ChartContainer({ id, className, children, config, ...props }: ChartContainerProps) {
41
56
  const uid = useId();
42
57
  // The id is pasted into an UNQUOTED `[data-chart=…]` selector below, so it has to be
@@ -54,6 +69,8 @@ export function ChartContainer({ id, className, children, config, ...props }: Ch
54
69
  // a horizontal drag is handed to recharts for tooltip scrubbing,
55
70
  // instead of the page scrolling underneath the gesture (feedback #292).
56
71
  "flex w-full touch-pan-y justify-center text-xs",
72
+ // physical anchoring for the unmirrored plot (see the doc comment above)
73
+ "[&_.recharts-surface]:[direction:ltr]",
57
74
  // muted axis labels + gridlines; one class each, since the tokens
58
75
  // already carry the light/dark values
59
76
  "[&_.recharts-cartesian-axis-tick_text]:fill-[var(--text-muted)]",
@@ -101,6 +118,19 @@ const CSS_IDENT = /^[A-Za-z_][A-Za-z0-9_-]*$/;
101
118
  const CSS_COLOR =
102
119
  /^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|(?:rgba?|hsla?)\([0-9A-Za-z.,%/ -]*\)|var\(--[A-Za-z0-9_-]+\))$/;
103
120
 
121
+ /**
122
+ * The colour an entry is painted with, or `undefined` when the style block refuses it.
123
+ *
124
+ * The one gate both sides go through: {@link ChartStyle} emits exactly these, and the
125
+ * legend and tooltip swatches show exactly these. They used to read `config[key].color`
126
+ * directly, so an entry the style block had skipped still showed its configured colour
127
+ * in the key — a swatch promising a colour the series was never painted in.
128
+ */
129
+ function paintedColor(key: string, entry: ChartSeriesConfig | undefined): string | undefined {
130
+ if (!entry?.color) return undefined;
131
+ return CSS_IDENT.test(key) && CSS_COLOR.test(entry.color) ? entry.color : undefined;
132
+ }
133
+
104
134
  // Inject each series colour as a `--color-<key>` custom property scoped to this
105
135
  // chart, so chart JSX can refer to `var(--color-total)` and the legend/tooltip
106
136
  // stay in sync from one source of truth.
@@ -127,7 +157,22 @@ function ChartStyle({ id, config }: { id: string; config: ChartConfig }) {
127
157
  return <style dangerouslySetInnerHTML={{ __html: css }} />;
128
158
  }
129
159
 
130
- export const ChartTooltip = RechartsTooltip;
160
+ /**
161
+ * recharts' `Tooltip`, with `filterNull` defaulting to **false**.
162
+ *
163
+ * recharts drops every payload entry whose value is `null`/`undefined` before the
164
+ * content sees it, so the missing-value path {@link ChartTooltipContent} documents —
165
+ * the em dash, `formatValue(undefined)` — never ran under the default: a series with a
166
+ * gap at the cursor simply vanished from the tooltip, which reads as "this series does
167
+ * not exist here" rather than "it has no value here". Kept, the entry prints "—".
168
+ *
169
+ * The other half of that filter — dropping series drawn with `hide` — is redone by
170
+ * `ChartTooltipContent` itself (honouring `includeHidden`), so a legend toggle still
171
+ * takes a series out of the tooltip. Pass `filterNull` to get recharts' behaviour back.
172
+ */
173
+ export function ChartTooltip(props: ComponentProps<typeof RechartsTooltip>) {
174
+ return <RechartsTooltip filterNull={false} {...props} />;
175
+ }
131
176
  export const ChartLegend = RechartsLegend;
132
177
 
133
178
  interface TooltipPayloadItem {
@@ -135,6 +180,8 @@ interface TooltipPayloadItem {
135
180
  name?: string | number;
136
181
  value?: number | string;
137
182
  color?: string;
183
+ /** A series drawn with `hide` — dropped here, see {@link ChartTooltip}. */
184
+ hide?: boolean;
138
185
  payload?: Record<string, unknown>;
139
186
  }
140
187
 
@@ -194,6 +241,8 @@ interface ChartTooltipContentProps {
194
241
  * spill past the right edge of this (usually horizontally-scrolling) container
195
242
  * — e.g. a wide chart whose rightmost bars sat off-screen (feedback #77). */
196
243
  boundaryRef?: RefObject<HTMLElement | null>;
244
+ /** Injected by recharts: keep series drawn with `hide` in the tooltip. */
245
+ includeHidden?: boolean;
197
246
  }
198
247
 
199
248
  export function ChartTooltipContent({
@@ -207,17 +256,29 @@ export function ChartTooltipContent({
207
256
  formatValue,
208
257
  coordinate,
209
258
  boundaryRef,
259
+ includeHidden = false,
210
260
  }: ChartTooltipContentProps) {
211
261
  const config = useChart();
212
262
  const tipRef = useRef<HTMLDivElement>(null);
213
- if (!active || !payload?.length) return null;
263
+ // `ChartTooltip` keeps null values (so they can print "—"), which also keeps the
264
+ // series a legend toggle switched off with `hide`; those go here instead.
265
+ const items = includeHidden ? payload : payload?.filter((item) => item.hide !== true);
266
+ if (!active || !items?.length) return null;
214
267
  // Recharts anchors the tooltip at coordinate.x inside the full (scrolled) chart
215
268
  // width; subtract the container's scrollLeft to get its on-screen x, then flip
216
269
  // left if the tooltip's own width would run past the visible right edge.
270
+ //
271
+ // Both the cursor and the flip are PHYSICAL (the plot is never mirrored, see
272
+ // `ChartContainer`), but an RTL scroller counts `scrollLeft` from its right edge —
273
+ // 0 at the start, negative going left — so there the content's left edge sits
274
+ // `scrollWidth - clientWidth + scrollLeft` px to the left of the visible one.
217
275
  let flip = false;
218
276
  const boundary = boundaryRef?.current;
219
277
  if (boundary && coordinate?.x != null) {
220
- const visibleX = coordinate.x - boundary.scrollLeft;
278
+ const offset = isRtl(boundary)
279
+ ? boundary.scrollWidth - boundary.clientWidth + boundary.scrollLeft
280
+ : boundary.scrollLeft;
281
+ const visibleX = coordinate.x - offset;
221
282
  const tipWidth = tipRef.current?.offsetWidth ?? 160;
222
283
  flip = visibleX + tipWidth + 12 > boundary.clientWidth;
223
284
  }
@@ -233,7 +294,7 @@ export function ChartTooltipContent({
233
294
  </div>
234
295
  )}
235
296
  <div className="grid gap-1.5">
236
- {payload.map((item, i) => {
297
+ {items.map((item, i) => {
237
298
  // Bars/lines/areas identify a series by dataKey; pies/treemaps key off
238
299
  // `name` (the nameKey value). Prefer whichever the config knows.
239
300
  const dk = item.dataKey != null ? String(item.dataKey) : undefined;
@@ -245,7 +306,7 @@ export function ChartTooltipContent({
245
306
  // or a bar with a per-cell colour, IS the colour the reader is pointing at,
246
307
  // while `item.color` is the series-wide one — every tile's swatch came out
247
308
  // `--chart-1` whatever colour the tile was.
248
- const color = series?.color ?? fill ?? item.color ?? "#64748b";
309
+ const color = paintedColor(cfgKey, series) ?? fill ?? item.color ?? "#64748b";
249
310
  const name = series?.label ?? nm ?? cfgKey;
250
311
  const raw = tooltipValue(item.value);
251
312
  let shown: ReactNode;
@@ -262,7 +323,7 @@ export function ChartTooltipContent({
262
323
  style={{ backgroundColor: color }}
263
324
  />
264
325
  <span className="text-[var(--text-muted)]">{name}</span>
265
- <span data-private className="ml-auto font-mono font-medium tabular-nums text-[var(--text-primary)]">
326
+ <span data-private className="ms-auto font-mono font-medium tabular-nums text-[var(--text-primary)]">
266
327
  {shown}
267
328
  </span>
268
329
  </div>
@@ -313,7 +374,7 @@ export function ChartLegendContent({
313
374
  const vv = item.value != null ? String(item.value) : undefined;
314
375
  const cfgKey = dk && config[dk] ? dk : vv && config[vv] ? vv : (dk ?? vv ?? String(i));
315
376
  const series = config[cfgKey];
316
- const color = series?.color ?? item.color ?? "#64748b";
377
+ const color = paintedColor(cfgKey, series) ?? item.color ?? "#64748b";
317
378
  const label = series?.label ?? vv ?? cfgKey;
318
379
  const off = hidden?.has(cfgKey) ?? false;
319
380
  const dimmed = off || (activeKey != null && activeKey !== cfgKey);
@@ -78,6 +78,15 @@ export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement
78
78
  * the row when there is a label, the box's own wrapper when there is not — which
79
79
  * is where layout (`self-end pb-2` to sit on a field's baseline) belongs. */
80
80
  inputClassName?: string;
81
+ /**
82
+ * Must be ticked to submit — a consent, an acceptance before paying. Reaches the
83
+ * `<input>` as the native `required` (so a `<form>` refuses to submit without it and a
84
+ * screen reader announces "required"), and draws the kit's required mark after the
85
+ * label, the same `aria-hidden` star as {@link Label}'s: the word is announced from
86
+ * the control, so a star inside the name would only be read out as noise. Write the
87
+ * label WITHOUT a literal "*". No mark without a label.
88
+ */
89
+ required?: boolean;
81
90
  }
82
91
 
83
92
  /**
@@ -103,6 +112,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Che
103
112
  inputClassName,
104
113
  id,
105
114
  disabled,
115
+ required,
106
116
  ...rest
107
117
  },
108
118
  ref,
@@ -138,6 +148,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Che
138
148
  ref={setRef}
139
149
  id={bare ? id : inputId}
140
150
  disabled={disabled}
151
+ required={required}
141
152
  {...rest}
142
153
  // After the spread, like Switch: a props object spread at a checkbox must not
143
154
  // be able to turn it into something else.
@@ -203,6 +214,11 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Che
203
214
  )}
204
215
  >
205
216
  {label}
217
+ {required && (
218
+ <span aria-hidden className="ms-0.5 text-[var(--danger)]">
219
+ *
220
+ </span>
221
+ )}
206
222
  </label>
207
223
  )}
208
224
  {showDescription && (