@eifi1/ui-kit 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/README.md +69 -33
  2. package/dist/chart.d.ts +1 -0
  3. package/dist/components/account-settings.js +3 -2
  4. package/dist/components/account-settings.js.map +1 -1
  5. package/dist/components/amount-input.d.ts +3 -0
  6. package/dist/components/amount-input.js +6 -4
  7. package/dist/components/amount-input.js.map +1 -1
  8. package/dist/components/autocomplete.d.ts +36 -7
  9. package/dist/components/autocomplete.js +68 -22
  10. package/dist/components/autocomplete.js.map +1 -1
  11. package/dist/components/calculator.d.ts +3 -0
  12. package/dist/components/calculator.js +2 -1
  13. package/dist/components/calculator.js.map +1 -1
  14. package/dist/components/chart-zoom.js +2 -1
  15. package/dist/components/chart-zoom.js.map +1 -1
  16. package/dist/components/chart.d.ts +33 -2
  17. package/dist/components/chart.js +20 -8
  18. package/dist/components/chart.js.map +1 -1
  19. package/dist/components/checkbox.d.ts +9 -0
  20. package/dist/components/checkbox.js +7 -2
  21. package/dist/components/checkbox.js.map +1 -1
  22. package/dist/components/chip.d.ts +34 -14
  23. package/dist/components/chip.js +63 -28
  24. package/dist/components/chip.js.map +1 -1
  25. package/dist/components/choice-card.d.ts +18 -1
  26. package/dist/components/choice-card.js +21 -8
  27. package/dist/components/choice-card.js.map +1 -1
  28. package/dist/components/combobox-core.d.ts +61 -2
  29. package/dist/components/combobox-core.js +68 -17
  30. package/dist/components/combobox-core.js.map +1 -1
  31. package/dist/components/combobox.d.ts +11 -7
  32. package/dist/components/combobox.js +51 -28
  33. package/dist/components/combobox.js.map +1 -1
  34. package/dist/components/currency-select.js +3 -3
  35. package/dist/components/currency-select.js.map +1 -1
  36. package/dist/components/danger-confirm.d.ts +30 -6
  37. package/dist/components/danger-confirm.js +51 -32
  38. package/dist/components/danger-confirm.js.map +1 -1
  39. package/dist/components/data-table-filter-popover.d.ts +1 -1
  40. package/dist/components/data-table-filters.d.ts +1 -1
  41. package/dist/components/data-table-pagination.d.ts +6 -3
  42. package/dist/components/data-table-pagination.js +6 -5
  43. package/dist/components/data-table-pagination.js.map +1 -1
  44. package/dist/components/data-table.d.ts +1 -1
  45. package/dist/components/data-table.js +332 -248
  46. package/dist/components/data-table.js.map +1 -1
  47. package/dist/components/date-picker.js +17 -7
  48. package/dist/components/date-picker.js.map +1 -1
  49. package/dist/components/dialog-frame.d.ts +28 -3
  50. package/dist/components/dialog-frame.js +47 -11
  51. package/dist/components/dialog-frame.js.map +1 -1
  52. package/dist/components/disclosure.d.ts +22 -2
  53. package/dist/components/disclosure.js +41 -8
  54. package/dist/components/disclosure.js.map +1 -1
  55. package/dist/components/dropdown.d.ts +7 -3
  56. package/dist/components/dropdown.js +6 -2
  57. package/dist/components/dropdown.js.map +1 -1
  58. package/dist/components/entity-combobox.d.ts +17 -8
  59. package/dist/components/entity-combobox.js +7 -5
  60. package/dist/components/entity-combobox.js.map +1 -1
  61. package/dist/components/file-button.d.ts +36 -5
  62. package/dist/components/file-button.js +42 -3
  63. package/dist/components/file-button.js.map +1 -1
  64. package/dist/components/file-dropzone.d.ts +9 -5
  65. package/dist/components/file-dropzone.js +14 -19
  66. package/dist/components/file-dropzone.js.map +1 -1
  67. package/dist/components/grouped-picker.js +2 -2
  68. package/dist/components/grouped-picker.js.map +1 -1
  69. package/dist/components/hover-menu.d.ts +8 -3
  70. package/dist/components/hover-menu.js +88 -4
  71. package/dist/components/hover-menu.js.map +1 -1
  72. package/dist/components/icon-picker.d.ts +10 -1
  73. package/dist/components/icon-picker.js +5 -1
  74. package/dist/components/icon-picker.js.map +1 -1
  75. package/dist/components/measured-grid.d.ts +165 -0
  76. package/dist/components/measured-grid.js +559 -0
  77. package/dist/components/measured-grid.js.map +1 -0
  78. package/dist/components/mini-calendar.js +7 -8
  79. package/dist/components/mini-calendar.js.map +1 -1
  80. package/dist/components/month-picker.js +13 -9
  81. package/dist/components/month-picker.js.map +1 -1
  82. package/dist/components/multi-entity-combobox.js +2 -2
  83. package/dist/components/multi-entity-combobox.js.map +1 -1
  84. package/dist/components/multi-select.d.ts +9 -1
  85. package/dist/components/multi-select.js +22 -8
  86. package/dist/components/multi-select.js.map +1 -1
  87. package/dist/components/number-field.d.ts +3 -0
  88. package/dist/components/number-input.d.ts +3 -0
  89. package/dist/components/number-input.js +3 -3
  90. package/dist/components/number-input.js.map +1 -1
  91. package/dist/components/numpad-sheet.d.ts +3 -0
  92. package/dist/components/numpad-sheet.js +3 -3
  93. package/dist/components/numpad-sheet.js.map +1 -1
  94. package/dist/components/picker-sheet.js +1 -1
  95. package/dist/components/picker-sheet.js.map +1 -1
  96. package/dist/components/popover.js +11 -2
  97. package/dist/components/popover.js.map +1 -1
  98. package/dist/components/series-chart-ticks.d.ts +13 -0
  99. package/dist/components/series-chart-ticks.js +29 -0
  100. package/dist/components/series-chart-ticks.js.map +1 -0
  101. package/dist/components/series-chart.js +33 -27
  102. package/dist/components/series-chart.js.map +1 -1
  103. package/dist/components/swatch-picker.d.ts +10 -1
  104. package/dist/components/swatch-picker.js +2 -0
  105. package/dist/components/swatch-picker.js.map +1 -1
  106. package/dist/components/swipeable-row.d.ts +11 -2
  107. package/dist/components/swipeable-row.js +5 -3
  108. package/dist/components/swipeable-row.js.map +1 -1
  109. package/dist/components/switch.d.ts +9 -0
  110. package/dist/components/switch.js +7 -2
  111. package/dist/components/switch.js.map +1 -1
  112. package/dist/components/tile-radio.js +5 -7
  113. package/dist/components/tile-radio.js.map +1 -1
  114. package/dist/components/toggle-group.d.ts +13 -3
  115. package/dist/components/toggle-group.js +21 -1
  116. package/dist/components/toggle-group.js.map +1 -1
  117. package/dist/components/toggle-legend.js +1 -2
  118. package/dist/components/toggle-legend.js.map +1 -1
  119. package/dist/components/treemap.d.ts +6 -1
  120. package/dist/components/treemap.js +16 -6
  121. package/dist/components/treemap.js.map +1 -1
  122. package/dist/components/ui.d.ts +31 -7
  123. package/dist/components/ui.js +26 -18
  124. package/dist/components/ui.js.map +1 -1
  125. package/dist/components/use-anchor-dir.d.ts +15 -0
  126. package/dist/components/use-anchor-dir.js +14 -0
  127. package/dist/components/use-anchor-dir.js.map +1 -0
  128. package/dist/components/use-mobile-reveal.d.ts +8 -1
  129. package/dist/components/use-mobile-reveal.js +3 -2
  130. package/dist/components/use-mobile-reveal.js.map +1 -1
  131. package/dist/components/use-table-state.d.ts +4 -4
  132. package/dist/components/use-table-state.js +12 -5
  133. package/dist/components/use-table-state.js.map +1 -1
  134. package/dist/{data-table-filters-DM1auUfH.d.ts → data-table-filters-noy0Abvi.d.ts} +40 -7
  135. package/dist/data-table.d.ts +1 -1
  136. package/dist/data-table.js.map +1 -1
  137. package/dist/feedback/feedback-attachment.d.ts +22 -3
  138. package/dist/feedback/feedback-attachment.js +15 -7
  139. package/dist/feedback/feedback-attachment.js.map +1 -1
  140. package/dist/feedback/feedback-dialog.d.ts +3 -2
  141. package/dist/feedback/feedback-dialog.js.map +1 -1
  142. package/dist/feedback.d.ts +1 -1
  143. package/dist/hooks/use-windowed-rows.d.ts +29 -0
  144. package/dist/hooks/use-windowed-rows.js +31 -0
  145. package/dist/hooks/use-windowed-rows.js.map +1 -0
  146. package/dist/i18n/defaults.d.ts +3 -0
  147. package/dist/i18n/defaults.js +5 -1
  148. package/dist/i18n/defaults.js.map +1 -1
  149. package/dist/i18n/kit-labels.d.ts +5 -0
  150. package/dist/i18n/kit-labels.js.map +1 -1
  151. package/dist/i18n/locales/de-CH.d.ts +43 -0
  152. package/dist/i18n/locales/de-CH.js +21 -0
  153. package/dist/i18n/locales/de-CH.js.map +1 -0
  154. package/dist/i18n/locales/de.d.ts +43 -0
  155. package/dist/i18n/locales/de.js +344 -0
  156. package/dist/i18n/locales/de.js.map +1 -0
  157. package/dist/i18n/locales/es.d.ts +43 -0
  158. package/dist/i18n/locales/es.js +339 -0
  159. package/dist/i18n/locales/es.js.map +1 -0
  160. package/dist/i18n/locales/fr.d.ts +43 -0
  161. package/dist/i18n/locales/fr.js +344 -0
  162. package/dist/i18n/locales/fr.js.map +1 -0
  163. package/dist/i18n/locales/hu.d.ts +43 -0
  164. package/dist/i18n/locales/hu.js +343 -0
  165. package/dist/i18n/locales/hu.js.map +1 -0
  166. package/dist/i18n/locales/it.d.ts +43 -0
  167. package/dist/i18n/locales/it.js +342 -0
  168. package/dist/i18n/locales/it.js.map +1 -0
  169. package/dist/i18n/locales/zh.d.ts +43 -0
  170. package/dist/i18n/locales/zh.js +338 -0
  171. package/dist/i18n/locales/zh.js.map +1 -0
  172. package/dist/index.d.ts +6 -3
  173. package/dist/index.js +4 -1
  174. package/dist/index.js.map +1 -1
  175. package/dist/lib/direction.d.ts +24 -0
  176. package/dist/lib/direction.js +19 -0
  177. package/dist/lib/direction.js.map +1 -0
  178. package/dist/rhf/form.js.map +1 -1
  179. package/dist/search/command-palette.d.ts +5 -0
  180. package/dist/search/command-palette.js +80 -50
  181. package/dist/search/command-palette.js.map +1 -1
  182. package/dist/shell/app-shell.d.ts +13 -1
  183. package/dist/shell/app-shell.js +133 -84
  184. package/dist/shell/app-shell.js.map +1 -1
  185. package/dist/shell/topbar-action-menu.js +1 -1
  186. package/dist/shell/topbar-action-menu.js.map +1 -1
  187. package/dist/shell/topbar-controls.d.ts +7 -3
  188. package/dist/shell/topbar-controls.js +38 -31
  189. package/dist/shell/topbar-controls.js.map +1 -1
  190. package/dist/theme/chart-palette.d.ts +7 -1
  191. package/dist/theme/chart-palette.js +2 -1
  192. package/dist/theme/chart-palette.js.map +1 -1
  193. package/dist/tour/tour.d.ts +11 -5
  194. package/dist/tour/tour.js +124 -58
  195. package/dist/tour/tour.js.map +1 -1
  196. package/dist/wizard/use-wizard.js +30 -13
  197. package/dist/wizard/use-wizard.js.map +1 -1
  198. package/dist/wizard/wizard-summary.js +1 -1
  199. package/dist/wizard/wizard-summary.js.map +1 -1
  200. package/package.json +12 -2
  201. package/src/components/account-settings.tsx +6 -2
  202. package/src/components/amount-input.tsx +10 -8
  203. package/src/components/autocomplete.tsx +123 -33
  204. package/src/components/calculator.tsx +5 -1
  205. package/src/components/chart-zoom.tsx +8 -1
  206. package/src/components/chart.tsx +68 -7
  207. package/src/components/checkbox.tsx +16 -0
  208. package/src/components/chip.tsx +131 -49
  209. package/src/components/choice-card.tsx +77 -36
  210. package/src/components/combobox-core.tsx +147 -20
  211. package/src/components/combobox.tsx +100 -44
  212. package/src/components/currency-select.tsx +6 -6
  213. package/src/components/danger-confirm.tsx +90 -36
  214. package/src/components/data-table-pagination.tsx +32 -19
  215. package/src/components/data-table.tsx +198 -36
  216. package/src/components/date-picker.tsx +24 -7
  217. package/src/components/dialog-frame.tsx +97 -22
  218. package/src/components/disclosure.tsx +75 -13
  219. package/src/components/dropdown.tsx +17 -5
  220. package/src/components/entity-combobox.tsx +26 -13
  221. package/src/components/file-button.tsx +102 -6
  222. package/src/components/file-dropzone.tsx +44 -35
  223. package/src/components/grouped-picker.tsx +2 -2
  224. package/src/components/hover-menu.tsx +139 -10
  225. package/src/components/icon-picker.tsx +14 -1
  226. package/src/components/measured-grid.tsx +852 -0
  227. package/src/components/mini-calendar.tsx +11 -8
  228. package/src/components/month-picker.tsx +18 -11
  229. package/src/components/multi-entity-combobox.tsx +2 -2
  230. package/src/components/multi-select.tsx +34 -7
  231. package/src/components/number-input.tsx +4 -4
  232. package/src/components/numpad-sheet.tsx +5 -3
  233. package/src/components/picker-sheet.tsx +1 -1
  234. package/src/components/popover.tsx +18 -3
  235. package/src/components/series-chart-ticks.ts +52 -0
  236. package/src/components/series-chart.tsx +39 -31
  237. package/src/components/swatch-picker.tsx +11 -0
  238. package/src/components/swipeable-row.tsx +21 -6
  239. package/src/components/switch.tsx +16 -0
  240. package/src/components/tile-radio.tsx +5 -7
  241. package/src/components/toggle-group.tsx +39 -3
  242. package/src/components/toggle-legend.tsx +5 -2
  243. package/src/components/treemap.tsx +28 -5
  244. package/src/components/ui.tsx +74 -30
  245. package/src/components/use-anchor-dir.ts +20 -0
  246. package/src/components/use-mobile-reveal.ts +10 -2
  247. package/src/components/use-table-state.ts +44 -7
  248. package/src/data-table.ts +1 -0
  249. package/src/feedback/feedback-attachment.tsx +36 -9
  250. package/src/feedback/feedback-dialog.tsx +3 -2
  251. package/src/hooks/use-windowed-rows.ts +69 -0
  252. package/src/i18n/defaults.ts +4 -0
  253. package/src/i18n/kit-labels.tsx +4 -0
  254. package/src/i18n/locales/de-CH.ts +30 -0
  255. package/src/i18n/locales/de.ts +365 -0
  256. package/src/i18n/locales/es.ts +358 -0
  257. package/src/i18n/locales/fr.ts +371 -0
  258. package/src/i18n/locales/hu.ts +359 -0
  259. package/src/i18n/locales/it.ts +360 -0
  260. package/src/i18n/locales/zh.ts +348 -0
  261. package/src/index.ts +10 -1
  262. package/src/lib/direction.ts +37 -0
  263. package/src/rhf/form.tsx +21 -0
  264. package/src/search/command-palette.tsx +77 -19
  265. package/src/shell/app-shell.tsx +200 -129
  266. package/src/shell/topbar-action-menu.tsx +1 -1
  267. package/src/shell/topbar-controls.tsx +19 -8
  268. package/src/theme/chart-palette.ts +9 -2
  269. package/src/tour/tour.tsx +119 -18
  270. package/src/wizard/use-wizard.ts +55 -17
  271. package/src/wizard/wizard-summary.tsx +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/rhf/form.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 * ```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/ui.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/ui.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 */\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/ui\";\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 <div data-slot=\"form-item\" {...rest} className={cn(\"grid 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-[var(--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-[11px] leading-tight text-[var(--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-[11px] leading-tight text-[var(--danger)]\", className)}\n >\n {body}\n </p>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/rhf/form.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 * ```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/ui.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/ui.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/ui\";\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 <div data-slot=\"form-item\" {...rest} className={cn(\"grid 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-[var(--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-[11px] leading-tight text-[var(--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-[11px] leading-tight text-[var(--danger)]\", className)}\n >\n {body}\n </p>\n );\n}\n"],"mappings":"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","names":[]}
@@ -36,6 +36,11 @@ interface CommandPaletteLabels {
36
36
  * `CommandPaletteLabels` keeps compiling; the default always carries it.
37
37
  */
38
38
  dialog?: string;
39
+ /**
40
+ * Shown in place of the results when the `search` provider throws or its promise
41
+ * rejects. Optional for the same reason as `dialog`; the default always carries it.
42
+ */
43
+ error?: string;
39
44
  }
40
45
  declare const DEFAULT_COMMAND_PALETTE_LABELS: CommandPaletteLabels;
41
46
  interface CommandPaletteProps {
@@ -1,16 +1,19 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { useEffect, useId, useMemo, useRef, useState } from "react";
4
4
  import { createPortal } from "react-dom";
5
5
  import { Search } from "lucide-react";
6
6
  import { cn } from "../lib/cn.js";
7
7
  import { useOverlayHistory } from "../hooks/use-overlay-history.js";
8
+ import { useFocusTrap } from "../hooks/use-focus-trap.js";
9
+ import { useBodyScrollLock } from "../hooks/use-body-scroll-lock.js";
8
10
  import { useKitLabels } from "../i18n/kit-labels.js";
9
11
  const DEFAULT_COMMAND_PALETTE_LABELS = {
10
12
  placeholder: "Search\u2026",
11
13
  empty: "No results",
12
14
  loading: "Searching\u2026",
13
- dialog: "Search"
15
+ dialog: "Search",
16
+ error: "Search failed. Try again."
14
17
  };
15
18
  const { dialog: DEFAULT_DIALOG_NAME, ...DEFAULTS_WITHOUT_DIALOG } = DEFAULT_COMMAND_PALETTE_LABELS;
16
19
  function useCommandKey(onOpen) {
@@ -37,19 +40,25 @@ function CommandPalette({
37
40
  const [query, setQuery] = useState("");
38
41
  const [results, setResults] = useState([]);
39
42
  const [loading, setLoading] = useState(false);
43
+ const [failed, setFailed] = useState(false);
40
44
  const [active, setActive] = useState(0);
45
+ const uid = useId();
46
+ const listId = `${uid}-list`;
47
+ const itemId = (id) => `${uid}-item-${id}`;
48
+ const groupId = (index) => `${uid}-group-${index}`;
49
+ const dialogRef = useRef(null);
41
50
  const inputRef = useRef(null);
42
51
  const listRef = useRef(null);
43
52
  const searchRef = useRef(search);
44
53
  searchRef.current = search;
45
54
  const reqId = useRef(0);
46
55
  useOverlayHistory(open, onClose);
56
+ useFocusTrap(dialogRef, { active: open, initialFocus: () => inputRef.current });
57
+ useBodyScrollLock(open);
47
58
  useEffect(() => {
48
59
  if (!open) return;
49
60
  setQuery("");
50
61
  setActive(0);
51
- const id = requestAnimationFrame(() => inputRef.current?.focus());
52
- return () => cancelAnimationFrame(id);
53
62
  }, [open]);
54
63
  useEffect(() => {
55
64
  if (!open) return;
@@ -60,8 +69,16 @@ function CommandPalette({
60
69
  const r = await Promise.resolve(searchRef.current(query));
61
70
  if (reqId.current === id) {
62
71
  setResults(r);
72
+ setFailed(false);
63
73
  setActive(0);
64
74
  }
75
+ } catch (err) {
76
+ if (reqId.current === id) {
77
+ setResults([]);
78
+ setFailed(true);
79
+ setActive(0);
80
+ }
81
+ console.error("CommandPalette: search failed", err);
65
82
  } finally {
66
83
  if (reqId.current === id) setLoading(false);
67
84
  }
@@ -99,7 +116,10 @@ function CommandPalette({
99
116
  } else if (e.key === "Enter") {
100
117
  e.preventDefault();
101
118
  choose(flat[active]);
102
- } else if (e.key === "Escape") {
119
+ }
120
+ };
121
+ const onDialogKeyDown = (e) => {
122
+ if (e.key === "Escape") {
103
123
  e.preventDefault();
104
124
  onClose();
105
125
  }
@@ -121,10 +141,13 @@ function CommandPalette({
121
141
  children: /* @__PURE__ */ jsxs(
122
142
  "div",
123
143
  {
144
+ ref: dialogRef,
124
145
  role: "dialog",
125
146
  "aria-modal": "true",
126
147
  "aria-label": dialogName,
127
- className: "flex max-h-[70vh] w-full max-w-xl flex-col overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--bg-surface)] shadow-2xl",
148
+ tabIndex: -1,
149
+ onKeyDown: onDialogKeyDown,
150
+ className: "flex max-h-[70vh] w-full max-w-xl flex-col overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--bg-surface)] shadow-2xl outline-none",
128
151
  children: [
129
152
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-[var(--border)] px-3", children: [
130
153
  /* @__PURE__ */ jsx(Search, { className: "size-4 shrink-0 text-[var(--text-placeholder)]" }),
@@ -134,8 +157,8 @@ function CommandPalette({
134
157
  ref: inputRef,
135
158
  role: "combobox",
136
159
  "aria-expanded": true,
137
- "aria-controls": "command-palette-list",
138
- "aria-activedescendant": flat[active] ? `command-item-${flat[active].id}` : void 0,
160
+ "aria-controls": listId,
161
+ "aria-activedescendant": flat[active] ? itemId(flat[active].id) : void 0,
139
162
  value: query,
140
163
  onChange: (e) => setQuery(e.target.value),
141
164
  onKeyDown,
@@ -145,48 +168,55 @@ function CommandPalette({
145
168
  ),
146
169
  loading && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[11px] text-[var(--text-placeholder)]", children: l.loading })
147
170
  ] }),
148
- /* @__PURE__ */ jsxs("ul", { id: "command-palette-list", ref: listRef, role: "listbox", className: "min-h-0 flex-1 overflow-y-auto py-1", children: [
149
- flat.length === 0 && !loading && /* @__PURE__ */ jsx("li", { className: "px-3 py-6 text-center text-sm text-[var(--text-muted)]", children: l.empty }),
150
- groups.map(({ group, items }) => /* @__PURE__ */ jsxs("li", { children: [
151
- /* @__PURE__ */ jsx("div", { className: "px-3 pb-0.5 pt-2 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]", children: group }),
152
- /* @__PURE__ */ jsx("ul", { children: items.map((item) => {
153
- flatIndex += 1;
154
- const idx = flatIndex;
155
- const isActive = idx === active;
156
- const shared = {
157
- id: `command-item-${item.id}`,
158
- role: "option",
159
- "aria-selected": isActive,
160
- "data-index": idx,
161
- onMouseMove: () => setActive(idx),
162
- className: cn(
163
- "flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm",
164
- isActive ? "bg-[var(--bg-active)] text-[var(--text-primary)]" : "text-[var(--text-secondary)]"
165
- )
166
- };
167
- const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
168
- item.icon && /* @__PURE__ */ jsx("span", { className: "flex size-4 shrink-0 items-center justify-center text-[var(--text-placeholder)]", children: item.icon }),
169
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label }),
170
- item.hint && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-[var(--text-placeholder)]", children: item.hint })
171
- ] });
172
- return /* @__PURE__ */ jsx("li", { children: item.href ? /* @__PURE__ */ jsx(
173
- "a",
174
- {
175
- ...shared,
176
- href: item.href,
177
- draggable: false,
178
- onClick: (e) => {
179
- if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
180
- return;
181
- e.preventDefault();
182
- choose(item);
183
- },
184
- children: inner
185
- }
186
- ) : /* @__PURE__ */ jsx("button", { type: "button", ...shared, onClick: () => choose(item), children: inner }) }, item.id);
187
- }) })
188
- ] }, group))
189
- ] }),
171
+ failed && !loading && /* @__PURE__ */ jsx("div", { role: "alert", className: "px-3 py-6 text-center text-sm text-[var(--text-muted)]", children: l.error ?? DEFAULT_COMMAND_PALETTE_LABELS.error }),
172
+ !failed && flat.length === 0 && !loading && /* @__PURE__ */ jsx("div", { className: "px-3 py-6 text-center text-sm text-[var(--text-muted)]", children: l.empty }),
173
+ /* @__PURE__ */ jsx("ul", { id: listId, ref: listRef, role: "listbox", "aria-label": dialogName, className: "min-h-0 flex-1 overflow-y-auto py-1", children: groups.map(({ group, items }, groupIndex) => /* @__PURE__ */ jsxs("li", { role: "presentation", children: [
174
+ /* @__PURE__ */ jsx(
175
+ "div",
176
+ {
177
+ id: groupId(groupIndex),
178
+ role: "presentation",
179
+ className: "px-3 pb-0.5 pt-2 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]",
180
+ children: group
181
+ }
182
+ ),
183
+ /* @__PURE__ */ jsx("ul", { role: "group", "aria-labelledby": groupId(groupIndex), children: items.map((item) => {
184
+ flatIndex += 1;
185
+ const idx = flatIndex;
186
+ const isActive = idx === active;
187
+ const shared = {
188
+ id: itemId(item.id),
189
+ role: "option",
190
+ "aria-selected": isActive,
191
+ "data-index": idx,
192
+ onMouseMove: () => setActive(idx),
193
+ className: cn(
194
+ "flex w-full items-center gap-2.5 px-3 py-2 text-start text-sm",
195
+ isActive ? "bg-[var(--bg-active)] text-[var(--text-primary)]" : "text-[var(--text-secondary)]"
196
+ )
197
+ };
198
+ const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
199
+ item.icon && /* @__PURE__ */ jsx("span", { className: "flex size-4 shrink-0 items-center justify-center text-[var(--text-placeholder)]", children: item.icon }),
200
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label }),
201
+ item.hint && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-[var(--text-placeholder)]", children: item.hint })
202
+ ] });
203
+ return /* @__PURE__ */ jsx("li", { role: "presentation", children: item.href ? /* @__PURE__ */ jsx(
204
+ "a",
205
+ {
206
+ ...shared,
207
+ href: item.href,
208
+ draggable: false,
209
+ onClick: (e) => {
210
+ if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
211
+ return;
212
+ e.preventDefault();
213
+ choose(item);
214
+ },
215
+ children: inner
216
+ }
217
+ ) : /* @__PURE__ */ jsx("button", { type: "button", ...shared, onClick: () => choose(item), children: inner }) }, item.id);
218
+ }) })
219
+ ] }, group)) }),
190
220
  l.hint && /* @__PURE__ */ jsx("div", { className: "border-t border-[var(--border)] px-3 py-1.5 text-[11px] text-[var(--text-placeholder)]", children: l.hint })
191
221
  ]
192
222
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/search/command-palette.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { Search } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useOverlayHistory } from \"../hooks/use-overlay-history\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\nexport interface CommandItem {\n id: string;\n /** Primary text. */\n label: string;\n /** Secondary text on the right (e.g. an amount, a date, a group hint). */\n hint?: string;\n /** Heading this item is grouped under. */\n group: string;\n /** Leading icon / element. */\n icon?: ReactNode;\n /** Invoked when the item is chosen (navigate, run an action, …). */\n onSelect: () => void;\n /** Where this item leads, when it is a navigation rather than an action. Renders\n * the row as a real anchor so it can be middle-/⌘-clicked into a new tab\n * (Keksdose feedback #451); `onSelect` still runs for a plain click and for ↵, so\n * in-app navigation stays client-side. `href` only adds what the browser can do\n * with a link and JavaScript cannot fake — the rejected alternative, an\n * `onAuxClick` calling `window.open`, gets the middle click and nothing else. */\n href?: string;\n}\n\nexport interface CommandPaletteLabels {\n placeholder: string;\n empty: string;\n loading: string;\n /** Hint shown in the footer, e.g. \"↑↓ to navigate · ↵ to select · esc to close\". */\n hint?: string;\n /**\n * Accessible name of the dialog itself. It used to borrow `placeholder`, which is\n * written to sit greyed-out in an empty box (\"Search…\", \"Type a command\"), not to\n * name a window — and a reader heard the ellipsis read out as part of the name.\n *\n * Optional, unlike `placeholder`, so a translation annotated as\n * `CommandPaletteLabels` keeps compiling; the default always carries it.\n */\n dialog?: string;\n}\n\nexport const DEFAULT_COMMAND_PALETTE_LABELS: CommandPaletteLabels = {\n placeholder: \"Search…\",\n empty: \"No results\",\n loading: \"Searching…\",\n dialog: \"Search\",\n};\n\n/** The defaults minus `dialog`, so a resolved `dialog` means someone SUPPLIED one —\n * see the dialog's `aria-label` for why that distinction matters. */\nconst { dialog: DEFAULT_DIALOG_NAME, ...DEFAULTS_WITHOUT_DIALOG } = DEFAULT_COMMAND_PALETTE_LABELS;\n\ninterface CommandPaletteProps {\n open: boolean;\n onClose: () => void;\n /**\n * Returns results for a query (called debounced as the user types; an empty\n * query is allowed — return defaults like pages/recent). Sync or async; stale\n * async responses are ignored.\n *\n * Its IDENTITY is not a re-run signal — it is read through a ref, so the palette\n * never re-searches merely because the caller rebuilt the callback. That is\n * deliberate: a provider closing over a list rendered from a query allocates a new\n * function on most renders, and re-running the search on each would make typing\n * stutter. Use {@link CommandPaletteProps.revision} when the DATA behind the\n * provider changes.\n */\n search: (query: string) => CommandItem[] | Promise<CommandItem[]>;\n /**\n * Anything whose change means the current results are out of date — typically the\n * arrays the provider searches over. Compared by identity (as an effect dependency\n * would be), so pass stable references: a `data = []` default allocates a fresh\n * array on every render while a query is pending and would re-search forever.\n *\n * Without it, a provider whose data arrives AFTER the palette opened never gets\n * asked again: the user's last keystroke settles the debounce, the queries resolve a\n * moment later, and the groups they would have contributed are simply absent until\n * another keystroke. That is the ordinary first open on a cold cache.\n */\n revision?: unknown;\n labels?: Partial<CommandPaletteLabels>;\n}\n\n/** Registers a global ⌘K / Ctrl-K shortcut that calls `onOpen`. */\nexport function useCommandKey(onOpen: () => void): void {\n useEffect(() => {\n const handler = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && (e.key === \"k\" || e.key === \"K\")) {\n e.preventDefault();\n onOpen();\n }\n };\n document.addEventListener(\"keydown\", handler);\n return () => document.removeEventListener(\"keydown\", handler);\n }, [onOpen]);\n}\n\n/**\n * A ⌘K-style command palette: a top-centred modal with a search field and a\n * grouped, keyboard-navigable result list. Domain-free — the app supplies\n * results via the `search` provider (pages, actions, records, …); each result\n * carries its own `onSelect`.\n */\nexport function CommandPalette({\n open,\n onClose,\n search,\n revision,\n labels,\n}: CommandPaletteProps) {\n const l = useKitLabels(\"commandPalette\", DEFAULTS_WITHOUT_DIALOG, labels);\n // `dialog` is new, and until it existed the dialog was named by `placeholder`. An\n // app that translated `placeholder` and has not heard of `dialog` would otherwise\n // go from a German name to an English \"Search\" on update — so a translated\n // placeholder keeps doing the job until a `dialog` is supplied.\n const dialogName =\n l.dialog ??\n (l.placeholder !== DEFAULT_COMMAND_PALETTE_LABELS.placeholder ? l.placeholder : DEFAULT_DIALOG_NAME);\n const [query, setQuery] = useState(\"\");\n const [results, setResults] = useState<CommandItem[]>([]);\n const [loading, setLoading] = useState(false);\n const [active, setActive] = useState(0);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLUListElement>(null);\n const searchRef = useRef(search);\n searchRef.current = search;\n const reqId = useRef(0);\n\n // Back dismisses the palette (Keksdose feedback #172). Declared with the other\n // hooks, above the `if (!open) return null` below — a hook past a conditional\n // return changes the hook order between renders. Passing `open` rather than\n // mounting-while-open is what lets it sit here.\n useOverlayHistory(open, onClose);\n\n // Reset + focus when opened.\n useEffect(() => {\n if (!open) return;\n setQuery(\"\");\n setActive(0);\n const id = requestAnimationFrame(() => inputRef.current?.focus());\n return () => cancelAnimationFrame(id);\n }, [open]);\n\n // Debounced, race-safe search.\n useEffect(() => {\n if (!open) return;\n const id = ++reqId.current;\n setLoading(true);\n const run = async () => {\n try {\n const r = await Promise.resolve(searchRef.current(query));\n if (reqId.current === id) {\n setResults(r);\n setActive(0);\n }\n } finally {\n if (reqId.current === id) setLoading(false);\n }\n };\n const t = setTimeout(run, query ? 150 : 0);\n return () => clearTimeout(t);\n // `revision`, not `search`: see the prop's docstring. The debounce is re-armed on a\n // revision change, which is right — data arriving is not a keystroke and there is\n // nothing to feel laggy about.\n }, [query, open, revision]);\n\n // Group results, preserving first-seen group order; keep a flat list for nav.\n const { groups, flat } = useMemo(() => {\n const order: string[] = [];\n const byGroup = new Map<string, CommandItem[]>();\n for (const item of results) {\n if (!byGroup.has(item.group)) {\n byGroup.set(item.group, []);\n order.push(item.group);\n }\n byGroup.get(item.group)!.push(item);\n }\n return {\n groups: order.map((g) => ({ group: g, items: byGroup.get(g)! })),\n flat: order.flatMap((g) => byGroup.get(g)!),\n };\n }, [results]);\n\n const choose = (item: CommandItem | undefined) => {\n if (!item) return;\n onClose();\n item.onSelect();\n };\n\n const onKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActive((i) => Math.min(i + 1, flat.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n choose(flat[active]);\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n onClose();\n }\n };\n\n // Keep the active row scrolled into view.\n useEffect(() => {\n const el = listRef.current?.querySelector<HTMLElement>(`[data-index=\"${active}\"]`);\n el?.scrollIntoView({ block: \"nearest\" });\n }, [active]);\n\n if (!open) return null;\n\n let flatIndex = -1;\n return createPortal(\n <div\n className=\"fixed inset-0 z-[70] flex items-start justify-center bg-black/40 p-4 pt-[10vh]\"\n onMouseDown={(e) => {\n if (e.target === e.currentTarget) onClose();\n }}\n >\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={dialogName}\n className=\"flex max-h-[70vh] w-full max-w-xl flex-col overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--bg-surface)] shadow-2xl\"\n >\n <div className=\"flex items-center gap-2 border-b border-[var(--border)] px-3\">\n <Search className=\"size-4 shrink-0 text-[var(--text-placeholder)]\" />\n <input\n ref={inputRef}\n role=\"combobox\"\n aria-expanded\n aria-controls=\"command-palette-list\"\n aria-activedescendant={flat[active] ? `command-item-${flat[active].id}` : undefined}\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n onKeyDown={onKeyDown}\n placeholder={l.placeholder}\n className=\"w-full bg-transparent py-3 text-sm outline-none placeholder:text-[var(--text-placeholder)] text-[var(--text-primary)]\"\n />\n {loading && <span className=\"shrink-0 text-[11px] text-[var(--text-placeholder)]\">{l.loading}</span>}\n </div>\n\n <ul id=\"command-palette-list\" ref={listRef} role=\"listbox\" className=\"min-h-0 flex-1 overflow-y-auto py-1\">\n {flat.length === 0 && !loading && (\n <li className=\"px-3 py-6 text-center text-sm text-[var(--text-muted)]\">{l.empty}</li>\n )}\n {groups.map(({ group, items }) => (\n <li key={group}>\n <div className=\"px-3 pb-0.5 pt-2 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]\">\n {group}\n </div>\n <ul>\n {items.map((item) => {\n flatIndex += 1;\n const idx = flatIndex;\n const isActive = idx === active;\n // Written once and worn by either tag below, so the anchor and the\n // button can never drift apart in looks or in listbox semantics.\n // `role=\"option\"` on an <a href> is fine: the browser's middle-click\n // behaviour keys off the element, not the ARIA role.\n const shared = {\n id: `command-item-${item.id}`,\n role: \"option\" as const,\n \"aria-selected\": isActive,\n \"data-index\": idx,\n onMouseMove: () => setActive(idx),\n className: cn(\n \"flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm\",\n isActive\n ? \"bg-[var(--bg-active)] text-[var(--text-primary)]\"\n : \"text-[var(--text-secondary)]\",\n ),\n };\n const inner = (\n <>\n {item.icon && (\n <span className=\"flex size-4 shrink-0 items-center justify-center text-[var(--text-placeholder)]\">\n {item.icon}\n </span>\n )}\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n {item.hint && (\n <span className=\"shrink-0 text-xs text-[var(--text-placeholder)]\">{item.hint}</span>\n )}\n </>\n );\n return (\n <li key={item.id}>\n {item.href ? (\n <a\n {...shared}\n href={item.href}\n draggable={false}\n onClick={(e) => {\n // A modified click belongs to the browser (feedback\n // #451) — and the palette deliberately stays OPEN for\n // it: ⌘-clicking three results in a row is the reason to\n // want this, and closing after the first would undo it.\n if (\n e.button !== 0 ||\n e.metaKey ||\n e.ctrlKey ||\n e.shiftKey ||\n e.altKey\n )\n return;\n e.preventDefault();\n choose(item);\n }}\n >\n {inner}\n </a>\n ) : (\n <button type=\"button\" {...shared} onClick={() => choose(item)}>\n {inner}\n </button>\n )}\n </li>\n );\n })}\n </ul>\n </li>\n ))}\n </ul>\n\n {l.hint && (\n <div className=\"border-t border-[var(--border)] px-3 py-1.5 text-[11px] text-[var(--text-placeholder)]\">\n {l.hint}\n </div>\n )}\n </div>\n </div>,\n document.body,\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/search/command-palette.tsx"],"sourcesContent":["import { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { Search } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useOverlayHistory } from \"../hooks/use-overlay-history\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { useBodyScrollLock } from \"../hooks/use-body-scroll-lock\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\nexport interface CommandItem {\n id: string;\n /** Primary text. */\n label: string;\n /** Secondary text on the right (e.g. an amount, a date, a group hint). */\n hint?: string;\n /** Heading this item is grouped under. */\n group: string;\n /** Leading icon / element. */\n icon?: ReactNode;\n /** Invoked when the item is chosen (navigate, run an action, …). */\n onSelect: () => void;\n /** Where this item leads, when it is a navigation rather than an action. Renders\n * the row as a real anchor so it can be middle-/⌘-clicked into a new tab\n * (Keksdose feedback #451); `onSelect` still runs for a plain click and for ↵, so\n * in-app navigation stays client-side. `href` only adds what the browser can do\n * with a link and JavaScript cannot fake — the rejected alternative, an\n * `onAuxClick` calling `window.open`, gets the middle click and nothing else. */\n href?: string;\n}\n\nexport interface CommandPaletteLabels {\n placeholder: string;\n empty: string;\n loading: string;\n /** Hint shown in the footer, e.g. \"↑↓ to navigate · ↵ to select · esc to close\". */\n hint?: string;\n /**\n * Accessible name of the dialog itself. It used to borrow `placeholder`, which is\n * written to sit greyed-out in an empty box (\"Search…\", \"Type a command\"), not to\n * name a window — and a reader heard the ellipsis read out as part of the name.\n *\n * Optional, unlike `placeholder`, so a translation annotated as\n * `CommandPaletteLabels` keeps compiling; the default always carries it.\n */\n dialog?: string;\n /**\n * Shown in place of the results when the `search` provider throws or its promise\n * rejects. Optional for the same reason as `dialog`; the default always carries it.\n */\n error?: string;\n}\n\nexport const DEFAULT_COMMAND_PALETTE_LABELS: CommandPaletteLabels = {\n placeholder: \"Search…\",\n empty: \"No results\",\n loading: \"Searching…\",\n dialog: \"Search\",\n error: \"Search failed. Try again.\",\n};\n\n/** The defaults minus `dialog`, so a resolved `dialog` means someone SUPPLIED one —\n * see the dialog's `aria-label` for why that distinction matters. */\nconst { dialog: DEFAULT_DIALOG_NAME, ...DEFAULTS_WITHOUT_DIALOG } = DEFAULT_COMMAND_PALETTE_LABELS;\n\ninterface CommandPaletteProps {\n open: boolean;\n onClose: () => void;\n /**\n * Returns results for a query (called debounced as the user types; an empty\n * query is allowed — return defaults like pages/recent). Sync or async; stale\n * async responses are ignored.\n *\n * Its IDENTITY is not a re-run signal — it is read through a ref, so the palette\n * never re-searches merely because the caller rebuilt the callback. That is\n * deliberate: a provider closing over a list rendered from a query allocates a new\n * function on most renders, and re-running the search on each would make typing\n * stutter. Use {@link CommandPaletteProps.revision} when the DATA behind the\n * provider changes.\n */\n search: (query: string) => CommandItem[] | Promise<CommandItem[]>;\n /**\n * Anything whose change means the current results are out of date — typically the\n * arrays the provider searches over. Compared by identity (as an effect dependency\n * would be), so pass stable references: a `data = []` default allocates a fresh\n * array on every render while a query is pending and would re-search forever.\n *\n * Without it, a provider whose data arrives AFTER the palette opened never gets\n * asked again: the user's last keystroke settles the debounce, the queries resolve a\n * moment later, and the groups they would have contributed are simply absent until\n * another keystroke. That is the ordinary first open on a cold cache.\n */\n revision?: unknown;\n labels?: Partial<CommandPaletteLabels>;\n}\n\n/** Registers a global ⌘K / Ctrl-K shortcut that calls `onOpen`. */\nexport function useCommandKey(onOpen: () => void): void {\n useEffect(() => {\n const handler = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && (e.key === \"k\" || e.key === \"K\")) {\n e.preventDefault();\n onOpen();\n }\n };\n document.addEventListener(\"keydown\", handler);\n return () => document.removeEventListener(\"keydown\", handler);\n }, [onOpen]);\n}\n\n/**\n * A ⌘K-style command palette: a top-centred modal with a search field and a\n * grouped, keyboard-navigable result list. Domain-free — the app supplies\n * results via the `search` provider (pages, actions, records, …); each result\n * carries its own `onSelect`.\n */\nexport function CommandPalette({\n open,\n onClose,\n search,\n revision,\n labels,\n}: CommandPaletteProps) {\n const l = useKitLabels(\"commandPalette\", DEFAULTS_WITHOUT_DIALOG, labels);\n // `dialog` is new, and until it existed the dialog was named by `placeholder`. An\n // app that translated `placeholder` and has not heard of `dialog` would otherwise\n // go from a German name to an English \"Search\" on update — so a translated\n // placeholder keeps doing the job until a `dialog` is supplied.\n const dialogName =\n l.dialog ??\n (l.placeholder !== DEFAULT_COMMAND_PALETTE_LABELS.placeholder ? l.placeholder : DEFAULT_DIALOG_NAME);\n const [query, setQuery] = useState(\"\");\n const [results, setResults] = useState<CommandItem[]>([]);\n const [loading, setLoading] = useState(false);\n const [failed, setFailed] = useState(false);\n const [active, setActive] = useState(0);\n // Per instance: these were the literal \"command-palette-list\" / \"command-item-<id>\",\n // so two palettes on one page (an app-wide one and a scoped one) pointed each\n // other's `aria-controls` and `aria-activedescendant` at the wrong list.\n const uid = useId();\n const listId = `${uid}-list`;\n const itemId = (id: string) => `${uid}-item-${id}`;\n const groupId = (index: number) => `${uid}-group-${index}`;\n const dialogRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLUListElement>(null);\n const searchRef = useRef(search);\n searchRef.current = search;\n const reqId = useRef(0);\n\n // Back dismisses the palette (Keksdose feedback #172). Declared with the other\n // hooks, above the `if (!open) return null` below — a hook past a conditional\n // return changes the hook order between renders. Passing `open` rather than\n // mounting-while-open is what lets it sit here.\n useOverlayHistory(open, onClose);\n\n // A modal like any other: Tab stays inside, the page behind does not scroll, and\n // focus goes back to whatever opened it — the palette used to drop it on <body>.\n // Initial focus is the field, not the container: typing is the only reason to open it.\n useFocusTrap(dialogRef, { active: open, initialFocus: () => inputRef.current });\n useBodyScrollLock(open);\n\n // Reset when opened.\n useEffect(() => {\n if (!open) return;\n setQuery(\"\");\n setActive(0);\n }, [open]);\n\n // Debounced, race-safe search.\n useEffect(() => {\n if (!open) return;\n const id = ++reqId.current;\n setLoading(true);\n const run = async () => {\n try {\n const r = await Promise.resolve(searchRef.current(query));\n if (reqId.current === id) {\n setResults(r);\n setFailed(false);\n setActive(0);\n }\n } catch (err) {\n // There was no catch: the previous query's results stayed on screen as if they\n // answered this one, and the rejection went unhandled. Clear them and say so.\n if (reqId.current === id) {\n setResults([]);\n setFailed(true);\n setActive(0);\n }\n console.error(\"CommandPalette: search failed\", err);\n } finally {\n if (reqId.current === id) setLoading(false);\n }\n };\n const t = setTimeout(run, query ? 150 : 0);\n return () => clearTimeout(t);\n // `revision`, not `search`: see the prop's docstring. The debounce is re-armed on a\n // revision change, which is right — data arriving is not a keystroke and there is\n // nothing to feel laggy about.\n }, [query, open, revision]);\n\n // Group results, preserving first-seen group order; keep a flat list for nav.\n const { groups, flat } = useMemo(() => {\n const order: string[] = [];\n const byGroup = new Map<string, CommandItem[]>();\n for (const item of results) {\n if (!byGroup.has(item.group)) {\n byGroup.set(item.group, []);\n order.push(item.group);\n }\n byGroup.get(item.group)!.push(item);\n }\n return {\n groups: order.map((g) => ({ group: g, items: byGroup.get(g)! })),\n flat: order.flatMap((g) => byGroup.get(g)!),\n };\n }, [results]);\n\n const choose = (item: CommandItem | undefined) => {\n if (!item) return;\n onClose();\n item.onSelect();\n };\n\n const onKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActive((i) => Math.min(i + 1, flat.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n choose(flat[active]);\n }\n };\n\n // Escape on the whole dialog, not only the field: once Tab has moved focus to a row\n // (or a footer control), Escape used to do nothing at all.\n const onDialogKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"Escape\") {\n e.preventDefault();\n onClose();\n }\n };\n\n // Keep the active row scrolled into view.\n useEffect(() => {\n const el = listRef.current?.querySelector<HTMLElement>(`[data-index=\"${active}\"]`);\n el?.scrollIntoView({ block: \"nearest\" });\n }, [active]);\n\n if (!open) return null;\n\n let flatIndex = -1;\n return createPortal(\n <div\n className=\"fixed inset-0 z-[70] flex items-start justify-center bg-black/40 p-4 pt-[10vh]\"\n onMouseDown={(e) => {\n if (e.target === e.currentTarget) onClose();\n }}\n >\n {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- Escape from anywhere in the dialog */}\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={dialogName}\n // The trap's fallback target when the field is not there to take focus.\n tabIndex={-1}\n // Escape bubbling up from anything inside — a dialog is where it belongs.\n onKeyDown={onDialogKeyDown}\n className=\"flex max-h-[70vh] w-full max-w-xl flex-col overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--bg-surface)] shadow-2xl outline-none\"\n >\n <div className=\"flex items-center gap-2 border-b border-[var(--border)] px-3\">\n <Search className=\"size-4 shrink-0 text-[var(--text-placeholder)]\" />\n <input\n ref={inputRef}\n role=\"combobox\"\n aria-expanded\n aria-controls={listId}\n aria-activedescendant={flat[active] ? itemId(flat[active].id) : undefined}\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n onKeyDown={onKeyDown}\n placeholder={l.placeholder}\n className=\"w-full bg-transparent py-3 text-sm outline-none placeholder:text-[var(--text-placeholder)] text-[var(--text-primary)]\"\n />\n {loading && <span className=\"shrink-0 text-[11px] text-[var(--text-placeholder)]\">{l.loading}</span>}\n </div>\n\n {/* The status lines sit outside the listbox: a listbox owns options and groups,\n and \"No results\" is neither. */}\n {failed && !loading && (\n <div role=\"alert\" className=\"px-3 py-6 text-center text-sm text-[var(--text-muted)]\">\n {l.error ?? DEFAULT_COMMAND_PALETTE_LABELS.error}\n </div>\n )}\n {!failed && flat.length === 0 && !loading && (\n <div className=\"px-3 py-6 text-center text-sm text-[var(--text-muted)]\">{l.empty}</div>\n )}\n {/* Listbox grouping: each group is a `role=\"group\"` named by its heading, and\n the list markup in between is presentational so the listbox's children are\n groups and options only — as a plain <li>/<ul> tree a reader announced\n \"list, 2 items\" between options and never tied the heading to its rows. */}\n <ul id={listId} ref={listRef} role=\"listbox\" aria-label={dialogName} className=\"min-h-0 flex-1 overflow-y-auto py-1\">\n {groups.map(({ group, items }, groupIndex) => (\n <li key={group} role=\"presentation\">\n <div\n id={groupId(groupIndex)}\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]\"\n >\n {group}\n </div>\n <ul role=\"group\" aria-labelledby={groupId(groupIndex)}>\n {items.map((item) => {\n flatIndex += 1;\n const idx = flatIndex;\n const isActive = idx === active;\n // Written once and worn by either tag below, so the anchor and the\n // button can never drift apart in looks or in listbox semantics.\n // `role=\"option\"` on an <a href> is fine: the browser's middle-click\n // behaviour keys off the element, not the ARIA role.\n const shared = {\n id: itemId(item.id),\n role: \"option\" as const,\n \"aria-selected\": isActive,\n \"data-index\": idx,\n onMouseMove: () => setActive(idx),\n className: cn(\n \"flex w-full items-center gap-2.5 px-3 py-2 text-start text-sm\",\n isActive\n ? \"bg-[var(--bg-active)] text-[var(--text-primary)]\"\n : \"text-[var(--text-secondary)]\",\n ),\n };\n const inner = (\n <>\n {item.icon && (\n <span className=\"flex size-4 shrink-0 items-center justify-center text-[var(--text-placeholder)]\">\n {item.icon}\n </span>\n )}\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n {item.hint && (\n <span className=\"shrink-0 text-xs text-[var(--text-placeholder)]\">{item.hint}</span>\n )}\n </>\n );\n return (\n <li key={item.id} role=\"presentation\">\n {item.href ? (\n <a\n {...shared}\n href={item.href}\n draggable={false}\n onClick={(e) => {\n // A modified click belongs to the browser (feedback\n // #451) — and the palette deliberately stays OPEN for\n // it: ⌘-clicking three results in a row is the reason to\n // want this, and closing after the first would undo it.\n if (\n e.button !== 0 ||\n e.metaKey ||\n e.ctrlKey ||\n e.shiftKey ||\n e.altKey\n )\n return;\n e.preventDefault();\n choose(item);\n }}\n >\n {inner}\n </a>\n ) : (\n <button type=\"button\" {...shared} onClick={() => choose(item)}>\n {inner}\n </button>\n )}\n </li>\n );\n })}\n </ul>\n </li>\n ))}\n </ul>\n\n {l.hint && (\n <div className=\"border-t border-[var(--border)] px-3 py-1.5 text-[11px] text-[var(--text-placeholder)]\">\n {l.hint}\n </div>\n )}\n </div>\n </div>,\n document.body,\n );\n}\n"],"mappings":"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","names":[]}
@@ -80,6 +80,18 @@ interface AppShellProps extends ComponentPropsWithoutRef<"div"> {
80
80
  * bottom bar — the phone's counterpart of the sidebar's second level. Default true;
81
81
  * `false` keeps the single bar, where a group entry only links to its own `to`. */
82
82
  mobileSubNav?: boolean;
83
+ /**
84
+ * Lay the shell out inside its PARENT'S box instead of the viewport: `h-full` rather
85
+ * than `h-dvh`, its own scroll container at every width, a bottom bar pinned to the
86
+ * shell rather than the window, `--app-nav-h` published on the shell alone, and no
87
+ * persisted collapse state unless `collapseStorageKey` is passed. For an app preview,
88
+ * a device frame, a docs example.
89
+ *
90
+ * Implied for a shell rendered inside another AppShell, which is the case this
91
+ * exists for: the inner one used to publish its own nav height on `<html>`, fight
92
+ * the outer one over it, and remove it on unmount from under the outer app.
93
+ */
94
+ embedded?: boolean;
83
95
  }
84
96
  /**
85
97
  * The responsive application shell: a top bar, a collapsible desktop sidebar
@@ -88,6 +100,6 @@ interface AppShellProps extends ComponentPropsWithoutRef<"div"> {
88
100
  * `NavLink`; domain-free — nav items, brand, footer and the top bar's actions
89
101
  * are all supplied by the app.
90
102
  */
91
- declare function AppShell({ nav, topBar, children, footer, sidebarFooter, collapseStorageKey, collapseLabel, expandLabel, subNav, mobileSubNav, toggleGroupLabel, className, ...rest }: AppShellProps): react.JSX.Element;
103
+ declare function AppShell({ nav, topBar, children, footer, sidebarFooter, collapseStorageKey: collapseStorageKeyProp, collapseLabel, expandLabel, subNav, mobileSubNav, toggleGroupLabel, embedded: embeddedProp, className, ...rest }: AppShellProps): react.JSX.Element;
92
104
 
93
105
  export { AppShell, type AppShellNavItem, type AppShellProps, type AppShellSubItem };