@yarcl/react 0.1.0 → 0.2.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 (223) hide show
  1. package/README.md +14 -2
  2. package/{src/apply.ts → dist/apply.d.ts} +4 -23
  3. package/dist/apply.js +37 -0
  4. package/dist/classes.d.ts +13 -0
  5. package/dist/classes.js +22 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +58 -0
  8. package/dist/color.js +49 -32
  9. package/dist/components/Accordion.d.ts +96 -0
  10. package/dist/components/Accordion.js +148 -0
  11. package/dist/components/Alert.d.ts +60 -0
  12. package/dist/components/Alert.js +61 -0
  13. package/dist/components/Avatar.d.ts +100 -0
  14. package/dist/components/Avatar.js +89 -0
  15. package/dist/components/Badge.d.ts +44 -0
  16. package/dist/components/Badge.js +43 -0
  17. package/dist/components/Breadcrumb.d.ts +77 -0
  18. package/dist/components/Breadcrumb.js +126 -0
  19. package/dist/components/Button.d.ts +18 -0
  20. package/dist/components/Button.js +33 -0
  21. package/dist/components/ButtonGroup.d.ts +29 -0
  22. package/dist/components/ButtonGroup.js +37 -0
  23. package/dist/components/Card.d.ts +36 -0
  24. package/dist/components/Card.js +26 -0
  25. package/dist/components/Checkbox.d.ts +28 -0
  26. package/dist/components/Checkbox.js +38 -0
  27. package/dist/components/Combobox.d.ts +75 -0
  28. package/dist/components/Combobox.js +362 -0
  29. package/dist/components/CommandPalette.d.ts +92 -0
  30. package/dist/components/CommandPalette.js +293 -0
  31. package/dist/components/DatePicker.d.ts +157 -0
  32. package/dist/components/DatePicker.js +230 -0
  33. package/dist/components/Dialog.d.ts +25 -0
  34. package/dist/components/Dialog.js +29 -0
  35. package/dist/components/Divider.d.ts +23 -0
  36. package/dist/components/Divider.js +25 -0
  37. package/dist/components/Drawer.d.ts +21 -0
  38. package/dist/components/Drawer.js +25 -0
  39. package/dist/components/EmptyState.d.ts +51 -0
  40. package/dist/components/EmptyState.js +47 -0
  41. package/dist/components/Field.d.ts +27 -0
  42. package/dist/components/Field.js +59 -0
  43. package/dist/components/Heading.d.ts +26 -0
  44. package/dist/components/Heading.js +25 -0
  45. package/dist/components/HoverCard.d.ts +47 -0
  46. package/dist/components/HoverCard.js +53 -0
  47. package/dist/components/IconButton.d.ts +19 -0
  48. package/dist/components/IconButton.js +32 -0
  49. package/dist/components/Inline.d.ts +21 -0
  50. package/dist/components/Inline.js +24 -0
  51. package/dist/components/Input.d.ts +19 -0
  52. package/dist/components/Input.js +25 -0
  53. package/dist/components/Label.d.ts +46 -0
  54. package/dist/components/Label.js +33 -0
  55. package/dist/components/Link.d.ts +27 -0
  56. package/dist/components/Link.js +26 -0
  57. package/dist/components/Menu.d.ts +73 -0
  58. package/dist/components/Menu.js +143 -0
  59. package/dist/components/Modal.d.ts +45 -0
  60. package/dist/components/Modal.js +110 -0
  61. package/dist/components/NumberInput.d.ts +49 -0
  62. package/dist/components/NumberInput.js +130 -0
  63. package/dist/components/Pagination.d.ts +86 -0
  64. package/dist/components/Pagination.js +119 -0
  65. package/dist/components/Popover.d.ts +72 -0
  66. package/dist/components/Popover.js +87 -0
  67. package/dist/components/Progress.d.ts +60 -0
  68. package/dist/components/Progress.js +59 -0
  69. package/dist/components/Radio.d.ts +32 -0
  70. package/dist/components/Radio.js +47 -0
  71. package/dist/components/RadioGroup.d.ts +57 -0
  72. package/dist/components/RadioGroup.js +68 -0
  73. package/dist/components/Select.d.ts +47 -0
  74. package/dist/components/Select.js +138 -0
  75. package/dist/components/Skeleton.d.ts +46 -0
  76. package/dist/components/Skeleton.js +40 -0
  77. package/dist/components/Slider.d.ts +126 -0
  78. package/dist/components/Slider.js +166 -0
  79. package/dist/components/Spinner.d.ts +23 -0
  80. package/dist/components/Spinner.js +23 -0
  81. package/dist/components/Stack.d.ts +35 -0
  82. package/dist/components/Stack.js +24 -0
  83. package/dist/components/Switch.d.ts +27 -0
  84. package/dist/components/Switch.js +33 -0
  85. package/dist/components/Table.d.ts +71 -0
  86. package/dist/components/Table.js +61 -0
  87. package/dist/components/Tabs.d.ts +66 -0
  88. package/dist/components/Tabs.js +109 -0
  89. package/dist/components/Text.d.ts +32 -0
  90. package/dist/components/Text.js +28 -0
  91. package/dist/components/Textarea.d.ts +15 -0
  92. package/dist/components/Textarea.js +24 -0
  93. package/dist/components/Toast.d.ts +90 -0
  94. package/dist/components/Toast.js +156 -0
  95. package/dist/components/ToggleGroup.d.ts +79 -0
  96. package/dist/components/ToggleGroup.js +101 -0
  97. package/dist/components/Tooltip.d.ts +46 -0
  98. package/dist/components/Tooltip.js +62 -0
  99. package/dist/components/VisuallyHidden.d.ts +18 -0
  100. package/dist/components/VisuallyHidden.js +22 -0
  101. package/dist/contrast.d.ts +32 -0
  102. package/dist/css.d.ts +2 -2
  103. package/dist/css.js +193 -157
  104. package/dist/define.d.ts +40 -15
  105. package/dist/define.js +30 -27
  106. package/dist/demo.d.ts +2 -0
  107. package/dist/demo.js +2 -0
  108. package/dist/esbuild.cjs +5 -0
  109. package/dist/esbuild.d.ts +4 -0
  110. package/dist/esbuild.js +6 -0
  111. package/dist/field-context.d.ts +16 -0
  112. package/dist/field-context.js +17 -0
  113. package/dist/floating.d.ts +12 -0
  114. package/dist/floating.js +52 -0
  115. package/dist/hooks.d.ts +1 -0
  116. package/dist/hooks.js +12 -0
  117. package/{src/index.ts → dist/index.d.ts} +11 -33
  118. package/dist/index.js +50 -0
  119. package/dist/init.d.ts +18 -0
  120. package/dist/init.js +224 -0
  121. package/dist/plugin.d.ts +10 -26
  122. package/dist/plugin.js +152 -78
  123. package/dist/reference/DemoYarcl.d.ts +12 -0
  124. package/dist/reference/DemoYarcl.js +312 -0
  125. package/dist/reference/DesignReference.d.ts +24 -0
  126. package/dist/reference/DesignReference.js +403 -0
  127. package/dist/reference/index.js +2 -0
  128. package/dist/rollup.cjs +5 -0
  129. package/dist/rollup.d.ts +4 -0
  130. package/dist/rollup.js +6 -0
  131. package/dist/rspack.cjs +5 -0
  132. package/dist/rspack.d.ts +4 -0
  133. package/dist/rspack.js +6 -0
  134. package/dist/runtime.d.ts +13 -0
  135. package/dist/runtime.js +30 -0
  136. package/dist/setup.d.ts +6 -0
  137. package/dist/setup.js +33 -0
  138. package/{src → dist}/styles.css +224 -106
  139. package/dist/themes/bloom.d.ts +295 -0
  140. package/dist/themes/bloom.js +202 -0
  141. package/dist/themes/brutalist.d.ts +277 -0
  142. package/dist/themes/brutalist.js +199 -0
  143. package/dist/themes/compact.d.ts +274 -0
  144. package/dist/themes/compact.js +200 -0
  145. package/dist/themes/contract.d.ts +23 -0
  146. package/dist/themes/editorial.d.ts +277 -0
  147. package/dist/themes/editorial.js +183 -0
  148. package/dist/themes/index.d.ts +1425 -0
  149. package/dist/themes/index.js +45 -0
  150. package/{src/types.ts → dist/types.d.ts} +40 -37
  151. package/dist/vite.cjs +5 -0
  152. package/dist/vite.d.ts +4 -0
  153. package/dist/vite.js +6 -0
  154. package/dist/webpack.cjs +5 -0
  155. package/dist/webpack.d.ts +4 -0
  156. package/dist/webpack.js +6 -0
  157. package/dist/yarcl.config.d.ts +276 -0
  158. package/dist/yarcl.config.js +273 -0
  159. package/package.json +69 -17
  160. package/src/classes.ts +0 -23
  161. package/src/color.ts +0 -48
  162. package/src/components/Accordion.tsx +0 -274
  163. package/src/components/Alert.tsx +0 -85
  164. package/src/components/Badge.tsx +0 -79
  165. package/src/components/Breadcrumb.tsx +0 -239
  166. package/src/components/Button.tsx +0 -58
  167. package/src/components/ButtonGroup.tsx +0 -61
  168. package/src/components/Card.tsx +0 -50
  169. package/src/components/Checkbox.tsx +0 -49
  170. package/src/components/Combobox.tsx +0 -522
  171. package/src/components/CommandPalette.tsx +0 -452
  172. package/src/components/DatePicker.tsx +0 -432
  173. package/src/components/Dialog.tsx +0 -39
  174. package/src/components/Divider.tsx +0 -34
  175. package/src/components/Drawer.tsx +0 -31
  176. package/src/components/Field.tsx +0 -61
  177. package/src/components/Heading.tsx +0 -40
  178. package/src/components/HoverCard.tsx +0 -108
  179. package/src/components/IconButton.tsx +0 -59
  180. package/src/components/Inline.tsx +0 -42
  181. package/src/components/Input.tsx +0 -28
  182. package/src/components/Label.tsx +0 -58
  183. package/src/components/Link.tsx +0 -41
  184. package/src/components/Menu.tsx +0 -207
  185. package/src/components/Modal.tsx +0 -164
  186. package/src/components/NumberInput.tsx +0 -208
  187. package/src/components/Pagination.tsx +0 -227
  188. package/src/components/Popover.tsx +0 -140
  189. package/src/components/Progress.tsx +0 -122
  190. package/src/components/Radio.tsx +0 -59
  191. package/src/components/RadioGroup.tsx +0 -114
  192. package/src/components/Select.tsx +0 -197
  193. package/src/components/Skeleton.tsx +0 -95
  194. package/src/components/Slider.tsx +0 -315
  195. package/src/components/Spinner.tsx +0 -38
  196. package/src/components/Stack.tsx +0 -57
  197. package/src/components/Switch.tsx +0 -42
  198. package/src/components/Table.tsx +0 -99
  199. package/src/components/Tabs.tsx +0 -159
  200. package/src/components/Text.tsx +0 -67
  201. package/src/components/Textarea.tsx +0 -26
  202. package/src/components/Toast.tsx +0 -190
  203. package/src/components/ToggleGroup.tsx +0 -227
  204. package/src/components/Tooltip.tsx +0 -78
  205. package/src/contrast.ts +0 -82
  206. package/src/css.ts +0 -228
  207. package/src/define.check.ts +0 -259
  208. package/src/define.ts +0 -379
  209. package/src/field-context.ts +0 -31
  210. package/src/floating.tsx +0 -61
  211. package/src/hooks.ts +0 -19
  212. package/src/plugin.ts +0 -94
  213. package/src/reference/DesignReference.tsx +0 -374
  214. package/src/runtime.ts +0 -38
  215. package/src/themes/bloom.ts +0 -72
  216. package/src/themes/brutalist.ts +0 -63
  217. package/src/themes/compact.ts +0 -63
  218. package/src/themes/contract.ts +0 -24
  219. package/src/themes/editorial.ts +0 -55
  220. package/src/themes/index.ts +0 -47
  221. package/src/yarcl.config.ts +0 -104
  222. /package/{src/reference/index.ts → dist/reference/index.d.ts} +0 -0
  223. /package/{src → dist}/reference/reference.css +0 -0
@@ -1,108 +0,0 @@
1
- import {
2
- autoUpdate,
3
- FloatingPortal,
4
- safePolygon,
5
- useDismiss,
6
- useFloating,
7
- useFocus,
8
- useHover,
9
- useInteractions,
10
- type Placement,
11
- } from '@floating-ui/react';
12
- import { useState, type ReactElement, type ReactNode } from 'react';
13
- import { cx, paddingClass, radiusClass } from '../classes';
14
- import { floatingMiddleware, useTrigger } from '../floating';
15
- import type { Radius, Spacing } from '../types';
16
- import { useConfig, useDefaults } from '../runtime';
17
-
18
-
19
- /** Props for {@link HoverCard}. */
20
- export interface HoverCardProps {
21
- /** Rich preview content. Can contain links; the pointer can move into the card without closing it. */
22
- content: ReactNode;
23
- /** A single focusable element, usually a {@link Link}. */
24
- children: ReactElement;
25
- /**
26
- * Preferred side. Flips when there isn't room.
27
- * @default 'bottom'
28
- */
29
- placement?: Placement;
30
- /**
31
- * Delay before opening, in ms.
32
- * @default config.timing.hoverOpenDelay
33
- */
34
- openDelay?: number;
35
- /**
36
- * Delay before closing after the pointer leaves, in ms.
37
- * @default config.timing.hoverCloseDelay
38
- */
39
- closeDelay?: number;
40
- /**
41
- * Inner padding, from the `spacing` config.
42
- * @default config.defaults.padding
43
- */
44
- padding?: Spacing;
45
- /**
46
- * Corner radius, from the `radii` config.
47
- * @default config.defaults.radius
48
- */
49
- radius?: Radius;
50
- }
51
-
52
- /**
53
- * A preview card shown on hover or focus, e.g. for a user or a link.
54
- * Supplementary only: don't put content here that isn't reachable elsewhere.
55
- *
56
- * @example
57
- * ```tsx
58
- * <HoverCard content={<UserPreview id="ada" />}>
59
- * <Link href="/users/ada">@ada</Link>
60
- * </HoverCard>
61
- * ```
62
- */
63
- export function HoverCard({
64
- content,
65
- children,
66
- placement = 'bottom',
67
- openDelay,
68
- closeDelay,
69
- padding,
70
- radius,
71
- }: HoverCardProps) {
72
- const own = useDefaults('HoverCard');
73
- const config = useConfig();
74
- const delay = { open: openDelay ?? config.timing.hoverOpenDelay, close: closeDelay ?? config.timing.hoverCloseDelay };
75
- const [open, setOpen] = useState(false);
76
- const { refs, floatingStyles, context } = useFloating({
77
- open,
78
- onOpenChange: setOpen,
79
- placement,
80
- transform: false,
81
- whileElementsMounted: autoUpdate,
82
- middleware: floatingMiddleware({ gap: 8 }),
83
- });
84
- const { getReferenceProps, getFloatingProps } = useInteractions([
85
- useHover(context, { delay, handleClose: safePolygon() }),
86
- useFocus(context),
87
- useDismiss(context),
88
- ]);
89
- const trigger = useTrigger(children, refs.setReference, getReferenceProps);
90
-
91
- return (
92
- <>
93
- {trigger}
94
- {open && (
95
- <FloatingPortal>
96
- <div
97
- ref={refs.setFloating}
98
- style={floatingStyles}
99
- className={cx('yarcl-floating yarcl-panel yarcl-hover-card', paddingClass(padding ?? own.padding), radiusClass(radius ?? own.radius))}
100
- {...getFloatingProps()}
101
- >
102
- {content}
103
- </div>
104
- </FloatingPortal>
105
- )}
106
- </>
107
- );
108
- }
@@ -1,59 +0,0 @@
1
- import type { ComponentProps } from 'react';
2
- import { colorClass, cx, radiusClass, sizeClass, variantClass } from '../classes';
3
- import type { TokenProps, VariantProps } from '../types';
4
- import { useButtonGroup } from './ButtonGroup';
5
- import { Spinner } from './Spinner';
6
- import { useDefaults } from '../runtime';
7
-
8
-
9
- /** Props for {@link IconButton}. `aria-label` is required because the button has no visible text. */
10
- export interface IconButtonProps extends Omit<ComponentProps<'button'>, 'color'>, TokenProps, VariantProps {
11
- /** Shows a {@link Spinner}, disables the button and sets `aria-busy`. */
12
- loading?: boolean;
13
- /** Accessible name, announced by screen readers in place of visible text. */
14
- 'aria-label': string;
15
- }
16
-
17
- /**
18
- * A square button containing only an icon.
19
- * Width equals the control height, so it lines up with {@link Button} and {@link Input} of the same size.
20
- *
21
- * @example
22
- * ```tsx
23
- * <IconButton aria-label="Search" variant="ghost"><SearchIcon /></IconButton>
24
- * ```
25
- */
26
- export function IconButton({
27
- size,
28
- radius,
29
- color,
30
- variant,
31
- loading,
32
- disabled,
33
- className,
34
- type = 'button',
35
- children,
36
- ...props
37
- }: IconButtonProps) {
38
- const own = useDefaults('IconButton');
39
- const group = useButtonGroup();
40
- return (
41
- <button
42
- type={type}
43
- className={cx(
44
- 'yarcl-button yarcl-icon-button',
45
- sizeClass(size ?? group?.size ?? own.size),
46
- radiusClass(radius ?? group?.radius ?? own.radius, size ?? group?.size ?? own.size),
47
- colorClass(color ?? group?.color ?? own.color),
48
- variantClass(variant ?? group?.variant ?? own.variant),
49
- className,
50
- )}
51
- disabled={disabled || loading}
52
- aria-busy={loading || undefined}
53
- {...props}
54
- >
55
- {loading && <Spinner label="" aria-hidden="true" />}
56
- {!loading && children}
57
- </button>
58
- );
59
- }
@@ -1,42 +0,0 @@
1
- import { cx, gapClass } from '../classes';
2
- import type { LayoutProps } from './Stack';
3
- import { useDefaults } from '../runtime';
4
-
5
-
6
- /** Props for {@link Inline}. */
7
- export interface InlineProps extends LayoutProps {
8
- /**
9
- * Wraps children onto new lines when they don't fit.
10
- * @default true
11
- */
12
- wrap?: boolean;
13
- }
14
-
15
- /**
16
- * Lays out children horizontally with a gap from the spacing scale. Centers children vertically by default.
17
- *
18
- * @example
19
- * ```tsx
20
- * <Inline gap="tight" justify="end">
21
- * <Button variant="outline">Cancel</Button>
22
- * <Button>Save</Button>
23
- * </Inline>
24
- * ```
25
- */
26
- export function Inline({ as = 'div', gap, align = 'center', justify, wrap = true, className, ...props }: InlineProps) {
27
- const own = useDefaults('Inline');
28
- const Tag = as as 'div';
29
- return (
30
- <Tag
31
- className={cx(
32
- 'yarcl-inline',
33
- gapClass(gap ?? own.gap),
34
- `yarcl-align-${align}`,
35
- justify && `yarcl-justify-${justify}`,
36
- !wrap && 'yarcl-inline-nowrap',
37
- className,
38
- )}
39
- {...props}
40
- />
41
- );
42
- }
@@ -1,28 +0,0 @@
1
- import type { ComponentProps } from 'react';
2
- import { colorClass, cx, radiusClass, sizeClass } from '../classes';
3
- import { useFieldProps } from '../field-context';
4
- import type { TokenProps } from '../types';
5
- import { useDefaults } from '../runtime';
6
-
7
-
8
- /**
9
- * Props for {@link Input}. Accepts all native `<input>` attributes except
10
- * `color` and `size`, which are replaced by design tokens.
11
- */
12
- export interface InputProps extends Omit<ComponentProps<'input'>, 'color' | 'size'>, TokenProps {}
13
-
14
- /**
15
- * A text input styled from the consumer's design tokens.
16
- * Shares the size scale with {@link Button}, so both have the same height at the same size.
17
- * `color` sets the focus border and ring. Inside a {@link Field}, it is labelled and described automatically.
18
- *
19
- * @example
20
- * ```tsx
21
- * <Input size="lg" placeholder="Email" />
22
- * ```
23
- */
24
- export function Input(props: InputProps) {
25
- const own = useDefaults('Input');
26
- const { size, radius, color, className, ...rest } = useFieldProps(props);
27
- return <input className={cx('yarcl-input', sizeClass(size ?? own.size), radiusClass(radius ?? own.radius, size ?? own.size), colorClass(color ?? own.color), className)} {...rest} />;
28
- }
@@ -1,58 +0,0 @@
1
- import type { ComponentProps } from 'react';
2
- import { colorClass, cx, typeClass } from '../classes';
3
- import type { Color, TextStyle } from '../types';
4
- import { useConfig, useDefaults } from '../runtime';
5
-
6
- /** Props for {@link Label}. */
7
- export interface LabelProps extends Omit<ComponentProps<'label'>, 'color'> {
8
- /**
9
- * Text style, from the `typography.styles` config.
10
- * @default config.defaults.labelStyle
11
- */
12
- textStyle?: TextStyle;
13
- /** Semantic color, from the `colors` config. Uses the default text color when omitted. */
14
- color?: Color;
15
- /**
16
- * Shows a required marker in `defaults.errorColor`. The marker is hidden from screen readers,
17
- * so also set `required` (or `aria-required`) on the control.
18
- */
19
- required?: boolean;
20
- /** Uses the muted color, to match a disabled control. */
21
- disabled?: boolean;
22
- }
23
-
24
- /**
25
- * A standalone form label, for layouts {@link Field} doesn't cover: a label beside its control,
26
- * one label for several controls, or a custom control. Connect it with `htmlFor`, or give it an
27
- * `id` and point the control's `aria-labelledby` at it.
28
- *
29
- * @example
30
- * ```tsx
31
- * <Label htmlFor="email" required>Email</Label>
32
- * <Input id="email" type="email" required />
33
- * ```
34
- */
35
- export function Label({ textStyle, color, required, disabled, className, children, ...props }: LabelProps) {
36
- const config = useConfig();
37
- const own = useDefaults('Label');
38
- const resolvedColor = color ?? own.color;
39
- return (
40
- <label
41
- className={cx(
42
- 'yarcl-label',
43
- typeClass(textStyle ?? own.textStyle ?? config.defaults.labelStyle),
44
- resolvedColor && cx('yarcl-label-colored', colorClass(resolvedColor)),
45
- disabled && 'yarcl-label-disabled',
46
- className,
47
- )}
48
- {...props}
49
- >
50
- {children}
51
- {required && (
52
- <span className="yarcl-label-required" aria-hidden="true">
53
- {' *'}
54
- </span>
55
- )}
56
- </label>
57
- );
58
- }
@@ -1,41 +0,0 @@
1
- import type { ComponentProps } from 'react';
2
- import { colorClass, cx } from '../classes';
3
- import type { Color } from '../types';
4
- import { useDefaults } from '../runtime';
5
-
6
-
7
- /** Props for {@link Link}. Accepts all native `<a>` attributes except `color`. */
8
- export interface LinkProps extends Omit<ComponentProps<'a'>, 'color'> {
9
- /**
10
- * Link color, from the `colors` config.
11
- * @default config.defaults.color
12
- */
13
- color?: Color;
14
- /**
15
- * When to underline. Keep `'always'` for links inside running text, so they don't rely on color alone.
16
- * @default 'always'
17
- */
18
- underline?: 'always' | 'hover' | 'none';
19
- /** Opens in a new tab with `rel="noopener noreferrer"`. */
20
- external?: boolean;
21
- }
22
-
23
- /**
24
- * A text link that inherits the surrounding text style.
25
- *
26
- * @example
27
- * ```tsx
28
- * <Link href="/docs">Read the docs</Link>
29
- * <Link href="https://example.com" external>Example</Link>
30
- * ```
31
- */
32
- export function Link({ color, underline = 'always', external, className, ...props }: LinkProps) {
33
- const own = useDefaults('Link');
34
- return (
35
- <a
36
- className={cx('yarcl-link', `yarcl-link-underline-${underline}`, colorClass(color ?? own.color), className)}
37
- {...(external && { target: '_blank', rel: 'noopener noreferrer' })}
38
- {...props}
39
- />
40
- );
41
- }
@@ -1,207 +0,0 @@
1
- import {
2
- autoUpdate,
3
- FloatingFocusManager,
4
- FloatingList,
5
- FloatingPortal,
6
- useClick,
7
- useDismiss,
8
- useFloating,
9
- useInteractions,
10
- useListItem,
11
- useListNavigation,
12
- useRole,
13
- useTypeahead,
14
- type Placement,
15
- } from '@floating-ui/react';
16
- import {
17
- createContext,
18
- useContext,
19
- useRef,
20
- useState,
21
- type ComponentProps,
22
- type ReactElement,
23
- type ReactNode,
24
- } from 'react';
25
- import { colorClass, cx, sizeClass } from '../classes';
26
- import { floatingMiddleware, useTrigger } from '../floating';
27
- import { useControllable } from '../hooks';
28
- import type { Color, Size } from '../types';
29
- import { useDefaults } from '../runtime';
30
-
31
-
32
- type MenuContextValue = ReturnType<typeof useMenuState>;
33
-
34
- const MenuContext = createContext<MenuContextValue | null>(null);
35
-
36
- function useMenuContext(component: string) {
37
- const context = useContext(MenuContext);
38
- if (!context) throw new Error(`yarcl: <${component}> must be inside <Menu>`);
39
- return context;
40
- }
41
-
42
- function useMenuState({ open: openProp, defaultOpen = false, onOpenChange, placement = 'bottom-start', size }: MenuProps) {
43
- const [open, setOpen] = useControllable(openProp, defaultOpen, onOpenChange);
44
- const [activeIndex, setActiveIndex] = useState<number | null>(null);
45
- const elementsRef = useRef<(HTMLElement | null)[]>([]);
46
- const labelsRef = useRef<(string | null)[]>([]);
47
-
48
- const floating = useFloating({
49
- open,
50
- onOpenChange: setOpen,
51
- placement,
52
- transform: false,
53
- whileElementsMounted: autoUpdate,
54
- middleware: floatingMiddleware(),
55
- });
56
- const interactions = useInteractions([
57
- useClick(floating.context),
58
- useDismiss(floating.context),
59
- useRole(floating.context, { role: 'menu' }),
60
- useListNavigation(floating.context, {
61
- listRef: elementsRef,
62
- activeIndex,
63
- onNavigate: setActiveIndex,
64
- loop: true,
65
- }),
66
- useTypeahead(floating.context, {
67
- listRef: labelsRef,
68
- activeIndex,
69
- onMatch: open ? setActiveIndex : undefined,
70
- }),
71
- ]);
72
-
73
- return { open, setOpen, activeIndex, setActiveIndex, elementsRef, labelsRef, size, ...floating, ...interactions };
74
- }
75
-
76
- /** Props for {@link Menu}. */
77
- export interface MenuProps {
78
- /** `Menu.Trigger` and `Menu.Content`. */
79
- children?: ReactNode;
80
- /** Controlled open state. */
81
- open?: boolean;
82
- /**
83
- * Initial open state when uncontrolled.
84
- * @default false
85
- */
86
- defaultOpen?: boolean;
87
- /** Called when the menu opens or closes. */
88
- onOpenChange?: (open: boolean) => void;
89
- /**
90
- * Preferred side. Flips when there isn't room.
91
- * @default 'bottom-start'
92
- */
93
- placement?: Placement;
94
- /**
95
- * Item size, from the `sizes` config.
96
- * @default config.defaults.size
97
- */
98
- size?: Size;
99
- }
100
-
101
- function MenuRoot(props: MenuProps) {
102
- const state = useMenuState(props);
103
- return <MenuContext.Provider value={state}>{props.children}</MenuContext.Provider>;
104
- }
105
-
106
- /** Props for `Menu.Trigger`. */
107
- export interface MenuTriggerProps {
108
- /** A single element, usually a {@link Button}. */
109
- children: ReactElement;
110
- }
111
-
112
- function MenuTrigger({ children }: MenuTriggerProps) {
113
- const { refs, getReferenceProps } = useMenuContext('Menu.Trigger');
114
- return useTrigger(children, refs.setReference, getReferenceProps);
115
- }
116
-
117
- /** Props for `Menu.Content`. */
118
- export interface MenuContentProps extends ComponentProps<'div'> {}
119
-
120
- function MenuContent({ className, style, children, ...props }: MenuContentProps) {
121
- const own = useDefaults('Menu');
122
- const { open, size, refs, floatingStyles, context, getFloatingProps, elementsRef, labelsRef } =
123
- useMenuContext('Menu.Content');
124
- if (!open) return null;
125
- return (
126
- <FloatingPortal>
127
- <FloatingFocusManager context={context} modal={false} initialFocus={0}>
128
- <div
129
- ref={refs.setFloating}
130
- style={{ ...floatingStyles, ...style }}
131
- className={cx('yarcl-floating yarcl-panel yarcl-listbox', sizeClass(size ?? own.size), colorClass(), className)}
132
- {...getFloatingProps(props)}
133
- >
134
- <FloatingList elementsRef={elementsRef} labelsRef={labelsRef}>
135
- {children}
136
- </FloatingList>
137
- </div>
138
- </FloatingFocusManager>
139
- </FloatingPortal>
140
- );
141
- }
142
-
143
- /** Props for `Menu.Item`. */
144
- export interface MenuItemProps extends Omit<ComponentProps<'button'>, 'color' | 'onSelect'> {
145
- /** Called when the item is chosen by click, Enter or Space. The menu closes afterwards. */
146
- onSelect?: () => void;
147
- /** Semantic color for the item, e.g. a destructive action. From the `colors` config. */
148
- color?: Color;
149
- /** Text used for type-to-select. Defaults to `children` when it is a string. */
150
- textValue?: string;
151
- }
152
-
153
- function MenuItem({ onSelect, color, textValue, disabled, className, children, ...props }: MenuItemProps) {
154
- const { activeIndex, setOpen, getItemProps } = useMenuContext('Menu.Item');
155
- const { ref, index } = useListItem({
156
- label: disabled ? null : (textValue ?? (typeof children === 'string' ? children : null)),
157
- });
158
- const active = activeIndex === index;
159
- return (
160
- <button
161
- ref={ref}
162
- type="button"
163
- role="menuitem"
164
- tabIndex={active ? 0 : -1}
165
- disabled={disabled}
166
- data-active={active || undefined}
167
- className={cx('yarcl-option', color && cx('yarcl-option-colored', colorClass(color)), className)}
168
- {...getItemProps({
169
- ...props,
170
- onClick() {
171
- onSelect?.();
172
- setOpen(false);
173
- },
174
- })}
175
- >
176
- {children}
177
- </button>
178
- );
179
- }
180
-
181
- /** A line between groups of `Menu.Item`s. */
182
- function MenuSeparator() {
183
- return <div role="separator" className="yarcl-listbox-separator" />;
184
- }
185
-
186
- /**
187
- * A list of actions opened from a trigger. Arrow keys move between items,
188
- * typing jumps to a matching item, Esc closes.
189
- *
190
- * @example
191
- * ```tsx
192
- * <Menu>
193
- * <Menu.Trigger><Button variant="outline">Actions</Button></Menu.Trigger>
194
- * <Menu.Content>
195
- * <Menu.Item onSelect={rename}>Rename</Menu.Item>
196
- * <Menu.Separator />
197
- * <Menu.Item color="danger" onSelect={remove}>Delete</Menu.Item>
198
- * </Menu.Content>
199
- * </Menu>
200
- * ```
201
- */
202
- export const Menu = Object.assign(MenuRoot, {
203
- Trigger: MenuTrigger,
204
- Content: MenuContent,
205
- Item: MenuItem,
206
- Separator: MenuSeparator,
207
- });