shelving 1.282.2 → 1.284.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 (240) hide show
  1. package/package.json +2 -2
  2. package/ui/README.md +2 -1
  3. package/ui/block/Address.d.ts +7 -7
  4. package/ui/block/Address.js +6 -6
  5. package/ui/block/Address.tsx +10 -9
  6. package/ui/block/Block.d.ts +3 -3
  7. package/ui/block/Block.js +3 -2
  8. package/ui/block/Block.tsx +5 -4
  9. package/ui/block/Blockquote.d.ts +3 -3
  10. package/ui/block/Blockquote.js +2 -2
  11. package/ui/block/Blockquote.tsx +4 -3
  12. package/ui/block/Caption.d.ts +3 -3
  13. package/ui/block/Caption.js +3 -3
  14. package/ui/block/Caption.tsx +5 -4
  15. package/ui/block/Card.d.ts +3 -2
  16. package/ui/block/Card.js +2 -2
  17. package/ui/block/Card.tsx +4 -2
  18. package/ui/block/Column.d.ts +3 -3
  19. package/ui/block/Column.js +2 -2
  20. package/ui/block/Column.tsx +4 -3
  21. package/ui/block/Definitions.d.ts +3 -3
  22. package/ui/block/Definitions.js +2 -2
  23. package/ui/block/Definitions.tsx +4 -3
  24. package/ui/block/Details.d.ts +3 -2
  25. package/ui/block/Details.js +2 -2
  26. package/ui/block/Details.tsx +4 -3
  27. package/ui/block/Divider.d.ts +3 -2
  28. package/ui/block/Divider.js +2 -2
  29. package/ui/block/Divider.tsx +4 -2
  30. package/ui/block/Heading.d.ts +3 -3
  31. package/ui/block/Heading.js +2 -2
  32. package/ui/block/Heading.tsx +4 -3
  33. package/ui/block/Image.d.ts +3 -2
  34. package/ui/block/Image.js +2 -2
  35. package/ui/block/Image.tsx +4 -2
  36. package/ui/block/Label.d.ts +1 -1
  37. package/ui/block/Label.js +2 -2
  38. package/ui/block/Label.tsx +2 -1
  39. package/ui/block/List.d.ts +3 -2
  40. package/ui/block/List.js +4 -4
  41. package/ui/block/List.tsx +6 -4
  42. package/ui/block/Panel.d.ts +3 -3
  43. package/ui/block/Panel.js +2 -2
  44. package/ui/block/Panel.tsx +4 -3
  45. package/ui/block/Paragraph.d.ts +3 -3
  46. package/ui/block/Paragraph.js +2 -2
  47. package/ui/block/Paragraph.tsx +4 -3
  48. package/ui/block/Preformatted.d.ts +3 -3
  49. package/ui/block/Preformatted.js +2 -2
  50. package/ui/block/Preformatted.tsx +4 -3
  51. package/ui/block/Row.d.ts +3 -3
  52. package/ui/block/Row.js +2 -2
  53. package/ui/block/Row.tsx +4 -3
  54. package/ui/block/Section.js +1 -1
  55. package/ui/block/Section.tsx +1 -0
  56. package/ui/block/Subheading.d.ts +1 -1
  57. package/ui/block/Subheading.js +2 -2
  58. package/ui/block/Subheading.tsx +2 -1
  59. package/ui/block/Title.d.ts +1 -1
  60. package/ui/block/Title.js +2 -2
  61. package/ui/block/Title.tsx +2 -1
  62. package/ui/block/Video.d.ts +3 -3
  63. package/ui/block/Video.js +2 -2
  64. package/ui/block/Video.tsx +4 -3
  65. package/ui/button/Button.d.ts +3 -2
  66. package/ui/button/Button.js +2 -2
  67. package/ui/button/Button.md +28 -0
  68. package/ui/button/Button.module.css +23 -5
  69. package/ui/button/Button.tsx +4 -3
  70. package/ui/button/Clickable.d.ts +3 -3
  71. package/ui/button/Clickable.js +6 -4
  72. package/ui/button/Clickable.tsx +9 -9
  73. package/ui/button/FullscreenButton.d.ts +3 -5
  74. package/ui/button/FullscreenButton.tsx +3 -2
  75. package/ui/button/RetryButton.d.ts +2 -2
  76. package/ui/button/RetryButton.tsx +2 -2
  77. package/ui/button/SubmitButton.d.ts +3 -3
  78. package/ui/button/SubmitButton.js +3 -2
  79. package/ui/button/SubmitButton.tsx +5 -3
  80. package/ui/dialog/Dialog.d.ts +6 -4
  81. package/ui/dialog/Dialog.js +7 -4
  82. package/ui/dialog/Dialog.tsx +19 -7
  83. package/ui/dialog/Modal.d.ts +3 -3
  84. package/ui/dialog/Modal.js +3 -3
  85. package/ui/dialog/Modal.tsx +5 -5
  86. package/ui/docs/DocumentationButtons.d.ts +3 -2
  87. package/ui/docs/DocumentationButtons.js +2 -2
  88. package/ui/docs/DocumentationButtons.tsx +4 -1
  89. package/ui/form/Field.d.ts +3 -3
  90. package/ui/form/Field.js +2 -2
  91. package/ui/form/Field.tsx +4 -3
  92. package/ui/form/Form.d.ts +2 -2
  93. package/ui/form/Form.js +3 -2
  94. package/ui/form/Form.tsx +5 -3
  95. package/ui/form/FormFields.d.ts +1 -1
  96. package/ui/form/FormFields.js +2 -2
  97. package/ui/form/FormFields.tsx +2 -2
  98. package/ui/form/FormFooter.d.ts +3 -3
  99. package/ui/form/FormFooter.js +2 -2
  100. package/ui/form/FormFooter.tsx +4 -4
  101. package/ui/form/FormMessage.d.ts +2 -2
  102. package/ui/form/FormMessage.tsx +2 -3
  103. package/ui/form/Progress.d.ts +3 -2
  104. package/ui/form/Progress.js +2 -2
  105. package/ui/form/Progress.tsx +4 -2
  106. package/ui/inline/Code.d.ts +3 -3
  107. package/ui/inline/Code.js +2 -2
  108. package/ui/inline/Code.tsx +4 -3
  109. package/ui/inline/Deleted.d.ts +3 -3
  110. package/ui/inline/Deleted.js +2 -2
  111. package/ui/inline/Deleted.tsx +4 -3
  112. package/ui/inline/Emphasis.d.ts +3 -3
  113. package/ui/inline/Emphasis.js +2 -2
  114. package/ui/inline/Emphasis.tsx +4 -3
  115. package/ui/inline/Inserted.d.ts +3 -3
  116. package/ui/inline/Inserted.js +2 -2
  117. package/ui/inline/Inserted.tsx +4 -3
  118. package/ui/inline/Link.d.ts +3 -2
  119. package/ui/inline/Link.js +2 -2
  120. package/ui/inline/Link.tsx +4 -2
  121. package/ui/inline/Mark.d.ts +3 -3
  122. package/ui/inline/Mark.js +2 -2
  123. package/ui/inline/Mark.tsx +4 -3
  124. package/ui/inline/Small.d.ts +3 -3
  125. package/ui/inline/Small.js +2 -2
  126. package/ui/inline/Small.tsx +4 -3
  127. package/ui/inline/Span.d.ts +3 -3
  128. package/ui/inline/Span.js +3 -2
  129. package/ui/inline/Span.tsx +5 -4
  130. package/ui/inline/Strong.d.ts +3 -3
  131. package/ui/inline/Strong.js +2 -2
  132. package/ui/inline/Strong.tsx +4 -3
  133. package/ui/inline/Subscript.d.ts +3 -3
  134. package/ui/inline/Subscript.js +3 -3
  135. package/ui/inline/Subscript.tsx +5 -5
  136. package/ui/inline/Superscript.d.ts +3 -3
  137. package/ui/inline/Superscript.js +3 -3
  138. package/ui/inline/Superscript.tsx +5 -5
  139. package/ui/input/ArrayInput.js +2 -2
  140. package/ui/input/ArrayInput.tsx +2 -1
  141. package/ui/input/ArrayRadioInputs.d.ts +1 -1
  142. package/ui/input/ArrayRadioInputs.js +2 -2
  143. package/ui/input/ArrayRadioInputs.tsx +2 -1
  144. package/ui/input/ButtonInput.d.ts +1 -1
  145. package/ui/input/ButtonInput.js +2 -2
  146. package/ui/input/ButtonInput.tsx +2 -1
  147. package/ui/input/CheckboxInput.d.ts +1 -1
  148. package/ui/input/CheckboxInput.js +2 -2
  149. package/ui/input/CheckboxInput.tsx +2 -0
  150. package/ui/input/ChoiceRadioInputs.js +2 -2
  151. package/ui/input/ChoiceRadioInputs.tsx +2 -1
  152. package/ui/input/DataInput.js +2 -2
  153. package/ui/input/DataInput.tsx +2 -1
  154. package/ui/input/DateInput.d.ts +1 -1
  155. package/ui/input/DateInput.js +2 -2
  156. package/ui/input/DateInput.tsx +2 -1
  157. package/ui/input/DictionaryInput.js +2 -2
  158. package/ui/input/DictionaryInput.tsx +2 -1
  159. package/ui/input/FileInput.d.ts +1 -1
  160. package/ui/input/FileInput.js +3 -2
  161. package/ui/input/FileInput.tsx +3 -1
  162. package/ui/input/Input.d.ts +10 -3
  163. package/ui/input/Input.js +2 -2
  164. package/ui/input/Input.tsx +11 -4
  165. package/ui/input/NumberInput.d.ts +1 -1
  166. package/ui/input/NumberInput.js +2 -2
  167. package/ui/input/NumberInput.tsx +2 -1
  168. package/ui/input/OutputInput.d.ts +1 -1
  169. package/ui/input/OutputInput.js +2 -2
  170. package/ui/input/OutputInput.tsx +2 -2
  171. package/ui/input/Popover.d.ts +4 -2
  172. package/ui/input/Popover.js +4 -3
  173. package/ui/input/Popover.tsx +6 -3
  174. package/ui/input/PopoverButton.d.ts +28 -0
  175. package/ui/input/{ButtonPopover.js → PopoverButton.js} +10 -8
  176. package/ui/input/PopoverButton.tsx +44 -0
  177. package/ui/input/PopoverButtonInput.d.ts +27 -0
  178. package/ui/input/{ButtonInputPopover.js → PopoverButtonInput.js} +10 -8
  179. package/ui/input/PopoverButtonInput.tsx +44 -0
  180. package/ui/input/QueryInput.d.ts +1 -1
  181. package/ui/input/QueryInput.js +2 -2
  182. package/ui/input/QueryInput.tsx +2 -0
  183. package/ui/input/RadioInput.d.ts +1 -1
  184. package/ui/input/RadioInput.js +2 -2
  185. package/ui/input/RadioInput.tsx +2 -0
  186. package/ui/input/SchemaInput.d.ts +1 -1
  187. package/ui/input/SchemaInput.js +2 -2
  188. package/ui/input/SchemaInput.tsx +2 -2
  189. package/ui/input/SelectInput.js +2 -2
  190. package/ui/input/SelectInput.tsx +2 -1
  191. package/ui/input/TextInput.d.ts +1 -1
  192. package/ui/input/TextInput.js +3 -3
  193. package/ui/input/TextInput.tsx +3 -2
  194. package/ui/input/index.d.ts +2 -2
  195. package/ui/input/index.js +2 -2
  196. package/ui/input/index.ts +2 -2
  197. package/ui/menu/Menu.d.ts +5 -5
  198. package/ui/menu/Menu.js +4 -4
  199. package/ui/menu/Menu.tsx +7 -6
  200. package/ui/misc/Icon.d.ts +2 -1
  201. package/ui/misc/Icon.js +2 -1
  202. package/ui/misc/Icon.tsx +3 -1
  203. package/ui/misc/Loading.d.ts +2 -3
  204. package/ui/misc/Loading.js +2 -2
  205. package/ui/misc/Loading.tsx +10 -3
  206. package/ui/misc/Tag.d.ts +3 -2
  207. package/ui/misc/Tag.js +2 -2
  208. package/ui/misc/Tag.tsx +4 -3
  209. package/ui/notice/Message.d.ts +3 -3
  210. package/ui/notice/Message.js +2 -2
  211. package/ui/notice/Message.tsx +4 -3
  212. package/ui/notice/Notice.d.ts +3 -3
  213. package/ui/notice/Notice.js +2 -2
  214. package/ui/notice/Notice.tsx +4 -2
  215. package/ui/notice/Notices.d.ts +3 -2
  216. package/ui/notice/Notices.js +2 -2
  217. package/ui/notice/Notices.tsx +4 -3
  218. package/ui/table/Cell.d.ts +3 -3
  219. package/ui/table/Cell.js +2 -2
  220. package/ui/table/Cell.tsx +4 -3
  221. package/ui/table/Table.d.ts +3 -3
  222. package/ui/table/Table.js +2 -2
  223. package/ui/table/Table.tsx +4 -3
  224. package/ui/tree/TreeBreadcrumbs.d.ts +3 -2
  225. package/ui/tree/TreeBreadcrumbs.js +2 -2
  226. package/ui/tree/TreeBreadcrumbs.tsx +4 -2
  227. package/ui/tree/TreeButton.d.ts +2 -1
  228. package/ui/tree/TreeButton.tsx +2 -1
  229. package/ui/util/ClassProps.md +56 -0
  230. package/ui/util/css.d.ts +1 -1
  231. package/ui/util/css.js +2 -2
  232. package/ui/util/css.test.ts +29 -0
  233. package/ui/util/css.ts +2 -2
  234. package/ui/util/props.d.ts +12 -0
  235. package/ui/util/props.test.tsx +100 -0
  236. package/ui/util/props.ts +13 -0
  237. package/ui/input/ButtonInputPopover.d.ts +0 -25
  238. package/ui/input/ButtonInputPopover.tsx +0 -41
  239. package/ui/input/ButtonPopover.d.ts +0 -25
  240. package/ui/input/ButtonPopover.tsx +0 -40
@@ -5,9 +5,10 @@ import type { ReactElement, ReactNode } from "react";
5
5
 
6
6
  import { isTruthy } from "../../util/boolean.js";
7
7
  import type { Callback } from "../../util/function.js";
8
- import { getModuleClass } from "../util/css.js";
8
+ import { getClass, getModuleClass } from "../util/css.js";
9
9
  import { eventPreventDefault } from "../util/event.js";
10
10
  import { eventLoopFocus } from "../util/focus.js";
11
+ import type { ClassProps } from "../util/props.js";
11
12
  import styles from "./Popover.module.css";
12
13
 
13
14
  /**
@@ -32,7 +33,7 @@ export type PopoverChildren = readonly [
32
33
  *
33
34
  * @see https://shelving.cc/ui/PopoverProps
34
35
  */
35
- export interface PopoverProps {
36
+ export interface PopoverProps extends ClassProps {
36
37
  /** Children for the popover. */
37
38
  children: PopoverChildren;
38
39
  /** Callback that gets called when the Popover closes. */
@@ -45,6 +46,7 @@ export interface PopoverProps {
45
46
  * Trigger element that reveals a floating popover panel beside it when active or focused.
46
47
  * - The first `children` node is the trigger; the rest become the popover contents.
47
48
  * - Closes on blur away or `Escape`, calling `onClose`.
49
+ * - `className` lands on the floating panel rather than the wrapper, since the panel is the surface worth styling.
48
50
  *
49
51
  * @returns A wrapper element containing the trigger and (when open) the popover panel.
50
52
  * @example <Popover>{trigger}{panelContent}</Popover>
@@ -56,6 +58,7 @@ export function Popover({
56
58
  children: [trigger, ...popover], //
57
59
  onClose,
58
60
  open = popover.flat().some(isTruthy),
61
+ className,
59
62
  }: PopoverProps): ReactElement {
60
63
  return (
61
64
  <div
@@ -68,7 +71,7 @@ export function Popover({
68
71
  >
69
72
  {trigger}
70
73
  {open ? (
71
- <section className={getModuleClass(styles, "panel")} onMouseDown={eventPreventDefault}>
74
+ <section className={getClass(getModuleClass(styles, "panel"), className)} onMouseDown={eventPreventDefault}>
72
75
  {popover}
73
76
  </section>
74
77
  ) : null}
@@ -0,0 +1,28 @@
1
+ /** biome-ignore-all lint/a11y/noStaticElementInteractions: This is fine we're only using this to listen for clicks on child buttons. */
2
+ /** biome-ignore-all lint/a11y/useKeyWithClickEvents: This is fine we're only using this to listen for clicks on child buttons. */
3
+ import { type ReactElement } from "react";
4
+ import { type ButtonVariants } from "../button/Button.js";
5
+ import type { ClassProps } from "../util/props.js";
6
+ import { type PopoverChildren } from "./Popover.js";
7
+ /**
8
+ * Props for `PopoverButton` — all the `Button` styling variants, plus `children` whose first node is the button label and the rest the popover contents.
9
+ *
10
+ * @see https://shelving.cc/ui/PopoverButtonProps
11
+ */
12
+ export interface PopoverButtonProps extends ButtonVariants, ClassProps {
13
+ children: PopoverChildren;
14
+ }
15
+ /**
16
+ * Popover variant of `<Button>` — a button that reveals floating popover content beside it when clicked.
17
+ * - The button is the primary element: it takes all the `ButtonVariants` styling props, and `className` lands on it (not the popover panel).
18
+ * - The first element passed to `children` is the button label; the rest become the popover contents.
19
+ *
20
+ * DH: Would love to use new HTML `popover="auto"` functionality for this but the anchor positioning it needs is not supported everywhere yet.
21
+ *
22
+ * @returns A `Button` that toggles its attached `Popover` open and closed.
23
+ * @example <PopoverButton>{label}{panel}</PopoverButton>
24
+ * @kind component
25
+ * @see https://shelving.cc/ui/PopoverButton
26
+ */
27
+ export declare function PopoverButton({ children: [buttonChildren, ...popoverChildren], //
28
+ className, ...props }: PopoverButtonProps): ReactElement;
@@ -5,17 +5,19 @@ import { useState } from "react";
5
5
  import { Button } from "../button/Button.js";
6
6
  import { Popover } from "./Popover.js";
7
7
  /**
8
- * A button that, when clicked, shows a popover next to it when clicked or focused.
9
- * - The first element passed to `children` is used as the content for the button, the rest is the content of the popover.
8
+ * Popover variant of `<Button>` — a button that reveals floating popover content beside it when clicked.
9
+ * - The button is the primary element: it takes all the `ButtonVariants` styling props, and `className` lands on it (not the popover panel).
10
+ * - The first element passed to `children` is the button label; the rest become the popover contents.
10
11
  *
11
12
  * DH: Would love to use new HTML `popover="auto"` functionality for this but the anchor positioning it needs is not supported everywhere yet.
12
13
  *
13
- * @returns A `Popover` wrapping a `Button` that toggles it open and closed.
14
- * @example <ButtonPopover>{label}{panel}</ButtonPopover>
15
- * @see https://shelving.cc/ui/ButtonPopover
14
+ * @returns A `Button` that toggles its attached `Popover` open and closed.
15
+ * @example <PopoverButton>{label}{panel}</PopoverButton>
16
+ * @kind component
17
+ * @see https://shelving.cc/ui/PopoverButton
16
18
  */
17
- export function ButtonPopover({ children: [buttonChildren, ...popoverChildren], //
18
- ...props }) {
19
+ export function PopoverButton({ children: [buttonChildren, ...popoverChildren], //
20
+ className, ...props }) {
19
21
  const [open, setOpen] = useState(false);
20
- return (_jsxs(Popover, { open: open, onClose: () => setOpen(false), children: [_jsx(Button, { onClick: () => setOpen(!open), ...props, children: buttonChildren }), popoverChildren] }));
22
+ return (_jsxs(Popover, { open: open, onClose: () => setOpen(false), children: [_jsx(Button, { onClick: () => setOpen(!open), ...props, className: className, children: buttonChildren }), popoverChildren] }));
21
23
  }
@@ -0,0 +1,44 @@
1
+ /** biome-ignore-all lint/a11y/noStaticElementInteractions: This is fine we're only using this to listen for clicks on child buttons. */
2
+ /** biome-ignore-all lint/a11y/useKeyWithClickEvents: This is fine we're only using this to listen for clicks on child buttons. */
3
+
4
+ import { type ReactElement, useState } from "react";
5
+ import { Button, type ButtonVariants } from "../button/Button.js";
6
+ import type { ClassProps } from "../util/props.js";
7
+ import { Popover, type PopoverChildren } from "./Popover.js";
8
+
9
+ /**
10
+ * Props for `PopoverButton` — all the `Button` styling variants, plus `children` whose first node is the button label and the rest the popover contents.
11
+ *
12
+ * @see https://shelving.cc/ui/PopoverButtonProps
13
+ */
14
+ export interface PopoverButtonProps extends ButtonVariants, ClassProps {
15
+ children: PopoverChildren;
16
+ }
17
+
18
+ /**
19
+ * Popover variant of `<Button>` — a button that reveals floating popover content beside it when clicked.
20
+ * - The button is the primary element: it takes all the `ButtonVariants` styling props, and `className` lands on it (not the popover panel).
21
+ * - The first element passed to `children` is the button label; the rest become the popover contents.
22
+ *
23
+ * DH: Would love to use new HTML `popover="auto"` functionality for this but the anchor positioning it needs is not supported everywhere yet.
24
+ *
25
+ * @returns A `Button` that toggles its attached `Popover` open and closed.
26
+ * @example <PopoverButton>{label}{panel}</PopoverButton>
27
+ * @kind component
28
+ * @see https://shelving.cc/ui/PopoverButton
29
+ */
30
+ export function PopoverButton({
31
+ children: [buttonChildren, ...popoverChildren], //
32
+ className,
33
+ ...props
34
+ }: PopoverButtonProps): ReactElement {
35
+ const [open, setOpen] = useState(false);
36
+ return (
37
+ <Popover open={open} onClose={() => setOpen(false)}>
38
+ <Button onClick={() => setOpen(!open)} {...props} className={className}>
39
+ {buttonChildren}
40
+ </Button>
41
+ {popoverChildren}
42
+ </Popover>
43
+ );
44
+ }
@@ -0,0 +1,27 @@
1
+ /** biome-ignore-all lint/a11y/noStaticElementInteractions: This is fine we're only using this to listen for clicks on child buttons. */
2
+ /** biome-ignore-all lint/a11y/useKeyWithClickEvents: This is fine we're only using this to listen for clicks on child buttons. */
3
+ import { type ReactElement } from "react";
4
+ import type { InputProps } from "./Input.js";
5
+ import { type PopoverChildren } from "./Popover.js";
6
+ /**
7
+ * Props for `PopoverButtonInput` — the shared `InputProps`, plus `children` whose first node is the button label and the rest the popover contents.
8
+ *
9
+ * @see https://shelving.cc/ui/PopoverButtonInputProps
10
+ */
11
+ export interface PopoverButtonInputProps extends InputProps {
12
+ children: PopoverChildren;
13
+ }
14
+ /**
15
+ * Popover variant of `<ButtonInput>` — an input-styled button that reveals floating popover content beside it when clicked.
16
+ * - The button is the primary element: it takes the shared `InputProps`, and `className` lands on it (not the popover panel).
17
+ * - The first element passed to `children` is the button label; the rest become the popover contents.
18
+ *
19
+ * DH: Would love to use new HTML `popover="auto"` functionality for this but the anchor positioning it needs is not supported everywhere yet.
20
+ *
21
+ * @returns A `ButtonInput` that toggles its attached `Popover` open and closed.
22
+ * @example <PopoverButtonInput name="filter">{label}{panel}</PopoverButtonInput>
23
+ * @kind component
24
+ * @see https://shelving.cc/ui/PopoverButtonInput
25
+ */
26
+ export declare function PopoverButtonInput({ children: [buttonChildren, ...popoverChildren], //
27
+ className, ...props }: PopoverButtonInputProps): ReactElement;
@@ -5,17 +5,19 @@ import { useState } from "react";
5
5
  import { ButtonInput } from "./ButtonInput.js";
6
6
  import { Popover } from "./Popover.js";
7
7
  /**
8
- * An input button that, when clicked, shows a popover next to it when clicked or focused.
9
- * - The first element passed to `children` is used as the content for the button, the rest is the content of the popover.
8
+ * Popover variant of `<ButtonInput>` — an input-styled button that reveals floating popover content beside it when clicked.
9
+ * - The button is the primary element: it takes the shared `InputProps`, and `className` lands on it (not the popover panel).
10
+ * - The first element passed to `children` is the button label; the rest become the popover contents.
10
11
  *
11
12
  * DH: Would love to use new HTML `popover="auto"` functionality for this but the anchor positioning it needs is not supported everywhere yet.
12
13
  *
13
- * @returns A `Popover` wrapping a `ButtonInput` that toggles it open and closed.
14
- * @example <ButtonInputPopover name="filter">{label}{panel}</ButtonInputPopover>
15
- * @see https://shelving.cc/ui/ButtonInputPopover
14
+ * @returns A `ButtonInput` that toggles its attached `Popover` open and closed.
15
+ * @example <PopoverButtonInput name="filter">{label}{panel}</PopoverButtonInput>
16
+ * @kind component
17
+ * @see https://shelving.cc/ui/PopoverButtonInput
16
18
  */
17
- export function ButtonInputPopover({ children: [buttonChildren, ...popoverChildren], //
18
- ...props }) {
19
+ export function PopoverButtonInput({ children: [buttonChildren, ...popoverChildren], //
20
+ className, ...props }) {
19
21
  const [open, setOpen] = useState(false);
20
- return (_jsxs(Popover, { open: open, onClose: () => setOpen(false), children: [_jsx(ButtonInput, { onClick: () => setOpen(!open), ...props, children: buttonChildren }), popoverChildren] }));
22
+ return (_jsxs(Popover, { open: open, onClose: () => setOpen(false), children: [_jsx(ButtonInput, { onClick: () => setOpen(!open), ...props, className: className, children: buttonChildren }), popoverChildren] }));
21
23
  }
@@ -0,0 +1,44 @@
1
+ /** biome-ignore-all lint/a11y/noStaticElementInteractions: This is fine we're only using this to listen for clicks on child buttons. */
2
+ /** biome-ignore-all lint/a11y/useKeyWithClickEvents: This is fine we're only using this to listen for clicks on child buttons. */
3
+
4
+ import { type ReactElement, useState } from "react";
5
+ import { ButtonInput } from "./ButtonInput.js";
6
+ import type { InputProps } from "./Input.js";
7
+ import { Popover, type PopoverChildren } from "./Popover.js";
8
+
9
+ /**
10
+ * Props for `PopoverButtonInput` — the shared `InputProps`, plus `children` whose first node is the button label and the rest the popover contents.
11
+ *
12
+ * @see https://shelving.cc/ui/PopoverButtonInputProps
13
+ */
14
+ export interface PopoverButtonInputProps extends InputProps {
15
+ children: PopoverChildren;
16
+ }
17
+
18
+ /**
19
+ * Popover variant of `<ButtonInput>` — an input-styled button that reveals floating popover content beside it when clicked.
20
+ * - The button is the primary element: it takes the shared `InputProps`, and `className` lands on it (not the popover panel).
21
+ * - The first element passed to `children` is the button label; the rest become the popover contents.
22
+ *
23
+ * DH: Would love to use new HTML `popover="auto"` functionality for this but the anchor positioning it needs is not supported everywhere yet.
24
+ *
25
+ * @returns A `ButtonInput` that toggles its attached `Popover` open and closed.
26
+ * @example <PopoverButtonInput name="filter">{label}{panel}</PopoverButtonInput>
27
+ * @kind component
28
+ * @see https://shelving.cc/ui/PopoverButtonInput
29
+ */
30
+ export function PopoverButtonInput({
31
+ children: [buttonChildren, ...popoverChildren], //
32
+ className,
33
+ ...props
34
+ }: PopoverButtonInputProps): ReactElement {
35
+ const [open, setOpen] = useState(false);
36
+ return (
37
+ <Popover open={open} onClose={() => setOpen(false)}>
38
+ <ButtonInput onClick={() => setOpen(!open)} {...props} className={className}>
39
+ {buttonChildren}
40
+ </ButtonInput>
41
+ {popoverChildren}
42
+ </Popover>
43
+ );
44
+ }
@@ -33,4 +33,4 @@ export interface QueryInputProps<I, O> extends ValueInputProps<O> {
33
33
  * @example <QueryInput schema={SEARCH} onQuery={search} value={user} onValue={setUser} />
34
34
  * @see https://shelving.cc/ui/QueryInput
35
35
  */
36
- export declare function QueryInput<I, O>({ empty, value, onValue, onQuery, formatter, ...props }: QueryInputProps<I, O>): ReactElement;
36
+ export declare function QueryInput<I, O>({ empty, value, onValue, onQuery, formatter, className, ...props }: QueryInputProps<I, O>): ReactElement;
@@ -19,7 +19,7 @@ import { SchemaInput } from "./SchemaInput.js";
19
19
  * @example <QueryInput schema={SEARCH} onQuery={search} value={user} onValue={setUser} />
20
20
  * @see https://shelving.cc/ui/QueryInput
21
21
  */
22
- export function QueryInput({ empty = "No results", value, onValue, onQuery, formatter = formatValue, ...props }) {
22
+ export function QueryInput({ empty = "No results", value, onValue, onQuery, formatter = formatValue, className, ...props }) {
23
23
  const store = useStore(useInstance(PayloadFetchStore, NONE, undefined, onQuery, SECOND));
24
24
  const busy = useStore(store.busy).value;
25
25
  // Is popover open?
@@ -28,7 +28,7 @@ export function QueryInput({ empty = "No results", value, onValue, onQuery, form
28
28
  store.payload.value = NONE;
29
29
  store.value = undefined;
30
30
  };
31
- return (_jsxs(Popover, { open: isOpen, onClose: close, children: [_jsx(SchemaInput, { ...props, value: value, onValue: v => {
31
+ return (_jsxs(Popover, { open: isOpen, onClose: close, children: [_jsx(SchemaInput, { ...props, className: className, value: value, onValue: v => {
32
32
  if (v)
33
33
  store.payload.value = v;
34
34
  else
@@ -53,6 +53,7 @@ export function QueryInput<I, O>({
53
53
  onValue,
54
54
  onQuery,
55
55
  formatter = formatValue,
56
+ className,
56
57
  ...props
57
58
  }: QueryInputProps<I, O>): ReactElement {
58
59
  const store = useStore(useInstance(PayloadFetchStore, NONE, undefined, onQuery, SECOND));
@@ -70,6 +71,7 @@ export function QueryInput<I, O>({
70
71
  <Popover open={isOpen} onClose={close}>
71
72
  <SchemaInput
72
73
  {...props}
74
+ className={className}
73
75
  value={value as I}
74
76
  onValue={v => {
75
77
  if (v) store.payload.value = v;
@@ -17,4 +17,4 @@ export interface RadioInputProps extends ValueInputProps<boolean>, OptionalChild
17
17
  * @example <RadioInput name="plan" value={isPro} onValue={selectPro}>Pro</RadioInput>
18
18
  * @see https://shelving.cc/ui/RadioInput
19
19
  */
20
- export declare function RadioInput({ name, title, placeholder, required, disabled, message, value, onValue, children, ...props }: RadioInputProps): ReactElement;
20
+ export declare function RadioInput({ name, title, placeholder, required, disabled, message, value, onValue, children, className, ...props }: RadioInputProps): ReactElement;
@@ -12,7 +12,7 @@ const INPUT_LABEL_CLASS = getModuleClass(INPUT_CSS, "label");
12
12
  * @example <RadioInput name="plan" value={isPro} onValue={selectPro}>Pro</RadioInput>
13
13
  * @see https://shelving.cc/ui/RadioInput
14
14
  */
15
- export function RadioInput({ name, title, placeholder = title || "Choose", required = false, disabled = false, message = "", value = false, onValue, children = placeholder, ...props }) {
15
+ export function RadioInput({ name, title, placeholder = title || "Choose", required = false, disabled = false, message = "", value = false, onValue, children = placeholder, className, ...props }) {
16
16
  return (_jsxs("label", { className: getClass(getInputClass(props), //
17
- getFlexClass(props), INPUT_LABEL_CLASS), children: [_jsx("input", { className: getModuleClass(INPUT_CSS, "radio"), type: "radio", name: name, defaultChecked: !!value, onChange: () => onValue(true), disabled: disabled, required: required, title: message, "aria-invalid": !!message }), _jsx("span", { children: children })] }));
17
+ getFlexClass(props), INPUT_LABEL_CLASS, className), children: [_jsx("input", { className: getModuleClass(INPUT_CSS, "radio"), type: "radio", name: name, defaultChecked: !!value, onChange: () => onValue(true), disabled: disabled, required: required, title: message, "aria-invalid": !!message }), _jsx("span", { children: children })] }));
18
18
  }
@@ -32,6 +32,7 @@ export function RadioInput({
32
32
  value = false,
33
33
  onValue,
34
34
  children = placeholder,
35
+ className,
35
36
  ...props
36
37
  }: RadioInputProps): ReactElement {
37
38
  return (
@@ -40,6 +41,7 @@ export function RadioInput({
40
41
  getInputClass(props), //
41
42
  getFlexClass(props),
42
43
  INPUT_LABEL_CLASS,
44
+ className,
43
45
  )}
44
46
  >
45
47
  <input
@@ -185,4 +185,4 @@ export interface SchemaFieldProps extends SchemaInputProps<Schema, unknown>, Opt
185
185
  * @kind component
186
186
  * @see https://shelving.cc/ui/SchemaField
187
187
  */
188
- export declare function SchemaField({ schema, children, ...props }: SchemaFieldProps): ReactElement;
188
+ export declare function SchemaField({ schema, children, className, ...props }: SchemaFieldProps): ReactElement;
@@ -173,6 +173,6 @@ export function DataSchemaInput({ schema, value, ...props }) {
173
173
  * @kind component
174
174
  * @see https://shelving.cc/ui/SchemaField
175
175
  */
176
- export function SchemaField({ schema, children, ...props }) {
177
- return (_jsx(Field, { ...props, ...schema, children: children ?? _jsx(SchemaInput, { schema: schema, ...props }) }));
176
+ export function SchemaField({ schema, children, className, ...props }) {
177
+ return (_jsx(Field, { ...props, ...schema, className: className, children: children ?? _jsx(SchemaInput, { schema: schema, ...props }) }));
178
178
  }
@@ -274,9 +274,9 @@ export interface SchemaFieldProps extends SchemaInputProps<Schema, unknown>, Opt
274
274
  * @kind component
275
275
  * @see https://shelving.cc/ui/SchemaField
276
276
  */
277
- export function SchemaField({ schema, children, ...props }: SchemaFieldProps): ReactElement {
277
+ export function SchemaField({ schema, children, className, ...props }: SchemaFieldProps): ReactElement {
278
278
  return (
279
- <Field {...props} {...schema}>
279
+ <Field {...props} {...schema} className={className}>
280
280
  {children ?? <SchemaInput schema={schema} {...props} />}
281
281
  </Field>
282
282
  );
@@ -3,6 +3,6 @@ import { getProps } from "../../util/object.js";
3
3
  import { getClass, getModuleClass } from "../util/css.js";
4
4
  import { getInputClass } from "./Input.js";
5
5
  import INPUT_CSS from "./Input.module.css";
6
- export function SelectInput({ name, placeholder = "Choose...", required = false, disabled = false, message = "", value = "", onValue, options, ...variants }) {
7
- return (_jsxs("select", { name: name, defaultValue: value, onChange: e => onValue(e.currentTarget.value || undefined), className: getClass(getInputClass(variants), getModuleClass(INPUT_CSS, "select")), title: message, "aria-invalid": !!message, disabled: disabled, required: required, children: [!required || !value ? (_jsx("option", { value: "", className: getModuleClass(INPUT_CSS, "empty"), children: placeholder })) : null, getProps(options).map(([k, t]) => (_jsx("option", { value: k, className: getModuleClass(INPUT_CSS, "value"), children: t }, k)))] }));
6
+ export function SelectInput({ name, placeholder = "Choose...", required = false, disabled = false, message = "", value = "", onValue, options, className, ...variants }) {
7
+ return (_jsxs("select", { name: name, defaultValue: value, onChange: e => onValue(e.currentTarget.value || undefined), className: getClass(getInputClass(variants), getModuleClass(INPUT_CSS, "select"), className), title: message, "aria-invalid": !!message, disabled: disabled, required: required, children: [!required || !value ? (_jsx("option", { value: "", className: getModuleClass(INPUT_CSS, "empty"), children: placeholder })) : null, getProps(options).map(([k, t]) => (_jsx("option", { value: k, className: getModuleClass(INPUT_CSS, "value"), children: t }, k)))] }));
8
8
  }
@@ -34,6 +34,7 @@ export function SelectInput({
34
34
  value = "",
35
35
  onValue,
36
36
  options,
37
+ className,
37
38
  ...variants
38
39
  }: SelectProps<string>): ReactElement {
39
40
  return (
@@ -41,7 +42,7 @@ export function SelectInput({
41
42
  name={name}
42
43
  defaultValue={value}
43
44
  onChange={e => onValue(e.currentTarget.value || undefined)}
44
- className={getClass(getInputClass(variants), getModuleClass(INPUT_CSS, "select"))}
45
+ className={getClass(getInputClass(variants), getModuleClass(INPUT_CSS, "select"), className)}
45
46
  title={message}
46
47
  aria-invalid={!!message}
47
48
  disabled={disabled}
@@ -26,5 +26,5 @@ export interface TextInputProps extends ValueInputProps<string>, InputVariants {
26
26
  * @see https://shelving.cc/ui/TextInput
27
27
  */
28
28
  export declare function TextInput({ name, title, placeholder, // Placeholder must be defined or `:placeholder-shown` CSS rules won't show.
29
- required, disabled, message, value, onValue, input, min, max, rows, formatter, ...variants }: TextInputProps): ReactElement;
29
+ required, disabled, message, value, onValue, input, min, max, rows, formatter, className, ...variants }: TextInputProps): ReactElement;
30
30
  export {};
@@ -13,7 +13,7 @@ import INPUT_CSS from "./Input.module.css";
13
13
  * @see https://shelving.cc/ui/TextInput
14
14
  */
15
15
  export function TextInput({ name, title = "", placeholder = title, // Placeholder must be defined or `:placeholder-shown` CSS rules won't show.
16
- required = false, disabled = false, message = "", value, onValue, input = "text", min = 0, max = Number.POSITIVE_INFINITY, rows = 1, formatter = PASSTHROUGH, ...variants }) {
16
+ required = false, disabled = false, message = "", value, onValue, input = "text", min = 0, max = Number.POSITIVE_INFINITY, rows = 1, formatter = PASSTHROUGH, className, ...variants }) {
17
17
  const onBlur = ({ currentTarget }) => {
18
18
  currentTarget.value = formatter(currentTarget.value);
19
19
  };
@@ -22,12 +22,12 @@ required = false, disabled = false, message = "", value, onValue, input = "text"
22
22
  onValue?.(formatter(e.currentTarget.value));
23
23
  _resize(e.currentTarget, rows);
24
24
  };
25
- return (_jsx("textarea", { ref: el => void (el && _resize(el, rows)), name: name, defaultValue: value !== undefined ? formatter(value) : "", minLength: Number.isFinite(min) ? min : 0, maxLength: Number.isFinite(max) ? max : undefined, rows: rows, required: required && min > 0, disabled: disabled, placeholder: placeholder || " ", className: getClass(getInputClass(variants), getModuleClass(INPUT_CSS, "text"), getModuleClass(INPUT_CSS, "multiline")), onInput: onChange, onChange: onChange, onBlur: onBlur, title: message, "aria-invalid": !!message }));
25
+ return (_jsx("textarea", { ref: el => void (el && _resize(el, rows)), name: name, defaultValue: value !== undefined ? formatter(value) : "", minLength: Number.isFinite(min) ? min : 0, maxLength: Number.isFinite(max) ? max : undefined, rows: rows, required: required && min > 0, disabled: disabled, placeholder: placeholder || " ", className: getClass(getInputClass(variants), getModuleClass(INPUT_CSS, "text"), getModuleClass(INPUT_CSS, "multiline"), className), onInput: onChange, onChange: onChange, onBlur: onBlur, title: message, "aria-invalid": !!message }));
26
26
  }
27
27
  const onChange = (e) => {
28
28
  onValue?.(formatter(e.currentTarget.value));
29
29
  };
30
- return (_jsx("input", { name: name, type: input, defaultValue: value !== undefined ? formatter(value) : "", minLength: Number.isFinite(min) ? min : 0, maxLength: Number.isFinite(max) ? max : undefined, required: required && min > 0, disabled: disabled, placeholder: placeholder || " ", className: getClass(getInputClass(variants), getModuleClass(INPUT_CSS, "text")), onInput: onChange, onChange: onChange, onBlur: onBlur, title: message, "aria-invalid": !!message }));
30
+ return (_jsx("input", { name: name, type: input, defaultValue: value !== undefined ? formatter(value) : "", minLength: Number.isFinite(min) ? min : 0, maxLength: Number.isFinite(max) ? max : undefined, required: required && min > 0, disabled: disabled, placeholder: placeholder || " ", className: getClass(getInputClass(variants), getModuleClass(INPUT_CSS, "text"), className), onInput: onChange, onChange: onChange, onBlur: onBlur, title: message, "aria-invalid": !!message }));
31
31
  }
32
32
  /**
33
33
  * @todo This can be removed when Firefox supports `field-sizing:` in CSS.
@@ -45,6 +45,7 @@ export function TextInput({
45
45
  max = Number.POSITIVE_INFINITY,
46
46
  rows = 1,
47
47
  formatter = PASSTHROUGH,
48
+ className,
48
49
  ...variants
49
50
  }: TextInputProps): ReactElement {
50
51
  const onBlur = ({ currentTarget }: SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>) => {
@@ -68,7 +69,7 @@ export function TextInput({
68
69
  required={required && min > 0}
69
70
  disabled={disabled}
70
71
  placeholder={placeholder || " "}
71
- className={getClass(getInputClass(variants), getModuleClass(INPUT_CSS, "text"), getModuleClass(INPUT_CSS, "multiline"))}
72
+ className={getClass(getInputClass(variants), getModuleClass(INPUT_CSS, "text"), getModuleClass(INPUT_CSS, "multiline"), className)}
72
73
  onInput={onChange}
73
74
  onChange={onChange}
74
75
  onBlur={onBlur}
@@ -92,7 +93,7 @@ export function TextInput({
92
93
  required={required && min > 0}
93
94
  disabled={disabled}
94
95
  placeholder={placeholder || " "}
95
- className={getClass(getInputClass(variants), getModuleClass(INPUT_CSS, "text"))}
96
+ className={getClass(getInputClass(variants), getModuleClass(INPUT_CSS, "text"), className)}
96
97
  onInput={onChange}
97
98
  onChange={onChange}
98
99
  onBlur={onBlur}
@@ -1,8 +1,6 @@
1
1
  export * from "./ArrayInput.js";
2
2
  export * from "./ArrayRadioInputs.js";
3
3
  export * from "./ButtonInput.js";
4
- export * from "./ButtonInputPopover.js";
5
- export * from "./ButtonPopover.js";
6
4
  export * from "./CheckboxInput.js";
7
5
  export * from "./ChoiceRadioInputs.js";
8
6
  export * from "./DataInput.js";
@@ -13,6 +11,8 @@ export * from "./Input.js";
13
11
  export * from "./NumberInput.js";
14
12
  export * from "./OutputInput.js";
15
13
  export * from "./Popover.js";
14
+ export * from "./PopoverButton.js";
15
+ export * from "./PopoverButtonInput.js";
16
16
  export * from "./QueryInput.js";
17
17
  export * from "./RadioInput.js";
18
18
  export * from "./SchemaInput.js";
package/ui/input/index.js CHANGED
@@ -1,8 +1,6 @@
1
1
  export * from "./ArrayInput.js";
2
2
  export * from "./ArrayRadioInputs.js";
3
3
  export * from "./ButtonInput.js";
4
- export * from "./ButtonInputPopover.js";
5
- export * from "./ButtonPopover.js";
6
4
  export * from "./CheckboxInput.js";
7
5
  export * from "./ChoiceRadioInputs.js";
8
6
  export * from "./DataInput.js";
@@ -13,6 +11,8 @@ export * from "./Input.js";
13
11
  export * from "./NumberInput.js";
14
12
  export * from "./OutputInput.js";
15
13
  export * from "./Popover.js";
14
+ export * from "./PopoverButton.js";
15
+ export * from "./PopoverButtonInput.js";
16
16
  export * from "./QueryInput.js";
17
17
  export * from "./RadioInput.js";
18
18
  export * from "./SchemaInput.js";
package/ui/input/index.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  export * from "./ArrayInput.js";
2
2
  export * from "./ArrayRadioInputs.js";
3
3
  export * from "./ButtonInput.js";
4
- export * from "./ButtonInputPopover.js";
5
- export * from "./ButtonPopover.js";
6
4
  export * from "./CheckboxInput.js";
7
5
  export * from "./ChoiceRadioInputs.js";
8
6
  export * from "./DataInput.js";
@@ -13,6 +11,8 @@ export * from "./Input.js";
13
11
  export * from "./NumberInput.js";
14
12
  export * from "./OutputInput.js";
15
13
  export * from "./Popover.js";
14
+ export * from "./PopoverButton.js";
15
+ export * from "./PopoverButtonInput.js";
16
16
  export * from "./QueryInput.js";
17
17
  export * from "./RadioInput.js";
18
18
  export * from "./SchemaInput.js";
package/ui/menu/Menu.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { type ClickableProps } from "../button/Clickable.js";
3
3
  import { type BlockVariants } from "../style/Block.js";
4
- import type { OptionalChildProps } from "../util/props.js";
4
+ import type { ClassProps, OptionalChildProps } from "../util/props.js";
5
5
  /**
6
6
  * Props for `<Menu>` — optional `<MenuItem>` `children`.
7
7
  *
8
8
  * @see https://shelving.cc/ui/MenuProps
9
9
  */
10
- export interface MenuProps extends BlockVariants, OptionalChildProps {
10
+ export interface MenuProps extends BlockVariants, OptionalChildProps, ClassProps {
11
11
  }
12
12
  /**
13
13
  * A `<menu>` list of `<MenuItem>` children.
@@ -17,13 +17,13 @@ export interface MenuProps extends BlockVariants, OptionalChildProps {
17
17
  * @kind component
18
18
  * @see https://shelving.cc/ui/Menu
19
19
  */
20
- export declare function Menu({ children, ...props }: MenuProps): ReactNode;
20
+ export declare function Menu({ children, className, ...props }: MenuProps): ReactNode;
21
21
  /**
22
22
  * Props for `<MenuItem>` — `<Clickable>` props plus `children` whose first node is the label and the rest the submenu.
23
23
  *
24
24
  * @see https://shelving.cc/ui/MenuItemProps
25
25
  */
26
- export interface MenuItemProps extends ClickableProps {
26
+ export interface MenuItemProps extends ClickableProps, ClassProps {
27
27
  /**
28
28
  * The first child becomes the link label (rendered inside the `<a>`).
29
29
  * - Any additional children form the submenu — only rendered when this item is "proud" (an ancestor of the current page). The caller is responsible for wrapping the submenu in a nested `<Menu>` if it wants the CSS `.menu .menu` descendant rules to apply.
@@ -38,4 +38,4 @@ export interface MenuItemProps extends ClickableProps {
38
38
  * @kind component
39
39
  * @see https://shelving.cc/ui/MenuItem
40
40
  */
41
- export declare function MenuItem({ href, children, ...props }: MenuItemProps): ReactNode;
41
+ export declare function MenuItem({ href, children, className, ...props }: MenuItemProps): ReactNode;
package/ui/menu/Menu.js CHANGED
@@ -19,9 +19,9 @@ const MENU_ACTIVE_CLASS = getModuleClass(MENU_CSS, "active");
19
19
  * @kind component
20
20
  * @see https://shelving.cc/ui/Menu
21
21
  */
22
- export function Menu({ children, ...props }) {
22
+ export function Menu({ children, className, ...props }) {
23
23
  return (_jsx("menu", { className: getClass(MENU_CLASS, //
24
- getBlockClass(props)), children: children }));
24
+ getBlockClass(props), className), children: children }));
25
25
  }
26
26
  /**
27
27
  * A `<li>` containing an `<a>` link, plus optional submenu content shown when this item is "proud".
@@ -31,12 +31,12 @@ export function Menu({ children, ...props }) {
31
31
  * @kind component
32
32
  * @see https://shelving.cc/ui/MenuItem
33
33
  */
34
- export function MenuItem({ href, children, ...props }) {
34
+ export function MenuItem({ href, children, className, ...props }) {
35
35
  const { url, root } = requireMeta();
36
36
  const link = getLink(href, url, root);
37
37
  const proud = isURLProud(url, link);
38
38
  const active = isURLActive(url, link);
39
39
  const [label, ...after] = isArray(children) ? children : [children];
40
- return (_jsxs("li", { className: MENU_ITEM_CLASS, children: [_jsx(Clickable, { href: href, ...props, className: getClass(MENU_LINK_CLASS, //
40
+ return (_jsxs("li", { className: getClass(MENU_ITEM_CLASS, className), children: [_jsx(Clickable, { href: href, ...props, className: getClass(MENU_LINK_CLASS, //
41
41
  proud && MENU_PROUD_CLASS, active && MENU_ACTIVE_CLASS), children: label }), proud && after] }));
42
42
  }
package/ui/menu/Menu.tsx CHANGED
@@ -5,7 +5,7 @@ import { Clickable, type ClickableProps } from "../button/Clickable.js";
5
5
  import { requireMeta } from "../misc/MetaContext.js";
6
6
  import { type BlockVariants, getBlockClass } from "../style/Block.js";
7
7
  import { getClass, getModuleClass } from "../util/css.js";
8
- import type { OptionalChildProps } from "../util/props.js";
8
+ import type { ClassProps, OptionalChildProps } from "../util/props.js";
9
9
  import MENU_CSS from "./Menu.module.css";
10
10
 
11
11
  const MENU_CLASS = getModuleClass(MENU_CSS, "menu");
@@ -19,7 +19,7 @@ const MENU_ACTIVE_CLASS = getModuleClass(MENU_CSS, "active");
19
19
  *
20
20
  * @see https://shelving.cc/ui/MenuProps
21
21
  */
22
- export interface MenuProps extends BlockVariants, OptionalChildProps {}
22
+ export interface MenuProps extends BlockVariants, OptionalChildProps, ClassProps {}
23
23
 
24
24
  /**
25
25
  * A `<menu>` list of `<MenuItem>` children.
@@ -29,12 +29,13 @@ export interface MenuProps extends BlockVariants, OptionalChildProps {}
29
29
  * @kind component
30
30
  * @see https://shelving.cc/ui/Menu
31
31
  */
32
- export function Menu({ children, ...props }: MenuProps): ReactNode {
32
+ export function Menu({ children, className, ...props }: MenuProps): ReactNode {
33
33
  return (
34
34
  <menu
35
35
  className={getClass(
36
36
  MENU_CLASS, //
37
37
  getBlockClass(props),
38
+ className,
38
39
  )}
39
40
  >
40
41
  {children}
@@ -47,7 +48,7 @@ export function Menu({ children, ...props }: MenuProps): ReactNode {
47
48
  *
48
49
  * @see https://shelving.cc/ui/MenuItemProps
49
50
  */
50
- export interface MenuItemProps extends ClickableProps {
51
+ export interface MenuItemProps extends ClickableProps, ClassProps {
51
52
  /**
52
53
  * The first child becomes the link label (rendered inside the `<a>`).
53
54
  * - Any additional children form the submenu — only rendered when this item is "proud" (an ancestor of the current page). The caller is responsible for wrapping the submenu in a nested `<Menu>` if it wants the CSS `.menu .menu` descendant rules to apply.
@@ -63,14 +64,14 @@ export interface MenuItemProps extends ClickableProps {
63
64
  * @kind component
64
65
  * @see https://shelving.cc/ui/MenuItem
65
66
  */
66
- export function MenuItem({ href, children, ...props }: MenuItemProps): ReactNode {
67
+ export function MenuItem({ href, children, className, ...props }: MenuItemProps): ReactNode {
67
68
  const { url, root } = requireMeta();
68
69
  const link = getLink(href, url, root);
69
70
  const proud = isURLProud(url, link);
70
71
  const active = isURLActive(url, link);
71
72
  const [label, ...after] = isArray(children) ? children : [children];
72
73
  return (
73
- <li className={MENU_ITEM_CLASS}>
74
+ <li className={getClass(MENU_ITEM_CLASS, className)}>
74
75
  <Clickable
75
76
  href={href}
76
77
  {...props}