@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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/dist/color.d.ts +12 -0
  4. package/dist/color.js +41 -0
  5. package/dist/css.d.ts +10 -0
  6. package/dist/css.js +175 -0
  7. package/dist/define.d.ts +383 -0
  8. package/dist/define.js +29 -0
  9. package/dist/plugin.d.ts +33 -0
  10. package/dist/plugin.js +80 -0
  11. package/package.json +66 -0
  12. package/src/apply.ts +46 -0
  13. package/src/classes.ts +23 -0
  14. package/src/color.ts +48 -0
  15. package/src/components/Accordion.tsx +274 -0
  16. package/src/components/Alert.tsx +85 -0
  17. package/src/components/Badge.tsx +79 -0
  18. package/src/components/Breadcrumb.tsx +239 -0
  19. package/src/components/Button.tsx +58 -0
  20. package/src/components/ButtonGroup.tsx +61 -0
  21. package/src/components/Card.tsx +50 -0
  22. package/src/components/Checkbox.tsx +49 -0
  23. package/src/components/Combobox.tsx +522 -0
  24. package/src/components/CommandPalette.tsx +452 -0
  25. package/src/components/DatePicker.tsx +432 -0
  26. package/src/components/Dialog.tsx +39 -0
  27. package/src/components/Divider.tsx +34 -0
  28. package/src/components/Drawer.tsx +31 -0
  29. package/src/components/Field.tsx +61 -0
  30. package/src/components/Heading.tsx +40 -0
  31. package/src/components/HoverCard.tsx +108 -0
  32. package/src/components/IconButton.tsx +59 -0
  33. package/src/components/Inline.tsx +42 -0
  34. package/src/components/Input.tsx +28 -0
  35. package/src/components/Label.tsx +58 -0
  36. package/src/components/Link.tsx +41 -0
  37. package/src/components/Menu.tsx +207 -0
  38. package/src/components/Modal.tsx +164 -0
  39. package/src/components/NumberInput.tsx +208 -0
  40. package/src/components/Pagination.tsx +227 -0
  41. package/src/components/Popover.tsx +140 -0
  42. package/src/components/Progress.tsx +122 -0
  43. package/src/components/Radio.tsx +59 -0
  44. package/src/components/RadioGroup.tsx +114 -0
  45. package/src/components/Select.tsx +197 -0
  46. package/src/components/Skeleton.tsx +95 -0
  47. package/src/components/Slider.tsx +315 -0
  48. package/src/components/Spinner.tsx +38 -0
  49. package/src/components/Stack.tsx +57 -0
  50. package/src/components/Switch.tsx +42 -0
  51. package/src/components/Table.tsx +99 -0
  52. package/src/components/Tabs.tsx +159 -0
  53. package/src/components/Text.tsx +67 -0
  54. package/src/components/Textarea.tsx +26 -0
  55. package/src/components/Toast.tsx +190 -0
  56. package/src/components/ToggleGroup.tsx +227 -0
  57. package/src/components/Tooltip.tsx +78 -0
  58. package/src/contrast.ts +82 -0
  59. package/src/css.ts +228 -0
  60. package/src/define.check.ts +259 -0
  61. package/src/define.ts +379 -0
  62. package/src/field-context.ts +31 -0
  63. package/src/floating.tsx +61 -0
  64. package/src/hooks.ts +19 -0
  65. package/src/index.ts +119 -0
  66. package/src/plugin.ts +94 -0
  67. package/src/reference/DesignReference.tsx +374 -0
  68. package/src/reference/index.ts +2 -0
  69. package/src/reference/reference.css +58 -0
  70. package/src/runtime.ts +38 -0
  71. package/src/styles.css +1971 -0
  72. package/src/themes/bloom.ts +72 -0
  73. package/src/themes/brutalist.ts +63 -0
  74. package/src/themes/compact.ts +63 -0
  75. package/src/themes/contract.ts +24 -0
  76. package/src/themes/editorial.ts +55 -0
  77. package/src/themes/index.ts +47 -0
  78. package/src/types.ts +69 -0
  79. package/src/yarcl.config.ts +104 -0
package/src/plugin.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { runnerImport, type Plugin } from 'vite';
5
+ import { generateCss } from './css.ts';
6
+ import type { YarclShape } from './define.ts';
7
+
8
+ /** Options for the yarcl Vite plugin. */
9
+ export interface YarclPluginOptions {
10
+ /**
11
+ * Path to the consumer's config file, relative to the Vite root.
12
+ * If the file does not exist, the library's default config is used.
13
+ * @default 'src/yarcl.config.ts'
14
+ */
15
+ config?: string;
16
+ }
17
+
18
+ const VIRTUAL_CSS = 'virtual:yarcl.css';
19
+ const RESOLVED_CSS = '\0' + VIRTUAL_CSS;
20
+ const PACKAGE = '@yarcl/react';
21
+ const defaultConfig = [new URL('./yarcl.config.ts', import.meta.url), new URL('../src/yarcl.config.ts', import.meta.url)]
22
+ .map((url) => fileURLToPath(url))
23
+ .find((file) => existsSync(file))!;
24
+
25
+ /**
26
+ * Vite plugin that connects the library to the consumer's config.
27
+ *
28
+ * - Aliases `@yarcl/config` to the consumer's config file (or the library default).
29
+ * - Generates `virtual:yarcl.css` from the config at build time: CSS variables on `:root`
30
+ * and one `yarcl-{group}-{key}` class per key. No inline styles, no runtime cost.
31
+ * - Warns when a color's foreground fails WCAG AA contrast.
32
+ * - Regenerates the CSS when the config file changes.
33
+ *
34
+ * Must be paired with a matching `paths` entry for `@yarcl/config` in the
35
+ * consumer's `tsconfig.json`, so the types resolve to the same file.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * // vite.config.ts
40
+ * import { yarcl } from '@yarcl/react/plugin';
41
+ *
42
+ * export default defineConfig({
43
+ * plugins: [react(), yarcl({ config: 'src/yarcl.config.ts' })],
44
+ * });
45
+ * ```
46
+ */
47
+ export function yarcl({ config = 'src/yarcl.config.ts' }: YarclPluginOptions = {}): Plugin {
48
+ let root = process.cwd();
49
+ let target = defaultConfig;
50
+ let watched = new Set<string>();
51
+
52
+ return {
53
+ name: 'yarcl',
54
+
55
+ config(userConfig) {
56
+ root = resolve(userConfig.root ?? process.cwd());
57
+ const consumerConfig = resolve(root, config);
58
+ target = existsSync(consumerConfig) ? consumerConfig : defaultConfig;
59
+ return {
60
+ resolve: { alias: { '@yarcl/config': target } },
61
+ optimizeDeps: {
62
+ exclude: [PACKAGE],
63
+ include: ['@floating-ui/react', 'date-fns', 'date-fns/locale', 'date-fns/locale/en-US'].map((dep) => `${PACKAGE} > ${dep}`),
64
+ },
65
+ ssr: { noExternal: [PACKAGE] },
66
+ };
67
+ },
68
+
69
+ resolveId(id) {
70
+ if (id === VIRTUAL_CSS) return RESOLVED_CSS;
71
+ },
72
+
73
+ async load(id) {
74
+ if (id !== RESOLVED_CSS) return;
75
+ const { module, dependencies } = await runnerImport<{ default: YarclShape }>(target, {
76
+ configFile: false,
77
+ root,
78
+ ssr: { noExternal: [PACKAGE] },
79
+ logLevel: 'error',
80
+ });
81
+ watched = new Set([target, ...dependencies.map((dep) => resolve(root, dep))]);
82
+ watched.forEach((file) => this.addWatchFile(file));
83
+ return generateCss(module.default, (message) => this.warn(message));
84
+ },
85
+
86
+ handleHotUpdate({ file, server, modules }) {
87
+ if (!watched.has(file)) return;
88
+ const css = server.moduleGraph.getModuleById(RESOLVED_CSS);
89
+ if (!css) return;
90
+ server.moduleGraph.invalidateModule(css);
91
+ return [...modules, css];
92
+ },
93
+ };
94
+ }
@@ -0,0 +1,374 @@
1
+ import type { ReactNode } from 'react';
2
+ import { contrast, parseHex, readableOn } from '../color';
3
+ import type { ColorPair, ColorToken, YarclShape } from '../define';
4
+ import {
5
+ Button,
6
+ Card,
7
+ Heading,
8
+ IconButton,
9
+ Inline,
10
+ Input,
11
+ Stack,
12
+ Table,
13
+ Text,
14
+ type Color,
15
+ type Density,
16
+ type Radius,
17
+ type Shadow,
18
+ type Size,
19
+ type Spacing,
20
+ type TextStyle,
21
+ type Variant,
22
+ } from '../index';
23
+ import { useConfig } from '../runtime';
24
+ import './reference.css';
25
+
26
+ const keys = <T extends string>(group: object) => Object.keys(group) as T[];
27
+
28
+ function ratio(background: string, token: ColorToken, mode: keyof ColorPair): string {
29
+ const bg = parseHex(background);
30
+ if (!bg) return 'n/a';
31
+ const onValue = typeof token.on === 'string' ? token.on : token.on?.[mode];
32
+ const fg = parseHex(onValue ?? readableOn(bg));
33
+ return fg ? `${contrast(bg, fg).toFixed(1)}:1` : 'n/a';
34
+ }
35
+
36
+ function Section({ title, description, children }: { title: string; description?: ReactNode; children: ReactNode }) {
37
+ return (
38
+ <Stack as="section" className="yarcl-ref-section">
39
+ <Heading level={2}>{title}</Heading>
40
+ {description != null && (
41
+ <Text as="p" muted>
42
+ {description}
43
+ </Text>
44
+ )}
45
+ {children}
46
+ </Stack>
47
+ );
48
+ }
49
+
50
+ function Code({ children }: { children: ReactNode }) {
51
+ return <code className="yarcl-ref-code">{children}</code>;
52
+ }
53
+
54
+ function KeyValues({ caption, rows }: { caption: string; rows: [string, ReactNode][] }) {
55
+ const config = useConfig();
56
+ return (
57
+ <Table caption={caption} density={config.defaults.density}>
58
+ <Table.Body>
59
+ {rows.map(([key, value]) => (
60
+ <Table.Row key={key}>
61
+ <Table.HeaderCell scope="row">{key}</Table.HeaderCell>
62
+ <Table.Cell>{value}</Table.Cell>
63
+ </Table.Row>
64
+ ))}
65
+ </Table.Body>
66
+ </Table>
67
+ );
68
+ }
69
+
70
+ const pair = (value: ColorPair) => (
71
+ <>
72
+ <Code>{value.light}</Code> / <Code>{value.dark}</Code>
73
+ </>
74
+ );
75
+
76
+ /** Props for {@link DesignReference}. */
77
+ export interface DesignReferenceProps {
78
+ /**
79
+ * Page heading.
80
+ * @default 'Design reference'
81
+ */
82
+ title?: ReactNode;
83
+ }
84
+
85
+ /**
86
+ * A living reference of the active design system, rendered from the consumer's config:
87
+ * colors with contrast ratios, the size scale with live controls, radii, variants, spacing,
88
+ * shadows, fonts, text styles, heading levels, density, other tokens and defaults.
89
+ * Import from `@yarcl/react/reference`.
90
+ *
91
+ * @example
92
+ * ```tsx
93
+ * import { DesignReference } from '@yarcl/react/reference';
94
+ *
95
+ * <DesignReference title="Acme design system" />
96
+ * ```
97
+ */
98
+ export function DesignReference({ title = 'Design reference' }: DesignReferenceProps) {
99
+ const config = useConfig();
100
+ const shape = config as unknown as YarclShape;
101
+ const colors = keys<Color>(config.colors);
102
+ const variants = keys<Variant>(config.variants);
103
+ const { typography } = shape;
104
+
105
+ return (
106
+ <Stack gap={config.defaults.padding} className="yarcl-ref">
107
+ <Heading level={1}>{title}</Heading>
108
+
109
+ <Section title="Colors" description="Semantic colors for the color prop. Ratios are foreground on background, light / dark.">
110
+ <div className="yarcl-ref-grid">
111
+ {colors.map((key) => {
112
+ const token = shape.colors[key];
113
+ return (
114
+ <Card key={key} padding={config.defaults.gap} className="yarcl-ref-color">
115
+ <div className={`yarcl-ref-swatch yarcl-color-${key}`}>Aa</div>
116
+ <Stack gap={config.defaults.gap}>
117
+ <Text textStyle={config.defaults.labelStyle}>{key}</Text>
118
+ <Text textStyle={config.defaults.helperStyle} muted>
119
+ {pair(token)}
120
+ </Text>
121
+ <Text textStyle={config.defaults.helperStyle} muted>
122
+ {ratio(token.light, token, 'light')} / {ratio(token.dark, token, 'dark')}
123
+ </Text>
124
+ </Stack>
125
+ </Card>
126
+ );
127
+ })}
128
+ </div>
129
+ </Section>
130
+
131
+ <Section title="Neutrals" description="Backgrounds, text and borders.">
132
+ <div className="yarcl-ref-grid">
133
+ {Object.entries(shape.neutrals).map(([key, value]) => (
134
+ <Card key={key} padding={config.defaults.gap} className="yarcl-ref-color">
135
+ <div className="yarcl-ref-swatch yarcl-ref-neutral" style={{ background: `var(--yarcl-neutral-${key})` }} />
136
+ <Text textStyle={config.defaults.labelStyle}>{key}</Text>
137
+ <Text textStyle={config.defaults.helperStyle} muted>
138
+ {pair(value)}
139
+ </Text>
140
+ </Card>
141
+ ))}
142
+ </div>
143
+ </Section>
144
+
145
+ <Section title="Sizes" description="One scale shared by every control, so controls of the same size line up.">
146
+ <Table caption="Control sizes" density={config.defaults.density}>
147
+ <Table.Head>
148
+ <Table.Row>
149
+ <Table.HeaderCell>Size</Table.HeaderCell>
150
+ <Table.HeaderCell>Height</Table.HeaderCell>
151
+ <Table.HeaderCell>Padding X</Table.HeaderCell>
152
+ <Table.HeaderCell>Font</Table.HeaderCell>
153
+ <Table.HeaderCell>Icon</Table.HeaderCell>
154
+ <Table.HeaderCell>Preview</Table.HeaderCell>
155
+ </Table.Row>
156
+ </Table.Head>
157
+ <Table.Body>
158
+ {keys<Size>(config.sizes).map((key) => {
159
+ const size = shape.sizes[key];
160
+ return (
161
+ <Table.Row key={key}>
162
+ <Table.HeaderCell scope="row">{key}</Table.HeaderCell>
163
+ <Table.Cell>
164
+ <Code>{size.height}</Code>
165
+ </Table.Cell>
166
+ <Table.Cell>
167
+ <Code>{size.paddingX}</Code>
168
+ </Table.Cell>
169
+ <Table.Cell>
170
+ <Code>{size.fontSize}</Code>
171
+ </Table.Cell>
172
+ <Table.Cell>
173
+ <Code>{size.iconSize}</Code>
174
+ </Table.Cell>
175
+ <Table.Cell>
176
+ <Inline gap={config.defaults.gap} wrap={false}>
177
+ <Input size={key} placeholder="Input" aria-label={`Input ${key}`} className="yarcl-ref-input" />
178
+ <Button size={key}>Button</Button>
179
+ <IconButton size={key} aria-label="Add">
180
+ +
181
+ </IconButton>
182
+ </Inline>
183
+ </Table.Cell>
184
+ </Table.Row>
185
+ );
186
+ })}
187
+ </Table.Body>
188
+ </Table>
189
+ </Section>
190
+
191
+ <Section
192
+ title="Radii"
193
+ description={
194
+ shape.defaults.radius === 'size'
195
+ ? 'Controls use the radius named like their size; other keys are exceptions.'
196
+ : `Default radius: ${shape.defaults.radius}.`
197
+ }
198
+ >
199
+ <Inline>
200
+ {keys<Radius>(config.radii).map((key) => (
201
+ <Stack key={key} gap={config.defaults.gap} align="center">
202
+ <div className={`yarcl-ref-radius yarcl-radius-${key}`} />
203
+ <Text textStyle={config.defaults.labelStyle}>{key}</Text>
204
+ <Text textStyle={config.defaults.helperStyle} muted>
205
+ <Code>{shape.radii[key]}</Code>
206
+ </Text>
207
+ </Stack>
208
+ ))}
209
+ </Inline>
210
+ </Section>
211
+
212
+ <Section title="Variants" description="Recipes applied on top of any color.">
213
+ {variants.map((key) => {
214
+ const recipe = shape.variants[key];
215
+ return (
216
+ <Inline key={key}>
217
+ <Text textStyle={config.defaults.labelStyle} className="yarcl-ref-label">
218
+ {key}
219
+ </Text>
220
+ {colors.map((color) => (
221
+ <Button key={color} variant={key} color={color} size={config.defaults.size}>
222
+ {color}
223
+ </Button>
224
+ ))}
225
+ <Text textStyle={config.defaults.helperStyle} muted>
226
+ background <Code>{recipe.background}</Code>, border <Code>{recipe.border}</Code>, text <Code>{recipe.text}</Code>
227
+ </Text>
228
+ </Inline>
229
+ );
230
+ })}
231
+ </Section>
232
+
233
+ <Section title="Spacing">
234
+ {keys<Spacing>(config.spacing).map((key) => (
235
+ <Inline key={key}>
236
+ <Text textStyle={config.defaults.labelStyle} className="yarcl-ref-label">
237
+ {key}
238
+ </Text>
239
+ <div className="yarcl-ref-space" style={{ width: `var(--yarcl-space-${key})` }} />
240
+ <Code>{shape.spacing[key]}</Code>
241
+ </Inline>
242
+ ))}
243
+ </Section>
244
+
245
+ <Section title="Shadows">
246
+ <Inline gap={config.defaults.padding}>
247
+ {keys<Shadow>(config.shadows).map((key) => (
248
+ <Card key={key} shadow={key} className="yarcl-ref-shadow">
249
+ <Text textStyle={config.defaults.labelStyle}>{key}</Text>
250
+ </Card>
251
+ ))}
252
+ </Inline>
253
+ </Section>
254
+
255
+ <Section title="Typography">
256
+ {(typography.fontFaces?.length ?? 0) > 0 && (
257
+ <Table caption="Font files" density={config.defaults.density}>
258
+ <Table.Head>
259
+ <Table.Row>
260
+ <Table.HeaderCell>Family</Table.HeaderCell>
261
+ <Table.HeaderCell>Source</Table.HeaderCell>
262
+ <Table.HeaderCell>Weight</Table.HeaderCell>
263
+ <Table.HeaderCell>Style</Table.HeaderCell>
264
+ </Table.Row>
265
+ </Table.Head>
266
+ <Table.Body>
267
+ {typography.fontFaces!.map((face, i) => (
268
+ <Table.Row key={i}>
269
+ <Table.Cell>{face.family}</Table.Cell>
270
+ <Table.Cell>
271
+ <Code>{[face.src].flat().join(', ')}</Code>
272
+ </Table.Cell>
273
+ <Table.Cell>{face.weight ?? 'n/a'}</Table.Cell>
274
+ <Table.Cell>{face.style ?? 'normal'}</Table.Cell>
275
+ </Table.Row>
276
+ ))}
277
+ </Table.Body>
278
+ </Table>
279
+ )}
280
+ <KeyValues
281
+ caption="Families"
282
+ rows={Object.entries(typography.families).map(([key, stack]) => [
283
+ key,
284
+ <span style={{ fontFamily: `var(--yarcl-font-${key})` }}>
285
+ The quick brown fox <Code>{stack}</Code>
286
+ </span>,
287
+ ])}
288
+ />
289
+ <Table caption="Text styles" density={config.defaults.density}>
290
+ <Table.Body>
291
+ {keys<TextStyle>(config.typography.styles).map((key) => {
292
+ const style = typography.styles[key];
293
+ return (
294
+ <Table.Row key={key}>
295
+ <Table.HeaderCell scope="row">{key}</Table.HeaderCell>
296
+ <Table.Cell>
297
+ <Text textStyle={key}>The quick brown fox</Text>
298
+ </Table.Cell>
299
+ <Table.Cell>
300
+ <Text textStyle={config.defaults.helperStyle} muted>
301
+ {style.family} · {style.size} · {style.weight} · {style.lineHeight}
302
+ {style.letterSpacing ? ` · ${style.letterSpacing}` : ''}
303
+ </Text>
304
+ </Table.Cell>
305
+ </Table.Row>
306
+ );
307
+ })}
308
+ </Table.Body>
309
+ </Table>
310
+ <KeyValues
311
+ caption="Heading levels"
312
+ rows={(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const).map((level) => [
313
+ level,
314
+ <Text textStyle={config.typography.headings[level]}>
315
+ {typography.headings[level]}
316
+ </Text>,
317
+ ])}
318
+ />
319
+ </Section>
320
+
321
+ <Section title="Modal sizes" description="Widths of dialogs and drawers.">
322
+ <KeyValues
323
+ caption="Modal sizes"
324
+ rows={Object.entries(shape.modalSizes).map(([key, value]): [string, ReactNode] => [key, <Code>{value}</Code>])}
325
+ />
326
+ </Section>
327
+
328
+ <Section title="Density">
329
+ <KeyValues
330
+ caption="Table density"
331
+ rows={keys<Density>(config.density).map((key) => {
332
+ const density = shape.density[key];
333
+ return [key, <Code>{`${density.paddingY} ${density.paddingX} · ${density.fontSize}`}</Code>];
334
+ })}
335
+ />
336
+ </Section>
337
+
338
+ <Section title="Other tokens">
339
+ <KeyValues
340
+ caption="Layers, motion, borders and focus"
341
+ rows={[
342
+ ...Object.entries(shape.zIndex).map(([k, v]): [string, ReactNode] => [`zIndex.${k}`, <Code>{v}</Code>]),
343
+ ...Object.entries(shape.motion).map(([k, v]): [string, ReactNode] => [`motion.${k}`, <Code>{v}</Code>]),
344
+ ...Object.entries(shape.borders).map(([k, v]): [string, ReactNode] => [`borders.${k}`, <Code>{v}</Code>]),
345
+ ['focusRing', <Code>{`${shape.focusRing.width} · offset ${shape.focusRing.offset} · ${shape.focusRing.color}`}</Code>],
346
+ ]}
347
+ />
348
+ </Section>
349
+
350
+ <Section
351
+ title="Defaults"
352
+ description="Values used when a component prop is omitted: the prop, then an enclosing group, then component defaults, then global defaults."
353
+ >
354
+ {Object.keys(shape.components ?? {}).length > 0 && (
355
+ <KeyValues
356
+ caption="Component defaults"
357
+ rows={Object.entries(shape.components ?? {}).map(([name, values]): [string, ReactNode] => [
358
+ name,
359
+ <Code>
360
+ {Object.entries(values ?? {})
361
+ .map(([prop, value]) => `${prop}: ${value}`)
362
+ .join(' · ')}
363
+ </Code>,
364
+ ])}
365
+ />
366
+ )}
367
+ <KeyValues
368
+ caption="Global defaults"
369
+ rows={Object.entries(shape.defaults).map(([key, value]): [string, ReactNode] => [key, <Code>{value}</Code>])}
370
+ />
371
+ </Section>
372
+ </Stack>
373
+ );
374
+ }
@@ -0,0 +1,2 @@
1
+ export { DesignReference } from './DesignReference';
2
+ export type { DesignReferenceProps } from './DesignReference';
@@ -0,0 +1,58 @@
1
+ .yarcl-ref-grid {
2
+ display: grid;
3
+ grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
4
+ gap: var(--yarcl-gap);
5
+ }
6
+
7
+ .yarcl-ref-color {
8
+ display: grid;
9
+ gap: 0.5rem;
10
+ }
11
+
12
+ .yarcl-ref-swatch {
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ height: 4rem;
17
+ font-weight: 700;
18
+ font-size: 1.25rem;
19
+ color: var(--yarcl-c-on);
20
+ background: var(--yarcl-c);
21
+ border-radius: 0.375rem;
22
+ }
23
+
24
+ .yarcl-ref-neutral {
25
+ border: var(--yarcl-border-width) solid var(--yarcl-neutral-border);
26
+ }
27
+
28
+ .yarcl-ref-code {
29
+ font-family: var(--yarcl-ref-mono, ui-monospace, 'SF Mono', Menlo, Consolas, monospace);
30
+ font-size: 0.85em;
31
+ overflow-wrap: anywhere;
32
+ }
33
+
34
+ .yarcl-ref-radius {
35
+ width: 4rem;
36
+ height: 4rem;
37
+ background: color-mix(in oklab, var(--yarcl-focus-color) 18%, var(--yarcl-neutral-surface));
38
+ border: var(--yarcl-border-width) solid var(--yarcl-focus-color);
39
+ border-radius: var(--yarcl-r);
40
+ }
41
+
42
+ .yarcl-ref-space {
43
+ height: 1rem;
44
+ background: var(--yarcl-focus-color);
45
+ border-radius: 2px;
46
+ }
47
+
48
+ .yarcl-ref-label {
49
+ min-width: 6rem;
50
+ }
51
+
52
+ .yarcl-ref-shadow {
53
+ width: 8rem;
54
+ }
55
+
56
+ .yarcl-ref-input {
57
+ width: 8rem;
58
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,38 @@
1
+ import buildConfig from '@yarcl/config';
2
+ import { useSyncExternalStore } from 'react';
3
+ import type { ComponentName, YarclShape } from './define';
4
+ import type { ComponentDefaults } from './types';
5
+
6
+ type Config = typeof buildConfig;
7
+
8
+ let active: Config = buildConfig;
9
+ const listeners = new Set<() => void>();
10
+
11
+ function subscribe(listener: () => void) {
12
+ listeners.add(listener);
13
+ return () => {
14
+ listeners.delete(listener);
15
+ };
16
+ }
17
+
18
+ export function activeConfig(): Config {
19
+ return active;
20
+ }
21
+
22
+ export function setActiveConfig(next: YarclShape | null) {
23
+ active = (next ?? buildConfig) as Config;
24
+ listeners.forEach((listener) => listener());
25
+ }
26
+
27
+ /**
28
+ * The config components currently read their defaults from: the build-time config, or the
29
+ * theme passed to `applyTheme`. Re-renders the caller when the theme changes.
30
+ */
31
+ export function useConfig(): Config {
32
+ return useSyncExternalStore(subscribe, activeConfig, () => buildConfig);
33
+ }
34
+
35
+ export function useDefaults(component: ComponentName): ComponentDefaults {
36
+ const config = useConfig();
37
+ return ((config as unknown as YarclShape).components?.[component] ?? {}) as ComponentDefaults;
38
+ }