@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,159 +0,0 @@
1
- import {
2
- createContext,
3
- useContext,
4
- useId,
5
- type ComponentProps,
6
- type KeyboardEvent,
7
- type ReactNode,
8
- } from 'react';
9
- import { colorClass, cx, sizeClass } from '../classes';
10
- import { useControllable } from '../hooks';
11
- import type { Color, Size } from '../types';
12
- import { useDefaults } from '../runtime';
13
-
14
-
15
- interface TabsContextValue {
16
- value: string;
17
- select: (value: string) => void;
18
- idFor: (kind: 'tab' | 'panel', value: string) => string;
19
- }
20
-
21
- const TabsContext = createContext<TabsContextValue | null>(null);
22
-
23
- function useTabsContext(component: string) {
24
- const context = useContext(TabsContext);
25
- if (!context) throw new Error(`yarcl: <${component}> must be inside <Tabs>`);
26
- return context;
27
- }
28
-
29
- /** Props shared by both forms of {@link TabsProps}. */
30
- export interface TabsBaseProps extends Omit<ComponentProps<'div'>, 'defaultValue' | 'color'> {
31
- /** Called with the newly selected tab's value. */
32
- onValueChange?: (value: string) => void;
33
- /**
34
- * Tab height and font size, from the `sizes` config.
35
- * @default config.defaults.size
36
- */
37
- size?: Size;
38
- /**
39
- * Color of the selected tab's indicator, from the `colors` config.
40
- * @default config.defaults.color
41
- */
42
- color?: Color;
43
- }
44
-
45
- /** Props for {@link Tabs}. Pass `value` (controlled) or `defaultValue` (uncontrolled). */
46
- export type TabsProps = TabsBaseProps &
47
- (
48
- | { /** Selected tab (controlled). */ value: string; defaultValue?: never }
49
- | { value?: never; /** Initially selected tab (uncontrolled). */ defaultValue: string }
50
- );
51
-
52
- function TabsRoot({ value: valueProp, defaultValue, onValueChange, size, color, className, ...props }: TabsProps) {
53
- const own = useDefaults('Tabs');
54
- const [value, select] = useControllable(valueProp, defaultValue ?? '', onValueChange);
55
- const baseId = useId();
56
- const idFor = (kind: 'tab' | 'panel', v: string) => `${baseId}-${kind}-${v.replace(/[^\w-]/g, '_')}`;
57
- return (
58
- <TabsContext.Provider value={{ value, select, idFor }}>
59
- <div className={cx('yarcl-tabs', sizeClass(size ?? own.size), colorClass(color ?? own.color), className)} {...props} />
60
- </TabsContext.Provider>
61
- );
62
- }
63
-
64
- /** Props for `Tabs.List`. */
65
- export interface TabsListProps extends ComponentProps<'div'> {
66
- /** `Tabs.Trigger`s. */
67
- children?: ReactNode;
68
- }
69
-
70
- function TabsList({ className, onKeyDown, ...props }: TabsListProps) {
71
- const { select } = useTabsContext('Tabs.List');
72
-
73
- function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {
74
- onKeyDown?.(event);
75
- const tabs = [...event.currentTarget.querySelectorAll<HTMLButtonElement>('[role="tab"]:not(:disabled)')];
76
- const current = tabs.indexOf(document.activeElement as HTMLButtonElement);
77
- if (current === -1) return;
78
- const next = {
79
- ArrowRight: (current + 1) % tabs.length,
80
- ArrowLeft: (current - 1 + tabs.length) % tabs.length,
81
- Home: 0,
82
- End: tabs.length - 1,
83
- }[event.key];
84
- if (next === undefined) return;
85
- event.preventDefault();
86
- tabs[next].focus();
87
- select(tabs[next].dataset.value!);
88
- }
89
-
90
- return <div role="tablist" className={cx('yarcl-tabs-list', className)} onKeyDown={handleKeyDown} {...props} />;
91
- }
92
-
93
- /** Props for `Tabs.Trigger`. */
94
- export interface TabsTriggerProps extends Omit<ComponentProps<'button'>, 'value'> {
95
- /** Identifies the tab; matches a `Tabs.Panel`'s `value`. */
96
- value: string;
97
- }
98
-
99
- function TabsTrigger({ value, className, onClick, ...props }: TabsTriggerProps) {
100
- const context = useTabsContext('Tabs.Trigger');
101
- const selected = context.value === value;
102
- return (
103
- <button
104
- type="button"
105
- role="tab"
106
- id={context.idFor('tab', value)}
107
- aria-controls={context.idFor('panel', value)}
108
- aria-selected={selected}
109
- tabIndex={selected ? 0 : -1}
110
- data-value={value}
111
- className={cx('yarcl-tabs-trigger', className)}
112
- onClick={(event) => {
113
- onClick?.(event);
114
- context.select(value);
115
- }}
116
- {...props}
117
- />
118
- );
119
- }
120
-
121
- /** Props for `Tabs.Panel`. */
122
- export interface TabsPanelProps extends Omit<ComponentProps<'div'>, 'value'> {
123
- /** Matches the `value` of the `Tabs.Trigger` that shows this panel. */
124
- value: string;
125
- }
126
-
127
- function TabsPanel({ value, className, ...props }: TabsPanelProps) {
128
- const context = useTabsContext('Tabs.Panel');
129
- const selected = context.value === value;
130
- return (
131
- <div
132
- role="tabpanel"
133
- id={context.idFor('panel', value)}
134
- aria-labelledby={context.idFor('tab', value)}
135
- tabIndex={0}
136
- hidden={!selected}
137
- className={cx('yarcl-tabs-panel', className)}
138
- {...props}
139
- />
140
- );
141
- }
142
-
143
- /**
144
- * Switches between panels of content. Arrow keys move between tabs, Home and End jump to the ends.
145
- * Tab height comes from the shared size scale.
146
- *
147
- * @example
148
- * ```tsx
149
- * <Tabs defaultValue="overview">
150
- * <Tabs.List aria-label="Project">
151
- * <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
152
- * <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
153
- * </Tabs.List>
154
- * <Tabs.Panel value="overview">…</Tabs.Panel>
155
- * <Tabs.Panel value="settings">…</Tabs.Panel>
156
- * </Tabs>
157
- * ```
158
- */
159
- export const Tabs = Object.assign(TabsRoot, { List: TabsList, Trigger: TabsTrigger, Panel: TabsPanel });
@@ -1,67 +0,0 @@
1
- import type { ComponentProps, CSSProperties } from 'react';
2
- import { colorClass, cx, typeClass } from '../classes';
3
- import type { Color, TextStyle } from '../types';
4
- import { useConfig, useDefaults } from '../runtime';
5
-
6
-
7
- /** Elements {@link Text} can render as. */
8
- export type TextElement = 'span' | 'p' | 'div' | 'strong' | 'em' | 'small' | 'code' | 'label';
9
-
10
- /** Props for {@link Text}. */
11
- export interface TextProps extends Omit<ComponentProps<'span'>, 'color'> {
12
- /**
13
- * Element to render.
14
- * @default 'span'
15
- */
16
- as?: TextElement;
17
- /**
18
- * Text style, from the `typography.styles` config.
19
- * @default config.defaults.textStyle
20
- */
21
- textStyle?: TextStyle;
22
- /** Semantic color, from the `colors` config. Inherits the surrounding color when omitted. */
23
- color?: Color;
24
- /** Uses the muted neutral color, for secondary text. */
25
- muted?: boolean;
26
- /** Truncates with an ellipsis: `true` for one line, a number for that many lines. */
27
- truncate?: boolean | number;
28
- }
29
-
30
- /**
31
- * Text in one of the consumer's text styles.
32
- *
33
- * @example
34
- * ```tsx
35
- * <Text as="p" textStyle="caption" muted>Updated 2 minutes ago</Text>
36
- * ```
37
- */
38
- export function Text({
39
- as = 'span',
40
- textStyle,
41
- color,
42
- muted,
43
- truncate,
44
- className,
45
- style,
46
- ...props
47
- }: TextProps) {
48
- const config = useConfig();
49
- const own = useDefaults('Text');
50
- const Tag = as as 'span';
51
- const lines = truncate === true ? 1 : truncate || 0;
52
- return (
53
- <Tag
54
- className={cx(
55
- 'yarcl-text',
56
- typeClass(textStyle ?? own.textStyle ?? config.defaults.textStyle),
57
- (color ?? own.color) && cx('yarcl-text-colored', colorClass(color ?? own.color)),
58
- muted && 'yarcl-text-muted',
59
- lines === 1 && 'yarcl-text-truncate',
60
- lines > 1 && 'yarcl-text-clamp',
61
- className,
62
- )}
63
- style={lines > 1 ? ({ '--yarcl-lines': lines, ...style } as CSSProperties) : style}
64
- {...props}
65
- />
66
- );
67
- }
@@ -1,26 +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
- /** Props for {@link Textarea}. Accepts all native `<textarea>` attributes except `color`. */
9
- export interface TextareaProps extends Omit<ComponentProps<'textarea'>, 'color'>, TokenProps {}
10
-
11
- /**
12
- * A multi-line text input. Uses the size's font size and padding, and grows from
13
- * a minimum of one control height. Inside a {@link Field}, it is labelled and described automatically.
14
- *
15
- * @example
16
- * ```tsx
17
- * <Textarea rows={4} placeholder="Message" />
18
- * ```
19
- */
20
- export function Textarea(props: TextareaProps) {
21
- const own = useDefaults('Textarea');
22
- const { size, radius, color, className, ...rest } = useFieldProps(props);
23
- return (
24
- <textarea className={cx('yarcl-textarea', sizeClass(size ?? own.size), radiusClass(radius ?? own.radius, size ?? own.size), colorClass(color ?? own.color), className)} {...rest} />
25
- );
26
- }
@@ -1,190 +0,0 @@
1
- import { useEffect, useRef, useState, useSyncExternalStore, type ReactNode } from 'react';
2
- import { createPortal } from 'react-dom';
3
- import { colorClass, cx } from '../classes';
4
- import type { Color } from '../types';
5
- import { useConfig, useDefaults } from '../runtime';
6
-
7
-
8
- /** Options for {@link toast}. */
9
- export interface ToastOptions {
10
- /** Main message. */
11
- title: ReactNode;
12
- /** Supporting text. */
13
- description?: ReactNode;
14
- /**
15
- * Accent color, from the `colors` config.
16
- * @default config.defaults.color
17
- */
18
- color?: Color;
19
- /**
20
- * Time before it dismisses itself, in ms. Paused while hovered or focused. `0` keeps it until dismissed.
21
- * @default config.timing.toastDuration
22
- */
23
- duration?: number;
24
- /** A single action, e.g. Undo. Choosing it dismisses the toast. */
25
- action?: { label: string; onClick: () => void };
26
- /** Announces immediately (`role="alert"`) instead of politely. Use for errors that need attention. */
27
- urgent?: boolean;
28
- }
29
-
30
- interface ToastEntry extends ToastOptions {
31
- id: string;
32
- }
33
-
34
- let entries: ToastEntry[] = [];
35
- let counter = 0;
36
- const listeners = new Set<() => void>();
37
-
38
- function emit(next: ToastEntry[]) {
39
- entries = next;
40
- listeners.forEach((listener) => listener());
41
- }
42
-
43
- function subscribe(listener: () => void) {
44
- listeners.add(listener);
45
- return () => listeners.delete(listener);
46
- }
47
-
48
- /**
49
- * Shows a toast notification. Requires one {@link Toaster} rendered in the app.
50
- * Returns the toast's id, for {@link toast.dismiss}.
51
- *
52
- * @example
53
- * ```ts
54
- * toast({ title: 'Saved', color: 'success' });
55
- * toast({ title: 'Message deleted', action: { label: 'Undo', onClick: restore } });
56
- * ```
57
- */
58
- export function toast(options: ToastOptions): string {
59
- const id = `yarcl-toast-${++counter}`;
60
- emit([...entries, { id, ...options }]);
61
- return id;
62
- }
63
-
64
- /** Dismisses a toast by id, or all toasts when called without one. */
65
- toast.dismiss = (id?: string) => emit(id ? entries.filter((entry) => entry.id !== id) : []);
66
-
67
- /** Props for {@link Toaster}. */
68
- export interface ToasterProps {
69
- /**
70
- * Screen corner or edge where toasts appear.
71
- * @default 'bottom-right'
72
- */
73
- placement?: 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
74
- /**
75
- * Maximum toasts shown at once. When exceeded, the oldest are dismissed.
76
- * @default 4
77
- */
78
- limit?: number;
79
- /**
80
- * Accessible name of the notifications region.
81
- * @default 'Notifications'
82
- */
83
- label?: string;
84
- }
85
-
86
- function useModalHost() {
87
- const [host, setHost] = useState<HTMLElement | null>(null);
88
- useEffect(() => {
89
- const update = () => {
90
- const modals = [...document.querySelectorAll('dialog')].filter((dialog) => dialog.matches(':modal'));
91
- setHost(modals.at(-1) ?? null);
92
- };
93
- const observer = new MutationObserver(update);
94
- observer.observe(document.body, { subtree: true, childList: true, attributes: true, attributeFilter: ['open'] });
95
- update();
96
- return () => observer.disconnect();
97
- }, []);
98
- return host;
99
- }
100
-
101
- /**
102
- * Renders toasts created with {@link toast}. Render once, near the app root.
103
- * Lives in the browser's top layer, so toasts show above everything. While a modal
104
- * {@link Dialog} or {@link Drawer} is open, toasts render inside it so they stay interactive.
105
- *
106
- * @example
107
- * ```tsx
108
- * <App />
109
- * <Toaster placement="top-right" />
110
- * ```
111
- */
112
- export function Toaster({ placement = 'bottom-right', limit = 4, label = 'Notifications' }: ToasterProps) {
113
- const all = useSyncExternalStore(subscribe, () => entries, () => entries);
114
- const visible = all.slice(-limit);
115
- const ref = useRef<HTMLElement>(null);
116
- const host = useModalHost();
117
-
118
- useEffect(() => {
119
- if (all.length > limit) emit(all.slice(-limit));
120
- }, [all, limit]);
121
-
122
- useEffect(() => {
123
- const region = ref.current;
124
- if (!region?.showPopover) return;
125
- if (region.matches(':popover-open')) region.hidePopover();
126
- if (visible.length > 0) region.showPopover();
127
- }, [visible.length, all, host]);
128
-
129
- const region = (
130
- <section
131
- ref={ref}
132
- popover="manual"
133
- aria-label={label}
134
- className={cx('yarcl-toaster', `yarcl-toaster-${placement}`)}
135
- >
136
- {(placement.startsWith('top') ? [...visible].reverse() : visible).map((entry) => (
137
- <ToastItem key={entry.id} entry={entry} />
138
- ))}
139
- </section>
140
- );
141
-
142
- return host ? createPortal(region, host) : region;
143
- }
144
-
145
- function ToastItem({ entry }: { entry: ToastEntry }) {
146
- const own = useDefaults('Toast');
147
- const config = useConfig();
148
- const { id, title, description, color, action, urgent } = entry;
149
- const duration = entry.duration ?? config.timing.toastDuration;
150
- const [paused, setPaused] = useState(false);
151
-
152
- useEffect(() => {
153
- if (paused || duration <= 0) return;
154
- const timer = setTimeout(() => toast.dismiss(id), duration);
155
- return () => clearTimeout(timer);
156
- }, [id, duration, paused]);
157
-
158
- return (
159
- <div
160
- role={urgent ? 'alert' : 'status'}
161
- className={cx('yarcl-toast', colorClass(color ?? own.color))}
162
- onMouseEnter={() => setPaused(true)}
163
- onMouseLeave={() => setPaused(false)}
164
- onFocus={() => setPaused(true)}
165
- onBlur={() => setPaused(false)}
166
- >
167
- <div className="yarcl-toast-text">
168
- <div className="yarcl-toast-title">{title}</div>
169
- {description != null && <div className="yarcl-toast-description">{description}</div>}
170
- </div>
171
- {action && (
172
- <button
173
- type="button"
174
- className="yarcl-toast-action"
175
- onClick={() => {
176
- action.onClick();
177
- toast.dismiss(id);
178
- }}
179
- >
180
- {action.label}
181
- </button>
182
- )}
183
- <button type="button" className="yarcl-toast-close" aria-label="Dismiss" onClick={() => toast.dismiss(id)}>
184
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
185
- <path d="M6 6l12 12M18 6 6 18" />
186
- </svg>
187
- </button>
188
- </div>
189
- );
190
- }
@@ -1,227 +0,0 @@
1
- import {
2
- createContext,
3
- useContext,
4
- useLayoutEffect,
5
- useRef,
6
- type ComponentProps,
7
- type KeyboardEvent,
8
- type ReactNode,
9
- } from 'react';
10
- import { cx } from '../classes';
11
- import { useControllable } from '../hooks';
12
- import type { Color, Radius, Size, Variant } from '../types';
13
- import { Button } from './Button';
14
- import { useConfig, useDefaults } from '../runtime';
15
-
16
-
17
- interface ToggleGroupContextValue {
18
- selected: string[];
19
- toggle: (value: string) => void;
20
- size?: Size;
21
- color?: Color;
22
- radius?: Radius | 'size';
23
- variant: Variant;
24
- selectedVariant: Variant;
25
- disabled?: boolean;
26
- }
27
-
28
- const ToggleGroupContext = createContext<ToggleGroupContextValue | null>(null);
29
-
30
- /** Props shared by both forms of {@link ToggleGroupProps}. */
31
- export interface ToggleGroupBaseProps extends Omit<ComponentProps<'div'>, 'defaultValue' | 'onChange' | 'color'> {
32
- /** Size of every item, from the `sizes` config. */
33
- size?: Size;
34
- /** Color of every item, from the `colors` config. */
35
- color?: Color;
36
- /** Radius of every item, from the `radii` config, or `'size'` to match the item size. */
37
- radius?: Radius | 'size';
38
- /**
39
- * Variant of unselected items, from the `variants` config.
40
- * @default config.defaults.softVariant
41
- */
42
- variant?: Variant;
43
- /**
44
- * Variant of selected items, from the `variants` config.
45
- * @default config.defaults.variant
46
- */
47
- selectedVariant?: Variant;
48
- /**
49
- * Joins the items into one segmented control.
50
- * @default true
51
- */
52
- attached?: boolean;
53
- /** Disables every item. */
54
- disabled?: boolean;
55
- /** `ToggleGroup.Item`s. */
56
- children?: ReactNode;
57
- }
58
-
59
- /** Props for {@link ToggleGroup}. `type` decides whether one or many items can be on. */
60
- export type ToggleGroupProps = ToggleGroupBaseProps &
61
- (
62
- | {
63
- /** One item on at a time, like a segmented control. */
64
- type: 'single';
65
- /** Controlled selected value. */
66
- value?: string | null;
67
- /** Initially selected value when uncontrolled. */
68
- defaultValue?: string | null;
69
- /** Called with the selected value, or `null` when the selected item is turned off. */
70
- onValueChange?: (value: string | null) => void;
71
- /** Prevents turning the selected item off, so one is always on. */
72
- required?: boolean;
73
- }
74
- | {
75
- /** Any number of items on, like formatting buttons. */
76
- type: 'multiple';
77
- /** Controlled selected values. */
78
- value?: string[];
79
- /** Initially selected values when uncontrolled. */
80
- defaultValue?: string[];
81
- /** Called with all selected values. */
82
- onValueChange?: (value: string[]) => void;
83
- required?: never;
84
- }
85
- );
86
-
87
- const toArray = (value: string | string[] | null | undefined) => (value == null ? [] : Array.isArray(value) ? value : [value]);
88
-
89
- function ToggleGroupRoot(props: ToggleGroupProps) {
90
- const config = useConfig();
91
- const own = useDefaults('ToggleGroup');
92
- const {
93
- type,
94
- value,
95
- defaultValue,
96
- onValueChange,
97
- required,
98
- size = own.size,
99
- color = own.color,
100
- radius = own.radius,
101
- variant = own.variant ?? config.defaults.softVariant,
102
- selectedVariant = own.selectedVariant ?? config.defaults.variant,
103
- attached = true,
104
- disabled,
105
- className,
106
- onKeyDown,
107
- children,
108
- ...rest
109
- } = props;
110
-
111
- const [selected, setSelected] = useControllable<string[]>(
112
- value === undefined ? undefined : toArray(value),
113
- toArray(defaultValue),
114
- (next) =>
115
- type === 'multiple'
116
- ? (onValueChange as (v: string[]) => void)?.(next)
117
- : (onValueChange as (v: string | null) => void)?.(next[0] ?? null),
118
- );
119
-
120
- const ref = useRef<HTMLDivElement>(null);
121
- useLayoutEffect(() => {
122
- const items = [...(ref.current?.querySelectorAll<HTMLButtonElement>('.yarcl-toggle-item') ?? [])];
123
- const stop = items.find((item) => item.getAttribute('aria-pressed') === 'true' && !item.disabled) ?? items.find((item) => !item.disabled);
124
- items.forEach((item) => (item.tabIndex = item === stop ? 0 : -1));
125
- });
126
-
127
- function toggle(item: string) {
128
- const on = selected.includes(item);
129
- if (type === 'multiple') setSelected(on ? selected.filter((v) => v !== item) : [...selected, item]);
130
- else if (!on) setSelected([item]);
131
- else if (!required) setSelected([]);
132
- }
133
-
134
- function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {
135
- onKeyDown?.(event);
136
- const items = [...event.currentTarget.querySelectorAll<HTMLButtonElement>('.yarcl-toggle-item:not(:disabled)')];
137
- const current = items.indexOf(document.activeElement as HTMLButtonElement);
138
- if (current === -1) return;
139
- const next = {
140
- ArrowRight: (current + 1) % items.length,
141
- ArrowDown: (current + 1) % items.length,
142
- ArrowLeft: (current - 1 + items.length) % items.length,
143
- ArrowUp: (current - 1 + items.length) % items.length,
144
- Home: 0,
145
- End: items.length - 1,
146
- }[event.key];
147
- if (next === undefined) return;
148
- event.preventDefault();
149
- items.forEach((item, i) => (item.tabIndex = i === next ? 0 : -1));
150
- items[next].focus();
151
- }
152
-
153
- return (
154
- <ToggleGroupContext.Provider
155
- value={{
156
- selected,
157
- toggle,
158
- size,
159
- color,
160
- radius,
161
- variant,
162
- selectedVariant,
163
- disabled,
164
- }}
165
- >
166
- <div
167
- ref={ref}
168
- role="group"
169
- className={cx(
170
- 'yarcl-button-group yarcl-button-group-horizontal yarcl-toggle-group',
171
- attached && 'yarcl-button-group-attached',
172
- className,
173
- )}
174
- onKeyDown={handleKeyDown}
175
- {...rest}
176
- >
177
- {children}
178
- </div>
179
- </ToggleGroupContext.Provider>
180
- );
181
- }
182
-
183
- /** Props for `ToggleGroup.Item`. */
184
- export interface ToggleGroupItemProps extends Omit<ComponentProps<'button'>, 'value' | 'color'> {
185
- /** Identifies the item in the group's value. */
186
- value: string;
187
- /** Makes the item square, for icon-only items. Give it an `aria-label`. */
188
- icon?: boolean;
189
- }
190
-
191
- function ToggleGroupItem({ value, icon, disabled, className, onClick, ...props }: ToggleGroupItemProps) {
192
- const context = useContext(ToggleGroupContext);
193
- if (!context) throw new Error('yarcl: <ToggleGroup.Item> must be inside <ToggleGroup>');
194
- const on = context.selected.includes(value);
195
- return (
196
- <Button
197
- aria-pressed={on}
198
- data-value={value}
199
- disabled={disabled || context.disabled}
200
- size={context.size}
201
- color={context.color}
202
- radius={context.radius}
203
- variant={on ? context.selectedVariant : context.variant}
204
- className={cx('yarcl-toggle-item', icon && 'yarcl-icon-button', className)}
205
- onClick={(event) => {
206
- onClick?.(event);
207
- context.toggle(value);
208
- }}
209
- {...props}
210
- />
211
- );
212
- }
213
-
214
- /**
215
- * A set of toggle buttons. `type="single"` behaves like a segmented control,
216
- * `type="multiple"` like formatting buttons. Items use `aria-pressed`; arrow keys move focus.
217
- *
218
- * @example
219
- * ```tsx
220
- * <ToggleGroup type="single" defaultValue="week" aria-label="Range" required>
221
- * <ToggleGroup.Item value="day">Day</ToggleGroup.Item>
222
- * <ToggleGroup.Item value="week">Week</ToggleGroup.Item>
223
- * <ToggleGroup.Item value="month">Month</ToggleGroup.Item>
224
- * </ToggleGroup>
225
- * ```
226
- */
227
- export const ToggleGroup = Object.assign(ToggleGroupRoot, { Item: ToggleGroupItem });