@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
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/rhf/form-core.tsx"],"sourcesContent":["/**\n * The react-hook-form adapter: the five parts every form is built from, and the hook\n * they share. `@eifi1/ui-kit/rhf`.\n *\n * The kit's own module since 0.34 (refactor audit 2026-10, API-6f): the bound `Rhf*`\n * fields build on these parts, and import them from here. ./form is the public face —\n * `Form`, and until 0.35 the parts under their deprecated names, which no app imports.\n *\n * ```tsx\n * <Form {...form}>\n * <FormField control={form.control} name=\"amount\" render={({ field }) => (\n * <FormItem>\n * <FormLabel required>Amount</FormLabel>\n * <FormControl>\n * <NumberField value={field.value} onCommit={field.onChange} />\n * </FormControl>\n * <FormMessage />\n * </FormItem>\n * )} />\n * </Form>\n * ```\n *\n * The shape is shadcn's `form.tsx`, on purpose: Kastlan builds every form from that\n * file, so moving onto this one is an import line rather than a rewrite. What differs\n * is what the shadcn file gets wrong:\n *\n * - **`aria-describedby` names only what is on screen.** shadcn always points the\n * control at a description id, rendered or not, and at the message id whenever the\n * field has an error, whether a message was rendered for it or not. A reference to\n * a node that does not exist describes the field as nothing. Here the description\n * and the message say when they are mounted, and the control points at exactly\n * those.\n * - **Merged, never replaced.** A control that already carries its own\n * `aria-describedby` keeps it, first — the same rule as the kit's `error` prop\n * (see `useFieldError` in components/field-parts.tsx).\n * - **No Radix.** `FormControl` is a one-child `cloneElement`, not `Slot`, and the\n * label is the kit's own `Label` — the kit carries no Radix dependency and this\n * entry is not the place to start.\n *\n * ## Why this is its own entry\n *\n * The main barrel once shipped a react-hook-form hook (`use-rhf-wizard-step`) and\n * removed it: it made react-hook-form an install every consumer's typecheck paid for,\n * for a hook nobody called (see the wizard section of src/index.ts). This is the\n * answer to that, not a reversal of it. Nothing outside `src/rhf*` imports\n * react-hook-form — `__tests__/packaging-contract.test.ts` holds that — and the peer is\n * optional, so an app that does not use it installs nothing and imports nothing.\n *\n * ## Painting the field\n *\n * `FormControl` sets `aria-invalid` on its child. `NumberField` and the pickers paint\n * from that attribute; `Input`, `Select` and `Textarea` paint only from their\n * `invalid` prop (see its note in components/field-base.tsx: a bare attribute announces but\n * does not paint), so pass it from the render prop —\n * `<Input {...field} invalid={fieldState.invalid} />`. `FormControl` does not inject\n * `invalid` itself because its child may be a native element, which would print it\n * into the DOM as an unknown attribute.\n *\n * ## Clearing a picker: `null`, unless the picker is told otherwise\n *\n * Nothing here touches a value. `FormField` is `Controller`, so what a control hands\n * `field.onChange` is exactly what lands in the form — and what the entity pickers\n * hand it on a clear is `null`: `EntityCombobox`'s \"×\", `InlineEntityCombobox`'s \"×\"\n * and its emptied text. (`MultiEntityCombobox` and `MultiSelect` clear to `[]`.)\n *\n * A zod schema that spells \"no choice\" as `\"\"` (`z.string()`, or `z.literal(\"\")` in a\n * union) then fails on the `null`. Say so on the PICKER, not here:\n *\n * ```tsx\n * <EntityCombobox clearValue=\"\" value={field.value} onChange={field.onChange} … />\n * ```\n *\n * `clearValue` also types `onChange` (`(v: V | \"\") => void` rather than\n * `V | null`) and reads `\"\"` back as \"nothing selected\", so an initial `defaultValues`\n * of `\"\"` shows the placeholder rather than an unresolvable id. The adapter does not\n * coerce because it cannot see the schema: mapping `null` to `\"\"` for every field\n * would break every schema that wants `null` (`z.string().nullable()`), which is the\n * pickers' default for a reason — it is the one value no option can have.\n */\nimport {\n Children,\n cloneElement,\n createContext,\n isValidElement,\n useCallback,\n useContext,\n useId,\n useLayoutEffect,\n useMemo,\n useState,\n type ComponentProps,\n type ReactElement,\n} from \"react\";\nimport {\n Controller,\n FormProvider,\n useFormContext,\n useFormState,\n type ControllerProps,\n type FieldPath,\n type FieldValues,\n} from \"react-hook-form\";\nimport { Label, type LabelProps } from \"../components/field-base\";\nimport { cn } from \"../lib/cn\";\n\n/** The form's `useForm()` return, spread in: `<Form {...form}>`. react-hook-form's own\n * `FormProvider`, under the name the shadcn file gave it. */\nexport const Form = FormProvider;\n\n// ── contexts ─────────────────────────────────────────────────────────────────\n\nconst FormFieldContext = createContext<{ name: string } | null>(null);\n\n/** The two optional nodes a control can be described by. */\ntype Described = \"description\" | \"message\";\n\ninterface FormItemContextValue {\n id: string;\n mounted: Record<Described, boolean>;\n mark: (slot: Described, on: boolean) => void;\n}\n\nconst FormItemContext = createContext<FormItemContextValue | null>(null);\n\n// ── FormField ────────────────────────────────────────────────────────────────\n\n/**\n * react-hook-form's `Controller`, with the field's name put in context for the parts\n * below it. Takes exactly `Controller`'s props.\n */\nexport function FormField<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformedValues = TFieldValues,\n>(props: ControllerProps<TFieldValues, TName, TTransformedValues>) {\n const value = useMemo(() => ({ name: props.name as string }), [props.name]);\n return (\n <FormFieldContext.Provider value={value}>\n <Controller {...props} />\n </FormFieldContext.Provider>\n );\n}\n\n// ── useFormField ─────────────────────────────────────────────────────────────\n\n/**\n * The ids and the state of the field around the caller: for a part of your own that\n * sits beside the kit's (a character counter that belongs in `aria-describedby`, a\n * label with a different look).\n *\n * Throws outside a `FormField` + `FormItem` pair. shadcn's version answered there with\n * ids like `undefined-form-item`, which label nothing and fail nowhere.\n */\nexport function useFormField() {\n const field = useContext(FormFieldContext);\n const item = useContext(FormItemContext);\n if (!field) throw new Error(\"useFormField must be used inside <FormField>.\");\n if (!item) throw new Error(\"useFormField must be used inside <FormItem>.\");\n const { getFieldState } = useFormContext();\n // Subscribes this part to the field's state, so a label or message re-renders when\n // the field's error changes without the whole form doing so.\n const formState = useFormState({ name: field.name });\n const state = getFieldState(field.name, formState);\n const formDescriptionId = `${item.id}-description`;\n const formMessageId = `${item.id}-message`;\n const describedBy = [\n item.mounted.description && formDescriptionId,\n item.mounted.message && formMessageId,\n ]\n .filter(Boolean)\n .join(\" \");\n return {\n id: item.id,\n name: field.name,\n formItemId: `${item.id}-control`,\n formDescriptionId,\n formMessageId,\n /** The ids of the description and message that are actually rendered, or\n * `undefined` when neither is. */\n describedBy: describedBy || undefined,\n ...state,\n };\n}\n\n// ── FormItem ─────────────────────────────────────────────────────────────────\n\nexport type FormItemProps = ComponentProps<\"div\">;\n\n/** One field's box: mints the id its label, control, description and message share. */\nexport function FormItem({ className, ...rest }: FormItemProps) {\n const id = useId();\n const [mounted, setMounted] = useState<Record<Described, boolean>>({\n description: false,\n message: false,\n });\n const mark = useCallback(\n (slot: Described, on: boolean) =>\n setMounted((prev) => (prev[slot] === on ? prev : { ...prev, [slot]: on })),\n [],\n );\n const value = useMemo(() => ({ id, mounted, mark }), [id, mounted, mark]);\n return (\n <FormItemContext.Provider value={value}>\n {/* `content-start`: stretched to a taller neighbour's height in a grid row, the\n item gave the extra to its rows and its control sat lower (showcase audit). */}\n <div data-slot=\"form-item\" {...rest} className={cn(\"grid content-start gap-1\", className)} />\n </FormItemContext.Provider>\n );\n}\n\n/** Report a describing node's presence to the item, so the control points at it only\n * while it is on screen. A layout effect, so the reference lands before paint. */\nfunction useDescribes(slot: Described, on: boolean) {\n const item = useContext(FormItemContext);\n const mark = item?.mark;\n useLayoutEffect(() => {\n if (!on || !mark) return;\n mark(slot, true);\n return () => mark(slot, false);\n }, [slot, on, mark]);\n}\n\n// ── FormLabel ────────────────────────────────────────────────────────────────\n\nexport type FormLabelProps = LabelProps;\n\n/**\n * The kit's {@link Label}, bound to the control by `htmlFor` and turned to the danger\n * colour while the field is invalid. `required` draws the kit's `aria-hidden` mark:\n * whether the field is required is the control's to announce.\n */\nexport function FormLabel({ className, ...rest }: FormLabelProps) {\n const { error, formItemId } = useFormField();\n return (\n <Label\n data-slot=\"form-label\"\n data-error={error ? true : undefined}\n htmlFor={formItemId}\n {...rest}\n className={cn(\"data-[error=true]:text-danger\", className)}\n />\n );\n}\n\n// ── FormControl ──────────────────────────────────────────────────────────────\n\nexport interface FormControlProps {\n /** Exactly one element: the kit field, or a native one. It receives `id`,\n * `aria-describedby` and `aria-invalid`. */\n children: ReactElement;\n}\n\ntype AriaProps = {\n id?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: ComponentProps<\"input\">[\"aria-invalid\"];\n};\n\n/**\n * Wires its one child to the item: the `id` the label points at, `aria-describedby`\n * naming the rendered description and message, and `aria-invalid` while the field has\n * an error.\n *\n * The child's own values win where it has them (an explicit `id`, a set\n * `aria-invalid`), as they did under Radix `Slot`; its own `aria-describedby` is\n * merged in front of the item's rather than replaced.\n */\nexport function FormControl({ children }: FormControlProps) {\n const { invalid, formItemId, describedBy } = useFormField();\n const child = Children.only(children);\n if (!isValidElement<AriaProps>(child)) return child;\n const own = child.props;\n const merged = [own[\"aria-describedby\"], describedBy].filter(Boolean).join(\" \");\n return cloneElement(child, {\n id: own.id ?? formItemId,\n \"aria-describedby\": merged || undefined,\n \"aria-invalid\": own[\"aria-invalid\"] ?? (invalid || undefined),\n });\n}\n\n// ── FormDescription / FormMessage ────────────────────────────────────────────\n\nexport type FormDescriptionProps = ComponentProps<\"p\">;\n\n/** Standing advice under the field (\"at least twelve characters\"). */\nexport function FormDescription({ className, ...rest }: FormDescriptionProps) {\n const { formDescriptionId } = useFormField();\n useDescribes(\"description\", true);\n return (\n <p\n data-slot=\"form-description\"\n {...rest}\n id={formDescriptionId}\n className={cn(\"text-caption leading-tight text-muted\", className)}\n />\n );\n}\n\nexport type FormMessageProps = ComponentProps<\"p\">;\n\n/**\n * The field's error message, from the form — the kit renders no string of its own\n * here; the schema or the `rules` carry the words. Without an error it renders its\n * `children`, if any, and otherwise nothing.\n *\n * Deliberately NOT `role=\"alert\"`, for the reason the kit's `error` prop gives: the\n * message is read when focus reaches the control, which is where a field's own error\n * is wanted, and an alert would interrupt on every keystroke of a form that\n * re-validates as you type.\n */\nexport function FormMessage({ className, children, ...rest }: FormMessageProps) {\n const { error, formMessageId } = useFormField();\n const body = error ? String(error.message ?? \"\") : children;\n const shown = body !== undefined && body !== null && body !== false && body !== \"\";\n useDescribes(\"message\", shown);\n if (!shown) return null;\n return (\n <p\n data-slot=\"form-message\"\n {...rest}\n id={formMessageId}\n className={cn(\"text-caption leading-tight text-danger\", className)}\n >\n {body}\n </p>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/rhf/form-core.tsx"],"sourcesContent":["/**\n * The react-hook-form adapter: the five parts every bound field is built from, and the\n * hook they share. Behind `@eifi1/ui-kit/rhf`, which exports `Form` alone of them.\n *\n * The kit's own module since 0.34 (refactor audit 2026-10, API-6f): the bound `Rhf*`\n * fields and `RhfField` build on these parts, and import them from here. ./form is the\n * public face — `Form`; the parts left the public surface in 0.35 (deprecated in 0.34),\n * when no app imported them any more.\n *\n * ```tsx\n * <Form {...form}>\n * <FormField control={form.control} name=\"amount\" render={({ field }) => (\n * <FormItem>\n * <FormLabel required>Amount</FormLabel>\n * <FormControl>\n * <NumberField value={field.value} onCommit={field.onChange} />\n * </FormControl>\n * <FormMessage />\n * </FormItem>\n * )} />\n * </Form>\n * ```\n *\n * The shape is shadcn's `form.tsx`, on purpose: Kastlan builds every form from that\n * file, so moving onto this one is an import line rather than a rewrite. What differs\n * is what the shadcn file gets wrong:\n *\n * - **`aria-describedby` names only what is on screen.** shadcn always points the\n * control at a description id, rendered or not, and at the message id whenever the\n * field has an error, whether a message was rendered for it or not. A reference to\n * a node that does not exist describes the field as nothing. Here the description\n * and the message say when they are mounted, and the control points at exactly\n * those.\n * - **Merged, never replaced.** A control that already carries its own\n * `aria-describedby` keeps it, first — the same rule as the kit's `error` prop\n * (see `useFieldError` in components/field-parts.tsx).\n * - **No Radix.** `FormControl` is a one-child `cloneElement`, not `Slot`, and the\n * label is the kit's own `Label` — the kit carries no Radix dependency and this\n * entry is not the place to start.\n *\n * ## Why this is its own entry\n *\n * The main barrel once shipped a react-hook-form hook (`use-rhf-wizard-step`) and\n * removed it: it made react-hook-form an install every consumer's typecheck paid for,\n * for a hook nobody called (see the wizard section of src/index.ts). This is the\n * answer to that, not a reversal of it. Nothing outside `src/rhf*` imports\n * react-hook-form — `__tests__/packaging-contract.test.ts` holds that — and the peer is\n * optional, so an app that does not use it installs nothing and imports nothing.\n *\n * ## Painting the field\n *\n * `FormControl` sets `aria-invalid` on its child. `NumberField` and the pickers paint\n * from that attribute; `Input`, `Select` and `Textarea` paint only from their\n * `invalid` prop (see its note in components/field-base.tsx: a bare attribute announces but\n * does not paint), so pass it from the render prop —\n * `<Input {...field} invalid={fieldState.invalid} />`. `FormControl` does not inject\n * `invalid` itself because its child may be a native element, which would print it\n * into the DOM as an unknown attribute.\n *\n * ## Clearing a picker: `null`, unless the picker is told otherwise\n *\n * Nothing here touches a value. `FormField` is `Controller`, so what a control hands\n * `field.onChange` is exactly what lands in the form — and what the entity pickers\n * hand it on a clear is `null`: `EntityCombobox`'s \"×\", `InlineEntityCombobox`'s \"×\"\n * and its emptied text. (`MultiEntityCombobox` and `MultiSelect` clear to `[]`.)\n *\n * A zod schema that spells \"no choice\" as `\"\"` (`z.string()`, or `z.literal(\"\")` in a\n * union) then fails on the `null`. Say so on the PICKER, not here:\n *\n * ```tsx\n * <EntityCombobox clearValue=\"\" value={field.value} onChange={field.onChange} … />\n * ```\n *\n * `clearValue` also types `onChange` (`(v: V | \"\") => void` rather than\n * `V | null`) and reads `\"\"` back as \"nothing selected\", so an initial `defaultValues`\n * of `\"\"` shows the placeholder rather than an unresolvable id. The adapter does not\n * coerce because it cannot see the schema: mapping `null` to `\"\"` for every field\n * would break every schema that wants `null` (`z.string().nullable()`), which is the\n * pickers' default for a reason — it is the one value no option can have.\n */\nimport {\n Children,\n cloneElement,\n createContext,\n isValidElement,\n useCallback,\n useContext,\n useId,\n useLayoutEffect,\n useMemo,\n useState,\n type ComponentProps,\n type ReactElement,\n} from \"react\";\nimport {\n Controller,\n FormProvider,\n useFormContext,\n useFormState,\n type ControllerProps,\n type FieldPath,\n type FieldValues,\n} from \"react-hook-form\";\nimport { Label, type LabelProps } from \"../components/field-base\";\nimport { cn } from \"../lib/cn\";\n\n/** The form's `useForm()` return, spread in: `<Form {...form}>`. react-hook-form's own\n * `FormProvider`, under the name the shadcn file gave it. */\nexport const Form = FormProvider;\n\n// ── contexts ─────────────────────────────────────────────────────────────────\n\nconst FormFieldContext = createContext<{ name: string } | null>(null);\n\n/** The two optional nodes a control can be described by. */\ntype Described = \"description\" | \"message\";\n\ninterface FormItemContextValue {\n id: string;\n mounted: Record<Described, boolean>;\n mark: (slot: Described, on: boolean) => void;\n}\n\nconst FormItemContext = createContext<FormItemContextValue | null>(null);\n\n// ── FormField ────────────────────────────────────────────────────────────────\n\n/**\n * react-hook-form's `Controller`, with the field's name put in context for the parts\n * below it. Takes exactly `Controller`'s props.\n */\nexport function FormField<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformedValues = TFieldValues,\n>(props: ControllerProps<TFieldValues, TName, TTransformedValues>) {\n const value = useMemo(() => ({ name: props.name as string }), [props.name]);\n return (\n <FormFieldContext.Provider value={value}>\n <Controller {...props} />\n </FormFieldContext.Provider>\n );\n}\n\n// ── useFormField ─────────────────────────────────────────────────────────────\n\n/**\n * The ids and the state of the field around the caller: for a part of your own that\n * sits beside the kit's (a character counter that belongs in `aria-describedby`, a\n * label with a different look).\n *\n * Throws outside a `FormField` + `FormItem` pair. shadcn's version answered there with\n * ids like `undefined-form-item`, which label nothing and fail nowhere.\n */\nexport function useFormField() {\n const field = useContext(FormFieldContext);\n const item = useContext(FormItemContext);\n if (!field) throw new Error(\"useFormField must be used inside <FormField>.\");\n if (!item) throw new Error(\"useFormField must be used inside <FormItem>.\");\n const { getFieldState } = useFormContext();\n // Subscribes this part to the field's state, so a label or message re-renders when\n // the field's error changes without the whole form doing so.\n const formState = useFormState({ name: field.name });\n const state = getFieldState(field.name, formState);\n const formDescriptionId = `${item.id}-description`;\n const formMessageId = `${item.id}-message`;\n const describedBy = [\n item.mounted.description && formDescriptionId,\n item.mounted.message && formMessageId,\n ]\n .filter(Boolean)\n .join(\" \");\n return {\n id: item.id,\n name: field.name,\n formItemId: `${item.id}-control`,\n formDescriptionId,\n formMessageId,\n /** The ids of the description and message that are actually rendered, or\n * `undefined` when neither is. */\n describedBy: describedBy || undefined,\n ...state,\n };\n}\n\n// ── FormItem ─────────────────────────────────────────────────────────────────\n\nexport type FormItemProps = ComponentProps<\"div\">;\n\n/** One field's box: mints the id its label, control, description and message share. */\nexport function FormItem({ className, ...rest }: FormItemProps) {\n const id = useId();\n const [mounted, setMounted] = useState<Record<Described, boolean>>({\n description: false,\n message: false,\n });\n const mark = useCallback(\n (slot: Described, on: boolean) =>\n setMounted((prev) => (prev[slot] === on ? prev : { ...prev, [slot]: on })),\n [],\n );\n const value = useMemo(() => ({ id, mounted, mark }), [id, mounted, mark]);\n return (\n <FormItemContext.Provider value={value}>\n {/* `content-start`: stretched to a taller neighbour's height in a grid row, the\n item gave the extra to its rows and its control sat lower (showcase audit). */}\n <div data-slot=\"form-item\" {...rest} className={cn(\"grid content-start gap-1\", className)} />\n </FormItemContext.Provider>\n );\n}\n\n/** Report a describing node's presence to the item, so the control points at it only\n * while it is on screen. A layout effect, so the reference lands before paint. */\nfunction useDescribes(slot: Described, on: boolean) {\n const item = useContext(FormItemContext);\n const mark = item?.mark;\n useLayoutEffect(() => {\n if (!on || !mark) return;\n mark(slot, true);\n return () => mark(slot, false);\n }, [slot, on, mark]);\n}\n\n// ── FormLabel ────────────────────────────────────────────────────────────────\n\nexport type FormLabelProps = LabelProps;\n\n/**\n * The kit's {@link Label}, bound to the control by `htmlFor` and turned to the danger\n * colour while the field is invalid. `required` draws the kit's `aria-hidden` mark:\n * whether the field is required is the control's to announce.\n */\nexport function FormLabel({ className, ...rest }: FormLabelProps) {\n const { error, formItemId } = useFormField();\n return (\n <Label\n data-slot=\"form-label\"\n data-error={error ? true : undefined}\n htmlFor={formItemId}\n {...rest}\n className={cn(\"data-[error=true]:text-danger\", className)}\n />\n );\n}\n\n// ── FormControl ──────────────────────────────────────────────────────────────\n\nexport interface FormControlProps {\n /** Exactly one element: the kit field, or a native one. It receives `id`,\n * `aria-describedby` and `aria-invalid`. */\n children: ReactElement;\n}\n\ntype AriaProps = {\n id?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: ComponentProps<\"input\">[\"aria-invalid\"];\n};\n\n/**\n * Wires its one child to the item: the `id` the label points at, `aria-describedby`\n * naming the rendered description and message, and `aria-invalid` while the field has\n * an error.\n *\n * The child's own values win where it has them (an explicit `id`, a set\n * `aria-invalid`), as they did under Radix `Slot`; its own `aria-describedby` is\n * merged in front of the item's rather than replaced.\n */\nexport function FormControl({ children }: FormControlProps) {\n const { invalid, formItemId, describedBy } = useFormField();\n const child = Children.only(children);\n if (!isValidElement<AriaProps>(child)) return child;\n const own = child.props;\n const merged = [own[\"aria-describedby\"], describedBy].filter(Boolean).join(\" \");\n return cloneElement(child, {\n id: own.id ?? formItemId,\n \"aria-describedby\": merged || undefined,\n \"aria-invalid\": own[\"aria-invalid\"] ?? (invalid || undefined),\n });\n}\n\n// ── FormDescription / FormMessage ────────────────────────────────────────────\n\nexport type FormDescriptionProps = ComponentProps<\"p\">;\n\n/** Standing advice under the field (\"at least twelve characters\"). */\nexport function FormDescription({ className, ...rest }: FormDescriptionProps) {\n const { formDescriptionId } = useFormField();\n useDescribes(\"description\", true);\n return (\n <p\n data-slot=\"form-description\"\n {...rest}\n id={formDescriptionId}\n className={cn(\"text-caption leading-tight text-muted\", className)}\n />\n );\n}\n\nexport type FormMessageProps = ComponentProps<\"p\">;\n\n/**\n * The field's error message, from the form — the kit renders no string of its own\n * here; the schema or the `rules` carry the words. Without an error it renders its\n * `children`, if any, and otherwise nothing.\n *\n * Deliberately NOT `role=\"alert\"`, for the reason the kit's `error` prop gives: the\n * message is read when focus reaches the control, which is where a field's own error\n * is wanted, and an alert would interrupt on every keystroke of a form that\n * re-validates as you type.\n */\nexport function FormMessage({ className, children, ...rest }: FormMessageProps) {\n const { error, formMessageId } = useFormField();\n const body = error ? String(error.message ?? \"\") : children;\n const shown = body !== undefined && body !== null && body !== false && body !== \"\";\n useDescribes(\"message\", shown);\n if (!shown) return null;\n return (\n <p\n data-slot=\"form-message\"\n {...rest}\n id={formMessageId}\n className={cn(\"text-caption leading-tight text-danger\", className)}\n >\n {body}\n </p>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,41 +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.js";
13
12
  export { Form } from "./form-core.js";
14
- /** @deprecated 0.34 — the bound `Rhf*` fields, or `RhfField` for a control of your own;
15
- * removed in 0.35 (API-6f). */
16
- export declare const FormField: typeof core.FormField;
17
- /** @deprecated 0.34 — `RhfField`, which reads the field's ids itself; removed in 0.35
18
- * (API-6f). */
19
- export declare const useFormField: typeof core.useFormField;
20
- /** @deprecated 0.34 — the bound `Rhf*` fields, or `RhfField`; removed in 0.35 (API-6f). */
21
- export declare const FormItem: typeof core.FormItem;
22
- /** @deprecated 0.34 — the bound fields' and `RhfField`'s `label`; removed in 0.35 (API-6f). */
23
- export declare const FormLabel: typeof core.FormLabel;
24
- /** @deprecated 0.34 — `RhfField`, which wires its control the same way; removed in 0.35
25
- * (API-6f). */
26
- export declare const FormControl: typeof core.FormControl;
27
- /** @deprecated 0.34 — the bound fields' and `RhfField`'s `hint`; removed in 0.35 (API-6f). */
28
- export declare const FormDescription: typeof core.FormDescription;
29
- /** @deprecated 0.34 — the bound fields and `RhfField`, which show the error themselves;
30
- * removed in 0.35 (API-6f). */
31
- export declare const FormMessage: typeof core.FormMessage;
32
- /** @deprecated 0.34 — goes with `FormItem`; removed in 0.35 (API-6f). */
33
- export type FormItemProps = core.FormItemProps;
34
- /** @deprecated 0.34 — goes with `FormLabel`; removed in 0.35 (API-6f). */
35
- export type FormLabelProps = core.FormLabelProps;
36
- /** @deprecated 0.34 — goes with `FormControl`; removed in 0.35 (API-6f). */
37
- export type FormControlProps = core.FormControlProps;
38
- /** @deprecated 0.34 — goes with `FormDescription`; removed in 0.35 (API-6f). */
39
- export type FormDescriptionProps = core.FormDescriptionProps;
40
- /** @deprecated 0.34 — goes with `FormMessage`; removed in 0.35 (API-6f). */
41
- export type FormMessageProps = core.FormMessageProps;
package/dist/rhf/form.js CHANGED
@@ -1,21 +1,6 @@
1
1
  "use client";
2
- import * as core from "./form-core.js";
3
2
  import { Form } from "./form-core.js";
4
- const FormField = core.FormField;
5
- const useFormField = core.useFormField;
6
- const FormItem = core.FormItem;
7
- const FormLabel = core.FormLabel;
8
- const FormControl = core.FormControl;
9
- const FormDescription = core.FormDescription;
10
- const FormMessage = core.FormMessage;
11
3
  export {
12
- Form,
13
- FormControl,
14
- FormDescription,
15
- FormField,
16
- FormItem,
17
- FormLabel,
18
- FormMessage,
19
- useFormField
4
+ Form
20
5
  };
21
6
  //# sourceMappingURL=form.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/rhf/form.tsx"],"sourcesContent":["/**\n * `Form`, and until 0.35 the shadcn scaffold under its old public names — the public face\n * of ./form-core, where the scaffold lives (its header has the whole story: what differs\n * from shadcn's file, why /rhf is its own entry, how a picker clears).\n *\n * 0.34 (refactor audit 2026-10, API-6f): no app builds a form from the scaffold any more.\n * kastlan, the one /rhf consumer, uses `Form`, the bound `Rhf*` fields and `RhfField` for\n * a control of its own, so the seven names and their props types leave the public\n * surface in 0.35. The bound fields import the parts from the core, so the removal is\n * the block below.\n */\nimport * as core from \"./form-core\";\n\nexport { Form } from \"./form-core\";\n\n/* ── Deprecated 0.34, removed in 0.35 ───────────────────────────────────────\n * Each is the core's own value under its public name. The replacement for all of them:\n * a bound field (`RhfTextField`, `RhfSelect`, …), or `RhfField` around a control of\n * your own — the same parts, folded into one element. */\n\n/** @deprecated 0.34 — the bound `Rhf*` fields, or `RhfField` for a control of your own;\n * removed in 0.35 (API-6f). */\nexport const FormField = core.FormField;\n/** @deprecated 0.34 — `RhfField`, which reads the field's ids itself; removed in 0.35\n * (API-6f). */\nexport const useFormField = core.useFormField;\n/** @deprecated 0.34 — the bound `Rhf*` fields, or `RhfField`; removed in 0.35 (API-6f). */\nexport const FormItem = core.FormItem;\n/** @deprecated 0.34 — the bound fields' and `RhfField`'s `label`; removed in 0.35 (API-6f). */\nexport const FormLabel = core.FormLabel;\n/** @deprecated 0.34 — `RhfField`, which wires its control the same way; removed in 0.35\n * (API-6f). */\nexport const FormControl = core.FormControl;\n/** @deprecated 0.34 — the bound fields' and `RhfField`'s `hint`; removed in 0.35 (API-6f). */\nexport const FormDescription = core.FormDescription;\n/** @deprecated 0.34 — the bound fields and `RhfField`, which show the error themselves;\n * removed in 0.35 (API-6f). */\nexport const FormMessage = core.FormMessage;\n/** @deprecated 0.34 — goes with `FormItem`; removed in 0.35 (API-6f). */\nexport type FormItemProps = core.FormItemProps;\n/** @deprecated 0.34 — goes with `FormLabel`; removed in 0.35 (API-6f). */\nexport type FormLabelProps = core.FormLabelProps;\n/** @deprecated 0.34 — goes with `FormControl`; removed in 0.35 (API-6f). */\nexport type FormControlProps = core.FormControlProps;\n/** @deprecated 0.34 — goes with `FormDescription`; removed in 0.35 (API-6f). */\nexport type FormDescriptionProps = core.FormDescriptionProps;\n/** @deprecated 0.34 — goes with `FormMessage`; removed in 0.35 (API-6f). */\nexport type FormMessageProps = core.FormMessageProps;\n"],"mappings":";AAWA,YAAY,UAAU;AAEtB,SAAS,YAAY;AASd,MAAM,YAAY,KAAK;AAGvB,MAAM,eAAe,KAAK;AAE1B,MAAM,WAAW,KAAK;AAEtB,MAAM,YAAY,KAAK;AAGvB,MAAM,cAAc,KAAK;AAEzB,MAAM,kBAAkB,KAAK;AAG7B,MAAM,cAAc,KAAK;","names":[]}
1
+ {"version":3,"sources":["../../src/rhf/form.tsx"],"sourcesContent":["/**\n * `Form` — the public face of ./form-core, where the shadcn scaffold the bound fields are\n * built from lives (its header has the whole story: what differs from shadcn's file, why\n * /rhf is its own entry, how a picker clears).\n *\n * The scaffold itself (`FormField`, `useFormField`, `FormItem`, `FormLabel`, `FormControl`,\n * `FormDescription`, `FormMessage` and five props types) was public until 0.35 (refactor\n * audit 2026-10, API-6f; deprecated in 0.34): no app builds a form from it any more.\n * kastlan, the one /rhf consumer, uses `Form`, the bound `Rhf*` fields and `RhfField` for\n * a control of its own — the same parts, folded into one element.\n */\nexport { Form } from \"./form-core\";\n"],"mappings":";AAWA,SAAS,YAAY;","names":[]}
@@ -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
  */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/rhf/line-items.tsx"],"sourcesContent":["/**\n * {@link LineItems} over react-hook-form's `useFieldArray`. `@eifi1/ui-kit/rhf`.\n *\n * ```tsx\n * <RhfLineItems\n * name=\"components\"\n * newItem={() => ({ type: \"net_rent\", amount: null, validFrom: \"\" })}\n * minItems={1}\n * columns={[\n * { key: \"type\", header: \"Type\", render: ({ name, label }) => (\n * <RhfSelect name={`${name}.type`} options={TYPES} aria-label={label} />\n * ) },\n * { key: \"amount\", header: \"Amount\", width: \"8rem\", align: \"end\", render: ({ name, label }) => (\n * <RhfMoneyField name={`${name}.amount`} aria-label={label} />\n * ) },\n * ]}\n * totals={{ amount: formatMoney(total) }}\n * />\n * ```\n *\n * The rows are the array's `fields` (keyed by their `id`, as react-hook-form asks),\n * Add appends `newItem()`, remove is `remove(index)`, and an error on the array\n * itself — the `rules` here, or a resolver's `lines: \"At least two lines\"` — is the\n * list's `error`. Each cell's `render` gets `name`, the row's path\n * (`\"components.2\"`), to bind its field to.\n *\n * `summary`, `fieldLabels`, `narrowColumns`, `removePlacement` and `removeAlign` pass\n * through to {@link LineItems}. With\n * `fieldLabels=\"floating\"` a cell's `fieldLabel` is the column's name; the bound\n * fields' own `label` is a `FormLabel` ABOVE the control, so for the floating\n * one render the kit field through `RhfField`:\n * `render={({ field, invalid }) => <Input label={fieldLabel} aria-label={label} {...field} invalid={invalid} />}`.\n */\nimport type { ReactNode } from \"react\";\nimport {\n useFieldArray,\n useFormState,\n type FieldArray,\n type FieldArrayPath,\n type FieldArrayWithId,\n type FieldValues,\n type UseFieldArrayProps,\n} from \"react-hook-form\";\nimport {\n LineItems,\n type LineItemCellContext,\n type LineItemsColumn,\n type LineItemsProps,\n} from \"../components/line-items\";\n\n/** A cell's context, with the row's path in the form. */\nexport interface RhfLineItemCellContext<TItem, TArrayName extends string>\n extends LineItemCellContext<TItem> {\n /** The row's path: `${name}.${index}` — bind the cell's field to\n * `` `${name}.amount` ``. */\n name: `${TArrayName}.${number}`;\n}\n\nexport interface RhfLineItemsColumn<TItem, TArrayName extends string>\n extends Omit<LineItemsColumn<TItem>, \"render\"> {\n render: (ctx: RhfLineItemCellContext<TItem, TArrayName>) => ReactNode;\n}\n\nexport interface RhfLineItemsProps<\n TFieldValues extends FieldValues = FieldValues,\n TArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>,\n TTransformed = TFieldValues,\n> extends Omit<\n LineItemsProps<FieldArrayWithId<TFieldValues, TArrayName, \"id\">>,\n \"items\" | \"columns\" | \"onAdd\" | \"onRemove\" | \"getKey\"\n > {\n name: TArrayName;\n /** `form.control`. Optional under `<Form {...form}>`. */\n control?: UseFieldArrayProps<TFieldValues, TArrayName, \"id\", TTransformed>[\"control\"];\n /** `useFieldArray`'s `rules` (`minLength`, `validate` …); its message is the list's\n * error. */\n rules?: UseFieldArrayProps<TFieldValues, TArrayName, \"id\", TTransformed>[\"rules\"];\n columns: readonly RhfLineItemsColumn<FieldArrayWithId<TFieldValues, TArrayName, \"id\">, TArrayName>[];\n /** A new row's values. Omitted: no add button. */\n newItem?: () => FieldArray<TFieldValues, TArrayName>;\n /** `false`: no remove buttons. Default `true`. */\n removable?: boolean;\n}\n\n/** The message of an error at `path` in react-hook-form's errors tree: the array's\n * own (`root`, from `rules`) or one a resolver put on the array. */\nfunction arrayError(errors: unknown, path: string): string | undefined {\n let node: unknown = errors;\n for (const key of path.split(\".\")) {\n if (node === null || typeof node !== \"object\") return undefined;\n node = (node as Record<string, unknown>)[key];\n }\n if (node === null || typeof node !== \"object\") return undefined;\n const { root, message } = node as { root?: { message?: unknown }; message?: unknown };\n const text = root?.message ?? message;\n return typeof text === \"string\" && text !== \"\" ? text : undefined;\n}\n\n/** {@link LineItems} bound to a field array. See the module note. */\nexport function RhfLineItems<\n TFieldValues extends FieldValues = FieldValues,\n TArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n columns,\n newItem,\n removable = true,\n error,\n ...rest\n}: RhfLineItemsProps<TFieldValues, TArrayName, TTransformed>) {\n const { fields, append, remove } = useFieldArray<TFieldValues, TArrayName, \"id\", TTransformed>({\n control,\n name,\n rules,\n });\n const { errors } = useFormState({ control, name: name as never });\n const bound: LineItemsColumn<FieldArrayWithId<TFieldValues, TArrayName, \"id\">>[] = columns.map(\n ({ render, ...column }) => ({\n ...column,\n render: (ctx) => render({ ...ctx, name: `${name}.${ctx.index}` as `${TArrayName}.${number}` }),\n }),\n );\n return (\n <LineItems\n {...rest}\n items={fields}\n columns={bound}\n getKey={(field) => field.id}\n onAdd={newItem ? () => append(newItem()) : undefined}\n onRemove={removable ? (index) => remove(index) : undefined}\n error={error ?? arrayError(errors, name)}\n />\n );\n}\n"],"mappings":";AA8HI;AA5FJ;AAAA,EACE;AAAA,EACA;AAAA,OAMK;AACP;AAAA,EACE;AAAA,OAIK;AAsCP,SAAS,WAAW,QAAiB,MAAkC;AACrE,MAAI,OAAgB;AACpB,aAAW,OAAO,KAAK,MAAM,GAAG,GAAG;AACjC,QAAI,SAAS,QAAQ,OAAO,SAAS,SAAU,QAAO;AACtD,WAAQ,KAAiC,GAAG;AAAA,EAC9C;AACA,MAAI,SAAS,QAAQ,OAAO,SAAS,SAAU,QAAO;AACtD,QAAM,EAAE,MAAM,QAAQ,IAAI;AAC1B,QAAM,OAAO,MAAM,WAAW;AAC9B,SAAO,OAAO,SAAS,YAAY,SAAS,KAAK,OAAO;AAC1D;AAGO,SAAS,aAId;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,GAAG;AACL,GAA8D;AAC5D,QAAM,EAAE,QAAQ,QAAQ,OAAO,IAAI,cAA4D;AAAA,IAC7F;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACD,QAAM,EAAE,OAAO,IAAI,aAAa,EAAE,SAAS,KAAoB,CAAC;AAChE,QAAM,QAA6E,QAAQ;AAAA,IACzF,CAAC,EAAE,QAAQ,GAAG,OAAO,OAAO;AAAA,MAC1B,GAAG;AAAA,MACH,QAAQ,CAAC,QAAQ,OAAO,EAAE,GAAG,KAAK,MAAM,GAAG,IAAI,IAAI,IAAI,KAAK,GAAgC,CAAC;AAAA,IAC/F;AAAA,EACF;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,OAAO;AAAA,MACP,SAAS;AAAA,MACT,QAAQ,CAAC,UAAU,MAAM;AAAA,MACzB,OAAO,UAAU,MAAM,OAAO,QAAQ,CAAC,IAAI;AAAA,MAC3C,UAAU,YAAY,CAAC,UAAU,OAAO,KAAK,IAAI;AAAA,MACjD,OAAO,SAAS,WAAW,QAAQ,IAAI;AAAA;AAAA,EACzC;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/rhf/line-items.tsx"],"sourcesContent":["/**\n * {@link LineItems} over react-hook-form's `useFieldArray`. `@eifi1/ui-kit/rhf`.\n *\n * ```tsx\n * <RhfLineItems\n * name=\"components\"\n * newItem={() => ({ type: \"net_rent\", amount: null, validFrom: \"\" })}\n * minItems={1}\n * columns={[\n * { key: \"type\", header: \"Type\", render: ({ name, label }) => (\n * <RhfSelect name={`${name}.type`} options={TYPES} aria-label={label} />\n * ) },\n * { key: \"amount\", header: \"Amount\", width: \"8rem\", align: \"end\", render: ({ name, label }) => (\n * <RhfMoneyField name={`${name}.amount`} aria-label={label} />\n * ) },\n * ]}\n * totals={{ amount: formatMoney(total) }}\n * />\n * ```\n *\n * The rows are the array's `fields` (keyed by their `id`, as react-hook-form asks),\n * Add appends `newItem()`, remove is `remove(index)`, and an error on the array\n * itself — the `rules` here, or a resolver's `lines: \"At least two lines\"` — is the\n * list's `error`. Each cell's `render` gets `name`, the row's path\n * (`\"components.2\"`), to bind its field to.\n *\n * `summary`, `fieldLabels`, `narrowColumns`, `removePlacement` and `removeAlign` pass\n * through to {@link LineItems}. With\n * `fieldLabels=\"floating\"` a cell's `fieldLabel` is the column's name; the bound\n * fields' own `label` is a label ABOVE the control, so for the floating\n * one render the kit field through `RhfField`:\n * `render={({ field, invalid }) => <Input label={fieldLabel} aria-label={label} {...field} invalid={invalid} />}`.\n */\nimport type { ReactNode } from \"react\";\nimport {\n useFieldArray,\n useFormState,\n type FieldArray,\n type FieldArrayPath,\n type FieldArrayWithId,\n type FieldValues,\n type UseFieldArrayProps,\n} from \"react-hook-form\";\nimport {\n LineItems,\n type LineItemCellContext,\n type LineItemsColumn,\n type LineItemsProps,\n} from \"../components/line-items\";\n\n/** A cell's context, with the row's path in the form. */\nexport interface RhfLineItemCellContext<TItem, TArrayName extends string>\n extends LineItemCellContext<TItem> {\n /** The row's path: `${name}.${index}` — bind the cell's field to\n * `` `${name}.amount` ``. */\n name: `${TArrayName}.${number}`;\n}\n\nexport interface RhfLineItemsColumn<TItem, TArrayName extends string>\n extends Omit<LineItemsColumn<TItem>, \"render\"> {\n render: (ctx: RhfLineItemCellContext<TItem, TArrayName>) => ReactNode;\n}\n\nexport interface RhfLineItemsProps<\n TFieldValues extends FieldValues = FieldValues,\n TArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>,\n TTransformed = TFieldValues,\n> extends Omit<\n LineItemsProps<FieldArrayWithId<TFieldValues, TArrayName, \"id\">>,\n \"items\" | \"columns\" | \"onAdd\" | \"onRemove\" | \"getKey\"\n > {\n name: TArrayName;\n /** `form.control`. Optional under `<Form {...form}>`. */\n control?: UseFieldArrayProps<TFieldValues, TArrayName, \"id\", TTransformed>[\"control\"];\n /** `useFieldArray`'s `rules` (`minLength`, `validate` …); its message is the list's\n * error. */\n rules?: UseFieldArrayProps<TFieldValues, TArrayName, \"id\", TTransformed>[\"rules\"];\n columns: readonly RhfLineItemsColumn<FieldArrayWithId<TFieldValues, TArrayName, \"id\">, TArrayName>[];\n /** A new row's values. Omitted: no add button. */\n newItem?: () => FieldArray<TFieldValues, TArrayName>;\n /** `false`: no remove buttons. Default `true`. */\n removable?: boolean;\n}\n\n/** The message of an error at `path` in react-hook-form's errors tree: the array's\n * own (`root`, from `rules`) or one a resolver put on the array. */\nfunction arrayError(errors: unknown, path: string): string | undefined {\n let node: unknown = errors;\n for (const key of path.split(\".\")) {\n if (node === null || typeof node !== \"object\") return undefined;\n node = (node as Record<string, unknown>)[key];\n }\n if (node === null || typeof node !== \"object\") return undefined;\n const { root, message } = node as { root?: { message?: unknown }; message?: unknown };\n const text = root?.message ?? message;\n return typeof text === \"string\" && text !== \"\" ? text : undefined;\n}\n\n/** {@link LineItems} bound to a field array. See the module note. */\nexport function RhfLineItems<\n TFieldValues extends FieldValues = FieldValues,\n TArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n columns,\n newItem,\n removable = true,\n error,\n ...rest\n}: RhfLineItemsProps<TFieldValues, TArrayName, TTransformed>) {\n const { fields, append, remove } = useFieldArray<TFieldValues, TArrayName, \"id\", TTransformed>({\n control,\n name,\n rules,\n });\n const { errors } = useFormState({ control, name: name as never });\n const bound: LineItemsColumn<FieldArrayWithId<TFieldValues, TArrayName, \"id\">>[] = columns.map(\n ({ render, ...column }) => ({\n ...column,\n render: (ctx) => render({ ...ctx, name: `${name}.${ctx.index}` as `${TArrayName}.${number}` }),\n }),\n );\n return (\n <LineItems\n {...rest}\n items={fields}\n columns={bound}\n getKey={(field) => field.id}\n onAdd={newItem ? () => append(newItem()) : undefined}\n onRemove={removable ? (index) => remove(index) : undefined}\n error={error ?? arrayError(errors, name)}\n />\n );\n}\n"],"mappings":";AA8HI;AA5FJ;AAAA,EACE;AAAA,EACA;AAAA,OAMK;AACP;AAAA,EACE;AAAA,OAIK;AAsCP,SAAS,WAAW,QAAiB,MAAkC;AACrE,MAAI,OAAgB;AACpB,aAAW,OAAO,KAAK,MAAM,GAAG,GAAG;AACjC,QAAI,SAAS,QAAQ,OAAO,SAAS,SAAU,QAAO;AACtD,WAAQ,KAAiC,GAAG;AAAA,EAC9C;AACA,MAAI,SAAS,QAAQ,OAAO,SAAS,SAAU,QAAO;AACtD,QAAM,EAAE,MAAM,QAAQ,IAAI;AAC1B,QAAM,OAAO,MAAM,WAAW;AAC9B,SAAO,OAAO,SAAS,YAAY,SAAS,KAAK,OAAO;AAC1D;AAGO,SAAS,aAId;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,GAAG;AACL,GAA8D;AAC5D,QAAM,EAAE,QAAQ,QAAQ,OAAO,IAAI,cAA4D;AAAA,IAC7F;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACD,QAAM,EAAE,OAAO,IAAI,aAAa,EAAE,SAAS,KAAoB,CAAC;AAChE,QAAM,QAA6E,QAAQ;AAAA,IACzF,CAAC,EAAE,QAAQ,GAAG,OAAO,OAAO;AAAA,MAC1B,GAAG;AAAA,MACH,QAAQ,CAAC,QAAQ,OAAO,EAAE,GAAG,KAAK,MAAM,GAAG,IAAI,IAAI,IAAI,KAAK,GAAgC,CAAC;AAAA,IAC/F;AAAA,EACF;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,OAAO;AAAA,MACP,SAAS;AAAA,MACT,QAAQ,CAAC,UAAU,MAAM;AAAA,MACzB,OAAO,UAAU,MAAM,OAAO,QAAQ,CAAC,IAAI;AAAA,MAC3C,UAAU,YAAY,CAAC,UAAU,OAAO,KAAK,IAAI;AAAA,MACjD,OAAO,SAAS,WAAW,QAAQ,IAAI;AAAA;AAAA,EACzC;AAEJ;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/rhf/use-rhf-wizard-step.ts"],"sourcesContent":["/**\n * The bridge between a react-hook-form step and the wizard's Next gate.\n * `@eifi1/ui-kit/rhf`.\n *\n * ```tsx\n * function AddressStep({ onUpdate }: { onUpdate: (v: Address) => void }) {\n * const form = useForm<Address>({ defaultValues });\n * useRhfWizardStep(form, { onValid: onUpdate });\n * return <Form {...form}>…</Form>;\n * }\n * ```\n *\n * Inside a `<StepperNav>` the hook registers the step's validator with the wizard, so\n * Next (and Finish on a form-bearing last step) runs it: on success the values go to\n * `onValid` and the wizard advances; on failure react-hook-form shows its own inline\n * `<FormMessage>`s, focuses the first field in error, and the wizard stays put — and,\n * since the result carries no per-field errors, says its \"fill in the required\n * fields\" line so the button did not seem to do nothing. It also RETURNS the\n * validator, for a nav of your own outside `<StepperNav>` (where there is no wizard\n * context to register with, so nothing is registered).\n *\n * Two modes:\n *\n * - **The whole form** (no `fields`): `form.handleSubmit`, exactly as kastlan's\n * `use-rhf-wizard-step.ts` — every rule and the resolver run, `onValid` receives\n * the TRANSFORMED values (a zod `.transform()` included), and focus follows the\n * form's own `shouldFocusError` (on by default).\n * - **Some fields** (`fields: [\"iban\", \"bic\"]`): `form.trigger(fields)`, for a wizard\n * that keeps one form across several steps, each of which validates only what it\n * shows. The first of those fields in error is focused (`shouldFocus`; the control\n * has to take `field.ref`), errors on fields of later steps are not raised, and\n * `onValid` receives `form.getValues()` — the untransformed values, because a\n * resolver's output is only produced for the whole form.\n *\n * Both callbacks are read through refs, so the validator is registered once per form\n * however the caller writes them (an inline `(v) => onUpdate(v)` is the usual case).\n *\n * The positional `useRhfWizardStep(form, onValid)` of the hook kastlan kept is the\n * whole-form mode, and still accepted.\n *\n * Why it lives here and not in `@eifi1/ui-kit/wizard`: it imports react-hook-form,\n * which only this entry may (see packaging-contract).\n */\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type { FieldPath, FieldValues, UseFormReturn } from \"react-hook-form\";\nimport { useOptionalWizardContext } from \"../wizard/wizard-context\";\n\nexport type RhfWizardStepOptions<\n TFieldValues extends FieldValues,\n TTransformed = TFieldValues,\n> =\n | {\n /** Validate only these fields — the step's share of a form that spans steps. */\n fields: readonly FieldPath<TFieldValues>[];\n /** Called with `form.getValues()` when the fields pass, before the wizard\n * advances. */\n onValid?: (values: TFieldValues) => void;\n }\n | {\n /** Omitted: validate the whole form through `handleSubmit`. */\n fields?: undefined;\n /** Called with the form's (transformed) values when it passes, before the\n * wizard advances. */\n onValid?: (values: TTransformed) => void;\n };\n\n/**\n * Connects `form` to the wizard's step validation and returns the step's validator.\n * See the module note for the two modes.\n */\nexport function useRhfWizardStep<\n TFieldValues extends FieldValues,\n TContext = unknown,\n TTransformed = TFieldValues,\n>(\n form: UseFormReturn<TFieldValues, TContext, TTransformed>,\n options?:\n | RhfWizardStepOptions<TFieldValues, TTransformed>\n | ((values: TTransformed) => void),\n): () => Promise<boolean> {\n const opts = typeof options === \"function\" ? { onValid: options } : (options ?? {});\n const optsRef = useRef(opts);\n useEffect(() => {\n optsRef.current = opts;\n });\n\n const validate = useCallback(async (): Promise<boolean> => {\n const current = optsRef.current;\n if (current.fields) {\n const ok = await form.trigger([...current.fields], { shouldFocus: true });\n if (ok) current.onValid?.(form.getValues());\n return ok;\n }\n const onValid = current.onValid as ((values: TTransformed) => void) | undefined;\n let ok = false;\n await form.handleSubmit(\n (values) => {\n ok = true;\n onValid?.(values);\n },\n () => {\n ok = false;\n },\n )();\n return ok;\n }, [form]);\n\n const wizard = useOptionalWizardContext();\n const register = wizard?.registerStepValidate;\n useEffect(() => register?.(validate), [register, validate]);\n\n return validate;\n}\n"],"mappings":";AA2CA,SAAS,aAAa,WAAW,cAAc;AAE/C,SAAS,gCAAgC;AAyBlC,SAAS,iBAKd,MACA,SAGwB;AACxB,QAAM,OAAO,OAAO,YAAY,aAAa,EAAE,SAAS,QAAQ,IAAK,WAAW,CAAC;AACjF,QAAM,UAAU,OAAO,IAAI;AAC3B,YAAU,MAAM;AACd,YAAQ,UAAU;AAAA,EACpB,CAAC;AAED,QAAM,WAAW,YAAY,YAA8B;AACzD,UAAM,UAAU,QAAQ;AACxB,QAAI,QAAQ,QAAQ;AAClB,YAAMA,MAAK,MAAM,KAAK,QAAQ,CAAC,GAAG,QAAQ,MAAM,GAAG,EAAE,aAAa,KAAK,CAAC;AACxE,UAAIA,IAAI,SAAQ,UAAU,KAAK,UAAU,CAAC;AAC1C,aAAOA;AAAA,IACT;AACA,UAAM,UAAU,QAAQ;AACxB,QAAI,KAAK;AACT,UAAM,KAAK;AAAA,MACT,CAAC,WAAW;AACV,aAAK;AACL,kBAAU,MAAM;AAAA,MAClB;AAAA,MACA,MAAM;AACJ,aAAK;AAAA,MACP;AAAA,IACF,EAAE;AACF,WAAO;AAAA,EACT,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,SAAS,yBAAyB;AACxC,QAAM,WAAW,QAAQ;AACzB,YAAU,MAAM,WAAW,QAAQ,GAAG,CAAC,UAAU,QAAQ,CAAC;AAE1D,SAAO;AACT;","names":["ok"]}
1
+ {"version":3,"sources":["../../src/rhf/use-rhf-wizard-step.ts"],"sourcesContent":["/**\n * The bridge between a react-hook-form step and the wizard's Next gate.\n * `@eifi1/ui-kit/rhf`.\n *\n * ```tsx\n * function AddressStep({ onUpdate }: { onUpdate: (v: Address) => void }) {\n * const form = useForm<Address>({ defaultValues });\n * useRhfWizardStep(form, { onValid: onUpdate });\n * return <Form {...form}>…</Form>;\n * }\n * ```\n *\n * Inside a `<StepperNav>` the hook registers the step's validator with the wizard, so\n * Next (and Finish on a form-bearing last step) runs it: on success the values go to\n * `onValid` and the wizard advances; on failure the bound fields show their inline\n * messages, react-hook-form focuses the first field in error, and the wizard stays\n * put — and, since the result carries no per-field errors, says its \"fill in the\n * required fields\" line so the button did not seem to do nothing. It also RETURNS the\n * validator, for a nav of your own outside `<StepperNav>` (where there is no wizard\n * context to register with, so nothing is registered).\n *\n * Two modes:\n *\n * - **The whole form** (no `fields`): `form.handleSubmit`, exactly as kastlan's\n * `use-rhf-wizard-step.ts` — every rule and the resolver run, `onValid` receives\n * the TRANSFORMED values (a zod `.transform()` included), and focus follows the\n * form's own `shouldFocusError` (on by default).\n * - **Some fields** (`fields: [\"iban\", \"bic\"]`): `form.trigger(fields)`, for a wizard\n * that keeps one form across several steps, each of which validates only what it\n * shows. The first of those fields in error is focused (`shouldFocus`; the control\n * has to take `field.ref`), errors on fields of later steps are not raised, and\n * `onValid` receives `form.getValues()` — the untransformed values, because a\n * resolver's output is only produced for the whole form.\n *\n * Both callbacks are read through refs, so the validator is registered once per form\n * however the caller writes them (an inline `(v) => onUpdate(v)` is the usual case).\n *\n * The positional `useRhfWizardStep(form, onValid)` of the hook kastlan kept is the\n * whole-form mode, and still accepted.\n *\n * Why it lives here and not in `@eifi1/ui-kit/wizard`: it imports react-hook-form,\n * which only this entry may (see packaging-contract).\n */\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type { FieldPath, FieldValues, UseFormReturn } from \"react-hook-form\";\nimport { useOptionalWizardContext } from \"../wizard/wizard-context\";\n\nexport type RhfWizardStepOptions<\n TFieldValues extends FieldValues,\n TTransformed = TFieldValues,\n> =\n | {\n /** Validate only these fields — the step's share of a form that spans steps. */\n fields: readonly FieldPath<TFieldValues>[];\n /** Called with `form.getValues()` when the fields pass, before the wizard\n * advances. */\n onValid?: (values: TFieldValues) => void;\n }\n | {\n /** Omitted: validate the whole form through `handleSubmit`. */\n fields?: undefined;\n /** Called with the form's (transformed) values when it passes, before the\n * wizard advances. */\n onValid?: (values: TTransformed) => void;\n };\n\n/**\n * Connects `form` to the wizard's step validation and returns the step's validator.\n * See the module note for the two modes.\n */\nexport function useRhfWizardStep<\n TFieldValues extends FieldValues,\n TContext = unknown,\n TTransformed = TFieldValues,\n>(\n form: UseFormReturn<TFieldValues, TContext, TTransformed>,\n options?:\n | RhfWizardStepOptions<TFieldValues, TTransformed>\n | ((values: TTransformed) => void),\n): () => Promise<boolean> {\n const opts = typeof options === \"function\" ? { onValid: options } : (options ?? {});\n const optsRef = useRef(opts);\n useEffect(() => {\n optsRef.current = opts;\n });\n\n const validate = useCallback(async (): Promise<boolean> => {\n const current = optsRef.current;\n if (current.fields) {\n const ok = await form.trigger([...current.fields], { shouldFocus: true });\n if (ok) current.onValid?.(form.getValues());\n return ok;\n }\n const onValid = current.onValid as ((values: TTransformed) => void) | undefined;\n let ok = false;\n await form.handleSubmit(\n (values) => {\n ok = true;\n onValid?.(values);\n },\n () => {\n ok = false;\n },\n )();\n return ok;\n }, [form]);\n\n const wizard = useOptionalWizardContext();\n const register = wizard?.registerStepValidate;\n useEffect(() => register?.(validate), [register, validate]);\n\n return validate;\n}\n"],"mappings":";AA2CA,SAAS,aAAa,WAAW,cAAc;AAE/C,SAAS,gCAAgC;AAyBlC,SAAS,iBAKd,MACA,SAGwB;AACxB,QAAM,OAAO,OAAO,YAAY,aAAa,EAAE,SAAS,QAAQ,IAAK,WAAW,CAAC;AACjF,QAAM,UAAU,OAAO,IAAI;AAC3B,YAAU,MAAM;AACd,YAAQ,UAAU;AAAA,EACpB,CAAC;AAED,QAAM,WAAW,YAAY,YAA8B;AACzD,UAAM,UAAU,QAAQ;AACxB,QAAI,QAAQ,QAAQ;AAClB,YAAMA,MAAK,MAAM,KAAK,QAAQ,CAAC,GAAG,QAAQ,MAAM,GAAG,EAAE,aAAa,KAAK,CAAC;AACxE,UAAIA,IAAI,SAAQ,UAAU,KAAK,UAAU,CAAC;AAC1C,aAAOA;AAAA,IACT;AACA,UAAM,UAAU,QAAQ;AACxB,QAAI,KAAK;AACT,UAAM,KAAK;AAAA,MACT,CAAC,WAAW;AACV,aAAK;AACL,kBAAU,MAAM;AAAA,MAClB;AAAA,MACA,MAAM;AACJ,aAAK;AAAA,MACP;AAAA,IACF,EAAE;AACF,WAAO;AAAA,EACT,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,SAAS,yBAAyB;AACxC,QAAM,WAAW,QAAQ;AACzB,YAAU,MAAM,WAAW,QAAQ,GAAG,CAAC,UAAU,QAAQ,CAAC;AAE1D,SAAO;AACT;","names":["ok"]}
package/dist/rhf.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/rhf.ts"],"sourcesContent":["// `@eifi1/ui-kit/rhf` — the react-hook-form adapter: Form and the bound Rhf* fields (and\n// RhfField as the escape hatch). The shadcn scaffold (FormField, FormItem, FormLabel,\n// FormControl, FormDescription, FormMessage, useFormField) is deprecated in 0.34.\n//\n// The only entry point in the package that requires `react-hook-form`, and an optional\n// peer for that reason: an app without a form library installs nothing for it. NOT\n// re-exported from the main barrel, which would make the peer everyone's — the reason\n// the barrel dropped its last react-hook-form import (see the wizard section of\n// src/index.ts). The packaging contract test holds that line.\nexport * from \"./rhf/form\";\n// The react-hook-form bridge to the wizard's Next gate (useWizard lives in /wizard).\nexport * from \"./rhf/use-rhf-wizard-step\";\n// Bound fields — one line per field (RhfTextField, RhfNumberField, RhfIntegerField, RhfMoneyField,\n// RhfDateField, RhfTextarea, RhfSelect, RhfCheckbox, RhfCombobox, RhfTextCombobox; 0.22: RhfTimeInput,\n// RhfDateRangePicker, RhfToggleGroup, RhfIbanInput, RhfPhoneInput; 0.23: RhfCountrySelect,\n// RhfMonthPicker; 0.24: RhfInlineEntityCombobox)\n// and RhfField, the shell they are built on.\nexport * from \"./rhf/fields\";\n// LineItems over useFieldArray.\nexport * from \"./rhf/line-items\";\n"],"mappings":";AASA,cAAc;AAEd,cAAc;AAMd,cAAc;AAEd,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/rhf.ts"],"sourcesContent":["// `@eifi1/ui-kit/rhf` — the react-hook-form adapter: Form and the bound Rhf* fields (and\n// RhfField as the escape hatch). The shadcn scaffold they are built from (FormField,\n// FormItem, FormLabel, FormControl, FormDescription, FormMessage, useFormField) left the\n// surface in 0.35 (refactor audit 2026-10, API-6f; deprecated in 0.34).\n//\n// The only entry point in the package that requires `react-hook-form`, and an optional\n// peer for that reason: an app without a form library installs nothing for it. NOT\n// re-exported from the main barrel, which would make the peer everyone's — the reason\n// the barrel dropped its last react-hook-form import (see the wizard section of\n// src/index.ts). The packaging contract test holds that line.\nexport * from \"./rhf/form\";\n// The react-hook-form bridge to the wizard's Next gate (useWizard lives in /wizard).\nexport * from \"./rhf/use-rhf-wizard-step\";\n// Bound fields — one line per field (RhfTextField, RhfNumberField, RhfIntegerField, RhfMoneyField,\n// RhfDateField, RhfTextarea, RhfSelect, RhfCheckbox, RhfCombobox, RhfTextCombobox; 0.22: RhfTimeInput,\n// RhfDateRangePicker, RhfToggleGroup, RhfIbanInput, RhfPhoneInput; 0.23: RhfCountrySelect,\n// RhfMonthPicker; 0.24: RhfInlineEntityCombobox)\n// and RhfField, the shell they are built on.\nexport * from \"./rhf/fields\";\n// LineItems over useFieldArray.\nexport * from \"./rhf/line-items\";\n"],"mappings":";AAUA,cAAc;AAEd,cAAc;AAMd,cAAc;AAEd,cAAc;","names":[]}
@@ -3,32 +3,20 @@ export interface OptionSwitcherOption<T extends string> {
3
3
  value: T;
4
4
  label: ReactNode;
5
5
  }
6
- /**
7
- * Names the trigger and the menu: one of the two spellings is required (API-1, refactor
8
- * audit 2026-10). `aria-label` wins when both are given. TopBarActionMenu has the same
9
- * type; both become a plain required `aria-label` when `ariaLabel` goes in 0.35.
10
- */
11
- type RequiredMenuName = {
12
- "aria-label": string;
13
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35. */
14
- ariaLabel?: string;
15
- } | {
16
- "aria-label"?: string;
17
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35. */
18
- ariaLabel: string;
19
- };
20
6
  /**
21
7
  * A top-bar hover menu that switches between a small set of options, with a
22
8
  * check on the active one and an optional heading. Generic glue over
23
9
  * {@link HoverMenu} — e.g. a dev role/impersonation switcher, an environment
24
10
  * picker. Controlled: the app owns `value` and `onSelect`.
25
11
  */
26
- export declare function OptionSwitcherMenu<T extends string>({ icon, "aria-label": ariaLabelAttr, ariaLabel: ariaLabelProp, title, heading, options, value, onSelect, }: {
12
+ export declare function OptionSwitcherMenu<T extends string>({ icon, "aria-label": ariaLabel, title, heading, options, value, onSelect, }: {
27
13
  icon: ReactNode;
14
+ /** Names the trigger and the menu. Required: the trigger is an icon (refactor audit
15
+ * 2026-10, API-1 — the `ariaLabel` spelling went in 0.35). */
16
+ "aria-label": string;
28
17
  title?: string;
29
18
  heading?: string;
30
19
  options: OptionSwitcherOption<T>[];
31
20
  value: T;
32
21
  onSelect: (value: T) => void;
33
- } & RequiredMenuName): import("react").JSX.Element;
34
- export {};
22
+ }): import("react").JSX.Element;
@@ -6,15 +6,13 @@ import { TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS } from "./topbar-controls.
6
6
  import { Tooltip } from "../components/tooltip.js";
7
7
  function OptionSwitcherMenu({
8
8
  icon,
9
- "aria-label": ariaLabelAttr,
10
- ariaLabel: ariaLabelProp,
9
+ "aria-label": ariaLabel,
11
10
  title,
12
11
  heading,
13
12
  options,
14
13
  value,
15
14
  onSelect
16
15
  }) {
17
- const ariaLabel = ariaLabelAttr ?? ariaLabelProp;
18
16
  return /* @__PURE__ */ jsx(
19
17
  HoverMenu,
20
18
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/option-switcher-menu.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { HoverMenu } from \"../components/hover-menu\";\nimport { TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS } from \"./topbar-controls\";\nimport { Tooltip } from \"../components/tooltip\";\n\nexport interface OptionSwitcherOption<T extends string> {\n value: T;\n label: ReactNode;\n}\n\n/**\n * Names the trigger and the menu: one of the two spellings is required (API-1, refactor\n * audit 2026-10). `aria-label` wins when both are given. TopBarActionMenu has the same\n * type; both become a plain required `aria-label` when `ariaLabel` goes in 0.35.\n */\ntype RequiredMenuName =\n | {\n \"aria-label\": string;\n /** @deprecated 0.34 — use `aria-label`; removed in 0.35. */\n ariaLabel?: string;\n }\n | {\n \"aria-label\"?: string;\n /** @deprecated 0.34 — use `aria-label`; removed in 0.35. */\n ariaLabel: string;\n };\n\n/**\n * A top-bar hover menu that switches between a small set of options, with a\n * check on the active one and an optional heading. Generic glue over\n * {@link HoverMenu} — e.g. a dev role/impersonation switcher, an environment\n * picker. Controlled: the app owns `value` and `onSelect`.\n */\nexport function OptionSwitcherMenu<T extends string>({\n icon,\n \"aria-label\": ariaLabelAttr,\n ariaLabel: ariaLabelProp,\n title,\n heading,\n options,\n value,\n onSelect,\n}: {\n icon: ReactNode;\n title?: string;\n heading?: string;\n options: OptionSwitcherOption<T>[];\n value: T;\n onSelect: (value: T) => void;\n} & RequiredMenuName) {\n // The DOM spelling wins; `ariaLabel` is the fallback for call sites not moved yet.\n const ariaLabel = ariaLabelAttr ?? ariaLabelProp;\n return (\n <HoverMenu\n aria-label={ariaLabel}\n trigger={({ toggle }) => (\n <Tooltip label={title} portal>\n <button type=\"button\" onClick={toggle} aria-label={ariaLabel} className={TOPBAR_TRIGGER_CLASS}>\n {icon}\n </button>\n </Tooltip>\n )}\n >\n {(close) => (\n <ul className=\"py-1\">\n {heading && (\n <li className=\"px-3 py-1 text-micro font-semibold uppercase tracking-wide text-placeholder\">\n {heading}\n </li>\n )}\n {options.map((opt) => (\n <li key={opt.value}>\n <button\n type=\"button\"\n onClick={() => {\n onSelect(opt.value);\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n <span>{opt.label}</span>\n {value === opt.value && <Check className=\"size-4 text-secondary\" />}\n </button>\n </li>\n ))}\n </ul>\n )}\n </HoverMenu>\n );\n}\n"],"mappings":";AA0DU,cAeI,YAfJ;AAzDV,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAC1B,SAAS,wBAAwB,4BAA4B;AAC7D,SAAS,eAAe;AA8BjB,SAAS,mBAAqC;AAAA,EACnD;AAAA,EACA,cAAc;AAAA,EACd,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOsB;AAEpB,QAAM,YAAY,iBAAiB;AACnC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAY;AAAA,MACZ,SAAS,CAAC,EAAE,OAAO,MACjB,oBAAC,WAAQ,OAAO,OAAO,QAAM,MAC3B,8BAAC,YAAO,MAAK,UAAS,SAAS,QAAQ,cAAY,WAAW,WAAW,sBACtE,gBACH,GACF;AAAA,MAGD,WAAC,UACA,qBAAC,QAAG,WAAU,QACX;AAAA,mBACC,oBAAC,QAAG,WAAU,+EACX,mBACH;AAAA,QAED,QAAQ,IAAI,CAAC,QACZ,oBAAC,QACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,MAAM;AACb,uBAAS,IAAI,KAAK;AAClB,oBAAM;AAAA,YACR;AAAA,YACA,WAAW;AAAA,YAEX;AAAA,kCAAC,UAAM,cAAI,OAAM;AAAA,cAChB,UAAU,IAAI,SAAS,oBAAC,SAAM,WAAU,yBAAwB;AAAA;AAAA;AAAA,QACnE,KAXO,IAAI,KAYb,CACD;AAAA,SACH;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/option-switcher-menu.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { HoverMenu } from \"../components/hover-menu\";\nimport { TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS } from \"./topbar-controls\";\nimport { Tooltip } from \"../components/tooltip\";\n\nexport interface OptionSwitcherOption<T extends string> {\n value: T;\n label: ReactNode;\n}\n\n/**\n * A top-bar hover menu that switches between a small set of options, with a\n * check on the active one and an optional heading. Generic glue over\n * {@link HoverMenu} — e.g. a dev role/impersonation switcher, an environment\n * picker. Controlled: the app owns `value` and `onSelect`.\n */\nexport function OptionSwitcherMenu<T extends string>({\n icon,\n \"aria-label\": ariaLabel,\n title,\n heading,\n options,\n value,\n onSelect,\n}: {\n icon: ReactNode;\n /** Names the trigger and the menu. Required: the trigger is an icon (refactor audit\n * 2026-10, API-1 — the `ariaLabel` spelling went in 0.35). */\n \"aria-label\": string;\n title?: string;\n heading?: string;\n options: OptionSwitcherOption<T>[];\n value: T;\n onSelect: (value: T) => void;\n}) {\n return (\n <HoverMenu\n aria-label={ariaLabel}\n trigger={({ toggle }) => (\n <Tooltip label={title} portal>\n <button type=\"button\" onClick={toggle} aria-label={ariaLabel} className={TOPBAR_TRIGGER_CLASS}>\n {icon}\n </button>\n </Tooltip>\n )}\n >\n {(close) => (\n <ul className=\"py-1\">\n {heading && (\n <li className=\"px-3 py-1 text-micro font-semibold uppercase tracking-wide text-placeholder\">\n {heading}\n </li>\n )}\n {options.map((opt) => (\n <li key={opt.value}>\n <button\n type=\"button\"\n onClick={() => {\n onSelect(opt.value);\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n <span>{opt.label}</span>\n {value === opt.value && <Check className=\"size-4 text-secondary\" />}\n </button>\n </li>\n ))}\n </ul>\n )}\n </HoverMenu>\n );\n}\n"],"mappings":";AAyCU,cAeI,YAfJ;AAxCV,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAC1B,SAAS,wBAAwB,4BAA4B;AAC7D,SAAS,eAAe;AAajB,SAAS,mBAAqC;AAAA,EACnD;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAUG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAY;AAAA,MACZ,SAAS,CAAC,EAAE,OAAO,MACjB,oBAAC,WAAQ,OAAO,OAAO,QAAM,MAC3B,8BAAC,YAAO,MAAK,UAAS,SAAS,QAAQ,cAAY,WAAW,WAAW,sBACtE,gBACH,GACF;AAAA,MAGD,WAAC,UACA,qBAAC,QAAG,WAAU,QACX;AAAA,mBACC,oBAAC,QAAG,WAAU,+EACX,mBACH;AAAA,QAED,QAAQ,IAAI,CAAC,QACZ,oBAAC,QACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,MAAM;AACb,uBAAS,IAAI,KAAK;AAClB,oBAAM;AAAA,YACR;AAAA,YACA,WAAW;AAAA,YAEX;AAAA,kCAAC,UAAM,cAAI,OAAM;AAAA,cAChB,UAAU,IAAI,SAAS,oBAAC,SAAM,WAAU,yBAAwB;AAAA;AAAA;AAAA,QACnE,KAXO,IAAI,KAYb,CACD;AAAA,SACH;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -4,9 +4,6 @@ export interface RoleSwitcherProps<R extends string> {
4
4
  onChange: (role: R) => void;
5
5
  /** Default: `topBar.switchRole` from the {@link UiKitProvider}, else English. */
6
6
  "aria-label"?: string;
7
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35 (refactor audit 2026-10, API-1).
8
- * Names the switcher only when `aria-label` is absent. */
9
- ariaLabel?: string;
10
7
  heading?: string;
11
8
  /** The trigger's tooltip. Default: `topBar.role(value)`. */
12
9
  title?: string;
@@ -21,4 +18,4 @@ export interface RoleSwitcherProps<R extends string> {
21
18
  * marker — the app is not showing you your own role — and the token carries that
22
19
  * amber across both themes instead of pinning one hue onto every palette.
23
20
  */
24
- export declare function RoleSwitcher<R extends string>({ roles, value, onChange, "aria-label": ariaLabelAttr, ariaLabel, heading, title, }: RoleSwitcherProps<R>): import("react").JSX.Element;
21
+ export declare function RoleSwitcher<R extends string>({ roles, value, onChange, "aria-label": ariaLabel, heading, title, }: RoleSwitcherProps<R>): import("react").JSX.Element;
@@ -7,12 +7,11 @@ function RoleSwitcher({
7
7
  roles,
8
8
  value,
9
9
  onChange,
10
- "aria-label": ariaLabelAttr,
11
- ariaLabel,
10
+ "aria-label": ariaLabel,
12
11
  heading,
13
12
  title
14
13
  }) {
15
- const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { switchRole: ariaLabelAttr ?? ariaLabel });
14
+ const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { switchRole: ariaLabel });
16
15
  return /* @__PURE__ */ jsx(
17
16
  OptionSwitcherMenu,
18
17
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/role-switcher.tsx"],"sourcesContent":["import { FlaskConical } from \"lucide-react\";\nimport { OptionSwitcherMenu } from \"./option-switcher-menu\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\nexport interface RoleSwitcherProps<R extends string> {\n roles: readonly R[];\n value: R;\n onChange: (role: R) => void;\n /** Default: `topBar.switchRole` from the {@link UiKitProvider}, else English. */\n \"aria-label\"?: string;\n /** @deprecated 0.34 — use `aria-label`; removed in 0.35 (refactor audit 2026-10, API-1).\n * Names the switcher only when `aria-label` is absent. */\n ariaLabel?: string;\n heading?: string;\n /** The trigger's tooltip. Default: `topBar.role(value)`. */\n title?: string;\n}\n\n/**\n * A reusable user-role switcher (feedback #337): a thin wrapper over\n * {@link OptionSwitcherMenu} with the standard flask icon, so an app drops it in\n * the top bar with just the role list + current value + a handler. Typically\n * dev-only / UI-only, but the app decides where and whether to render it.\n *\n * The flask wears `--warning` rather than a fixed `amber-500`: it is a caution\n * marker — the app is not showing you your own role — and the token carries that\n * amber across both themes instead of pinning one hue onto every palette.\n */\nexport function RoleSwitcher<R extends string>({\n roles,\n value,\n onChange,\n \"aria-label\": ariaLabelAttr,\n ariaLabel,\n heading,\n title,\n}: RoleSwitcherProps<R>) {\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { switchRole: ariaLabelAttr ?? ariaLabel });\n return (\n <OptionSwitcherMenu<R>\n aria-label={labels.switchRole}\n title={title ?? labels.role(value)}\n heading={heading}\n icon={<FlaskConical className=\"size-5 text-warning\" />}\n options={roles.map((r) => ({ value: r, label: r }))}\n value={value}\n onSelect={onChange}\n />\n );\n}\n"],"mappings":";AA2CY;AA3CZ,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,wBAAwB,oBAAoB;AA0B9C,SAAS,aAA+B;AAAA,EAC7C;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,SAAS,aAAa,UAAU,wBAAwB,EAAE,YAAY,iBAAiB,UAAU,CAAC;AACxG,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAY,OAAO;AAAA,MACnB,OAAO,SAAS,OAAO,KAAK,KAAK;AAAA,MACjC;AAAA,MACA,MAAM,oBAAC,gBAAa,WAAU,uBAAsB;AAAA,MACpD,SAAS,MAAM,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,EAAE,EAAE;AAAA,MAClD;AAAA,MACA,UAAU;AAAA;AAAA,EACZ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/role-switcher.tsx"],"sourcesContent":["import { FlaskConical } from \"lucide-react\";\nimport { OptionSwitcherMenu } from \"./option-switcher-menu\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\nexport interface RoleSwitcherProps<R extends string> {\n roles: readonly R[];\n value: R;\n onChange: (role: R) => void;\n /** Default: `topBar.switchRole` from the {@link UiKitProvider}, else English. */\n \"aria-label\"?: string;\n heading?: string;\n /** The trigger's tooltip. Default: `topBar.role(value)`. */\n title?: string;\n}\n\n/**\n * A reusable user-role switcher (feedback #337): a thin wrapper over\n * {@link OptionSwitcherMenu} with the standard flask icon, so an app drops it in\n * the top bar with just the role list + current value + a handler. Typically\n * dev-only / UI-only, but the app decides where and whether to render it.\n *\n * The flask wears `--warning` rather than a fixed `amber-500`: it is a caution\n * marker — the app is not showing you your own role — and the token carries that\n * amber across both themes instead of pinning one hue onto every palette.\n */\nexport function RoleSwitcher<R extends string>({\n roles,\n value,\n onChange,\n \"aria-label\": ariaLabel,\n heading,\n title,\n}: RoleSwitcherProps<R>) {\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { switchRole: ariaLabel });\n return (\n <OptionSwitcherMenu<R>\n aria-label={labels.switchRole}\n title={title ?? labels.role(value)}\n heading={heading}\n icon={<FlaskConical className=\"size-5 text-warning\" />}\n options={roles.map((r) => ({ value: r, label: r }))}\n value={value}\n onSelect={onChange}\n />\n );\n}\n"],"mappings":";AAuCY;AAvCZ,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,wBAAwB,oBAAoB;AAuB9C,SAAS,aAA+B;AAAA,EAC7C;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,SAAS,aAAa,UAAU,wBAAwB,EAAE,YAAY,UAAU,CAAC;AACvF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,cAAY,OAAO;AAAA,MACnB,OAAO,SAAS,OAAO,KAAK,KAAK;AAAA,MACjC;AAAA,MACA,MAAM,oBAAC,gBAAa,WAAU,uBAAsB;AAAA,MACpD,SAAS,MAAM,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,EAAE,EAAE;AAAA,MAClD;AAAA,MACA,UAAU;AAAA;AAAA,EACZ;AAEJ;","names":[]}
@@ -88,20 +88,6 @@ export interface TopBarMenuHeader {
88
88
  */
89
89
  extra?: ReactNode | ((close: () => void) => ReactNode);
90
90
  }
91
- /**
92
- * Names the trigger and the menu: one of the two spellings is required (API-1, refactor
93
- * audit 2026-10). `aria-label` wins when both are given. OptionSwitcherMenu has the same
94
- * type; both become a plain required `aria-label` when `ariaLabel` goes in 0.35.
95
- */
96
- type RequiredMenuName = {
97
- "aria-label": string;
98
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35. */
99
- ariaLabel?: string;
100
- } | {
101
- "aria-label"?: string;
102
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35. */
103
- ariaLabel: string;
104
- };
105
91
  /**
106
92
  * A top-bar icon button that opens a {@link HoverMenu} of actions and/or links,
107
93
  * with an optional uppercase heading and divider rules. Generic glue over the
@@ -119,7 +105,7 @@ type RequiredMenuName = {
119
105
  * hover panel at 390px, kept inside the viewport by `HoverMenu`, and a heading wraps
120
106
  * rather than widening it.
121
107
  */
122
- export declare function TopBarActionMenu({ icon, iconBadge, trigger, triggerClassName, href, "aria-label": ariaLabelAttr, ariaLabel: ariaLabelProp, heading, header, entries, children, footer, footerLabel, align, panelClassName, }: {
108
+ export declare function TopBarActionMenu({ icon, iconBadge, trigger, triggerClassName, href, "aria-label": ariaLabel, heading, header, entries, children, footer, footerLabel, align, panelClassName, }: {
123
109
  /** The trigger's face for the usual icon button. Ignored when `trigger` is given. */
124
110
  icon?: ReactNode;
125
111
  /**
@@ -157,6 +143,9 @@ export declare function TopBarActionMenu({ icon, iconBadge, trigger, triggerClas
157
143
  * inside interactive content.
158
144
  */
159
145
  href?: string;
146
+ /** Names the trigger and the menu. Required: the trigger is an icon (refactor audit
147
+ * 2026-10, API-1 — the `ariaLabel` spelling went in 0.35). */
148
+ "aria-label": string;
160
149
  heading?: string;
161
150
  /** A non-interactive identity block above the rows. */
162
151
  header?: TopBarMenuHeader;
@@ -175,9 +164,8 @@ export declare function TopBarActionMenu({ icon, iconBadge, trigger, triggerClas
175
164
  footer?: ReactNode | ((close: () => void) => ReactNode);
176
165
  /** Wraps the footer in a `<nav>` with this name ("Legal"). Without it, a plain block. */
177
166
  footerLabel?: string;
178
- /** Which edge of the trigger the panel lines up with (see `HoverMenu`, whose type this
179
- * is: `left`/`right` are deprecated 0.34). Default `end`. */
167
+ /** Which edge of the trigger the panel lines up with, `start` or `end` in the reading
168
+ * direction (see `HoverMenu`, whose type this is). Default `end`. */
180
169
  align?: HoverMenuProps["align"];
181
170
  panelClassName?: string;
182
- } & RequiredMenuName): import("react").JSX.Element;
183
- export {};
171
+ }): import("react").JSX.Element;
@@ -55,8 +55,7 @@ function TopBarActionMenu({
55
55
  trigger,
56
56
  triggerClassName,
57
57
  href,
58
- "aria-label": ariaLabelAttr,
59
- ariaLabel: ariaLabelProp,
58
+ "aria-label": ariaLabel,
60
59
  heading,
61
60
  header,
62
61
  entries,
@@ -66,7 +65,6 @@ function TopBarActionMenu({
66
65
  align,
67
66
  panelClassName
68
67
  }) {
69
- const ariaLabel = ariaLabelAttr ?? ariaLabelProp;
70
68
  const uid = useId();
71
69
  return /* @__PURE__ */ jsx(
72
70
  HoverMenu,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/topbar-action-menu.tsx"],"sourcesContent":["import { useId, useLayoutEffect, useRef } from \"react\";\nimport type { KeyboardEvent as ReactKeyboardEvent, ReactNode } from \"react\";\nimport { Link } from \"react-router\";\nimport { cn } from \"../lib/cn\";\nimport { HoverMenu, type HoverMenuProps } from \"../components/hover-menu\";\nimport { MenuItem } from \"../components/menu-item\";\nimport type { MenuItemTone } from \"../components/menu-item\";\nimport { StatusDot } from \"../components/status-dot\";\nimport type { UserAvatarBadge } from \"../components/user-avatar\";\nimport { TOPBAR_TRIGGER_CLASS } from \"./topbar-controls\";\n\n/**\n * One row of a {@link TopBarActionMenu}. An `action` (default) runs `onSelect`, a\n * `link` navigates via react-router, a `divider` draws a rule, and a `heading` names\n * the rows after it. Action and link rows close the menu automatically when chosen.\n * `kind` may be omitted for actions, keeping the common case terse.\n *\n * The rows are {@link MenuItem}s, so they take its options: `tone: \"danger\"` for the\n * log-out row, `checked` (+ `checkable`) for a choice, `current` for the link to the\n * page already open, `disabled`.\n */\nexport type TopBarMenuEntry =\n | {\n kind?: \"action\";\n key: string;\n icon?: ReactNode;\n label: ReactNode;\n trailing?: ReactNode;\n onSelect: () => void;\n tone?: MenuItemTone;\n /** Makes the row a choice (`menuitemradio`, or `menuitemcheckbox` with\n * `checkable: \"checkbox\"`) and says whether it is chosen. */\n checked?: boolean;\n checkable?: \"radio\" | \"checkbox\";\n disabled?: boolean;\n }\n | {\n kind: \"link\";\n key: string;\n icon?: ReactNode;\n label: ReactNode;\n trailing?: ReactNode;\n to: string;\n tone?: MenuItemTone;\n /** The link to the page already open: `aria-current=\"page\"` and a check mark. */\n current?: boolean;\n disabled?: boolean;\n }\n | { kind: \"divider\"; key: string }\n | {\n /**\n * A group heading (0.23, keksdose's guided-tours menu): the rows after it, up to\n * the next heading, the next `divider` or the end of `entries`, are ONE group\n * named by `label`. keksdose files its tours by topic and separated the blocks\n * with bare dividers — \"the kit's menu has no heading row to name them with\"\n * (tour-menu.tsx) — so a sighted reader had to guess what each block was and a\n * screen reader was told nothing at all.\n *\n * Not a row: it takes no focus, the arrow keys (and Home/End) pass over it, and it\n * never closes the menu. It is announced the way the kit's other grouped lists\n * announce theirs (`CommandPalette`): the group's rows sit in a `role=\"group\"`\n * whose `aria-labelledby` is this heading, so a reader says \"On this page, group\"\n * when the focus enters the first row, and nothing between rows.\n *\n * A divider ENDS the group, so a closing row such as \"All tours…\" behind a rule\n * belongs to no topic. A heading with no rows under it is not drawn — an empty\n * group names nothing; build the list from data and it may well be empty.\n *\n * The menu-wide `heading` prop is the panel's title, drawn once above everything;\n * with group headings it is usually redundant (two stacked caps lines read as one),\n * and the trigger's `aria-label` already names the menu.\n */\n kind: \"heading\";\n key: string;\n /** The group's name, shown above its rows and announced as the group's name. Keep\n * it short: it is set in small caps and wraps on a narrow panel. */\n label: ReactNode;\n };\n\ntype HeadingEntry = Extract<TopBarMenuEntry, { kind: \"heading\" }>;\ntype DividerEntry = Extract<TopBarMenuEntry, { kind: \"divider\" }>;\ntype RowEntry = Exclude<TopBarMenuEntry, HeadingEntry | DividerEntry>;\n\n/** What the panel draws from `entries`: loose rows and rules, and headed groups. */\ntype Block = { kind: \"loose\"; entry: RowEntry | DividerEntry } | { kind: \"group\"; heading: HeadingEntry; rows: RowEntry[] };\n\n/**\n * Cut `entries` into blocks: a heading opens a group that takes the rows after it until\n * the next heading or divider; everything else stands alone, as it always has.\n */\nfunction toBlocks(entries: readonly TopBarMenuEntry[]): Block[] {\n const blocks: Block[] = [];\n let group: Extract<Block, { kind: \"group\" }> | null = null;\n for (const entry of entries) {\n if (entry.kind === \"heading\") {\n group = { kind: \"group\", heading: entry, rows: [] };\n blocks.push(group);\n } else if (entry.kind === \"divider\") {\n group = null;\n blocks.push({ kind: \"loose\", entry });\n } else if (group) {\n group.rows.push(entry);\n } else {\n blocks.push({ kind: \"loose\", entry });\n }\n }\n // A heading with nothing under it names nothing: not drawn (see the `heading` kind).\n return blocks.filter((block) => block.kind === \"loose\" || block.rows.length > 0);\n}\n\n/**\n * The non-interactive identity block at the top of an account menu — kastlan's name +\n * role, keksdose's display name + email + role chip. It is text, not a `menuitem`: the\n * arrow keys pass over it.\n */\nexport interface TopBarMenuHeader {\n /** The first line — the user's name. */\n title: ReactNode;\n /** The muted second line — an email, a role. */\n subtitle?: ReactNode;\n /**\n * Anything below — a role chip. A link or button here stays out of the arrow keys\n * and in the Tab order, like the footer's.\n *\n * Pass a function to get `close`, as `footer` does — keksdose F7: its admin role\n * chip links to the admin area, and a router link does not unmount the menu, so\n * without `close` the panel stayed open over the page it had just navigated to.\n */\n extra?: ReactNode | ((close: () => void) => ReactNode);\n}\n\n/** What a control is, as far as `HoverMenu` is concerned, when it lives in the header\n * or the footer: not a menu row. */\nconst SECTION_CONTROLS = \"a[href]:not([role]),button:not([role])\";\nconst TABBABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]';\n\n/**\n * A header or footer inside the menu panel.\n *\n * `HoverMenu` promotes EVERY role-less button and link in its panel to a `menuitem` out\n * of the Tab order, and turns every list into `role=\"none\"` — right for the rows, wrong\n * for a footer of legal links, which would join the arrow-key cycle and drop out of Tab.\n * So before it looks (a child's layout effect runs before its parent's), the controls\n * in here get their own role spelled out, which `HoverMenu` leaves alone, and any list\n * keeps its list semantics.\n */\nfunction MenuSection({ className, children }: { className: string; children: ReactNode }) {\n const ref = useRef<HTMLLIElement>(null);\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el) return;\n for (const c of el.querySelectorAll<HTMLElement>(SECTION_CONTROLS)) {\n c.setAttribute(\"role\", c.tagName === \"A\" ? \"link\" : \"button\");\n }\n for (const l of el.querySelectorAll<HTMLElement>(\"ul:not([role]),ol:not([role])\")) l.setAttribute(\"role\", \"list\");\n for (const l of el.querySelectorAll<HTMLElement>(\"li:not([role])\")) l.setAttribute(\"role\", \"listitem\");\n });\n return (\n <li ref={ref} role=\"none\" className={className}>\n {children}\n </li>\n );\n}\n\n/**\n * `HoverMenu` closes on any Tab inside the panel — right when Tab leaves it, wrong when\n * Tab is on its way from a row to a footer link, or from one footer link to the next:\n * the panel would unmount under the focus. So a Tab whose next stop is still inside the\n * panel stops here and the browser moves focus; one that leaves reaches `HoverMenu`,\n * which closes.\n */\nfunction keepTabInsidePanel(e: ReactKeyboardEvent<HTMLUListElement>) {\n if (e.key !== \"Tab\") return;\n const from = e.target as HTMLElement;\n const stops = Array.from(e.currentTarget.querySelectorAll<HTMLElement>(TABBABLE)).filter(\n (el) => el.getAttribute(\"tabindex\") !== \"-1\",\n );\n const way = e.shiftKey ? Node.DOCUMENT_POSITION_PRECEDING : Node.DOCUMENT_POSITION_FOLLOWING;\n if (stops.some((el) => el !== from && from.compareDocumentPosition(el) & way)) e.stopPropagation();\n}\n\n/**\n * Names the trigger and the menu: one of the two spellings is required (API-1, refactor\n * audit 2026-10). `aria-label` wins when both are given. OptionSwitcherMenu has the same\n * type; both become a plain required `aria-label` when `ariaLabel` goes in 0.35.\n */\ntype RequiredMenuName =\n | {\n \"aria-label\": string;\n /** @deprecated 0.34 — use `aria-label`; removed in 0.35. */\n ariaLabel?: string;\n }\n | {\n \"aria-label\"?: string;\n /** @deprecated 0.34 — use `aria-label`; removed in 0.35. */\n ariaLabel: string;\n };\n\n/**\n * A top-bar icon button that opens a {@link HoverMenu} of actions and/or links,\n * with an optional uppercase heading and divider rules. Generic glue over the\n * shared top-bar chrome — e.g. the feedback launcher or the guided-tours menu —\n * so those call sites describe their rows as data instead of rebuilding the\n * trigger + list markup. For a checkmarked value-switcher use\n * {@link OptionSwitcherMenu} instead.\n *\n * It is also the ACCOUNT menu kastlan and keksdose each hand-build on `HoverMenu`: a\n * custom `trigger` (the avatar), a `header` (name + role / email), the rows, and a\n * free-content `footer` (the legal links).\n *\n * A long menu is grouped by topic with `{ kind: \"heading\" }` entries (0.23) — see\n * {@link TopBarMenuEntry}. There is no separate phone layout: the panel is the same\n * hover panel at 390px, kept inside the viewport by `HoverMenu`, and a heading wraps\n * rather than widening it.\n */\nexport function TopBarActionMenu({\n icon,\n iconBadge,\n trigger,\n triggerClassName,\n href,\n \"aria-label\": ariaLabelAttr,\n ariaLabel: ariaLabelProp,\n heading,\n header,\n entries,\n children,\n footer,\n footerLabel,\n align,\n panelClassName,\n}: {\n /** The trigger's face for the usual icon button. Ignored when `trigger` is given. */\n icon?: ReactNode;\n /**\n * A status dot in the top-end corner of `icon` — keksdose's unread-support dot on the\n * feedback trigger. Like `UserAvatar`'s `badge`, its `label` joins the trigger's name\n * (\"Send feedback 2 unread\"), so the dot is not something only the eye is told about.\n * Ignored with `trigger`: give the face its own badge there.\n */\n iconBadge?: UserAvatarBadge | null;\n /**\n * A face of your own for the trigger — the account menu's `UserAvatar`. It is drawn\n * INSIDE the kit's button, which keeps the wiring: `aria-haspopup`, `aria-expanded`,\n * the toggle, the keyboard. `open` is there for a chevron that turns.\n *\n * The button is named by `aria-label` followed by any readable text in the face — a\n * `UserAvatar` `badge` label reaches a screen reader that way (\"Account menu 3\n * unread\"), where an `aria-label` on the button would have hidden it. A\n * `UserAvatar`'s initials are `aria-hidden`, so the person's name is only in the\n * name if `aria-label` carries it (\"Marcel Eifert, account menu\").\n */\n trigger?: (state: { open: boolean }) => ReactNode;\n /** Extra classes for the trigger button — `rounded-full` round an avatar. */\n triggerClassName?: string;\n /**\n * Make the trigger a LINK to this route (keksdose dev #585, Marcel: \"clicking on the\n * guided tours icon shall lead to the tours page like the all tours element does\").\n * A click or Enter follows it and shuts the panel; hover still opens the panel after\n * the usual delay, and ↓/↑ on the focused trigger still open it onto the first/last\n * row, so the rows stay reachable by keyboard. A tap on a touch screen follows the\n * link — so use it where the destination offers everything the menu does. Rendered as\n * an `<a>` (the router's `Link`) with `aria-haspopup=\"menu\"` and `aria-expanded`,\n * named by `aria-label`: a link inside the kit's button would be interactive content\n * inside interactive content.\n */\n href?: string;\n heading?: string;\n /** A non-interactive identity block above the rows. */\n header?: TopBarMenuHeader;\n entries: TopBarMenuEntry[];\n /**\n * Rows the entry data cannot describe (a `Disclosure` sub-list, an app's own\n * section), after `entries`. Render `<li>`s holding `MenuItem`s or plain controls;\n * `close` shuts the menu.\n */\n children?: (close: () => void) => ReactNode;\n /**\n * Free content below the rows, behind a rule — the legal links. Its links and\n * buttons are NOT menu rows: the arrow keys skip them and Tab reaches them. Pass a\n * function to get `close` for the links' `onClick`.\n */\n footer?: ReactNode | ((close: () => void) => ReactNode);\n /** Wraps the footer in a `<nav>` with this name (\"Legal\"). Without it, a plain block. */\n footerLabel?: string;\n /** Which edge of the trigger the panel lines up with (see `HoverMenu`, whose type this\n * is: `left`/`right` are deprecated 0.34). Default `end`. */\n align?: HoverMenuProps[\"align\"];\n panelClassName?: string;\n} & RequiredMenuName) {\n // The DOM spelling wins; `ariaLabel` is the fallback for call sites not moved yet.\n const ariaLabel = ariaLabelAttr ?? ariaLabelProp;\n // Names the groups' headings for `aria-labelledby`. Positional rather than from the\n // entry's `key`, which is the caller's text and may hold a space — and a space in\n // `aria-labelledby` is a list separator.\n const uid = useId();\n return (\n <HoverMenu\n aria-label={ariaLabel}\n align={align}\n trigger={({ open, toggle, close }) => {\n // One element for both kinds: the router's link when `href` is set, else the\n // kit's button. Same classes and face either way.\n const control = (className: string, children: ReactNode, label?: string) =>\n href ? (\n <Link to={href} onClick={close} aria-label={label} className={className}>\n {children}\n </Link>\n ) : (\n <button type=\"button\" onClick={toggle} aria-label={label} className={className}>\n {children}\n </button>\n );\n if (trigger) {\n return control(\n cn(TOPBAR_TRIGGER_CLASS, \"relative\", triggerClassName),\n <>\n <span className=\"sr-only\">{ariaLabel}</span>\n {/* The space keeps the name from running into the face's text (\"Account\n menu3 unread\"); as a flex item it takes no room. */}\n {\" \"}\n {trigger({ open })}\n </>,\n );\n }\n if (iconBadge) {\n // Named by its content, like the custom-trigger branch, so the badge label is\n // part of the name; an `aria-label` would have hidden it.\n return control(\n cn(TOPBAR_TRIGGER_CLASS, \"relative\", triggerClassName),\n <>\n <span className=\"sr-only\">{ariaLabel}</span>{\" \"}\n <span aria-hidden className=\"relative inline-flex\">\n {icon}\n <StatusDot ring tone={iconBadge.tone ?? \"danger\"} className=\"absolute -end-0.5 -top-0.5\" />\n </span>{\" \"}\n <span className=\"sr-only\">{iconBadge.label}</span>\n </>,\n );\n }\n return control(cn(TOPBAR_TRIGGER_CLASS, triggerClassName), icon, ariaLabel);\n }}\n >\n {(close) => {\n const footerBody = typeof footer === \"function\" ? footer(close) : footer;\n const headerExtra = typeof header?.extra === \"function\" ? header.extra(close) : header?.extra;\n const footerClass =\n \"flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted [&_a:hover]:text-primary [&_a]:rounded-sm [&_a:focus-visible]:outline-none [&_a:focus-visible]:ring-focus [&_a:focus-visible]:ring-brand\";\n /** One action or link row — loose, or inside a heading's group. */\n const row = (entry: RowEntry) => (\n <li key={entry.key}>\n {entry.kind === \"link\" ? (\n <MenuItem\n href={entry.to}\n renderLink={({ href, ...p }) => <Link to={href} {...p} />}\n onClick={close}\n leading={entry.icon}\n trailing={entry.trailing}\n tone={entry.tone}\n current={entry.current}\n disabled={entry.disabled}\n >\n {entry.label}\n </MenuItem>\n ) : (\n <MenuItem\n onClick={() => {\n entry.onSelect();\n close();\n }}\n leading={entry.icon}\n trailing={entry.trailing}\n tone={entry.tone}\n checked={entry.checked}\n checkable={entry.checkable}\n disabled={entry.disabled}\n >\n {entry.label}\n </MenuItem>\n )}\n </li>\n );\n return (\n // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a Tab bubbling up from a row or footer link, see keepTabInsidePanel\n <ul className={cn(\"py-1\", panelClassName)} onKeyDown={keepTabInsidePanel}>\n {header && (\n <MenuSection className=\"mb-1 border-b border-subtle px-3 py-2\">\n <div className=\"truncate text-sm font-medium text-primary\">{header.title}</div>\n {header.subtitle != null && (\n <div className=\"truncate text-xs text-muted\">{header.subtitle}</div>\n )}\n {headerExtra != null && headerExtra !== false && <div className=\"mt-1\">{headerExtra}</div>}\n </MenuSection>\n )}\n {heading && (\n <li className=\"px-3 py-1 text-micro font-semibold uppercase tracking-wide text-placeholder\">\n {heading}\n </li>\n )}\n {toBlocks(entries).map((block, i, blocks) => {\n if (block.kind === \"loose\") {\n const entry = block.entry;\n if (entry.kind === \"divider\") {\n return <li key={entry.key} className=\"my-1 border-t border-subtle\" />;\n }\n return row(entry);\n }\n const headingId = `${uid}-group-${i}`;\n const before = blocks[i - 1];\n // Under a rule (or at the top of the rows) the rule's margin is the gap;\n // straight under another group's last row the heading makes its own.\n const tight = !before || (before.kind === \"loose\" && before.entry.kind === \"divider\");\n return (\n <li key={block.heading.key} role=\"none\">\n <div\n id={headingId}\n role=\"presentation\"\n className={cn(\n \"px-3 pb-0.5 text-caption font-semibold uppercase tracking-wide text-muted [overflow-wrap:anywhere]\",\n tight ? \"pt-1\" : \"pt-2.5\",\n )}\n >\n {block.heading.label}\n </div>\n <ul role=\"group\" aria-labelledby={headingId}>\n {block.rows.map(row)}\n </ul>\n </li>\n );\n })}\n {children?.(close)}\n {footerBody != null && footerBody !== false && (\n <MenuSection className=\"mt-1 border-t border-subtle px-3 py-2\">\n {footerLabel ? (\n <nav aria-label={footerLabel} className={footerClass}>\n {footerBody}\n </nav>\n ) : (\n <div className={footerClass}>{footerBody}</div>\n )}\n </MenuSection>\n )}\n </ul>\n );\n }}\n </HoverMenu>\n );\n}\n"],"mappings":"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","names":["children","href"]}
1
+ {"version":3,"sources":["../../src/shell/topbar-action-menu.tsx"],"sourcesContent":["import { useId, useLayoutEffect, useRef } from \"react\";\nimport type { KeyboardEvent as ReactKeyboardEvent, ReactNode } from \"react\";\nimport { Link } from \"react-router\";\nimport { cn } from \"../lib/cn\";\nimport { HoverMenu, type HoverMenuProps } from \"../components/hover-menu\";\nimport { MenuItem } from \"../components/menu-item\";\nimport type { MenuItemTone } from \"../components/menu-item\";\nimport { StatusDot } from \"../components/status-dot\";\nimport type { UserAvatarBadge } from \"../components/user-avatar\";\nimport { TOPBAR_TRIGGER_CLASS } from \"./topbar-controls\";\n\n/**\n * One row of a {@link TopBarActionMenu}. An `action` (default) runs `onSelect`, a\n * `link` navigates via react-router, a `divider` draws a rule, and a `heading` names\n * the rows after it. Action and link rows close the menu automatically when chosen.\n * `kind` may be omitted for actions, keeping the common case terse.\n *\n * The rows are {@link MenuItem}s, so they take its options: `tone: \"danger\"` for the\n * log-out row, `checked` (+ `checkable`) for a choice, `current` for the link to the\n * page already open, `disabled`.\n */\nexport type TopBarMenuEntry =\n | {\n kind?: \"action\";\n key: string;\n icon?: ReactNode;\n label: ReactNode;\n trailing?: ReactNode;\n onSelect: () => void;\n tone?: MenuItemTone;\n /** Makes the row a choice (`menuitemradio`, or `menuitemcheckbox` with\n * `checkable: \"checkbox\"`) and says whether it is chosen. */\n checked?: boolean;\n checkable?: \"radio\" | \"checkbox\";\n disabled?: boolean;\n }\n | {\n kind: \"link\";\n key: string;\n icon?: ReactNode;\n label: ReactNode;\n trailing?: ReactNode;\n to: string;\n tone?: MenuItemTone;\n /** The link to the page already open: `aria-current=\"page\"` and a check mark. */\n current?: boolean;\n disabled?: boolean;\n }\n | { kind: \"divider\"; key: string }\n | {\n /**\n * A group heading (0.23, keksdose's guided-tours menu): the rows after it, up to\n * the next heading, the next `divider` or the end of `entries`, are ONE group\n * named by `label`. keksdose files its tours by topic and separated the blocks\n * with bare dividers — \"the kit's menu has no heading row to name them with\"\n * (tour-menu.tsx) — so a sighted reader had to guess what each block was and a\n * screen reader was told nothing at all.\n *\n * Not a row: it takes no focus, the arrow keys (and Home/End) pass over it, and it\n * never closes the menu. It is announced the way the kit's other grouped lists\n * announce theirs (`CommandPalette`): the group's rows sit in a `role=\"group\"`\n * whose `aria-labelledby` is this heading, so a reader says \"On this page, group\"\n * when the focus enters the first row, and nothing between rows.\n *\n * A divider ENDS the group, so a closing row such as \"All tours…\" behind a rule\n * belongs to no topic. A heading with no rows under it is not drawn — an empty\n * group names nothing; build the list from data and it may well be empty.\n *\n * The menu-wide `heading` prop is the panel's title, drawn once above everything;\n * with group headings it is usually redundant (two stacked caps lines read as one),\n * and the trigger's `aria-label` already names the menu.\n */\n kind: \"heading\";\n key: string;\n /** The group's name, shown above its rows and announced as the group's name. Keep\n * it short: it is set in small caps and wraps on a narrow panel. */\n label: ReactNode;\n };\n\ntype HeadingEntry = Extract<TopBarMenuEntry, { kind: \"heading\" }>;\ntype DividerEntry = Extract<TopBarMenuEntry, { kind: \"divider\" }>;\ntype RowEntry = Exclude<TopBarMenuEntry, HeadingEntry | DividerEntry>;\n\n/** What the panel draws from `entries`: loose rows and rules, and headed groups. */\ntype Block = { kind: \"loose\"; entry: RowEntry | DividerEntry } | { kind: \"group\"; heading: HeadingEntry; rows: RowEntry[] };\n\n/**\n * Cut `entries` into blocks: a heading opens a group that takes the rows after it until\n * the next heading or divider; everything else stands alone, as it always has.\n */\nfunction toBlocks(entries: readonly TopBarMenuEntry[]): Block[] {\n const blocks: Block[] = [];\n let group: Extract<Block, { kind: \"group\" }> | null = null;\n for (const entry of entries) {\n if (entry.kind === \"heading\") {\n group = { kind: \"group\", heading: entry, rows: [] };\n blocks.push(group);\n } else if (entry.kind === \"divider\") {\n group = null;\n blocks.push({ kind: \"loose\", entry });\n } else if (group) {\n group.rows.push(entry);\n } else {\n blocks.push({ kind: \"loose\", entry });\n }\n }\n // A heading with nothing under it names nothing: not drawn (see the `heading` kind).\n return blocks.filter((block) => block.kind === \"loose\" || block.rows.length > 0);\n}\n\n/**\n * The non-interactive identity block at the top of an account menu — kastlan's name +\n * role, keksdose's display name + email + role chip. It is text, not a `menuitem`: the\n * arrow keys pass over it.\n */\nexport interface TopBarMenuHeader {\n /** The first line — the user's name. */\n title: ReactNode;\n /** The muted second line — an email, a role. */\n subtitle?: ReactNode;\n /**\n * Anything below — a role chip. A link or button here stays out of the arrow keys\n * and in the Tab order, like the footer's.\n *\n * Pass a function to get `close`, as `footer` does — keksdose F7: its admin role\n * chip links to the admin area, and a router link does not unmount the menu, so\n * without `close` the panel stayed open over the page it had just navigated to.\n */\n extra?: ReactNode | ((close: () => void) => ReactNode);\n}\n\n/** What a control is, as far as `HoverMenu` is concerned, when it lives in the header\n * or the footer: not a menu row. */\nconst SECTION_CONTROLS = \"a[href]:not([role]),button:not([role])\";\nconst TABBABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]';\n\n/**\n * A header or footer inside the menu panel.\n *\n * `HoverMenu` promotes EVERY role-less button and link in its panel to a `menuitem` out\n * of the Tab order, and turns every list into `role=\"none\"` — right for the rows, wrong\n * for a footer of legal links, which would join the arrow-key cycle and drop out of Tab.\n * So before it looks (a child's layout effect runs before its parent's), the controls\n * in here get their own role spelled out, which `HoverMenu` leaves alone, and any list\n * keeps its list semantics.\n */\nfunction MenuSection({ className, children }: { className: string; children: ReactNode }) {\n const ref = useRef<HTMLLIElement>(null);\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el) return;\n for (const c of el.querySelectorAll<HTMLElement>(SECTION_CONTROLS)) {\n c.setAttribute(\"role\", c.tagName === \"A\" ? \"link\" : \"button\");\n }\n for (const l of el.querySelectorAll<HTMLElement>(\"ul:not([role]),ol:not([role])\")) l.setAttribute(\"role\", \"list\");\n for (const l of el.querySelectorAll<HTMLElement>(\"li:not([role])\")) l.setAttribute(\"role\", \"listitem\");\n });\n return (\n <li ref={ref} role=\"none\" className={className}>\n {children}\n </li>\n );\n}\n\n/**\n * `HoverMenu` closes on any Tab inside the panel — right when Tab leaves it, wrong when\n * Tab is on its way from a row to a footer link, or from one footer link to the next:\n * the panel would unmount under the focus. So a Tab whose next stop is still inside the\n * panel stops here and the browser moves focus; one that leaves reaches `HoverMenu`,\n * which closes.\n */\nfunction keepTabInsidePanel(e: ReactKeyboardEvent<HTMLUListElement>) {\n if (e.key !== \"Tab\") return;\n const from = e.target as HTMLElement;\n const stops = Array.from(e.currentTarget.querySelectorAll<HTMLElement>(TABBABLE)).filter(\n (el) => el.getAttribute(\"tabindex\") !== \"-1\",\n );\n const way = e.shiftKey ? Node.DOCUMENT_POSITION_PRECEDING : Node.DOCUMENT_POSITION_FOLLOWING;\n if (stops.some((el) => el !== from && from.compareDocumentPosition(el) & way)) e.stopPropagation();\n}\n\n/**\n * A top-bar icon button that opens a {@link HoverMenu} of actions and/or links,\n * with an optional uppercase heading and divider rules. Generic glue over the\n * shared top-bar chrome — e.g. the feedback launcher or the guided-tours menu —\n * so those call sites describe their rows as data instead of rebuilding the\n * trigger + list markup. For a checkmarked value-switcher use\n * {@link OptionSwitcherMenu} instead.\n *\n * It is also the ACCOUNT menu kastlan and keksdose each hand-build on `HoverMenu`: a\n * custom `trigger` (the avatar), a `header` (name + role / email), the rows, and a\n * free-content `footer` (the legal links).\n *\n * A long menu is grouped by topic with `{ kind: \"heading\" }` entries (0.23) — see\n * {@link TopBarMenuEntry}. There is no separate phone layout: the panel is the same\n * hover panel at 390px, kept inside the viewport by `HoverMenu`, and a heading wraps\n * rather than widening it.\n */\nexport function TopBarActionMenu({\n icon,\n iconBadge,\n trigger,\n triggerClassName,\n href,\n \"aria-label\": ariaLabel,\n heading,\n header,\n entries,\n children,\n footer,\n footerLabel,\n align,\n panelClassName,\n}: {\n /** The trigger's face for the usual icon button. Ignored when `trigger` is given. */\n icon?: ReactNode;\n /**\n * A status dot in the top-end corner of `icon` — keksdose's unread-support dot on the\n * feedback trigger. Like `UserAvatar`'s `badge`, its `label` joins the trigger's name\n * (\"Send feedback 2 unread\"), so the dot is not something only the eye is told about.\n * Ignored with `trigger`: give the face its own badge there.\n */\n iconBadge?: UserAvatarBadge | null;\n /**\n * A face of your own for the trigger — the account menu's `UserAvatar`. It is drawn\n * INSIDE the kit's button, which keeps the wiring: `aria-haspopup`, `aria-expanded`,\n * the toggle, the keyboard. `open` is there for a chevron that turns.\n *\n * The button is named by `aria-label` followed by any readable text in the face — a\n * `UserAvatar` `badge` label reaches a screen reader that way (\"Account menu 3\n * unread\"), where an `aria-label` on the button would have hidden it. A\n * `UserAvatar`'s initials are `aria-hidden`, so the person's name is only in the\n * name if `aria-label` carries it (\"Marcel Eifert, account menu\").\n */\n trigger?: (state: { open: boolean }) => ReactNode;\n /** Extra classes for the trigger button — `rounded-full` round an avatar. */\n triggerClassName?: string;\n /**\n * Make the trigger a LINK to this route (keksdose dev #585, Marcel: \"clicking on the\n * guided tours icon shall lead to the tours page like the all tours element does\").\n * A click or Enter follows it and shuts the panel; hover still opens the panel after\n * the usual delay, and ↓/↑ on the focused trigger still open it onto the first/last\n * row, so the rows stay reachable by keyboard. A tap on a touch screen follows the\n * link — so use it where the destination offers everything the menu does. Rendered as\n * an `<a>` (the router's `Link`) with `aria-haspopup=\"menu\"` and `aria-expanded`,\n * named by `aria-label`: a link inside the kit's button would be interactive content\n * inside interactive content.\n */\n href?: string;\n /** Names the trigger and the menu. Required: the trigger is an icon (refactor audit\n * 2026-10, API-1 — the `ariaLabel` spelling went in 0.35). */\n \"aria-label\": string;\n heading?: string;\n /** A non-interactive identity block above the rows. */\n header?: TopBarMenuHeader;\n entries: TopBarMenuEntry[];\n /**\n * Rows the entry data cannot describe (a `Disclosure` sub-list, an app's own\n * section), after `entries`. Render `<li>`s holding `MenuItem`s or plain controls;\n * `close` shuts the menu.\n */\n children?: (close: () => void) => ReactNode;\n /**\n * Free content below the rows, behind a rule — the legal links. Its links and\n * buttons are NOT menu rows: the arrow keys skip them and Tab reaches them. Pass a\n * function to get `close` for the links' `onClick`.\n */\n footer?: ReactNode | ((close: () => void) => ReactNode);\n /** Wraps the footer in a `<nav>` with this name (\"Legal\"). Without it, a plain block. */\n footerLabel?: string;\n /** Which edge of the trigger the panel lines up with, `start` or `end` in the reading\n * direction (see `HoverMenu`, whose type this is). Default `end`. */\n align?: HoverMenuProps[\"align\"];\n panelClassName?: string;\n}) {\n // Names the groups' headings for `aria-labelledby`. Positional rather than from the\n // entry's `key`, which is the caller's text and may hold a space — and a space in\n // `aria-labelledby` is a list separator.\n const uid = useId();\n return (\n <HoverMenu\n aria-label={ariaLabel}\n align={align}\n trigger={({ open, toggle, close }) => {\n // One element for both kinds: the router's link when `href` is set, else the\n // kit's button. Same classes and face either way.\n const control = (className: string, children: ReactNode, label?: string) =>\n href ? (\n <Link to={href} onClick={close} aria-label={label} className={className}>\n {children}\n </Link>\n ) : (\n <button type=\"button\" onClick={toggle} aria-label={label} className={className}>\n {children}\n </button>\n );\n if (trigger) {\n return control(\n cn(TOPBAR_TRIGGER_CLASS, \"relative\", triggerClassName),\n <>\n <span className=\"sr-only\">{ariaLabel}</span>\n {/* The space keeps the name from running into the face's text (\"Account\n menu3 unread\"); as a flex item it takes no room. */}\n {\" \"}\n {trigger({ open })}\n </>,\n );\n }\n if (iconBadge) {\n // Named by its content, like the custom-trigger branch, so the badge label is\n // part of the name; an `aria-label` would have hidden it.\n return control(\n cn(TOPBAR_TRIGGER_CLASS, \"relative\", triggerClassName),\n <>\n <span className=\"sr-only\">{ariaLabel}</span>{\" \"}\n <span aria-hidden className=\"relative inline-flex\">\n {icon}\n <StatusDot ring tone={iconBadge.tone ?? \"danger\"} className=\"absolute -end-0.5 -top-0.5\" />\n </span>{\" \"}\n <span className=\"sr-only\">{iconBadge.label}</span>\n </>,\n );\n }\n return control(cn(TOPBAR_TRIGGER_CLASS, triggerClassName), icon, ariaLabel);\n }}\n >\n {(close) => {\n const footerBody = typeof footer === \"function\" ? footer(close) : footer;\n const headerExtra = typeof header?.extra === \"function\" ? header.extra(close) : header?.extra;\n const footerClass =\n \"flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted [&_a:hover]:text-primary [&_a]:rounded-sm [&_a:focus-visible]:outline-none [&_a:focus-visible]:ring-focus [&_a:focus-visible]:ring-brand\";\n /** One action or link row — loose, or inside a heading's group. */\n const row = (entry: RowEntry) => (\n <li key={entry.key}>\n {entry.kind === \"link\" ? (\n <MenuItem\n href={entry.to}\n renderLink={({ href, ...p }) => <Link to={href} {...p} />}\n onClick={close}\n leading={entry.icon}\n trailing={entry.trailing}\n tone={entry.tone}\n current={entry.current}\n disabled={entry.disabled}\n >\n {entry.label}\n </MenuItem>\n ) : (\n <MenuItem\n onClick={() => {\n entry.onSelect();\n close();\n }}\n leading={entry.icon}\n trailing={entry.trailing}\n tone={entry.tone}\n checked={entry.checked}\n checkable={entry.checkable}\n disabled={entry.disabled}\n >\n {entry.label}\n </MenuItem>\n )}\n </li>\n );\n return (\n // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a Tab bubbling up from a row or footer link, see keepTabInsidePanel\n <ul className={cn(\"py-1\", panelClassName)} onKeyDown={keepTabInsidePanel}>\n {header && (\n <MenuSection className=\"mb-1 border-b border-subtle px-3 py-2\">\n <div className=\"truncate text-sm font-medium text-primary\">{header.title}</div>\n {header.subtitle != null && (\n <div className=\"truncate text-xs text-muted\">{header.subtitle}</div>\n )}\n {headerExtra != null && headerExtra !== false && <div className=\"mt-1\">{headerExtra}</div>}\n </MenuSection>\n )}\n {heading && (\n <li className=\"px-3 py-1 text-micro font-semibold uppercase tracking-wide text-placeholder\">\n {heading}\n </li>\n )}\n {toBlocks(entries).map((block, i, blocks) => {\n if (block.kind === \"loose\") {\n const entry = block.entry;\n if (entry.kind === \"divider\") {\n return <li key={entry.key} className=\"my-1 border-t border-subtle\" />;\n }\n return row(entry);\n }\n const headingId = `${uid}-group-${i}`;\n const before = blocks[i - 1];\n // Under a rule (or at the top of the rows) the rule's margin is the gap;\n // straight under another group's last row the heading makes its own.\n const tight = !before || (before.kind === \"loose\" && before.entry.kind === \"divider\");\n return (\n <li key={block.heading.key} role=\"none\">\n <div\n id={headingId}\n role=\"presentation\"\n className={cn(\n \"px-3 pb-0.5 text-caption font-semibold uppercase tracking-wide text-muted [overflow-wrap:anywhere]\",\n tight ? \"pt-1\" : \"pt-2.5\",\n )}\n >\n {block.heading.label}\n </div>\n <ul role=\"group\" aria-labelledby={headingId}>\n {block.rows.map(row)}\n </ul>\n </li>\n );\n })}\n {children?.(close)}\n {footerBody != null && footerBody !== false && (\n <MenuSection className=\"mt-1 border-t border-subtle px-3 py-2\">\n {footerLabel ? (\n <nav aria-label={footerLabel} className={footerClass}>\n {footerBody}\n </nav>\n ) : (\n <div className={footerClass}>{footerBody}</div>\n )}\n </MenuSection>\n )}\n </ul>\n );\n }}\n </HoverMenu>\n );\n}\n"],"mappings":"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","names":["children","href"]}