@yarcl/react 0.1.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.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/color.d.ts +12 -0
- package/dist/color.js +41 -0
- package/dist/css.d.ts +10 -0
- package/dist/css.js +175 -0
- package/dist/define.d.ts +383 -0
- package/dist/define.js +29 -0
- package/dist/plugin.d.ts +33 -0
- package/dist/plugin.js +80 -0
- package/package.json +66 -0
- package/src/apply.ts +46 -0
- package/src/classes.ts +23 -0
- package/src/color.ts +48 -0
- package/src/components/Accordion.tsx +274 -0
- package/src/components/Alert.tsx +85 -0
- package/src/components/Badge.tsx +79 -0
- package/src/components/Breadcrumb.tsx +239 -0
- package/src/components/Button.tsx +58 -0
- package/src/components/ButtonGroup.tsx +61 -0
- package/src/components/Card.tsx +50 -0
- package/src/components/Checkbox.tsx +49 -0
- package/src/components/Combobox.tsx +522 -0
- package/src/components/CommandPalette.tsx +452 -0
- package/src/components/DatePicker.tsx +432 -0
- package/src/components/Dialog.tsx +39 -0
- package/src/components/Divider.tsx +34 -0
- package/src/components/Drawer.tsx +31 -0
- package/src/components/Field.tsx +61 -0
- package/src/components/Heading.tsx +40 -0
- package/src/components/HoverCard.tsx +108 -0
- package/src/components/IconButton.tsx +59 -0
- package/src/components/Inline.tsx +42 -0
- package/src/components/Input.tsx +28 -0
- package/src/components/Label.tsx +58 -0
- package/src/components/Link.tsx +41 -0
- package/src/components/Menu.tsx +207 -0
- package/src/components/Modal.tsx +164 -0
- package/src/components/NumberInput.tsx +208 -0
- package/src/components/Pagination.tsx +227 -0
- package/src/components/Popover.tsx +140 -0
- package/src/components/Progress.tsx +122 -0
- package/src/components/Radio.tsx +59 -0
- package/src/components/RadioGroup.tsx +114 -0
- package/src/components/Select.tsx +197 -0
- package/src/components/Skeleton.tsx +95 -0
- package/src/components/Slider.tsx +315 -0
- package/src/components/Spinner.tsx +38 -0
- package/src/components/Stack.tsx +57 -0
- package/src/components/Switch.tsx +42 -0
- package/src/components/Table.tsx +99 -0
- package/src/components/Tabs.tsx +159 -0
- package/src/components/Text.tsx +67 -0
- package/src/components/Textarea.tsx +26 -0
- package/src/components/Toast.tsx +190 -0
- package/src/components/ToggleGroup.tsx +227 -0
- package/src/components/Tooltip.tsx +78 -0
- package/src/contrast.ts +82 -0
- package/src/css.ts +228 -0
- package/src/define.check.ts +259 -0
- package/src/define.ts +379 -0
- package/src/field-context.ts +31 -0
- package/src/floating.tsx +61 -0
- package/src/hooks.ts +19 -0
- package/src/index.ts +119 -0
- package/src/plugin.ts +94 -0
- package/src/reference/DesignReference.tsx +374 -0
- package/src/reference/index.ts +2 -0
- package/src/reference/reference.css +58 -0
- package/src/runtime.ts +38 -0
- package/src/styles.css +1971 -0
- package/src/themes/bloom.ts +72 -0
- package/src/themes/brutalist.ts +63 -0
- package/src/themes/compact.ts +63 -0
- package/src/themes/contract.ts +24 -0
- package/src/themes/editorial.ts +55 -0
- package/src/themes/index.ts +47 -0
- package/src/types.ts +69 -0
- package/src/yarcl.config.ts +104 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ComponentProps, CSSProperties } from 'react';
|
|
2
|
+
import { cx, radiusClass, sizeClass, typeClass } from '../classes';
|
|
3
|
+
import type { Radius, Size, TextStyle } from '../types';
|
|
4
|
+
import { useDefaults } from '../runtime';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/** Props for {@link Skeleton}. */
|
|
8
|
+
export interface SkeletonProps extends Omit<ComponentProps<'span'>, 'children'> {
|
|
9
|
+
/**
|
|
10
|
+
* `text`: lines matching a text style; `control`: the height of a control of the given `size`;
|
|
11
|
+
* `circle`: a circle, e.g. for an avatar; `rect`: a block sized by `width` and `height`.
|
|
12
|
+
* @default 'text'
|
|
13
|
+
*/
|
|
14
|
+
shape?: 'text' | 'control' | 'circle' | 'rect';
|
|
15
|
+
/** Number of lines for `shape="text"`. The last line is shorter. */
|
|
16
|
+
lines?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Text style whose font size and line height the lines match, for `shape="text"`.
|
|
19
|
+
* @default config.defaults.textStyle
|
|
20
|
+
*/
|
|
21
|
+
textStyle?: TextStyle;
|
|
22
|
+
/**
|
|
23
|
+
* Control size, from the `sizes` config. Sets the height for `shape="control"` and the diameter for `shape="circle"`.
|
|
24
|
+
* @default config.defaults.size
|
|
25
|
+
*/
|
|
26
|
+
size?: Size;
|
|
27
|
+
/**
|
|
28
|
+
* Corner radius for `control` and `rect`, from the `radii` config.
|
|
29
|
+
* @default config.defaults.radius
|
|
30
|
+
*/
|
|
31
|
+
radius?: Radius | 'size';
|
|
32
|
+
/** Width as a CSS length. */
|
|
33
|
+
width?: string;
|
|
34
|
+
/** Height as a CSS length, for `shape="rect"`. */
|
|
35
|
+
height?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A placeholder shown while content loads. Hidden from assistive technology;
|
|
40
|
+
* mark the loading region with `aria-busy` instead.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* <Stack aria-busy>
|
|
45
|
+
* <Skeleton shape="text" textStyle="title" width="40%" />
|
|
46
|
+
* <Skeleton shape="text" lines={3} />
|
|
47
|
+
* <Skeleton shape="control" width="12rem" />
|
|
48
|
+
* </Stack>
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function Skeleton({
|
|
52
|
+
shape = 'text',
|
|
53
|
+
lines = 1,
|
|
54
|
+
textStyle,
|
|
55
|
+
size,
|
|
56
|
+
radius,
|
|
57
|
+
width,
|
|
58
|
+
height,
|
|
59
|
+
className,
|
|
60
|
+
style,
|
|
61
|
+
...props
|
|
62
|
+
}: SkeletonProps) {
|
|
63
|
+
const own = useDefaults('Skeleton');
|
|
64
|
+
const dimensions = { width, height, ...style } as CSSProperties;
|
|
65
|
+
|
|
66
|
+
if (shape === 'text') {
|
|
67
|
+
return (
|
|
68
|
+
<span
|
|
69
|
+
aria-hidden="true"
|
|
70
|
+
className={cx('yarcl-skeleton-text', textStyle && typeClass(textStyle), className)}
|
|
71
|
+
style={dimensions}
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
{Array.from({ length: lines }, (_, i) => (
|
|
75
|
+
<span key={i} className="yarcl-skeleton yarcl-skeleton-line" />
|
|
76
|
+
))}
|
|
77
|
+
</span>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<span
|
|
83
|
+
aria-hidden="true"
|
|
84
|
+
className={cx(
|
|
85
|
+
'yarcl-skeleton',
|
|
86
|
+
`yarcl-skeleton-${shape}`,
|
|
87
|
+
shape !== 'rect' && sizeClass(size ?? own.size),
|
|
88
|
+
shape !== 'circle' && radiusClass(radius ?? own.radius, size ?? own.size),
|
|
89
|
+
className,
|
|
90
|
+
)}
|
|
91
|
+
style={dimensions}
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useContext,
|
|
3
|
+
useId,
|
|
4
|
+
useRef,
|
|
5
|
+
type ComponentProps,
|
|
6
|
+
type CSSProperties,
|
|
7
|
+
type KeyboardEvent,
|
|
8
|
+
type PointerEvent,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { colorClass, cx, radiusClass, sizeClass } from '../classes';
|
|
11
|
+
import { FieldContext, useFieldProps } from '../field-context';
|
|
12
|
+
import { useControllable } from '../hooks';
|
|
13
|
+
import type { TokenProps } from '../types';
|
|
14
|
+
import { useDefaults } from '../runtime';
|
|
15
|
+
|
|
16
|
+
/** A {@link Slider} value: one number, or a `[start, end]` pair for a range. */
|
|
17
|
+
export type SliderValue = number | [number, number];
|
|
18
|
+
|
|
19
|
+
/** Props for {@link Slider}. Accepts all native `<div>` attributes except `color`, `defaultValue`, `onChange` and `children`. */
|
|
20
|
+
export interface SliderProps<V extends SliderValue = number>
|
|
21
|
+
extends TokenProps,
|
|
22
|
+
Omit<ComponentProps<'div'>, 'color' | 'defaultValue' | 'onChange' | 'children'> {
|
|
23
|
+
/**
|
|
24
|
+
* Controlled value. Pass a `[start, end]` pair for a range with two thumbs.
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* <Slider value={volume} onValueChange={setVolume} aria-label="Volume" />
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
value?: V;
|
|
31
|
+
/**
|
|
32
|
+
* Initial value when uncontrolled. Pass a `[start, end]` pair for a range with two thumbs.
|
|
33
|
+
* @default min
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* <Slider defaultValue={[20, 80]} aria-label="Price" />
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
defaultValue?: V;
|
|
40
|
+
/**
|
|
41
|
+
* Called with the new value while the user drags or presses a key.
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* <Slider onValueChange={(value) => setVolume(value)} aria-label="Volume" />
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
onValueChange?: (value: V) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Lowest value.
|
|
50
|
+
* @default 0
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* <Slider min={-10} max={10} aria-label="Offset" />
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
min?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Highest value.
|
|
59
|
+
* @default 100
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* <Slider max={10} aria-label="Rating" />
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
max?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Interval between values. Values snap to multiples of it, counted from `min`.
|
|
68
|
+
* @default 1
|
|
69
|
+
* @example
|
|
70
|
+
* ```tsx
|
|
71
|
+
* <Slider step={5} aria-label="Opacity" />
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
step?: number;
|
|
75
|
+
/**
|
|
76
|
+
* Jump for PageUp and PageDown.
|
|
77
|
+
* @default a tenth of the range, rounded to a whole number of steps
|
|
78
|
+
* @example
|
|
79
|
+
* ```tsx
|
|
80
|
+
* <Slider step={1} largeStep={25} aria-label="Progress" />
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
largeStep?: number;
|
|
84
|
+
/**
|
|
85
|
+
* Prevents interaction and dims the slider.
|
|
86
|
+
* @default false
|
|
87
|
+
* @example
|
|
88
|
+
* ```tsx
|
|
89
|
+
* <Slider disabled defaultValue={40} aria-label="Volume" />
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
disabled?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Form field name. Renders a hidden input per thumb with its value.
|
|
95
|
+
* @example
|
|
96
|
+
* ```tsx
|
|
97
|
+
* <Slider name="volume" aria-label="Volume" />
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
name?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Formats a value for screen readers (`aria-valuetext`).
|
|
103
|
+
* @example
|
|
104
|
+
* ```tsx
|
|
105
|
+
* <Slider formatValue={(value) => `${value} percent`} aria-label="Volume" />
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
formatValue?: (value: number) => string;
|
|
109
|
+
/**
|
|
110
|
+
* Names of the two thumbs of a range, added to the slider's label.
|
|
111
|
+
* @default ['Minimum', 'Maximum']
|
|
112
|
+
* @example
|
|
113
|
+
* ```tsx
|
|
114
|
+
* <Slider defaultValue={[9, 17]} thumbLabels={['Opens', 'Closes']} aria-label="Hours" />
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
thumbLabels?: readonly [string, string];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const clamp = (value: number, low: number, high: number) => Math.min(Math.max(value, low), high);
|
|
121
|
+
|
|
122
|
+
function decimals(value: number): number {
|
|
123
|
+
const text = String(value);
|
|
124
|
+
const exponent = text.match(/e-(\d+)$/);
|
|
125
|
+
if (exponent) return Number(exponent[1]);
|
|
126
|
+
return text.split('.')[1]?.length ?? 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const DEFAULT_THUMB_LABELS = ['Minimum', 'Maximum'] as const;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Picks a number, or a range with two thumbs, by dragging along a track.
|
|
133
|
+
* Each thumb has `role="slider"` and supports the arrow keys, Home, End, PageUp and PageDown.
|
|
134
|
+
* Shares the size scale with the other controls. Works inside a {@link Field}; outside one, give it an `aria-label`.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```tsx
|
|
138
|
+
* <Field label="Volume">
|
|
139
|
+
* <Slider defaultValue={40} />
|
|
140
|
+
* </Field>
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export function Slider<V extends SliderValue = number>(props: SliderProps<V>) {
|
|
144
|
+
const own = useDefaults('Slider');
|
|
145
|
+
const field = useContext(FieldContext);
|
|
146
|
+
const uid = useId();
|
|
147
|
+
const {
|
|
148
|
+
value: valueProp,
|
|
149
|
+
defaultValue,
|
|
150
|
+
onValueChange,
|
|
151
|
+
min = 0,
|
|
152
|
+
max = 100,
|
|
153
|
+
step = 1,
|
|
154
|
+
largeStep,
|
|
155
|
+
disabled = false,
|
|
156
|
+
name,
|
|
157
|
+
formatValue,
|
|
158
|
+
thumbLabels = DEFAULT_THUMB_LABELS,
|
|
159
|
+
size,
|
|
160
|
+
radius,
|
|
161
|
+
color,
|
|
162
|
+
className,
|
|
163
|
+
style,
|
|
164
|
+
id,
|
|
165
|
+
required: _required,
|
|
166
|
+
'aria-label': ariaLabel,
|
|
167
|
+
'aria-labelledby': ariaLabelledBy,
|
|
168
|
+
'aria-describedby': ariaDescribedBy,
|
|
169
|
+
'aria-invalid': ariaInvalid,
|
|
170
|
+
onPointerDown,
|
|
171
|
+
onPointerMove,
|
|
172
|
+
onPointerUp,
|
|
173
|
+
onLostPointerCapture,
|
|
174
|
+
...rest
|
|
175
|
+
} = useFieldProps(props) as SliderProps<V> & { required?: boolean };
|
|
176
|
+
|
|
177
|
+
const [value, setValue] = useControllable<V>(valueProp, defaultValue ?? (min as V), onValueChange);
|
|
178
|
+
const trackRef = useRef<HTMLDivElement>(null);
|
|
179
|
+
const thumbRefs = useRef<(HTMLDivElement | null)[]>([]);
|
|
180
|
+
const dragging = useRef<number | null>(null);
|
|
181
|
+
|
|
182
|
+
const range = Array.isArray(value);
|
|
183
|
+
const values = (range ? value : [value]).map((v) => clamp(v, min, max));
|
|
184
|
+
const precision = decimals(step);
|
|
185
|
+
const big = largeStep ?? Math.max(step, Math.round((max - min) / 10 / step) * step);
|
|
186
|
+
|
|
187
|
+
const snap = (raw: number) => Number(clamp(min + Math.round((raw - min) / step) * step, min, max).toFixed(precision));
|
|
188
|
+
const lowerBound = (index: number) => (range && index === 1 ? values[0] : min);
|
|
189
|
+
const upperBound = (index: number) => (range && index === 0 ? values[1] : max);
|
|
190
|
+
const percent = (v: number) => (max > min ? ((v - min) / (max - min)) * 100 : 0);
|
|
191
|
+
|
|
192
|
+
function update(index: number, raw: number) {
|
|
193
|
+
const next = clamp(snap(raw), lowerBound(index), upperBound(index));
|
|
194
|
+
if (next === values[index]) return;
|
|
195
|
+
const nextValues = values.slice();
|
|
196
|
+
nextValues[index] = next;
|
|
197
|
+
setValue((range ? [nextValues[0], nextValues[1]] : nextValues[0]) as V);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const rtl = () => trackRef.current != null && getComputedStyle(trackRef.current).direction === 'rtl';
|
|
201
|
+
|
|
202
|
+
function valueAt(clientX: number): number {
|
|
203
|
+
const rect = trackRef.current!.getBoundingClientRect();
|
|
204
|
+
const ratio = rect.width > 0 ? clamp((clientX - rect.left) / rect.width, 0, 1) : 0;
|
|
205
|
+
return min + (rtl() ? 1 - ratio : ratio) * (max - min);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function handlePointerDown(event: PointerEvent<HTMLDivElement>) {
|
|
209
|
+
onPointerDown?.(event);
|
|
210
|
+
if (disabled || event.defaultPrevented || event.button !== 0) return;
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
const raw = valueAt(event.clientX);
|
|
213
|
+
let index = 0;
|
|
214
|
+
if (range) {
|
|
215
|
+
const [start, end] = values;
|
|
216
|
+
const toStart = Math.abs(raw - start);
|
|
217
|
+
const toEnd = Math.abs(raw - end);
|
|
218
|
+
index = toEnd < toStart || (toEnd === toStart && raw > end) ? 1 : 0;
|
|
219
|
+
}
|
|
220
|
+
dragging.current = index;
|
|
221
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
222
|
+
thumbRefs.current[index]?.focus();
|
|
223
|
+
update(index, raw);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function handlePointerMove(event: PointerEvent<HTMLDivElement>) {
|
|
227
|
+
onPointerMove?.(event);
|
|
228
|
+
if (dragging.current != null) update(dragging.current, valueAt(event.clientX));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function handlePointerUp(event: PointerEvent<HTMLDivElement>) {
|
|
232
|
+
onPointerUp?.(event);
|
|
233
|
+
dragging.current = null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function handleLostPointerCapture(event: PointerEvent<HTMLDivElement>) {
|
|
237
|
+
onLostPointerCapture?.(event);
|
|
238
|
+
dragging.current = null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function handleKeyDown(index: number, event: KeyboardEvent<HTMLDivElement>) {
|
|
242
|
+
if (disabled) return;
|
|
243
|
+
const current = values[index];
|
|
244
|
+
const forward = rtl() ? -step : step;
|
|
245
|
+
const moves: Record<string, number> = {
|
|
246
|
+
ArrowRight: current + forward,
|
|
247
|
+
ArrowLeft: current - forward,
|
|
248
|
+
ArrowUp: current + step,
|
|
249
|
+
ArrowDown: current - step,
|
|
250
|
+
PageUp: current + big,
|
|
251
|
+
PageDown: current - big,
|
|
252
|
+
Home: lowerBound(index),
|
|
253
|
+
End: upperBound(index),
|
|
254
|
+
};
|
|
255
|
+
if (!(event.key in moves)) return;
|
|
256
|
+
event.preventDefault();
|
|
257
|
+
update(index, moves[event.key]);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const outerLabelledBy = ariaLabelledBy ?? (field ? field.labelId : undefined);
|
|
261
|
+
const start = range ? percent(values[0]) : 0;
|
|
262
|
+
const end = percent(values[values.length - 1]);
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<div
|
|
266
|
+
className={cx(
|
|
267
|
+
'yarcl-slider',
|
|
268
|
+
sizeClass(size ?? own.size),
|
|
269
|
+
radiusClass(radius ?? own.radius, size ?? own.size),
|
|
270
|
+
colorClass(color ?? own.color),
|
|
271
|
+
className,
|
|
272
|
+
)}
|
|
273
|
+
style={{ ...style, '--yarcl-slider-start': `${start}%`, '--yarcl-slider-end': `${end}%` } as CSSProperties}
|
|
274
|
+
data-disabled={disabled || undefined}
|
|
275
|
+
onPointerDown={handlePointerDown}
|
|
276
|
+
onPointerMove={handlePointerMove}
|
|
277
|
+
onPointerUp={handlePointerUp}
|
|
278
|
+
onLostPointerCapture={handleLostPointerCapture}
|
|
279
|
+
{...rest}
|
|
280
|
+
>
|
|
281
|
+
<div ref={trackRef} className="yarcl-slider-track">
|
|
282
|
+
<div className="yarcl-slider-range" />
|
|
283
|
+
{values.map((v, index) => {
|
|
284
|
+
const thumbId = index === 0 && id ? id : `${uid}-thumb-${index}`;
|
|
285
|
+
const thumbLabel = range ? thumbLabels[index] : undefined;
|
|
286
|
+
return (
|
|
287
|
+
<div
|
|
288
|
+
key={index}
|
|
289
|
+
ref={(el) => {
|
|
290
|
+
thumbRefs.current[index] = el;
|
|
291
|
+
}}
|
|
292
|
+
id={thumbId}
|
|
293
|
+
role="slider"
|
|
294
|
+
tabIndex={disabled ? undefined : 0}
|
|
295
|
+
className="yarcl-slider-thumb"
|
|
296
|
+
style={{ '--yarcl-slider-position': `${percent(v)}%` } as CSSProperties}
|
|
297
|
+
aria-valuemin={lowerBound(index)}
|
|
298
|
+
aria-valuemax={upperBound(index)}
|
|
299
|
+
aria-valuenow={v}
|
|
300
|
+
aria-valuetext={formatValue?.(v)}
|
|
301
|
+
aria-orientation="horizontal"
|
|
302
|
+
aria-disabled={disabled || undefined}
|
|
303
|
+
aria-invalid={ariaInvalid}
|
|
304
|
+
aria-describedby={ariaDescribedBy}
|
|
305
|
+
aria-label={thumbLabel && !outerLabelledBy && ariaLabel ? `${ariaLabel} ${thumbLabel}` : (thumbLabel ?? ariaLabel)}
|
|
306
|
+
aria-labelledby={outerLabelledBy && (thumbLabel ? `${outerLabelledBy} ${thumbId}` : outerLabelledBy)}
|
|
307
|
+
onKeyDown={(event) => handleKeyDown(index, event)}
|
|
308
|
+
/>
|
|
309
|
+
);
|
|
310
|
+
})}
|
|
311
|
+
</div>
|
|
312
|
+
{name != null && values.map((v, index) => <input key={index} type="hidden" name={name} value={v} disabled={disabled} />)}
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
import { colorClass, cx, sizeClass } from '../classes';
|
|
3
|
+
import type { Color, Size } from '../types';
|
|
4
|
+
import { useDefaults } from '../runtime';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/** Props for {@link Spinner}. */
|
|
8
|
+
export interface SpinnerProps extends Omit<ComponentProps<'span'>, 'color'> {
|
|
9
|
+
/** Diameter from the size's `iconSize`, from the `sizes` config. Inherits the surrounding control's size when omitted. */
|
|
10
|
+
size?: Size;
|
|
11
|
+
/** Semantic color, from the `colors` config. Uses the current text color when omitted. */
|
|
12
|
+
color?: Color;
|
|
13
|
+
/**
|
|
14
|
+
* Accessible label announced to screen readers.
|
|
15
|
+
* @default 'Loading'
|
|
16
|
+
*/
|
|
17
|
+
label?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* An indeterminate loading indicator. Sized like an icon, so it fits inside controls of the same size.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <Spinner size="lg" color="brand" />
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function Spinner({ size, color, label = 'Loading', className, ...props }: SpinnerProps) {
|
|
29
|
+
const own = useDefaults('Spinner');
|
|
30
|
+
return (
|
|
31
|
+
<span
|
|
32
|
+
role="status"
|
|
33
|
+
aria-label={label}
|
|
34
|
+
className={cx('yarcl-spinner', (size ?? own.size) && sizeClass(size ?? own.size), (color ?? own.color) && cx('yarcl-spinner-colored', colorClass(color ?? own.color)), className)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
import { cx, gapClass } from '../classes';
|
|
3
|
+
import type { Align, Justify, Spacing } from '../types';
|
|
4
|
+
import { useDefaults } from '../runtime';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/** Elements layout components can render as. */
|
|
8
|
+
export type LayoutElement = 'div' | 'section' | 'article' | 'aside' | 'header' | 'footer' | 'main' | 'nav' | 'form' | 'ul' | 'ol';
|
|
9
|
+
|
|
10
|
+
/** Props shared by {@link Stack} and {@link Inline}. */
|
|
11
|
+
export interface LayoutProps extends ComponentProps<'div'> {
|
|
12
|
+
/**
|
|
13
|
+
* Element to render.
|
|
14
|
+
* @default 'div'
|
|
15
|
+
*/
|
|
16
|
+
as?: LayoutElement;
|
|
17
|
+
/**
|
|
18
|
+
* Space between children, from the `spacing` config.
|
|
19
|
+
* @default config.defaults.gap
|
|
20
|
+
*/
|
|
21
|
+
gap?: Spacing;
|
|
22
|
+
/** Cross-axis alignment of children. */
|
|
23
|
+
align?: Align;
|
|
24
|
+
/** Main-axis distribution of children. */
|
|
25
|
+
justify?: Justify;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Props for {@link Stack}. */
|
|
29
|
+
export type StackProps = LayoutProps;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Lays out children vertically with a gap from the spacing scale.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* <Stack gap="loose">
|
|
37
|
+
* <Heading level={2}>Profile</Heading>
|
|
38
|
+
* <Field label="Name"><Input /></Field>
|
|
39
|
+
* </Stack>
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function Stack({ as = 'div', gap, align, justify, className, ...props }: StackProps) {
|
|
43
|
+
const own = useDefaults('Stack');
|
|
44
|
+
const Tag = as as 'div';
|
|
45
|
+
return (
|
|
46
|
+
<Tag
|
|
47
|
+
className={cx(
|
|
48
|
+
'yarcl-stack',
|
|
49
|
+
gapClass(gap ?? own.gap),
|
|
50
|
+
align && `yarcl-align-${align}`,
|
|
51
|
+
justify && `yarcl-justify-${justify}`,
|
|
52
|
+
className,
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
2
|
+
import { colorClass, cx, sizeClass } from '../classes';
|
|
3
|
+
import { useFieldProps } from '../field-context';
|
|
4
|
+
import type { Color, Size } from '../types';
|
|
5
|
+
import { useDefaults } from '../runtime';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
/** Props for {@link Switch}. Accepts all native `<input>` attributes except `type`, `color` and `size`. */
|
|
9
|
+
export interface SwitchProps extends Omit<ComponentProps<'input'>, 'type' | 'color' | 'size' | 'children'> {
|
|
10
|
+
/** Label shown next to the switch. */
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* Switch size, from the `sizes` config (scaled from the size's `iconSize`).
|
|
14
|
+
* @default config.defaults.size
|
|
15
|
+
*/
|
|
16
|
+
size?: Size;
|
|
17
|
+
/**
|
|
18
|
+
* Track color when on, from the `colors` config.
|
|
19
|
+
* @default config.defaults.color
|
|
20
|
+
*/
|
|
21
|
+
color?: Color;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* An on/off toggle. A native checkbox with `role="switch"`, so `checked`,
|
|
26
|
+
* `defaultChecked` and `onChange` work as usual.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <Switch defaultChecked>Notifications</Switch>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function Switch(props: SwitchProps) {
|
|
34
|
+
const own = useDefaults('Switch');
|
|
35
|
+
const { children, size, color, className, style, ...rest } = useFieldProps(props);
|
|
36
|
+
return (
|
|
37
|
+
<label className={cx('yarcl-switch', sizeClass(size ?? own.size), colorClass(color ?? own.color), className)} style={style}>
|
|
38
|
+
<input type="checkbox" role="switch" className="yarcl-switch-input" {...rest} />
|
|
39
|
+
{children != null && <span className="yarcl-switch-label">{children}</span>}
|
|
40
|
+
</label>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
2
|
+
import { cx, densityClass } from '../classes';
|
|
3
|
+
import type { Density } from '../types';
|
|
4
|
+
import { useDefaults } from '../runtime';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/** Props for {@link Table}. */
|
|
8
|
+
export interface TableProps extends ComponentProps<'table'> {
|
|
9
|
+
/**
|
|
10
|
+
* Cell padding and font size, from the `density` config.
|
|
11
|
+
* @default config.defaults.density
|
|
12
|
+
*/
|
|
13
|
+
density?: Density;
|
|
14
|
+
/** Shades every other row. */
|
|
15
|
+
striped?: boolean;
|
|
16
|
+
/** Highlights the row under the pointer. Use when rows are clickable. */
|
|
17
|
+
interactive?: boolean;
|
|
18
|
+
/** Visible caption describing the table. Also its accessible name. */
|
|
19
|
+
caption?: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function TableRoot({ density, striped, interactive, caption, className, children, ...props }: TableProps) {
|
|
23
|
+
const own = useDefaults('Table');
|
|
24
|
+
return (
|
|
25
|
+
<div className="yarcl-table-wrap">
|
|
26
|
+
<table
|
|
27
|
+
className={cx(
|
|
28
|
+
'yarcl-table',
|
|
29
|
+
densityClass(density ?? own.density),
|
|
30
|
+
striped && 'yarcl-table-striped',
|
|
31
|
+
interactive && 'yarcl-table-interactive',
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
36
|
+
{caption != null && <caption className="yarcl-table-caption">{caption}</caption>}
|
|
37
|
+
{children}
|
|
38
|
+
</table>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Horizontal alignment of a cell's content. `end` also uses tabular numbers, for numeric columns. */
|
|
44
|
+
export type CellAlign = 'start' | 'center' | 'end';
|
|
45
|
+
|
|
46
|
+
/** Props for `Table.HeaderCell`. */
|
|
47
|
+
export interface TableHeaderCellProps extends Omit<ComponentProps<'th'>, 'align'> {
|
|
48
|
+
/**
|
|
49
|
+
* Content alignment.
|
|
50
|
+
* @default 'start'
|
|
51
|
+
*/
|
|
52
|
+
align?: CellAlign;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function TableHeaderCell({ align = 'start', scope = 'col', className, ...props }: TableHeaderCellProps) {
|
|
56
|
+
return <th scope={scope} className={cx(`yarcl-cell-${align}`, className)} {...props} />;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Props for `Table.Cell`. */
|
|
60
|
+
export interface TableCellProps extends Omit<ComponentProps<'td'>, 'align'> {
|
|
61
|
+
/**
|
|
62
|
+
* Content alignment.
|
|
63
|
+
* @default 'start'
|
|
64
|
+
*/
|
|
65
|
+
align?: CellAlign;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function TableCell({ align = 'start', className, ...props }: TableCellProps) {
|
|
69
|
+
return <td className={cx(`yarcl-cell-${align}`, className)} {...props} />;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A data table with density from the config. Scrolls horizontally when it doesn't fit.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```tsx
|
|
77
|
+
* <Table density="compact" striped caption="Invoices">
|
|
78
|
+
* <Table.Head>
|
|
79
|
+
* <Table.Row>
|
|
80
|
+
* <Table.HeaderCell>Customer</Table.HeaderCell>
|
|
81
|
+
* <Table.HeaderCell align="end">Amount</Table.HeaderCell>
|
|
82
|
+
* </Table.Row>
|
|
83
|
+
* </Table.Head>
|
|
84
|
+
* <Table.Body>
|
|
85
|
+
* <Table.Row>
|
|
86
|
+
* <Table.Cell>Ada</Table.Cell>
|
|
87
|
+
* <Table.Cell align="end">$120.00</Table.Cell>
|
|
88
|
+
* </Table.Row>
|
|
89
|
+
* </Table.Body>
|
|
90
|
+
* </Table>
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export const Table = Object.assign(TableRoot, {
|
|
94
|
+
Head: (props: ComponentProps<'thead'>) => <thead {...props} />,
|
|
95
|
+
Body: (props: ComponentProps<'tbody'>) => <tbody {...props} />,
|
|
96
|
+
Row: (props: ComponentProps<'tr'>) => <tr {...props} />,
|
|
97
|
+
HeaderCell: TableHeaderCell,
|
|
98
|
+
Cell: TableCell,
|
|
99
|
+
});
|