@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,164 +0,0 @@
1
- import {
2
- cloneElement,
3
- useEffect,
4
- useId,
5
- useRef,
6
- type MouseEvent,
7
- type ReactElement,
8
- type ReactNode,
9
- type SyntheticEvent,
10
- } from 'react';
11
- import { cx, radiusClass, typeClass } from '../classes';
12
- import { useControllable } from '../hooks';
13
- import type { ModalSize, Radius } from '../types';
14
- import { useConfig } from '../runtime';
15
-
16
- /** Props shared by {@link Dialog} and {@link Drawer}. */
17
- export interface ModalProps {
18
- /** Heading of the dialog. Also its accessible name. */
19
- title: ReactNode;
20
- /** Supporting text below the title. Also its accessible description. */
21
- description?: ReactNode;
22
- /** Body content. Scrolls when it doesn't fit. */
23
- children?: ReactNode;
24
- /** Actions shown at the bottom, e.g. Cancel and Confirm buttons. */
25
- footer?: ReactNode;
26
- /** An element that opens the dialog when clicked, e.g. a {@link Button}. Optional when controlled. */
27
- trigger?: ReactElement<{ onClick?: (event: MouseEvent) => void }>;
28
- /** Controlled open state. */
29
- open?: boolean;
30
- /**
31
- * Initial open state when uncontrolled.
32
- * @default false
33
- */
34
- defaultOpen?: boolean;
35
- /** Called when the dialog opens or closes, including by Esc, the close button or a backdrop click. */
36
- onOpenChange?: (open: boolean) => void;
37
- /**
38
- * Closes when the backdrop is clicked.
39
- * @default true
40
- */
41
- closeOnBackdrop?: boolean;
42
- /**
43
- * Width, from the `modalSizes` config. Height follows the content, up to the viewport.
44
- * @default config.defaults.modalSize (Drawer: its component default, `sm` in the library defaults)
45
- */
46
- size?: ModalSize;
47
- }
48
-
49
- const openModals: HTMLDialogElement[] = [];
50
-
51
- function syncBackdrops() {
52
- openModals.forEach((dialog, i) => dialog.toggleAttribute('data-yarcl-covered', i < openModals.length - 1));
53
- }
54
-
55
- function removeOpenModal(dialog: HTMLDialogElement) {
56
- const index = openModals.indexOf(dialog);
57
- if (index === -1) return;
58
- openModals.splice(index, 1);
59
- dialog.removeAttribute('data-yarcl-covered');
60
- syncBackdrops();
61
- }
62
-
63
- function CloseIcon() {
64
- return (
65
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
66
- <path d="M6 6l12 12M18 6 6 18" />
67
- </svg>
68
- );
69
- }
70
-
71
- export function useModalDialog(open: boolean, setOpen: (open: boolean) => void, closeOnBackdrop = true) {
72
- const ref = useRef<HTMLDialogElement>(null);
73
-
74
- useEffect(() => {
75
- const dialog = ref.current;
76
- if (!dialog) return;
77
- if (open && !dialog.open) {
78
- dialog.showModal();
79
- openModals.push(dialog);
80
- syncBackdrops();
81
- } else if (!open) {
82
- if (dialog.open) dialog.close();
83
- removeOpenModal(dialog);
84
- }
85
- }, [open]);
86
-
87
- useEffect(() => {
88
- const dialog = ref.current;
89
- return () => {
90
- if (dialog) removeOpenModal(dialog);
91
- };
92
- }, []);
93
-
94
- return {
95
- ref,
96
- onClose(event: SyntheticEvent<HTMLDialogElement>) {
97
- if (event.target === event.currentTarget && open) setOpen(false);
98
- },
99
- onClick(event: MouseEvent<HTMLDialogElement>) {
100
- if (closeOnBackdrop && event.target === event.currentTarget) setOpen(false);
101
- },
102
- };
103
- }
104
-
105
- export function Modal({
106
- title,
107
- description,
108
- children,
109
- footer,
110
- trigger,
111
- open: openProp,
112
- defaultOpen = false,
113
- onOpenChange,
114
- closeOnBackdrop = true,
115
- size,
116
- className,
117
- radius,
118
- }: ModalProps & { className: string; radius?: Radius }) {
119
- const config = useConfig();
120
- const [open, setOpen] = useControllable(openProp, defaultOpen, onOpenChange);
121
- const dialogProps = useModalDialog(open, setOpen, closeOnBackdrop);
122
- const titleId = useId();
123
- const descriptionId = useId();
124
-
125
- return (
126
- <>
127
- {trigger &&
128
- cloneElement(trigger, {
129
- onClick(event: MouseEvent) {
130
- trigger.props.onClick?.(event);
131
- setOpen(true);
132
- },
133
- })}
134
- <dialog
135
- {...dialogProps}
136
- className={cx('yarcl-modal', `yarcl-modal-size-${size ?? config.defaults.modalSize}`, radiusClass(radius), className)}
137
- aria-labelledby={titleId}
138
- aria-describedby={description != null ? descriptionId : undefined}
139
- >
140
- {open && (
141
- <div className="yarcl-modal-content">
142
- <div className="yarcl-modal-header">
143
- <div className="yarcl-modal-heading">
144
- <h2 id={titleId} className={cx('yarcl-modal-title', typeClass(config.typography.headings.h2))}>
145
- {title}
146
- </h2>
147
- {description != null && (
148
- <p id={descriptionId} className="yarcl-modal-description">
149
- {description}
150
- </p>
151
- )}
152
- </div>
153
- <button type="button" className="yarcl-modal-close" aria-label="Close" onClick={() => setOpen(false)}>
154
- <CloseIcon />
155
- </button>
156
- </div>
157
- {children != null && <div className="yarcl-modal-body">{children}</div>}
158
- {footer != null && <div className="yarcl-modal-footer">{footer}</div>}
159
- </div>
160
- )}
161
- </dialog>
162
- </>
163
- );
164
- }
@@ -1,208 +0,0 @@
1
- import { useId, useState, type ComponentProps, type KeyboardEvent, type FocusEvent } from 'react';
2
- import { colorClass, cx, radiusClass, sizeClass } from '../classes';
3
- import { useFieldProps } from '../field-context';
4
- import { ChevronIcon } from '../floating';
5
- import { useControllable } from '../hooks';
6
- import type { TokenProps } from '../types';
7
- import { useDefaults } from '../runtime';
8
-
9
- /**
10
- * Props for {@link NumberInput}. Native `<input>` attributes go to the inner input;
11
- * `className` and `style` go to the outer box.
12
- */
13
- export interface NumberInputProps
14
- extends TokenProps,
15
- Omit<ComponentProps<'input'>, 'color' | 'size' | 'type' | 'value' | 'defaultValue' | 'onChange' | 'min' | 'max' | 'step'> {
16
- /** Controlled value. `null` means the input is empty. */
17
- value?: number | null;
18
- /**
19
- * Initial value when uncontrolled.
20
- * @default null
21
- */
22
- defaultValue?: number | null;
23
- /** Called with the new value after a step, or when typed text is committed on blur or Enter. */
24
- onValueChange?: (value: number | null) => void;
25
- /** Lowest allowed value. Typed values are clamped to it, and `Home` jumps to it. */
26
- min?: number;
27
- /** Highest allowed value. Typed values are clamped to it, and `End` jumps to it. */
28
- max?: number;
29
- /**
30
- * Amount added or removed by the stepper buttons and the arrow keys. `PageUp` and `PageDown` step ten times as far.
31
- * @default 1
32
- */
33
- step?: number;
34
- /**
35
- * Accessible label of the increment button.
36
- * @default 'Increase'
37
- */
38
- incrementLabel?: string;
39
- /**
40
- * Accessible label of the decrement button.
41
- * @default 'Decrease'
42
- */
43
- decrementLabel?: string;
44
- }
45
-
46
- function decimals(n: number): number {
47
- const text = String(n);
48
- const exp = text.match(/e-(\d+)$/);
49
- if (exp) return Number(exp[1]);
50
- return text.split('.')[1]?.length ?? 0;
51
- }
52
-
53
- function parse(text: string): number | null | undefined {
54
- const trimmed = text.trim();
55
- if (trimmed === '') return null;
56
- const n = Number(trimmed);
57
- return Number.isFinite(n) ? n : undefined;
58
- }
59
-
60
- /**
61
- * A numeric input with increment and decrement buttons, exposed as a `spinbutton`.
62
- * Arrow keys step by `step`, `PageUp` and `PageDown` by ten steps, `Home` and `End` jump to `min` and `max`.
63
- * Shares the size scale with {@link Input}, so both have the same height at the same size. Works inside a {@link Field}.
64
- *
65
- * @example
66
- * ```tsx
67
- * <Field label="Guests">
68
- * <NumberInput min={1} max={10} defaultValue={2} onValueChange={setGuests} />
69
- * </Field>
70
- * ```
71
- */
72
- export function NumberInput(props: NumberInputProps) {
73
- const own = useDefaults('NumberInput');
74
- const fallbackId = useId();
75
- const {
76
- value: valueProp,
77
- defaultValue = null,
78
- onValueChange,
79
- min,
80
- max,
81
- step = 1,
82
- incrementLabel = 'Increase',
83
- decrementLabel = 'Decrease',
84
- size,
85
- radius,
86
- color,
87
- className,
88
- style,
89
- id = fallbackId,
90
- disabled,
91
- readOnly,
92
- onBlur,
93
- onKeyDown,
94
- ...rest
95
- } = useFieldProps(props);
96
- const [value, setValue] = useControllable(valueProp, defaultValue, onValueChange);
97
- const [draft, setDraft] = useState<string | null>(null);
98
-
99
- const clamp = (n: number) => Math.min(max ?? Infinity, Math.max(min ?? -Infinity, n));
100
- const locked = disabled || readOnly;
101
-
102
- function update(next: number | null) {
103
- setDraft(null);
104
- if (next !== value) setValue(next);
105
- }
106
-
107
- function current(): number | null {
108
- if (draft === null) return value;
109
- const parsed = parse(draft);
110
- return parsed === undefined ? value : parsed === null ? null : clamp(parsed);
111
- }
112
-
113
- function stepBy(amount: number) {
114
- if (locked) return;
115
- const base = current();
116
- if (base === null) {
117
- update(clamp(amount > 0 ? (min ?? 0) : (max ?? 0)));
118
- return;
119
- }
120
- const precision = Math.max(decimals(base), decimals(step));
121
- update(clamp(Number((base + amount).toFixed(precision))));
122
- }
123
-
124
- function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
125
- onKeyDown?.(event);
126
- if (event.defaultPrevented) return;
127
- const actions: Record<string, (() => void) | undefined> = {
128
- ArrowUp: () => stepBy(step),
129
- ArrowDown: () => stepBy(-step),
130
- PageUp: () => stepBy(step * 10),
131
- PageDown: () => stepBy(-step * 10),
132
- Home: min === undefined || locked ? undefined : () => update(min),
133
- End: max === undefined || locked ? undefined : () => update(max),
134
- Enter: draft === null ? undefined : () => update(current()),
135
- };
136
- const action = actions[event.key];
137
- if (!action) return;
138
- if (event.key !== 'Enter') event.preventDefault();
139
- action();
140
- }
141
-
142
- function handleBlur(event: FocusEvent<HTMLInputElement>) {
143
- onBlur?.(event);
144
- if (draft !== null) update(current());
145
- }
146
-
147
- const shown = current();
148
- const canIncrement = !locked && (shown === null || max === undefined || shown < max);
149
- const canDecrement = !locked && (shown === null || min === undefined || shown > min);
150
-
151
- return (
152
- <div
153
- className={cx(
154
- 'yarcl-number-input',
155
- sizeClass(size ?? own.size),
156
- radiusClass(radius ?? own.radius, size ?? own.size),
157
- colorClass(color ?? own.color),
158
- className,
159
- )}
160
- style={style}
161
- >
162
- <input
163
- {...rest}
164
- id={id}
165
- type="text"
166
- inputMode="decimal"
167
- autoComplete="off"
168
- role="spinbutton"
169
- aria-valuenow={value ?? undefined}
170
- aria-valuemin={min}
171
- aria-valuemax={max}
172
- className="yarcl-input yarcl-number-input-field"
173
- value={draft ?? (value === null ? '' : String(value))}
174
- disabled={disabled}
175
- readOnly={readOnly}
176
- onChange={(event) => setDraft(event.target.value)}
177
- onKeyDown={handleKeyDown}
178
- onBlur={handleBlur}
179
- />
180
- <div className="yarcl-number-input-steppers">
181
- <button
182
- type="button"
183
- tabIndex={-1}
184
- className="yarcl-number-input-stepper yarcl-number-input-increment"
185
- aria-label={incrementLabel}
186
- aria-controls={id}
187
- disabled={!canIncrement}
188
- onMouseDown={(event) => event.preventDefault()}
189
- onClick={() => stepBy(step)}
190
- >
191
- <ChevronIcon />
192
- </button>
193
- <button
194
- type="button"
195
- tabIndex={-1}
196
- className="yarcl-number-input-stepper yarcl-number-input-decrement"
197
- aria-label={decrementLabel}
198
- aria-controls={id}
199
- disabled={!canDecrement}
200
- onMouseDown={(event) => event.preventDefault()}
201
- onClick={() => stepBy(-step)}
202
- >
203
- <ChevronIcon />
204
- </button>
205
- </div>
206
- </div>
207
- );
208
- }
@@ -1,227 +0,0 @@
1
- import type { ComponentProps, KeyboardEvent } from 'react';
2
- import { colorClass, cx, radiusClass, sizeClass, variantClass } from '../classes';
3
- import { useControllable } from '../hooks';
4
- import type { Color, Radius, Size, Variant } from '../types';
5
- import { Button } from './Button';
6
- import { IconButton } from './IconButton';
7
- import { useConfig, useDefaults } from '../runtime';
8
-
9
- /** Props for {@link Pagination}. */
10
- export interface PaginationProps extends Omit<ComponentProps<'nav'>, 'color' | 'onChange'> {
11
- /**
12
- * Total number of pages.
13
- * @example
14
- * ```tsx
15
- * <Pagination count={Math.ceil(total / perPage)} />
16
- * ```
17
- */
18
- count: number;
19
- /** Controlled current page, starting at 1. */
20
- page?: number;
21
- /**
22
- * Initial page when uncontrolled.
23
- * @default 1
24
- */
25
- defaultPage?: number;
26
- /** Called with the new page when the user picks one. */
27
- onPageChange?: (page: number) => void;
28
- /**
29
- * Pages shown on each side of the current page.
30
- * @default 1
31
- */
32
- siblings?: number;
33
- /**
34
- * Pages always shown at the start and end.
35
- * @default 1
36
- */
37
- boundaries?: number;
38
- /** Size of every button, from the `sizes` config. */
39
- size?: Size;
40
- /** Color of every button, from the `colors` config. */
41
- color?: Color;
42
- /** Radius of every button, from the `radii` config, or `'size'` to match the button size. */
43
- radius?: Radius | 'size';
44
- /**
45
- * Variant of the other pages and the previous and next buttons, from the `variants` config.
46
- * @default config.defaults.softVariant
47
- */
48
- variant?: Variant;
49
- /**
50
- * Variant of the current page, from the `variants` config.
51
- * @default config.defaults.variant
52
- */
53
- selectedVariant?: Variant;
54
- /**
55
- * Joins the buttons into one control with shared borders.
56
- * @default false
57
- */
58
- attached?: boolean;
59
- /** Disables every button. */
60
- disabled?: boolean;
61
- /**
62
- * Accessible name of the navigation landmark.
63
- * @default 'Pagination'
64
- */
65
- 'aria-label'?: string;
66
- /**
67
- * Accessible name of the previous button.
68
- * @default 'Previous page'
69
- */
70
- previousLabel?: string;
71
- /**
72
- * Accessible name of the next button.
73
- * @default 'Next page'
74
- */
75
- nextLabel?: string;
76
- /**
77
- * Accessible name of each page button.
78
- * @default (page) => `Page ${page}`
79
- */
80
- pageLabel?: (page: number) => string;
81
- }
82
-
83
- type Item = number | 'start-ellipsis' | 'end-ellipsis';
84
-
85
- const range = (start: number, end: number) => Array.from({ length: Math.max(0, end - start + 1) }, (_, i) => start + i);
86
-
87
- function pageItems(page: number, count: number, siblings: number, boundaries: number): Item[] {
88
- const startPages = range(1, Math.min(boundaries, count));
89
- const endPages = range(Math.max(count - boundaries + 1, boundaries + 1), count);
90
- const siblingsStart = Math.max(Math.min(page - siblings, count - boundaries - siblings * 2 - 1), boundaries + 2);
91
- const siblingsEnd = Math.min(Math.max(page + siblings, boundaries + siblings * 2 + 2), count - boundaries - 1);
92
- const items: Item[] = [...startPages];
93
- if (siblingsStart > boundaries + 2) items.push('start-ellipsis');
94
- else if (boundaries + 1 < count - boundaries) items.push(boundaries + 1);
95
- items.push(...range(siblingsStart, siblingsEnd));
96
- if (siblingsEnd < count - boundaries - 1) items.push('end-ellipsis');
97
- else if (count - boundaries > boundaries) items.push(count - boundaries);
98
- items.push(...endPages);
99
- return items.filter((item, i) => typeof item !== 'number' || (item >= 1 && item <= count && items.indexOf(item) === i));
100
- }
101
-
102
- const Chevron = ({ d }: { d: string }) => (
103
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
104
- <path d={d} />
105
- </svg>
106
- );
107
-
108
- /**
109
- * Navigation between pages of results: previous and next buttons, the pages around the current one,
110
- * and the first and last pages with an ellipsis for the gaps. Renders a `<nav>` landmark; the current
111
- * page has `aria-current="page"`. Arrow keys, <kbd>Home</kbd> and <kbd>End</kbd> move focus between buttons.
112
- *
113
- * @example
114
- * ```tsx
115
- * <Pagination count={20} page={page} onPageChange={setPage} aria-label="Search results" />
116
- * ```
117
- */
118
- export function Pagination({
119
- count,
120
- page,
121
- defaultPage = 1,
122
- onPageChange,
123
- siblings = 1,
124
- boundaries = 1,
125
- size,
126
- color,
127
- radius,
128
- variant,
129
- selectedVariant,
130
- attached = false,
131
- disabled,
132
- previousLabel = 'Previous page',
133
- nextLabel = 'Next page',
134
- pageLabel = (n) => `Page ${n}`,
135
- 'aria-label': ariaLabel = 'Pagination',
136
- className,
137
- onKeyDown,
138
- ...props
139
- }: PaginationProps) {
140
- const config = useConfig();
141
- const own = useDefaults('Pagination');
142
- const resolvedSize = size ?? own.size ?? config.defaults.size;
143
- const resolvedRadius = radius ?? own.radius ?? config.defaults.radius;
144
- const resolvedColor = color ?? own.color ?? config.defaults.color;
145
- const resolvedVariant = variant ?? own.variant ?? config.defaults.softVariant;
146
- const resolvedSelected = selectedVariant ?? own.selectedVariant ?? config.defaults.variant;
147
-
148
- const total = Math.max(0, Math.floor(count));
149
- const [current, setCurrent] = useControllable(page, defaultPage, onPageChange);
150
- const active = Math.min(Math.max(1, current), Math.max(1, total));
151
- const items = pageItems(active, total, Math.max(0, siblings), Math.max(0, boundaries));
152
-
153
- const go = (next: number) => {
154
- if (disabled || next === active || next < 1 || next > total) return;
155
- setCurrent(next);
156
- };
157
-
158
- function handleKeyDown(event: KeyboardEvent<HTMLElement>) {
159
- onKeyDown?.(event);
160
- const buttons = [...event.currentTarget.querySelectorAll<HTMLButtonElement>('button:not(:disabled)')];
161
- const index = buttons.indexOf(document.activeElement as HTMLButtonElement);
162
- if (index === -1) return;
163
- const next = {
164
- ArrowRight: Math.min(index + 1, buttons.length - 1),
165
- ArrowLeft: Math.max(index - 1, 0),
166
- Home: 0,
167
- End: buttons.length - 1,
168
- }[event.key];
169
- if (next === undefined) return;
170
- event.preventDefault();
171
- buttons[next].focus();
172
- }
173
-
174
- const shared = { size: resolvedSize, radius: resolvedRadius, color: resolvedColor, disabled };
175
- const edge = (label: string, target: number, d: string) => {
176
- const unavailable = target < 1 || target > total;
177
- return (
178
- <IconButton
179
- {...shared}
180
- variant={resolvedVariant}
181
- aria-label={label}
182
- aria-disabled={unavailable || undefined}
183
- onClick={() => go(target)}
184
- >
185
- <Chevron d={d} />
186
- </IconButton>
187
- );
188
- };
189
-
190
- return (
191
- <nav aria-label={ariaLabel} className={cx('yarcl-pagination', className)} onKeyDown={handleKeyDown} {...props}>
192
- <div className={cx('yarcl-button-group yarcl-button-group-horizontal', attached && 'yarcl-button-group-attached')}>
193
- {edge(previousLabel, active - 1, 'm15 18-6-6 6-6')}
194
- {items.map((item) =>
195
- typeof item === 'number' ? (
196
- <Button
197
- key={item}
198
- {...shared}
199
- variant={item === active ? resolvedSelected : resolvedVariant}
200
- className="yarcl-pagination-page"
201
- aria-label={pageLabel(item)}
202
- aria-current={item === active ? 'page' : undefined}
203
- onClick={() => go(item)}
204
- >
205
- {item}
206
- </Button>
207
- ) : (
208
- <span
209
- key={item}
210
- aria-hidden="true"
211
- className={cx(
212
- 'yarcl-button yarcl-pagination-ellipsis',
213
- sizeClass(resolvedSize),
214
- radiusClass(resolvedRadius, resolvedSize),
215
- colorClass(resolvedColor),
216
- variantClass(resolvedVariant),
217
- )}
218
- >
219
- …
220
- </span>
221
- ),
222
- )}
223
- {edge(nextLabel, active + 1, 'm9 18 6-6-6-6')}
224
- </div>
225
- </nav>
226
- );
227
- }