@eifi1/ui-kit 0.34.0 → 0.35.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 (241) hide show
  1. package/README.md +82 -38
  2. package/dist/billing/billing-banner.d.ts +5 -2
  3. package/dist/billing/billing-banner.js.map +1 -1
  4. package/dist/billing/plan-limit-notice.d.ts +5 -2
  5. package/dist/billing/plan-limit-notice.js.map +1 -1
  6. package/dist/chart.js.map +1 -1
  7. package/dist/components/amount-input.d.ts +0 -3
  8. package/dist/components/amount-input.js +2 -2
  9. package/dist/components/amount-input.js.map +1 -1
  10. package/dist/components/calculator.d.ts +1 -4
  11. package/dist/components/calculator.js +1 -3
  12. package/dist/components/calculator.js.map +1 -1
  13. package/dist/components/chart-zoom.d.ts +0 -37
  14. package/dist/components/chart-zoom.js +1 -28
  15. package/dist/components/chart-zoom.js.map +1 -1
  16. package/dist/components/chip-input.d.ts +0 -4
  17. package/dist/components/chip-input.js +1 -5
  18. package/dist/components/chip-input.js.map +1 -1
  19. package/dist/components/combobox.d.ts +1 -1
  20. package/dist/components/combobox.js.map +1 -1
  21. package/dist/components/copy-button.d.ts +5 -1
  22. package/dist/components/copy-button.js.map +1 -1
  23. package/dist/components/currency-option-list.d.ts +3 -8
  24. package/dist/components/currency-option-list.js +1 -29
  25. package/dist/components/currency-option-list.js.map +1 -1
  26. package/dist/components/danger-confirm.d.ts +3 -3
  27. package/dist/components/danger-confirm.js.map +1 -1
  28. package/dist/components/data-table-filter-popover.d.ts +1 -1
  29. package/dist/components/data-table-filter-popover.js +1 -1
  30. package/dist/components/data-table-filter-popover.js.map +1 -1
  31. package/dist/components/data-table-labels.d.ts +1 -42
  32. package/dist/components/data-table-labels.js +2 -31
  33. package/dist/components/data-table-labels.js.map +1 -1
  34. package/dist/components/data-table-shared.d.ts +17 -1
  35. package/dist/components/data-table-shared.js +22 -0
  36. package/dist/components/data-table-shared.js.map +1 -1
  37. package/dist/components/data-table.d.ts +12 -2
  38. package/dist/components/data-table.js +18 -14
  39. package/dist/components/data-table.js.map +1 -1
  40. package/dist/components/dialog-frame.d.ts +3 -1
  41. package/dist/components/dialog-frame.js.map +1 -1
  42. package/dist/components/dropdown.d.ts +18 -15
  43. package/dist/components/dropdown.js +11 -3
  44. package/dist/components/dropdown.js.map +1 -1
  45. package/dist/components/entity-combobox.d.ts +1 -1
  46. package/dist/components/entity-combobox.js.map +1 -1
  47. package/dist/components/field-base.d.ts +0 -5
  48. package/dist/components/field-base.js +0 -5
  49. package/dist/components/field-base.js.map +1 -1
  50. package/dist/components/field-parts.d.ts +3 -11
  51. package/dist/components/field-parts.js +1 -9
  52. package/dist/components/field-parts.js.map +1 -1
  53. package/dist/components/field-strip.d.ts +8 -23
  54. package/dist/components/field-strip.js +24 -22
  55. package/dist/components/field-strip.js.map +1 -1
  56. package/dist/components/field-sync.d.ts +0 -4
  57. package/dist/components/field-sync.js +0 -4
  58. package/dist/components/field-sync.js.map +1 -1
  59. package/dist/components/field.d.ts +4 -4
  60. package/dist/components/field.js.map +1 -1
  61. package/dist/components/full-bleed-dialog.js +2 -2
  62. package/dist/components/full-bleed-dialog.js.map +1 -1
  63. package/dist/components/hover-menu.d.ts +6 -13
  64. package/dist/components/hover-menu.js +3 -3
  65. package/dist/components/hover-menu.js.map +1 -1
  66. package/dist/components/inline-entity-combobox.d.ts +1 -1
  67. package/dist/components/inline-entity-combobox.js.map +1 -1
  68. package/dist/components/modal.d.ts +9 -1
  69. package/dist/components/modal.js +7 -4
  70. package/dist/components/modal.js.map +1 -1
  71. package/dist/components/multi-select.d.ts +5 -16
  72. package/dist/components/multi-select.js +5 -10
  73. package/dist/components/multi-select.js.map +1 -1
  74. package/dist/components/number-input.d.ts +0 -3
  75. package/dist/components/number-input.js +2 -4
  76. package/dist/components/number-input.js.map +1 -1
  77. package/dist/components/popover.js +2 -2
  78. package/dist/components/popover.js.map +1 -1
  79. package/dist/components/series-chart.js.map +1 -1
  80. package/dist/components/server-wake.d.ts +0 -6
  81. package/dist/components/server-wake.js.map +1 -1
  82. package/dist/components/settings-fields.d.ts +4 -2
  83. package/dist/components/settings-fields.js.map +1 -1
  84. package/dist/components/stat-tile.d.ts +8 -2
  85. package/dist/components/stat-tile.js.map +1 -1
  86. package/dist/components/swipeable-row.d.ts +2 -17
  87. package/dist/components/swipeable-row.js +11 -16
  88. package/dist/components/swipeable-row.js.map +1 -1
  89. package/dist/components/tooltip.d.ts +9 -1
  90. package/dist/components/tooltip.js +20 -5
  91. package/dist/components/tooltip.js.map +1 -1
  92. package/dist/components/translation-review.d.ts +10 -2
  93. package/dist/components/translation-review.js +11 -3
  94. package/dist/components/translation-review.js.map +1 -1
  95. package/dist/components/use-table-state.d.ts +31 -3
  96. package/dist/components/use-table-state.js +53 -8
  97. package/dist/components/use-table-state.js.map +1 -1
  98. package/dist/components/user-avatar.d.ts +2 -2
  99. package/dist/components/user-avatar.js.map +1 -1
  100. package/dist/components/wizard-stepper.d.ts +1 -4
  101. package/dist/components/wizard-stepper.js +2 -3
  102. package/dist/components/wizard-stepper.js.map +1 -1
  103. package/dist/currencies.d.ts +23 -0
  104. package/dist/currencies.js +38 -0
  105. package/dist/currencies.js.map +1 -0
  106. package/dist/demo/demo-banner.d.ts +7 -2
  107. package/dist/demo/demo-banner.js.map +1 -1
  108. package/dist/eslint/index.d.ts +83 -0
  109. package/dist/eslint/index.js +69 -0
  110. package/dist/eslint/index.js.map +1 -0
  111. package/dist/eslint/role-utilities.d.ts +48 -0
  112. package/dist/eslint/role-utilities.generated.d.ts +2 -0
  113. package/dist/eslint/role-utilities.generated.js +143 -0
  114. package/dist/eslint/role-utilities.generated.js.map +1 -0
  115. package/dist/eslint/role-utilities.js +43 -0
  116. package/dist/eslint/role-utilities.js.map +1 -0
  117. package/dist/feedback/feedback-menu.d.ts +2 -2
  118. package/dist/feedback/feedback-menu.js.map +1 -1
  119. package/dist/feedback/feedback-thread.d.ts +3 -8
  120. package/dist/feedback/feedback-thread.js +0 -2
  121. package/dist/feedback/feedback-thread.js.map +1 -1
  122. package/dist/hooks/use-dismiss.d.ts +16 -3
  123. package/dist/hooks/use-dismiss.js +14 -2
  124. package/dist/hooks/use-dismiss.js.map +1 -1
  125. package/dist/i18n/kit-labels.d.ts +14 -4
  126. package/dist/i18n/kit-labels.js.map +1 -1
  127. package/dist/i18n/locales/en.js +1 -1
  128. package/dist/i18n/locales/en.js.map +2 -2
  129. package/dist/i18n/locales/fr.js +3 -2
  130. package/dist/i18n/locales/fr.js.map +1 -1
  131. package/dist/i18n/locales/it.js +3 -2
  132. package/dist/i18n/locales/it.js.map +1 -1
  133. package/dist/index.d.ts +2 -1
  134. package/dist/index.js +2 -2
  135. package/dist/index.js.map +1 -1
  136. package/dist/lib/refs.d.ts +13 -0
  137. package/dist/lib/refs.js +15 -0
  138. package/dist/lib/refs.js.map +1 -0
  139. package/dist/rhf/fields.d.ts +10 -10
  140. package/dist/rhf/fields.js.map +1 -1
  141. package/dist/rhf/form-core.d.ts +5 -4
  142. package/dist/rhf/form-core.js.map +1 -1
  143. package/dist/rhf/form.d.ts +7 -36
  144. package/dist/rhf/form.js +1 -16
  145. package/dist/rhf/form.js.map +1 -1
  146. package/dist/rhf/line-items.d.ts +1 -1
  147. package/dist/rhf/line-items.js.map +1 -1
  148. package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
  149. package/dist/rhf.js.map +1 -1
  150. package/dist/shell/option-switcher-menu.d.ts +5 -17
  151. package/dist/shell/option-switcher-menu.js +1 -3
  152. package/dist/shell/option-switcher-menu.js.map +1 -1
  153. package/dist/shell/role-switcher.d.ts +1 -4
  154. package/dist/shell/role-switcher.js +2 -3
  155. package/dist/shell/role-switcher.js.map +1 -1
  156. package/dist/shell/topbar-action-menu.d.ts +7 -19
  157. package/dist/shell/topbar-action-menu.js +1 -3
  158. package/dist/shell/topbar-action-menu.js.map +1 -1
  159. package/dist/shell/topbar-controls.d.ts +3 -12
  160. package/dist/shell/topbar-controls.js +6 -9
  161. package/dist/shell/topbar-controls.js.map +1 -1
  162. package/dist/testing/index.d.ts +1 -1
  163. package/dist/testing/index.js +2 -0
  164. package/dist/testing/index.js.map +1 -1
  165. package/dist/testing/viewport.d.ts +25 -2
  166. package/dist/testing/viewport.js +61 -10
  167. package/dist/testing/viewport.js.map +1 -1
  168. package/dist/theme/palette-presets.d.ts +0 -7
  169. package/dist/theme/palette-presets.js +0 -279
  170. package/dist/theme/palette-presets.js.map +1 -1
  171. package/dist/wizard/types.d.ts +0 -5
  172. package/dist/wizard/types.js +1 -5
  173. package/dist/wizard/types.js.map +1 -1
  174. package/package.json +9 -1
  175. package/src/billing/billing-banner.tsx +5 -2
  176. package/src/billing/plan-limit-notice.tsx +5 -2
  177. package/src/chart.ts +5 -3
  178. package/src/components/amount-input.tsx +2 -6
  179. package/src/components/calculator.tsx +2 -7
  180. package/src/components/chart-zoom.tsx +4 -47
  181. package/src/components/chip-input.tsx +0 -7
  182. package/src/components/combobox.tsx +1 -1
  183. package/src/components/copy-button.tsx +5 -1
  184. package/src/components/currency-option-list.tsx +8 -42
  185. package/src/components/danger-confirm.tsx +3 -3
  186. package/src/components/data-table-filter-popover.tsx +2 -1
  187. package/src/components/data-table-labels.ts +2 -84
  188. package/src/components/data-table-shared.tsx +55 -2
  189. package/src/components/data-table.tsx +29 -17
  190. package/src/components/dialog-frame.tsx +3 -1
  191. package/src/components/dropdown.tsx +42 -18
  192. package/src/components/entity-combobox.tsx +1 -1
  193. package/src/components/field-base.tsx +0 -11
  194. package/src/components/field-parts.tsx +5 -19
  195. package/src/components/field-strip.tsx +9 -25
  196. package/src/components/field-sync.tsx +0 -7
  197. package/src/components/field.tsx +4 -4
  198. package/src/components/full-bleed-dialog.tsx +4 -3
  199. package/src/components/hover-menu.tsx +12 -16
  200. package/src/components/inline-entity-combobox.tsx +1 -1
  201. package/src/components/modal.tsx +20 -4
  202. package/src/components/multi-select.tsx +13 -30
  203. package/src/components/number-input.tsx +3 -8
  204. package/src/components/popover.tsx +5 -3
  205. package/src/components/series-chart.tsx +1 -1
  206. package/src/components/server-wake.tsx +0 -6
  207. package/src/components/settings-fields.tsx +4 -2
  208. package/src/components/stat-tile.tsx +8 -2
  209. package/src/components/swipeable-row.tsx +15 -37
  210. package/src/components/tooltip.tsx +37 -6
  211. package/src/components/translation-review.tsx +25 -7
  212. package/src/components/use-table-state.ts +91 -10
  213. package/src/components/user-avatar.tsx +2 -2
  214. package/src/components/wizard-stepper.tsx +2 -6
  215. package/src/currencies.ts +70 -0
  216. package/src/demo/demo-banner.tsx +7 -2
  217. package/src/eslint/index.ts +161 -0
  218. package/src/eslint/role-utilities.generated.ts +146 -0
  219. package/src/eslint/role-utilities.ts +99 -0
  220. package/src/feedback/feedback-menu.tsx +2 -2
  221. package/src/feedback/feedback-thread.tsx +3 -8
  222. package/src/hooks/use-dismiss.ts +39 -4
  223. package/src/i18n/kit-labels.tsx +15 -5
  224. package/src/i18n/locales/fr.ts +3 -2
  225. package/src/i18n/locales/it.ts +3 -2
  226. package/src/index.ts +3 -1
  227. package/src/lib/refs.ts +22 -0
  228. package/src/rhf/fields.tsx +10 -10
  229. package/src/rhf/form-core.tsx +5 -4
  230. package/src/rhf/form.tsx +7 -43
  231. package/src/rhf/line-items.tsx +1 -1
  232. package/src/rhf/use-rhf-wizard-step.ts +4 -4
  233. package/src/rhf.ts +3 -2
  234. package/src/shell/option-switcher-menu.tsx +5 -22
  235. package/src/shell/role-switcher.tsx +2 -6
  236. package/src/shell/topbar-action-menu.tsx +7 -24
  237. package/src/shell/topbar-controls.tsx +6 -18
  238. package/src/testing/index.ts +3 -1
  239. package/src/testing/viewport.ts +125 -19
  240. package/src/theme/palette-presets.ts +7 -293
  241. package/src/wizard/types.ts +0 -8
@@ -27,7 +27,7 @@
27
27
  * - **The red border.** Every field gets `invalid` from the field state, so an errored
28
28
  * field PAINTS as well as announces. kastlan's number, money, date and entity fields
29
29
  * passed only `aria-invalid` through `FormControl`, or nothing, and `Input`, `Select`
30
- * and `Textarea` paint only from the prop (see the note in ./form).
30
+ * and `Textarea` paint only from the prop (see the note in ./form-core).
31
31
  * - **Focus on error.** `field.ref` reaches the focusable element itself — the input,
32
32
  * a picker's trigger, a toggle group whose `focus()` lands on its option (since 0.34
33
33
  * every kit field takes a ref; the number field and the pickers were reached by an id
@@ -37,7 +37,7 @@
37
37
  * through react-hook-form's context. Pass it for the field's value types
38
38
  * (`control={form.control}` checks `name` against the form's values).
39
39
  *
40
- * The common props: `name`, `control`, `label` (a {@link FormLabel} above the field;
40
+ * The common props: `name`, `control`, `label` (the label above the field;
41
41
  * omitted, none — then give the field an `aria-label`), `hint` (standing advice under
42
42
  * the field, attached to it with `aria-describedby`), `required` (the label's mark,
43
43
  * and `aria-required` where the control takes it — validation is `rules`' or the
@@ -110,7 +110,7 @@ export interface RhfFieldBaseProps<
110
110
  control?: ControllerProps<TFieldValues, TName, TTransformed>["control"];
111
111
  /** react-hook-form's `rules` (`required`, `min`, `validate` …). */
112
112
  rules?: ControllerProps<TFieldValues, TName, TTransformed>["rules"];
113
- /** A {@link FormLabel} above the field. Omitted: no label — name the control with
113
+ /** The label above the field. Omitted: no label — name the control with
114
114
  * an `aria-label` instead. */
115
115
  label?: ReactNode;
116
116
  /** Standing advice under the field ("As printed on the card"), attached to the
@@ -146,8 +146,8 @@ export interface RhfFieldRenderContext<
146
146
  invalid: boolean;
147
147
  /** The error's message, if any. */
148
148
  error: string | undefined;
149
- /** The control's id, which the label points at. `FormControl` sets it on the
150
- * rendered element already; this is for a control that routes it elsewhere. */
149
+ /** The control's id, which the label points at. The rendered element has it already
150
+ * (unless `asControl` is false); this is for a control that routes it elsewhere. */
151
151
  id: string;
152
152
  /** The label's id, for a control that is named with `aria-labelledby`. */
153
153
  labelId: string;
@@ -160,16 +160,16 @@ export interface RhfFieldProps<
160
160
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
161
161
  TTransformed = TFieldValues,
162
162
  > extends RhfFieldBaseProps<TFieldValues, TName, TTransformed> {
163
- /** The control. Wrapped in {@link FormControl} (its `id`, `aria-describedby` and
164
- * `aria-invalid`) unless `asControl` is false. */
163
+ /** The control. Given the field's `id`, `aria-describedby` and `aria-invalid` unless
164
+ * `asControl` is false. */
165
165
  render: (ctx: RhfFieldRenderContext<TFieldValues, TName>) => ReactElement;
166
166
  /** `false`: render the control as is, for one that wires its own ids from the
167
167
  * context. Default `true`. */
168
168
  asControl?: boolean;
169
- /** `false`: no {@link FormMessage}, for a control that renders its own error (its
169
+ /** `false`: no error message under the field, for a control that renders its own (its
170
170
  * `error` prop). Default `true`. */
171
171
  message?: boolean;
172
- /** `false`: no {@link FormLabel}, for a control that carries its label itself (a
172
+ /** `false`: no label above the field, for a control that carries its label itself (a
173
173
  * checkbox). Default `true`. */
174
174
  showLabel?: boolean;
175
175
  }
@@ -1622,7 +1622,7 @@ export type RhfToggleGroupProps<
1622
1622
  * asked for it with {@link RhfTimeInput} and {@link RhfDateRangePicker}: the controls
1623
1623
  * its forms still had to wire by hand through {@link RhfField}'s render.
1624
1624
  *
1625
- * The label is the form's {@link FormLabel} above the bare group, as on every other
1625
+ * The label is the field's own label above the bare group, as on every other
1626
1626
  * bound field — the `labelPlacement="above"` shape, not the group's own field chrome —
1627
1627
  * and it names the group (`aria-labelledby`); the hint and the message describe it,
1628
1628
  * and an error paints the group's border. Focus on error goes to the option a
@@ -1,10 +1,11 @@
1
1
  /**
2
- * The react-hook-form adapter: the five parts every form is built from, and the hook
3
- * they share. `@eifi1/ui-kit/rhf`.
2
+ * The react-hook-form adapter: the five parts every bound field is built from, and the
3
+ * hook they share. Behind `@eifi1/ui-kit/rhf`, which exports `Form` alone of them.
4
4
  *
5
5
  * The kit's own module since 0.34 (refactor audit 2026-10, API-6f): the bound `Rhf*`
6
- * fields build on these parts, and import them from here. ./form is the public face —
7
- * `Form`, and until 0.35 the parts under their deprecated names, which no app imports.
6
+ * fields and `RhfField` build on these parts, and import them from here. ./form is the
7
+ * public face — `Form`; the parts left the public surface in 0.35 (deprecated in 0.34),
8
+ * when no app imported them any more.
8
9
  *
9
10
  * ```tsx
10
11
  * <Form {...form}>
package/src/rhf/form.tsx CHANGED
@@ -1,48 +1,12 @@
1
1
  /**
2
- * `Form`, and until 0.35 the shadcn scaffold under its old public names — the public face
3
- * of ./form-core, where the scaffold lives (its header has the whole story: what differs
4
- * from shadcn's file, why /rhf is its own entry, how a picker clears).
2
+ * `Form` — the public face of ./form-core, where the shadcn scaffold the bound fields are
3
+ * built from lives (its header has the whole story: what differs from shadcn's file, why
4
+ * /rhf is its own entry, how a picker clears).
5
5
  *
6
- * 0.34 (refactor audit 2026-10, API-6f): no app builds a form from the scaffold any more.
6
+ * The scaffold itself (`FormField`, `useFormField`, `FormItem`, `FormLabel`, `FormControl`,
7
+ * `FormDescription`, `FormMessage` and five props types) was public until 0.35 (refactor
8
+ * audit 2026-10, API-6f; deprecated in 0.34): no app builds a form from it any more.
7
9
  * kastlan, the one /rhf consumer, uses `Form`, the bound `Rhf*` fields and `RhfField` for
8
- * a control of its own, so the seven names and their props types leave the public
9
- * surface in 0.35. The bound fields import the parts from the core, so the removal is
10
- * the block below.
10
+ * a control of its own — the same parts, folded into one element.
11
11
  */
12
- import * as core from "./form-core";
13
-
14
12
  export { Form } from "./form-core";
15
-
16
- /* ── Deprecated 0.34, removed in 0.35 ───────────────────────────────────────
17
- * Each is the core's own value under its public name. The replacement for all of them:
18
- * a bound field (`RhfTextField`, `RhfSelect`, …), or `RhfField` around a control of
19
- * your own — the same parts, folded into one element. */
20
-
21
- /** @deprecated 0.34 — the bound `Rhf*` fields, or `RhfField` for a control of your own;
22
- * removed in 0.35 (API-6f). */
23
- export const FormField = core.FormField;
24
- /** @deprecated 0.34 — `RhfField`, which reads the field's ids itself; removed in 0.35
25
- * (API-6f). */
26
- export const useFormField = core.useFormField;
27
- /** @deprecated 0.34 — the bound `Rhf*` fields, or `RhfField`; removed in 0.35 (API-6f). */
28
- export const FormItem = core.FormItem;
29
- /** @deprecated 0.34 — the bound fields' and `RhfField`'s `label`; removed in 0.35 (API-6f). */
30
- export const FormLabel = core.FormLabel;
31
- /** @deprecated 0.34 — `RhfField`, which wires its control the same way; removed in 0.35
32
- * (API-6f). */
33
- export const FormControl = core.FormControl;
34
- /** @deprecated 0.34 — the bound fields' and `RhfField`'s `hint`; removed in 0.35 (API-6f). */
35
- export const FormDescription = core.FormDescription;
36
- /** @deprecated 0.34 — the bound fields and `RhfField`, which show the error themselves;
37
- * removed in 0.35 (API-6f). */
38
- export const FormMessage = core.FormMessage;
39
- /** @deprecated 0.34 — goes with `FormItem`; removed in 0.35 (API-6f). */
40
- export type FormItemProps = core.FormItemProps;
41
- /** @deprecated 0.34 — goes with `FormLabel`; removed in 0.35 (API-6f). */
42
- export type FormLabelProps = core.FormLabelProps;
43
- /** @deprecated 0.34 — goes with `FormControl`; removed in 0.35 (API-6f). */
44
- export type FormControlProps = core.FormControlProps;
45
- /** @deprecated 0.34 — goes with `FormDescription`; removed in 0.35 (API-6f). */
46
- export type FormDescriptionProps = core.FormDescriptionProps;
47
- /** @deprecated 0.34 — goes with `FormMessage`; removed in 0.35 (API-6f). */
48
- export type FormMessageProps = core.FormMessageProps;
@@ -27,7 +27,7 @@
27
27
  * `summary`, `fieldLabels`, `narrowColumns`, `removePlacement` and `removeAlign` pass
28
28
  * through to {@link LineItems}. With
29
29
  * `fieldLabels="floating"` a cell's `fieldLabel` is the column's name; the bound
30
- * fields' own `label` is a `FormLabel` ABOVE the control, so for the floating
30
+ * fields' own `label` is a label ABOVE the control, so for the floating
31
31
  * one render the kit field through `RhfField`:
32
32
  * `render={({ field, invalid }) => <Input label={fieldLabel} aria-label={label} {...field} invalid={invalid} />}`.
33
33
  */
@@ -12,10 +12,10 @@
12
12
  *
13
13
  * Inside a `<StepperNav>` the hook registers the step's validator with the wizard, so
14
14
  * Next (and Finish on a form-bearing last step) runs it: on success the values go to
15
- * `onValid` and the wizard advances; on failure react-hook-form shows its own inline
16
- * `<FormMessage>`s, focuses the first field in error, and the wizard stays put — and,
17
- * since the result carries no per-field errors, says its "fill in the required
18
- * fields" line so the button did not seem to do nothing. It also RETURNS the
15
+ * `onValid` and the wizard advances; on failure the bound fields show their inline
16
+ * messages, react-hook-form focuses the first field in error, and the wizard stays
17
+ * put — and, since the result carries no per-field errors, says its "fill in the
18
+ * required fields" line so the button did not seem to do nothing. It also RETURNS the
19
19
  * validator, for a nav of your own outside `<StepperNav>` (where there is no wizard
20
20
  * context to register with, so nothing is registered).
21
21
  *
package/src/rhf.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // `@eifi1/ui-kit/rhf` — the react-hook-form adapter: Form and the bound Rhf* fields (and
2
- // RhfField as the escape hatch). The shadcn scaffold (FormField, FormItem, FormLabel,
3
- // FormControl, FormDescription, FormMessage, useFormField) is deprecated in 0.34.
2
+ // RhfField as the escape hatch). The shadcn scaffold they are built from (FormField,
3
+ // FormItem, FormLabel, FormControl, FormDescription, FormMessage, useFormField) left the
4
+ // surface in 0.35 (refactor audit 2026-10, API-6f; deprecated in 0.34).
4
5
  //
5
6
  // The only entry point in the package that requires `react-hook-form`, and an optional
6
7
  // peer for that reason: an app without a form library installs nothing for it. NOT
@@ -9,23 +9,6 @@ export interface OptionSwitcherOption<T extends string> {
9
9
  label: ReactNode;
10
10
  }
11
11
 
12
- /**
13
- * Names the trigger and the menu: one of the two spellings is required (API-1, refactor
14
- * audit 2026-10). `aria-label` wins when both are given. TopBarActionMenu has the same
15
- * type; both become a plain required `aria-label` when `ariaLabel` goes in 0.35.
16
- */
17
- type RequiredMenuName =
18
- | {
19
- "aria-label": string;
20
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35. */
21
- ariaLabel?: string;
22
- }
23
- | {
24
- "aria-label"?: string;
25
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35. */
26
- ariaLabel: string;
27
- };
28
-
29
12
  /**
30
13
  * A top-bar hover menu that switches between a small set of options, with a
31
14
  * check on the active one and an optional heading. Generic glue over
@@ -34,8 +17,7 @@ type RequiredMenuName =
34
17
  */
35
18
  export function OptionSwitcherMenu<T extends string>({
36
19
  icon,
37
- "aria-label": ariaLabelAttr,
38
- ariaLabel: ariaLabelProp,
20
+ "aria-label": ariaLabel,
39
21
  title,
40
22
  heading,
41
23
  options,
@@ -43,14 +25,15 @@ export function OptionSwitcherMenu<T extends string>({
43
25
  onSelect,
44
26
  }: {
45
27
  icon: ReactNode;
28
+ /** Names the trigger and the menu. Required: the trigger is an icon (refactor audit
29
+ * 2026-10, API-1 — the `ariaLabel` spelling went in 0.35). */
30
+ "aria-label": string;
46
31
  title?: string;
47
32
  heading?: string;
48
33
  options: OptionSwitcherOption<T>[];
49
34
  value: T;
50
35
  onSelect: (value: T) => void;
51
- } & RequiredMenuName) {
52
- // The DOM spelling wins; `ariaLabel` is the fallback for call sites not moved yet.
53
- const ariaLabel = ariaLabelAttr ?? ariaLabelProp;
36
+ }) {
54
37
  return (
55
38
  <HoverMenu
56
39
  aria-label={ariaLabel}
@@ -8,9 +8,6 @@ export interface RoleSwitcherProps<R extends string> {
8
8
  onChange: (role: R) => void;
9
9
  /** Default: `topBar.switchRole` from the {@link UiKitProvider}, else English. */
10
10
  "aria-label"?: string;
11
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35 (refactor audit 2026-10, API-1).
12
- * Names the switcher only when `aria-label` is absent. */
13
- ariaLabel?: string;
14
11
  heading?: string;
15
12
  /** The trigger's tooltip. Default: `topBar.role(value)`. */
16
13
  title?: string;
@@ -30,12 +27,11 @@ export function RoleSwitcher<R extends string>({
30
27
  roles,
31
28
  value,
32
29
  onChange,
33
- "aria-label": ariaLabelAttr,
34
- ariaLabel,
30
+ "aria-label": ariaLabel,
35
31
  heading,
36
32
  title,
37
33
  }: RoleSwitcherProps<R>) {
38
- const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { switchRole: ariaLabelAttr ?? ariaLabel });
34
+ const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { switchRole: ariaLabel });
39
35
  return (
40
36
  <OptionSwitcherMenu<R>
41
37
  aria-label={labels.switchRole}
@@ -179,23 +179,6 @@ function keepTabInsidePanel(e: ReactKeyboardEvent<HTMLUListElement>) {
179
179
  if (stops.some((el) => el !== from && from.compareDocumentPosition(el) & way)) e.stopPropagation();
180
180
  }
181
181
 
182
- /**
183
- * Names the trigger and the menu: one of the two spellings is required (API-1, refactor
184
- * audit 2026-10). `aria-label` wins when both are given. OptionSwitcherMenu has the same
185
- * type; both become a plain required `aria-label` when `ariaLabel` goes in 0.35.
186
- */
187
- type RequiredMenuName =
188
- | {
189
- "aria-label": string;
190
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35. */
191
- ariaLabel?: string;
192
- }
193
- | {
194
- "aria-label"?: string;
195
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35. */
196
- ariaLabel: string;
197
- };
198
-
199
182
  /**
200
183
  * A top-bar icon button that opens a {@link HoverMenu} of actions and/or links,
201
184
  * with an optional uppercase heading and divider rules. Generic glue over the
@@ -219,8 +202,7 @@ export function TopBarActionMenu({
219
202
  trigger,
220
203
  triggerClassName,
221
204
  href,
222
- "aria-label": ariaLabelAttr,
223
- ariaLabel: ariaLabelProp,
205
+ "aria-label": ariaLabel,
224
206
  heading,
225
207
  header,
226
208
  entries,
@@ -265,6 +247,9 @@ export function TopBarActionMenu({
265
247
  * inside interactive content.
266
248
  */
267
249
  href?: string;
250
+ /** Names the trigger and the menu. Required: the trigger is an icon (refactor audit
251
+ * 2026-10, API-1 — the `ariaLabel` spelling went in 0.35). */
252
+ "aria-label": string;
268
253
  heading?: string;
269
254
  /** A non-interactive identity block above the rows. */
270
255
  header?: TopBarMenuHeader;
@@ -283,13 +268,11 @@ export function TopBarActionMenu({
283
268
  footer?: ReactNode | ((close: () => void) => ReactNode);
284
269
  /** Wraps the footer in a `<nav>` with this name ("Legal"). Without it, a plain block. */
285
270
  footerLabel?: string;
286
- /** Which edge of the trigger the panel lines up with (see `HoverMenu`, whose type this
287
- * is: `left`/`right` are deprecated 0.34). Default `end`. */
271
+ /** Which edge of the trigger the panel lines up with, `start` or `end` in the reading
272
+ * direction (see `HoverMenu`, whose type this is). Default `end`. */
288
273
  align?: HoverMenuProps["align"];
289
274
  panelClassName?: string;
290
- } & RequiredMenuName) {
291
- // The DOM spelling wins; `ariaLabel` is the fallback for call sites not moved yet.
292
- const ariaLabel = ariaLabelAttr ?? ariaLabelProp;
275
+ }) {
293
276
  // Names the groups' headings for `aria-labelledby`. Positional rather than from the
294
277
  // entry's `key`, which is the caller's text and may hold a space — and a space in
295
278
  // `aria-labelledby` is a list separator.
@@ -29,8 +29,7 @@ function MenuHeading({ children }: { children: string }) {
29
29
  export function ThemeToggle({
30
30
  mode,
31
31
  onToggle,
32
- "aria-label": ariaLabelAttr,
33
- ariaLabel,
32
+ "aria-label": ariaLabel,
34
33
  title,
35
34
  className,
36
35
  }: {
@@ -38,15 +37,12 @@ export function ThemeToggle({
38
37
  onToggle: () => void;
39
38
  /** Default: `topBar.theme` from the {@link UiKitProvider}, else English. */
40
39
  "aria-label"?: string;
41
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35 (refactor audit 2026-10, API-1).
42
- * Names the control only when `aria-label` is absent. */
43
- ariaLabel?: string;
44
40
  title?: string;
45
41
  className?: string;
46
42
  }) {
47
43
  // This had no default at all, so a toggle rendered without `aria-label` was an
48
44
  // icon button with no name — a reader heard "button" and nothing else.
49
- const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { theme: ariaLabelAttr ?? ariaLabel });
45
+ const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { theme: ariaLabel });
50
46
  const Icon = mode === "dark" ? Moon : Sun;
51
47
  return (
52
48
  <Tooltip label={title} portal>
@@ -69,8 +65,7 @@ export function PaletteMenu({
69
65
  activeId,
70
66
  mode,
71
67
  onSelect,
72
- "aria-label": ariaLabelAttr,
73
- ariaLabel,
68
+ "aria-label": ariaLabel,
74
69
  heading,
75
70
  }: {
76
71
  palettes: PalettePreset[];
@@ -79,13 +74,10 @@ export function PaletteMenu({
79
74
  onSelect: (id: string) => void;
80
75
  /** Default: `topBar.palette` from the {@link UiKitProvider}, else English. */
81
76
  "aria-label"?: string;
82
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35 (refactor audit 2026-10, API-1).
83
- * Names the control only when `aria-label` is absent. */
84
- ariaLabel?: string;
85
77
  /** Optional small heading row above the list. */
86
78
  heading?: string;
87
79
  }) {
88
- const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { palette: ariaLabelAttr ?? ariaLabel });
80
+ const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { palette: ariaLabel });
89
81
  const activeName = palettes.find((p) => p.id === activeId)?.name ?? activeId;
90
82
  return (
91
83
  <HoverMenu
@@ -177,8 +169,7 @@ export function LanguageMenu({
177
169
  options,
178
170
  current,
179
171
  onChange,
180
- "aria-label": ariaLabelAttr,
181
- ariaLabel,
172
+ "aria-label": ariaLabel,
182
173
  heading,
183
174
  }: {
184
175
  options: LanguageOption[];
@@ -186,14 +177,11 @@ export function LanguageMenu({
186
177
  onChange: (code: string) => void;
187
178
  /** Default: `topBar.language` from the {@link UiKitProvider}, else English. */
188
179
  "aria-label"?: string;
189
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35 (refactor audit 2026-10, API-1).
190
- * Names the control only when `aria-label` is absent. */
191
- ariaLabel?: string;
192
180
  /** Optional small heading row above the list — as on `TopBarActionMenu`,
193
181
  * `OptionSwitcherMenu` and `PaletteMenu`. */
194
182
  heading?: string;
195
183
  }) {
196
- const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { language: ariaLabelAttr ?? ariaLabel });
184
+ const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { language: ariaLabel });
197
185
  const active = options.find((o) => o.code === current);
198
186
  return (
199
187
  <HoverMenu
@@ -13,8 +13,10 @@ export {
13
13
  } from "./css-variables";
14
14
  // 0.34 (refactor audit 2026-10, TST-10): the environment jsdom lacks — a `matchMedia`
15
15
  // that answers from a viewport width (right at every text size) and a `ResizeObserver`
16
- // the test drives. Framework-free: each returns its own undo.
16
+ // the test drives. Framework-free: each returns its own undo. 0.35: `setViewportWidth`,
17
+ // a live resize of the stub in force — its lists flip and fire `change` (keksdose).
17
18
  export {
19
+ setViewportWidth,
18
20
  stubResizeObserver,
19
21
  stubViewportWidth,
20
22
  viewportMatchMedia,
@@ -85,14 +85,91 @@ function condition(part: string, width: number, options: ViewportOptions): boole
85
85
  }
86
86
  }
87
87
 
88
+ /** A whole query: comma lists are alternatives, `and` joins conditions, as in CSS. */
89
+ function answer(query: string, width: number, options: ViewportOptions): boolean {
90
+ return query
91
+ .split(",")
92
+ .some((alternative) =>
93
+ alternative.split(/\s+and\s+/).every((part) => condition(part, width, options)),
94
+ );
95
+ }
96
+
97
+ /** A list {@link viewportMatchMedia} handed out: what {@link setViewportWidth} flips. */
98
+ interface StubMediaQueryList {
99
+ matches: boolean;
100
+ readonly media: string;
101
+ onchange: ((this: MediaQueryList, event: MediaQueryListEvent) => unknown) | null;
102
+ addEventListener(type: string, listener: EventListenerOrEventListenerObject | null): void;
103
+ removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null): void;
104
+ addListener(listener: EventListenerOrEventListenerObject | null): void;
105
+ removeListener(listener: EventListenerOrEventListenerObject | null): void;
106
+ dispatchEvent(event: Event): boolean;
107
+ }
108
+
109
+ /** One stub's viewport: its width now, and every list it has handed out. */
110
+ interface StubViewport {
111
+ width: number;
112
+ readonly options: ViewportOptions;
113
+ readonly lists: Set<StubMediaQueryList>;
114
+ }
115
+
116
+ /** Each `matchMedia` {@link viewportMatchMedia} made → its viewport. Keyed by the function,
117
+ * so {@link setViewportWidth} finds whichever stub is in force however it was put in. */
118
+ const VIEWPORTS = new WeakMap<object, StubViewport>();
119
+
120
+ function mediaQueryList(query: string, viewport: StubViewport): StubMediaQueryList {
121
+ // One set for both APIs: in a browser `addListener(fn)` IS `addEventListener("change",
122
+ // fn)`, so adding the same function through both registers it once.
123
+ const listeners = new Set<EventListenerOrEventListenerObject>();
124
+ const list: StubMediaQueryList = {
125
+ matches: answer(query, viewport.width, viewport.options),
126
+ media: query,
127
+ onchange: null,
128
+ addEventListener(type, listener) {
129
+ if (type === "change" && listener) listeners.add(listener);
130
+ },
131
+ removeEventListener(type, listener) {
132
+ if (type === "change" && listener) listeners.delete(listener);
133
+ },
134
+ addListener(listener) {
135
+ if (listener) listeners.add(listener);
136
+ },
137
+ removeListener(listener) {
138
+ if (listener) listeners.delete(listener);
139
+ },
140
+ dispatchEvent(event) {
141
+ if (event.type !== "change") return true;
142
+ const self = list as unknown as MediaQueryList;
143
+ list.onchange?.call(self, event as MediaQueryListEvent);
144
+ for (const listener of [...listeners]) {
145
+ if (typeof listener === "function") listener.call(self, event);
146
+ else listener.handleEvent(event);
147
+ }
148
+ return !event.defaultPrevented;
149
+ },
150
+ };
151
+ return list;
152
+ }
153
+
154
+ /** The `change` a browser fires at a list whose answer flipped: a `MediaQueryListEvent`'s
155
+ * `matches` and `media`, on an `Event` (jsdom has no `MediaQueryListEvent`). */
156
+ function changeEvent(list: StubMediaQueryList): Event {
157
+ const event = new Event("change");
158
+ for (const [key, value] of Object.entries({ matches: list.matches, media: list.media, target: list, currentTarget: list })) {
159
+ Object.defineProperty(event, key, { value, enumerable: true });
160
+ }
161
+ return event;
162
+ }
163
+
88
164
  /**
89
165
  * A `window.matchMedia` for a viewport `width` CSS px wide.
90
166
  *
91
167
  * Width queries answer from `width`: the range syntax the kit writes for `max-*`
92
168
  * (`(width < 768px)`) and every other comparison, `(min-width: …)` and `(max-width: …)`,
93
169
  * in px or rem; `and` and comma lists combine as in CSS. The features in `options` answer
94
- * as set there; every other query is false. The lists it returns never change, so
95
- * nothing fires `change`: a test that changes the width stubs again and renders again.
170
+ * as set there; every other query is false. The lists keep their listeners, and
171
+ * {@link setViewportWidth} resizes the viewport under them the way a browser window
172
+ * does: each list whose answer flips fires `change`.
96
173
  *
97
174
  * Hand it to the runner's own global stub where the suite undoes stubs that way —
98
175
  * `vi.stubGlobal("matchMedia", viewportMatchMedia(375))` — or use
@@ -102,23 +179,52 @@ export function viewportMatchMedia(
102
179
  width: number,
103
180
  options: ViewportOptions = {},
104
181
  ): (query: string) => MediaQueryList {
105
- const answer = (query: string) =>
106
- query
107
- .split(",")
108
- .some((alternative) =>
109
- alternative.split(/\s+and\s+/).every((part) => condition(part, width, options)),
110
- );
111
- return (query: string) =>
112
- ({
113
- matches: answer(query),
114
- media: query,
115
- onchange: null,
116
- addEventListener: () => {},
117
- removeEventListener: () => {},
118
- addListener: () => {},
119
- removeListener: () => {},
120
- dispatchEvent: () => false,
121
- }) as unknown as MediaQueryList;
182
+ const viewport: StubViewport = { width, options, lists: new Set() };
183
+ const matchMedia = (query: string) => {
184
+ const list = mediaQueryList(query, viewport);
185
+ viewport.lists.add(list);
186
+ return list as unknown as MediaQueryList;
187
+ };
188
+ VIEWPORTS.set(matchMedia, viewport);
189
+ return matchMedia;
190
+ }
191
+
192
+ /**
193
+ * Resize the stubbed viewport to `width` CSS px, live — the window being dragged narrower,
194
+ * a phone turned (0.35, keksdose's 0.34 report). Every list the stub in force has handed
195
+ * out answers from the new width, and each one whose answer flipped fires `change` to its
196
+ * `addEventListener` listeners, its legacy `addListener` ones and its `onchange`, as in a
197
+ * browser; a list whose answer stayed the same tells nobody. So a mounted component that
198
+ * listens to a breakpoint (`usePhoneLayout()`, `useBreakpoint()`) switches without being
199
+ * rendered again.
200
+ *
201
+ * The stub in force is whatever `window.matchMedia` is now — one {@link stubViewportWidth}
202
+ * or `vi.stubGlobal("matchMedia", viewportMatchMedia(…))` put in; its features
203
+ * (`options`) stay as they were. Throws when `matchMedia` is not one of these stubs, rather
204
+ * than resize nothing. Synchronous: wrap it in `act()` when a listener sets state.
205
+ *
206
+ * ```ts
207
+ * undo = stubViewportWidth(1280);
208
+ * render(<Page />);
209
+ * act(() => setViewportWidth(375));
210
+ * expect(screen.getByRole("list")).toBeInTheDocument(); // the cards
211
+ * ```
212
+ */
213
+ export function setViewportWidth(width: number): void {
214
+ const viewport = typeof window === "undefined" ? undefined : VIEWPORTS.get(window.matchMedia);
215
+ if (!viewport) {
216
+ throw new Error(
217
+ "setViewportWidth: no viewport stub is in force — put one in first with " +
218
+ "stubViewportWidth(width), or vi.stubGlobal(\"matchMedia\", viewportMatchMedia(width)).",
219
+ );
220
+ }
221
+ viewport.width = width;
222
+ for (const list of [...viewport.lists]) {
223
+ const matches = answer(list.media, width, viewport.options);
224
+ if (matches === list.matches) continue;
225
+ list.matches = matches;
226
+ list.dispatchEvent(changeEvent(list));
227
+ }
122
228
  }
123
229
 
124
230
  /**