@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
package/src/color.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
type Rgb = [number, number, number];
|
|
2
|
+
|
|
3
|
+
export function parseHex(value: string): Rgb | null {
|
|
4
|
+
const hex = value.trim().replace(/^#/, '');
|
|
5
|
+
if (!/^([0-9a-f]{3}|[0-9a-f]{6})$/i.test(hex)) return null;
|
|
6
|
+
const full = hex.length === 3 ? [...hex].map((c) => c + c).join('') : hex;
|
|
7
|
+
return [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16)) as Rgb;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function luminance([r, g, b]: Rgb): number {
|
|
11
|
+
const [lr, lg, lb] = [r, g, b].map((c) => {
|
|
12
|
+
const s = c / 255;
|
|
13
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
14
|
+
});
|
|
15
|
+
return 0.2126 * lr + 0.7152 * lg + 0.0722 * lb;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function contrast(a: Rgb, b: Rgb): number {
|
|
19
|
+
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
|
20
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const WHITE: Rgb = [255, 255, 255];
|
|
24
|
+
const BLACK: Rgb = [0, 0, 0];
|
|
25
|
+
|
|
26
|
+
export function mix(a: Rgb, b: Rgb, amount: number): Rgb {
|
|
27
|
+
return a.map((channel, i) => Math.round(channel + (b[i] - channel) * amount)) as Rgb;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function toHex(rgb: Rgb): string {
|
|
31
|
+
return `#${rgb.map((c) => c.toString(16).padStart(2, '0')).join('')}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The color, mixed toward `toward` in small steps until it reaches `min` contrast
|
|
36
|
+
* against every background. Returns the color unchanged when it already passes.
|
|
37
|
+
*/
|
|
38
|
+
export function readableText(color: Rgb, backgrounds: Rgb[], toward: Rgb, min: number): Rgb {
|
|
39
|
+
for (let step = 0; step <= 20; step++) {
|
|
40
|
+
const candidate = mix(color, toward, step / 20);
|
|
41
|
+
if (backgrounds.every((bg) => contrast(candidate, bg) >= min)) return candidate;
|
|
42
|
+
}
|
|
43
|
+
return toward;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function readableOn(background: Rgb): string {
|
|
47
|
+
return contrast(background, WHITE) >= contrast(background, BLACK) ? '#ffffff' : '#000000';
|
|
48
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useId,
|
|
5
|
+
type ComponentProps,
|
|
6
|
+
type KeyboardEvent,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { colorClass, cx, radiusClass, sizeClass } from '../classes';
|
|
10
|
+
import { ChevronIcon } from '../floating';
|
|
11
|
+
import { useControllable } from '../hooks';
|
|
12
|
+
import type { Color, Radius, Size } from '../types';
|
|
13
|
+
import { useDefaults } from '../runtime';
|
|
14
|
+
|
|
15
|
+
interface AccordionContextValue {
|
|
16
|
+
open: string[];
|
|
17
|
+
toggle: (value: string) => void;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
idFor: (kind: 'trigger' | 'content', value: string) => string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface AccordionItemContextValue {
|
|
23
|
+
value: string;
|
|
24
|
+
open: boolean;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const AccordionContext = createContext<AccordionContextValue | null>(null);
|
|
29
|
+
const AccordionItemContext = createContext<AccordionItemContextValue | null>(null);
|
|
30
|
+
|
|
31
|
+
function useAccordionContext(component: string) {
|
|
32
|
+
const context = useContext(AccordionContext);
|
|
33
|
+
if (!context) throw new Error(`yarcl: <${component}> must be inside <Accordion>`);
|
|
34
|
+
return context;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function useItemContext(component: string) {
|
|
38
|
+
const context = useContext(AccordionItemContext);
|
|
39
|
+
if (!context) throw new Error(`yarcl: <${component}> must be inside <Accordion.Item>`);
|
|
40
|
+
return context;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Props shared by both forms of {@link AccordionProps}. */
|
|
44
|
+
export interface AccordionBaseProps extends Omit<ComponentProps<'div'>, 'defaultValue' | 'color'> {
|
|
45
|
+
/**
|
|
46
|
+
* Trigger height, padding and font size, from the `sizes` config.
|
|
47
|
+
* @default config.defaults.size
|
|
48
|
+
*/
|
|
49
|
+
size?: Size;
|
|
50
|
+
/**
|
|
51
|
+
* Corner radius of the outer frame, from the `radii` config, or `'size'` to match the size.
|
|
52
|
+
* @default config.defaults.radius
|
|
53
|
+
*/
|
|
54
|
+
radius?: Radius | 'size';
|
|
55
|
+
/**
|
|
56
|
+
* Color of the open item's indicator, from the `colors` config.
|
|
57
|
+
* @default config.defaults.color
|
|
58
|
+
*/
|
|
59
|
+
color?: Color;
|
|
60
|
+
/** Disables every item. */
|
|
61
|
+
disabled?: boolean;
|
|
62
|
+
/** `Accordion.Item`s. */
|
|
63
|
+
children?: ReactNode;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Props for {@link Accordion}. `type` decides whether one or many items can be open. */
|
|
67
|
+
export type AccordionProps = AccordionBaseProps &
|
|
68
|
+
(
|
|
69
|
+
| {
|
|
70
|
+
/** One item open at a time. */
|
|
71
|
+
type: 'single';
|
|
72
|
+
/** Open item (controlled), or `null` when all are closed. */
|
|
73
|
+
value?: string | null;
|
|
74
|
+
/** Initially open item (uncontrolled). */
|
|
75
|
+
defaultValue?: string | null;
|
|
76
|
+
/** Called with the open item, or `null` when it closes. */
|
|
77
|
+
onValueChange?: (value: string | null) => void;
|
|
78
|
+
/**
|
|
79
|
+
* Lets the open item close, leaving all closed. When `false`, one item always stays open.
|
|
80
|
+
* @default true
|
|
81
|
+
*/
|
|
82
|
+
collapsible?: boolean;
|
|
83
|
+
}
|
|
84
|
+
| {
|
|
85
|
+
/** Any number of items open. */
|
|
86
|
+
type: 'multiple';
|
|
87
|
+
/** Open items (controlled). */
|
|
88
|
+
value?: string[];
|
|
89
|
+
/** Initially open items (uncontrolled). */
|
|
90
|
+
defaultValue?: string[];
|
|
91
|
+
/** Called with all open items. */
|
|
92
|
+
onValueChange?: (value: string[]) => void;
|
|
93
|
+
collapsible?: never;
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const toArray = (value: string | string[] | null | undefined) => (value == null ? [] : Array.isArray(value) ? value : [value]);
|
|
98
|
+
|
|
99
|
+
function AccordionRoot(props: AccordionProps) {
|
|
100
|
+
const own = useDefaults('Accordion');
|
|
101
|
+
const {
|
|
102
|
+
type,
|
|
103
|
+
value,
|
|
104
|
+
defaultValue,
|
|
105
|
+
onValueChange,
|
|
106
|
+
collapsible = true,
|
|
107
|
+
size,
|
|
108
|
+
radius,
|
|
109
|
+
color,
|
|
110
|
+
disabled,
|
|
111
|
+
className,
|
|
112
|
+
onKeyDown,
|
|
113
|
+
...rest
|
|
114
|
+
} = props;
|
|
115
|
+
|
|
116
|
+
const [open, setOpen] = useControllable<string[]>(
|
|
117
|
+
value === undefined ? undefined : toArray(value),
|
|
118
|
+
toArray(defaultValue),
|
|
119
|
+
(next) =>
|
|
120
|
+
type === 'multiple'
|
|
121
|
+
? (onValueChange as (v: string[]) => void)?.(next)
|
|
122
|
+
: (onValueChange as (v: string | null) => void)?.(next[0] ?? null),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const baseId = useId();
|
|
126
|
+
const idFor = (kind: 'trigger' | 'content', v: string) => `${baseId}-${kind}-${v.replace(/[^\w-]/g, '_')}`;
|
|
127
|
+
|
|
128
|
+
function toggle(item: string) {
|
|
129
|
+
const on = open.includes(item);
|
|
130
|
+
if (type === 'multiple') setOpen(on ? open.filter((v) => v !== item) : [...open, item]);
|
|
131
|
+
else if (!on) setOpen([item]);
|
|
132
|
+
else if (collapsible) setOpen([]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {
|
|
136
|
+
onKeyDown?.(event);
|
|
137
|
+
const root = event.currentTarget;
|
|
138
|
+
const triggers = [...root.querySelectorAll<HTMLButtonElement>('.yarcl-accordion-trigger:not(:disabled)')].filter(
|
|
139
|
+
(trigger) => trigger.closest('.yarcl-accordion') === root,
|
|
140
|
+
);
|
|
141
|
+
const current = triggers.indexOf(document.activeElement as HTMLButtonElement);
|
|
142
|
+
if (current === -1) return;
|
|
143
|
+
const next = {
|
|
144
|
+
ArrowDown: (current + 1) % triggers.length,
|
|
145
|
+
ArrowUp: (current - 1 + triggers.length) % triggers.length,
|
|
146
|
+
Home: 0,
|
|
147
|
+
End: triggers.length - 1,
|
|
148
|
+
}[event.key];
|
|
149
|
+
if (next === undefined) return;
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
triggers[next].focus();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const resolvedSize = size ?? own.size;
|
|
155
|
+
return (
|
|
156
|
+
<AccordionContext.Provider value={{ open, toggle, disabled, idFor }}>
|
|
157
|
+
<div
|
|
158
|
+
className={cx(
|
|
159
|
+
'yarcl-accordion',
|
|
160
|
+
sizeClass(resolvedSize),
|
|
161
|
+
radiusClass(radius ?? own.radius, resolvedSize),
|
|
162
|
+
colorClass(color ?? own.color),
|
|
163
|
+
className,
|
|
164
|
+
)}
|
|
165
|
+
onKeyDown={handleKeyDown}
|
|
166
|
+
{...rest}
|
|
167
|
+
/>
|
|
168
|
+
</AccordionContext.Provider>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Props for `Accordion.Item`. */
|
|
173
|
+
export interface AccordionItemProps extends Omit<ComponentProps<'div'>, 'value'> {
|
|
174
|
+
/** Identifies the item in the accordion's value. */
|
|
175
|
+
value: string;
|
|
176
|
+
/** Prevents opening or closing this item. */
|
|
177
|
+
disabled?: boolean;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function AccordionItem({ value, disabled, className, ...props }: AccordionItemProps) {
|
|
181
|
+
const context = useAccordionContext('Accordion.Item');
|
|
182
|
+
const open = context.open.includes(value);
|
|
183
|
+
const itemDisabled = disabled || context.disabled;
|
|
184
|
+
return (
|
|
185
|
+
<AccordionItemContext.Provider value={{ value, open, disabled: itemDisabled }}>
|
|
186
|
+
<div
|
|
187
|
+
data-state={open ? 'open' : 'closed'}
|
|
188
|
+
data-disabled={itemDisabled || undefined}
|
|
189
|
+
className={cx('yarcl-accordion-item', className)}
|
|
190
|
+
{...props}
|
|
191
|
+
/>
|
|
192
|
+
</AccordionItemContext.Provider>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Props for `Accordion.Trigger`. */
|
|
197
|
+
export interface AccordionTriggerProps extends ComponentProps<'button'> {
|
|
198
|
+
/**
|
|
199
|
+
* Level of the heading that wraps the button, to fit the page outline.
|
|
200
|
+
* @default 3
|
|
201
|
+
*/
|
|
202
|
+
level?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function AccordionTrigger({ level = 3, className, onClick, children, ...props }: AccordionTriggerProps) {
|
|
206
|
+
const context = useAccordionContext('Accordion.Trigger');
|
|
207
|
+
const item = useItemContext('Accordion.Trigger');
|
|
208
|
+
const Heading = `h${level}` as 'h3';
|
|
209
|
+
return (
|
|
210
|
+
<Heading className="yarcl-accordion-heading">
|
|
211
|
+
<button
|
|
212
|
+
type="button"
|
|
213
|
+
id={context.idFor('trigger', item.value)}
|
|
214
|
+
aria-controls={context.idFor('content', item.value)}
|
|
215
|
+
aria-expanded={item.open}
|
|
216
|
+
disabled={item.disabled}
|
|
217
|
+
className={cx('yarcl-accordion-trigger', className)}
|
|
218
|
+
onClick={(event) => {
|
|
219
|
+
onClick?.(event);
|
|
220
|
+
if (!event.defaultPrevented) context.toggle(item.value);
|
|
221
|
+
}}
|
|
222
|
+
{...props}
|
|
223
|
+
>
|
|
224
|
+
<span className="yarcl-accordion-label">{children}</span>
|
|
225
|
+
<ChevronIcon />
|
|
226
|
+
</button>
|
|
227
|
+
</Heading>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Props for `Accordion.Content`. */
|
|
232
|
+
export type AccordionContentProps = ComponentProps<'div'>;
|
|
233
|
+
|
|
234
|
+
function AccordionContent({ className, children, ...props }: AccordionContentProps) {
|
|
235
|
+
const context = useAccordionContext('Accordion.Content');
|
|
236
|
+
const item = useItemContext('Accordion.Content');
|
|
237
|
+
return (
|
|
238
|
+
<div
|
|
239
|
+
role="region"
|
|
240
|
+
id={context.idFor('content', item.value)}
|
|
241
|
+
aria-labelledby={context.idFor('trigger', item.value)}
|
|
242
|
+
data-state={item.open ? 'open' : 'closed'}
|
|
243
|
+
className={cx('yarcl-accordion-content', className)}
|
|
244
|
+
{...props}
|
|
245
|
+
>
|
|
246
|
+
<div className="yarcl-accordion-body">{children}</div>
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Stacked sections that expand and collapse. Each trigger is a button inside a heading;
|
|
253
|
+
* Up and Down move between triggers, Home and End jump to the ends. The panel height animates
|
|
254
|
+
* with the `motion` tokens, and snaps when reduced motion is preferred.
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```tsx
|
|
258
|
+
* <Accordion type="single" defaultValue="shipping">
|
|
259
|
+
* <Accordion.Item value="shipping">
|
|
260
|
+
* <Accordion.Trigger>Shipping</Accordion.Trigger>
|
|
261
|
+
* <Accordion.Content>Orders ship within two working days.</Accordion.Content>
|
|
262
|
+
* </Accordion.Item>
|
|
263
|
+
* <Accordion.Item value="returns">
|
|
264
|
+
* <Accordion.Trigger>Returns</Accordion.Trigger>
|
|
265
|
+
* <Accordion.Content>Free returns within 30 days.</Accordion.Content>
|
|
266
|
+
* </Accordion.Item>
|
|
267
|
+
* </Accordion>
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
export const Accordion = Object.assign(AccordionRoot, {
|
|
271
|
+
Item: AccordionItem,
|
|
272
|
+
Trigger: AccordionTrigger,
|
|
273
|
+
Content: AccordionContent,
|
|
274
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
2
|
+
import { colorClass, cx, radiusClass, softVariantClass } from '../classes';
|
|
3
|
+
import type { Color, Radius, Variant } from '../types';
|
|
4
|
+
import { useDefaults } from '../runtime';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/** Props for {@link Alert}. */
|
|
8
|
+
export interface AlertProps extends Omit<ComponentProps<'div'>, 'color' | 'title'> {
|
|
9
|
+
/** Bold first line. */
|
|
10
|
+
title?: ReactNode;
|
|
11
|
+
/** Icon shown before the text. */
|
|
12
|
+
icon?: ReactNode;
|
|
13
|
+
/** Actions shown after the text, e.g. a {@link Button} or {@link Link}. */
|
|
14
|
+
action?: ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* Semantic color, from the `colors` config.
|
|
17
|
+
* @default config.defaults.color
|
|
18
|
+
*/
|
|
19
|
+
color?: Color;
|
|
20
|
+
/**
|
|
21
|
+
* Style recipe, from the `variants` config.
|
|
22
|
+
* @default config.defaults.softVariant
|
|
23
|
+
*/
|
|
24
|
+
variant?: Variant;
|
|
25
|
+
/**
|
|
26
|
+
* Corner radius, from the `radii` config.
|
|
27
|
+
* @default config.defaults.radius
|
|
28
|
+
*/
|
|
29
|
+
radius?: Radius;
|
|
30
|
+
/** Shows a dismiss button. Called when it is pressed; hide the alert in response. */
|
|
31
|
+
onDismiss?: () => void;
|
|
32
|
+
/**
|
|
33
|
+
* Announces the alert when it appears: `'polite'` (`role="status"`) or `'assertive'` (`role="alert"`).
|
|
34
|
+
* Leave unset for messages present on page load.
|
|
35
|
+
*/
|
|
36
|
+
live?: 'polite' | 'assertive';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* An inline message that stays on the page, e.g. a warning above a form.
|
|
41
|
+
* For transient messages use {@link toast}.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* <Alert color="warning" title="Trial ends in 3 days" action={<Button size="sm">Upgrade</Button>}>
|
|
46
|
+
* Add a payment method to keep your projects.
|
|
47
|
+
* </Alert>
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function Alert({
|
|
51
|
+
title,
|
|
52
|
+
icon,
|
|
53
|
+
action,
|
|
54
|
+
color,
|
|
55
|
+
variant,
|
|
56
|
+
radius,
|
|
57
|
+
onDismiss,
|
|
58
|
+
live,
|
|
59
|
+
className,
|
|
60
|
+
children,
|
|
61
|
+
...props
|
|
62
|
+
}: AlertProps) {
|
|
63
|
+
const own = useDefaults('Alert');
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
role={live === 'assertive' ? 'alert' : live === 'polite' ? 'status' : undefined}
|
|
67
|
+
className={cx('yarcl-alert', colorClass(color ?? own.color), softVariantClass(variant ?? own.variant), radiusClass(radius ?? own.radius), className)}
|
|
68
|
+
{...props}
|
|
69
|
+
>
|
|
70
|
+
{icon != null && <span className="yarcl-alert-icon">{icon}</span>}
|
|
71
|
+
<div className="yarcl-alert-text">
|
|
72
|
+
{title != null && <div className="yarcl-alert-title">{title}</div>}
|
|
73
|
+
{children != null && <div className="yarcl-alert-body">{children}</div>}
|
|
74
|
+
</div>
|
|
75
|
+
{action != null && <div className="yarcl-alert-action">{action}</div>}
|
|
76
|
+
{onDismiss && (
|
|
77
|
+
<button type="button" className="yarcl-alert-dismiss" aria-label="Dismiss" onClick={onDismiss}>
|
|
78
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
|
|
79
|
+
<path d="M6 6l12 12M18 6 6 18" />
|
|
80
|
+
</svg>
|
|
81
|
+
</button>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
import { colorClass, cx, radiusClass, sizeClass, softVariantClass } from '../classes';
|
|
3
|
+
import type { Color, Radius, Size, Variant } from '../types';
|
|
4
|
+
import { useDefaults } from '../runtime';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/** Props for {@link Badge}. */
|
|
8
|
+
export interface BadgeProps extends Omit<ComponentProps<'span'>, 'color'> {
|
|
9
|
+
/**
|
|
10
|
+
* Semantic color, from the `colors` config.
|
|
11
|
+
* @default config.defaults.color
|
|
12
|
+
*/
|
|
13
|
+
color?: Color;
|
|
14
|
+
/**
|
|
15
|
+
* Style recipe, from the `variants` config.
|
|
16
|
+
* @default config.defaults.softVariant
|
|
17
|
+
*/
|
|
18
|
+
variant?: Variant;
|
|
19
|
+
/**
|
|
20
|
+
* Scales with the size's font size, from the `sizes` config.
|
|
21
|
+
* @default config.defaults.size
|
|
22
|
+
*/
|
|
23
|
+
size?: Size;
|
|
24
|
+
/**
|
|
25
|
+
* Corner radius, from the `radii` config.
|
|
26
|
+
* @default config.defaults.radius
|
|
27
|
+
*/
|
|
28
|
+
radius?: Radius | 'size';
|
|
29
|
+
/** Shows a remove button, making the badge a removable tag. Called when it is pressed. */
|
|
30
|
+
onRemove?: () => void;
|
|
31
|
+
/**
|
|
32
|
+
* Accessible label of the remove button.
|
|
33
|
+
* @default 'Remove'
|
|
34
|
+
*/
|
|
35
|
+
removeLabel?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A small label for status, counts or categories. With `onRemove`, a removable tag.
|
|
40
|
+
* Uses the same variant recipes as {@link Button}.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* <Badge color="success">Active</Badge>
|
|
45
|
+
* <Badge onRemove={() => removeTag('react')}>react</Badge>
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function Badge({
|
|
49
|
+
color,
|
|
50
|
+
variant,
|
|
51
|
+
size,
|
|
52
|
+
radius,
|
|
53
|
+
onRemove,
|
|
54
|
+
removeLabel = 'Remove',
|
|
55
|
+
className,
|
|
56
|
+
children,
|
|
57
|
+
...props
|
|
58
|
+
}: BadgeProps) {
|
|
59
|
+
const own = useDefaults('Badge');
|
|
60
|
+
return (
|
|
61
|
+
<span
|
|
62
|
+
className={cx('yarcl-badge', colorClass(color ?? own.color), softVariantClass(variant ?? own.variant), sizeClass(size ?? own.size), radiusClass(radius ?? own.radius, size ?? own.size), className)}
|
|
63
|
+
{...props}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
{onRemove && <BadgeRemove aria-label={removeLabel} onClick={onRemove} />}
|
|
67
|
+
</span>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function BadgeRemove(props: Omit<ComponentProps<'button'>, 'children'>) {
|
|
72
|
+
return (
|
|
73
|
+
<button type="button" className="yarcl-badge-remove" {...props}>
|
|
74
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" aria-hidden="true">
|
|
75
|
+
<path d="M7 7l10 10M17 7 7 17" />
|
|
76
|
+
</svg>
|
|
77
|
+
</button>
|
|
78
|
+
);
|
|
79
|
+
}
|