@rinzai/zen 0.14.0 → 0.15.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/CHANGELOG.md +138 -0
- package/README.md +16 -1
- package/dist/carousel/index.d.ts.map +1 -1
- package/dist/carousel/index.js +5 -5
- package/dist/docs/README.md +205 -0
- package/dist/docs/accordion.md +122 -0
- package/dist/docs/alert.md +69 -0
- package/dist/docs/animated-number.md +105 -0
- package/dist/docs/avatar.md +91 -0
- package/dist/docs/backdrop.md +94 -0
- package/dist/docs/badge.md +37 -0
- package/dist/docs/breadcrumbs.md +44 -0
- package/dist/docs/button-group.md +76 -0
- package/dist/docs/button.md +131 -0
- package/dist/docs/calendar-heatmap.md +73 -0
- package/dist/docs/calendar.md +101 -0
- package/dist/docs/canvas.md +584 -0
- package/dist/docs/card.md +180 -0
- package/dist/docs/carousel.md +162 -0
- package/dist/docs/chart.md +325 -0
- package/dist/docs/checkbox.md +88 -0
- package/dist/docs/chip.md +52 -0
- package/dist/docs/code-block.md +116 -0
- package/dist/docs/code-input.md +52 -0
- package/dist/docs/collapse.md +46 -0
- package/dist/docs/color-picker.md +145 -0
- package/dist/docs/combobox.md +208 -0
- package/dist/docs/command-palette.md +65 -0
- package/dist/docs/date-picker.md +167 -0
- package/dist/docs/dialog.md +271 -0
- package/dist/docs/disclosure.md +88 -0
- package/dist/docs/editable-cell.md +90 -0
- package/dist/docs/empty-state.md +46 -0
- package/dist/docs/field.md +82 -0
- package/dist/docs/file-drop.md +44 -0
- package/dist/docs/filter-bar.md +66 -0
- package/dist/docs/header.md +51 -0
- package/dist/docs/hover-card.md +110 -0
- package/dist/docs/icon-picker.md +65 -0
- package/dist/docs/input-group.md +89 -0
- package/dist/docs/input.md +33 -0
- package/dist/docs/inset.md +57 -0
- package/dist/docs/kbd.md +34 -0
- package/dist/docs/keypad.md +51 -0
- package/dist/docs/load-more.md +66 -0
- package/dist/docs/locale-provider.md +99 -0
- package/dist/docs/marquee.md +86 -0
- package/dist/docs/menu.md +403 -0
- package/dist/docs/meter.md +102 -0
- package/dist/docs/money-input.md +169 -0
- package/dist/docs/month-picker.md +80 -0
- package/dist/docs/navigation-menu.md +157 -0
- package/dist/docs/number-field.md +129 -0
- package/dist/docs/page-header.md +44 -0
- package/dist/docs/page-transition.md +93 -0
- package/dist/docs/pagination.md +42 -0
- package/dist/docs/pills.md +85 -0
- package/dist/docs/player-bar.md +158 -0
- package/dist/docs/popover.md +200 -0
- package/dist/docs/progress-ring.md +70 -0
- package/dist/docs/radio-cards.md +82 -0
- package/dist/docs/radio-group.md +86 -0
- package/dist/docs/range-slider.md +64 -0
- package/dist/docs/repeat-picker.md +44 -0
- package/dist/docs/resizable.md +107 -0
- package/dist/docs/seek-bar.md +68 -0
- package/dist/docs/segmented.md +89 -0
- package/dist/docs/select.md +148 -0
- package/dist/docs/separator.md +76 -0
- package/dist/docs/side-nav.md +75 -0
- package/dist/docs/skeleton.md +89 -0
- package/dist/docs/slider.md +87 -0
- package/dist/docs/sortable.md +182 -0
- package/dist/docs/sparkline.md +56 -0
- package/dist/docs/spinner.md +69 -0
- package/dist/docs/status-pill.md +81 -0
- package/dist/docs/stepper.md +59 -0
- package/dist/docs/tab-bar.md +78 -0
- package/dist/docs/table-of-contents.md +43 -0
- package/dist/docs/table.md +714 -0
- package/dist/docs/tabs.md +140 -0
- package/dist/docs/tag-input.md +45 -0
- package/dist/docs/textarea.md +25 -0
- package/dist/docs/theme-toggle.md +85 -0
- package/dist/docs/theming.md +193 -0
- package/dist/docs/time-field.md +71 -0
- package/dist/docs/timeline.md +50 -0
- package/dist/docs/toast.md +53 -0
- package/dist/docs/toggle-group.md +83 -0
- package/dist/docs/toggle.md +38 -0
- package/dist/docs/toolbar.md +97 -0
- package/dist/docs/tooltip.md +60 -0
- package/dist/docs/tree.md +295 -0
- package/dist/docs/trend.md +50 -0
- package/dist/icons/arrow-path.d.ts +3 -0
- package/dist/icons/arrow-path.d.ts.map +1 -0
- package/dist/icons/arrows-right-left.d.ts +3 -0
- package/dist/icons/arrows-right-left.d.ts.map +1 -0
- package/dist/icons/backward.d.ts +3 -0
- package/dist/icons/backward.d.ts.map +1 -0
- package/dist/icons/forward.d.ts +3 -0
- package/dist/icons/forward.d.ts.map +1 -0
- package/dist/icons/heart-solid.d.ts +3 -0
- package/dist/icons/heart-solid.d.ts.map +1 -0
- package/dist/icons/heart.d.ts +3 -0
- package/dist/icons/heart.d.ts.map +1 -0
- package/dist/icons/musical-note.d.ts +3 -0
- package/dist/icons/musical-note.d.ts.map +1 -0
- package/dist/icons/pause.d.ts +3 -0
- package/dist/icons/pause.d.ts.map +1 -0
- package/dist/icons/play.d.ts +3 -0
- package/dist/icons/play.d.ts.map +1 -0
- package/dist/icons/queue-list.d.ts +3 -0
- package/dist/icons/queue-list.d.ts.map +1 -0
- package/dist/icons/speaker-wave.d.ts +3 -0
- package/dist/icons/speaker-wave.d.ts.map +1 -0
- package/dist/icons/speaker-x-mark.d.ts +3 -0
- package/dist/icons/speaker-x-mark.d.ts.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib.js +80 -74
- package/dist/marquee/index.d.ts +15 -0
- package/dist/marquee/index.d.ts.map +1 -0
- package/dist/marquee/index.js +90 -0
- package/dist/menu/index.d.ts.map +1 -1
- package/dist/menu/index.js +7 -6
- package/dist/player-bar/index.d.ts +11 -0
- package/dist/player-bar/index.d.ts.map +1 -0
- package/dist/player-bar/index.js +17 -0
- package/dist/radio-cards/index.d.ts +2 -1
- package/dist/radio-cards/index.d.ts.map +1 -1
- package/dist/radio-cards/index.js +3 -3
- package/dist/seek-bar/index.d.ts +24 -0
- package/dist/seek-bar/index.d.ts.map +1 -0
- package/dist/seek-bar/index.js +120 -0
- package/dist/segmented/index.d.ts +3 -2
- package/dist/segmented/index.d.ts.map +1 -1
- package/dist/segmented/index.js +19 -18
- package/dist/slider/index.d.ts +2 -1
- package/dist/slider/index.d.ts.map +1 -1
- package/dist/tab-bar/index.d.ts +4 -2
- package/dist/tab-bar/index.d.ts.map +1 -1
- package/dist/tab-bar/index.js +24 -19
- package/dist/textarea/index.d.ts +6 -2
- package/dist/textarea/index.d.ts.map +1 -1
- package/dist/textarea/index.js +2 -1
- package/dist/theme.css +40 -0
- package/dist/utils/duration.d.ts +3 -0
- package/dist/utils/duration.d.ts.map +1 -0
- package/dist/utils/duration.js +7 -0
- package/dist/utils/messages.d.ts +2 -0
- package/dist/utils/messages.d.ts.map +1 -1
- package/dist/utils/messages.js +3 -1
- package/dist/utils/slot.d.ts +5 -2
- package/dist/utils/slot.d.ts.map +1 -1
- package/dist/utils/slot.js +16 -11
- package/dist/utils/useAnchoredPopup.d.ts.map +1 -1
- package/dist/utils/useAnchoredPopup.js +3 -2
- package/dist/utils/useHeightVar.d.ts +7 -0
- package/dist/utils/useHeightVar.d.ts.map +1 -0
- package/dist/utils/useHeightVar.js +17 -0
- package/dist/utils/useMediaPlayer.d.ts +38 -0
- package/dist/utils/useMediaPlayer.d.ts.map +1 -0
- package/dist/utils/useMediaPlayer.js +91 -0
- package/dist/utils/useMediaSession.d.ts +24 -0
- package/dist/utils/useMediaSession.d.ts.map +1 -0
- package/dist/utils/useMediaSession.js +76 -0
- package/dist/zen.css +1 -1
- package/package.json +4 -2
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Tabs
|
|
2
|
+
|
|
3
|
+
Switch between panels in place, with an underline or a glowing pill that slides to the active tab. To go to another page, use Pills.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { Tabs, TabList, Tab, TabPanel } from '@rinzai/zen';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### Underline
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Tab, TabList, TabPanel, Tabs } from '@rinzai/zen';
|
|
15
|
+
|
|
16
|
+
export default function Underline() {
|
|
17
|
+
return (
|
|
18
|
+
<Tabs defaultValue="month" className="w-96">
|
|
19
|
+
<TabList>
|
|
20
|
+
<Tab value="month">Month</Tab>
|
|
21
|
+
<Tab value="year">Year</Tab>
|
|
22
|
+
<Tab value="all">All time</Tab>
|
|
23
|
+
</TabList>
|
|
24
|
+
<TabPanel value="month" className="text-muted-foreground text-sm">
|
|
25
|
+
Spending for September.
|
|
26
|
+
</TabPanel>
|
|
27
|
+
<TabPanel value="year" className="text-muted-foreground text-sm">
|
|
28
|
+
Spending for 2026 so far.
|
|
29
|
+
</TabPanel>
|
|
30
|
+
<TabPanel value="all" className="text-muted-foreground text-sm">
|
|
31
|
+
Everything since you started.
|
|
32
|
+
</TabPanel>
|
|
33
|
+
</Tabs>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Pills
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { Tab, TabList, TabPanel, Tabs } from '@rinzai/zen';
|
|
42
|
+
|
|
43
|
+
export default function Pills() {
|
|
44
|
+
return (
|
|
45
|
+
<Tabs defaultValue="all" className="w-96">
|
|
46
|
+
<TabList variant="pills">
|
|
47
|
+
<Tab value="all">All</Tab>
|
|
48
|
+
<Tab value="expenses">Expenses</Tab>
|
|
49
|
+
<Tab value="income">Income</Tab>
|
|
50
|
+
</TabList>
|
|
51
|
+
<TabPanel value="all" className="text-muted-foreground text-sm">
|
|
52
|
+
Every entry this month.
|
|
53
|
+
</TabPanel>
|
|
54
|
+
<TabPanel value="expenses" className="text-muted-foreground text-sm">
|
|
55
|
+
Money going out.
|
|
56
|
+
</TabPanel>
|
|
57
|
+
<TabPanel value="income" className="text-muted-foreground text-sm">
|
|
58
|
+
Money coming in.
|
|
59
|
+
</TabPanel>
|
|
60
|
+
</Tabs>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Panels
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { Meter, Tab, TabList, TabPanel, Tabs } from '@rinzai/zen';
|
|
69
|
+
|
|
70
|
+
const BUDGETS = [
|
|
71
|
+
{ name: 'Groceries', spent: 412, limit: 500 },
|
|
72
|
+
{ name: 'Eating out', spent: 188, limit: 150 },
|
|
73
|
+
{ name: 'Transport', spent: 64, limit: 120 },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
/** The last panel slides out as the picked one slides in, and the space between eases to fit. */
|
|
77
|
+
export default function Panels() {
|
|
78
|
+
return (
|
|
79
|
+
<Tabs defaultValue="summary" className="w-full max-w-md">
|
|
80
|
+
<TabList>
|
|
81
|
+
<Tab value="summary">Summary</Tab>
|
|
82
|
+
<Tab value="budgets">Budgets</Tab>
|
|
83
|
+
<Tab value="notes">Notes</Tab>
|
|
84
|
+
</TabList>
|
|
85
|
+
<TabPanel value="summary" className="text-sm">
|
|
86
|
+
<p className="text-2xl font-semibold tabular-nums">$1,845.11</p>
|
|
87
|
+
<p className="text-muted-foreground">Spent this month, $210 less than last.</p>
|
|
88
|
+
</TabPanel>
|
|
89
|
+
<TabPanel value="budgets" className="flex flex-col gap-4 text-sm">
|
|
90
|
+
{BUDGETS.map((b) => (
|
|
91
|
+
<Meter
|
|
92
|
+
key={b.name}
|
|
93
|
+
label={b.name}
|
|
94
|
+
value={b.spent}
|
|
95
|
+
max={b.limit}
|
|
96
|
+
detail={`$${b.spent} / $${b.limit}`}
|
|
97
|
+
/>
|
|
98
|
+
))}
|
|
99
|
+
</TabPanel>
|
|
100
|
+
<TabPanel value="notes" className="text-muted-foreground text-sm">
|
|
101
|
+
Rent goes out on the 5th.
|
|
102
|
+
</TabPanel>
|
|
103
|
+
</Tabs>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## API reference
|
|
109
|
+
|
|
110
|
+
### Tabs
|
|
111
|
+
|
|
112
|
+
Tab set with an underline that slides to the active tab. Controlled (`value`) or not (`defaultValue`).
|
|
113
|
+
|
|
114
|
+
| Prop | Type | Default | Description |
|
|
115
|
+
| --- | --- | --- | --- |
|
|
116
|
+
| `defaultValue` | `string` | | |
|
|
117
|
+
| `onValueChange` | `((value: string) => void)` | | |
|
|
118
|
+
| `value` | `string` | | |
|
|
119
|
+
|
|
120
|
+
### TabList
|
|
121
|
+
|
|
122
|
+
The row of tabs. `underline` (default): in-page tabs, a bar sliding under
|
|
123
|
+
the active tab. `pills`: the Pills look, a glowing pill sliding along a
|
|
124
|
+
glass track (for navigation between pages, use Pills itself).
|
|
125
|
+
|
|
126
|
+
| Prop | Type | Default | Description |
|
|
127
|
+
| --- | --- | --- | --- |
|
|
128
|
+
| `variant` | `TabListVariant` | `underline` | |
|
|
129
|
+
|
|
130
|
+
### Tab
|
|
131
|
+
|
|
132
|
+
| Prop | Type | Default | Description |
|
|
133
|
+
| --- | --- | --- | --- |
|
|
134
|
+
| `value` (required) | `string` | | |
|
|
135
|
+
|
|
136
|
+
### TabPanel
|
|
137
|
+
|
|
138
|
+
| Prop | Type | Default | Description |
|
|
139
|
+
| --- | --- | --- | --- |
|
|
140
|
+
| `value` (required) | `string` | | |
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Tag Input
|
|
2
|
+
|
|
3
|
+
A field of short values (tags, emails): Enter or a comma adds one, Backspace takes the last back.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { TagInput } from '@rinzai/zen';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### Default
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Field, TagInput } from '@rinzai/zen';
|
|
15
|
+
import { useState } from 'react';
|
|
16
|
+
|
|
17
|
+
/** An entry's labels. Type one and press Enter or a comma; paste a list to add each. */
|
|
18
|
+
export default function Default() {
|
|
19
|
+
const [tags, setTags] = useState(['groceries', 'weekly']);
|
|
20
|
+
return (
|
|
21
|
+
<Field label="Labels" hint="Press Enter or a comma after each." className="w-full max-w-sm">
|
|
22
|
+
<TagInput value={tags} onValueChange={setTags} placeholder="Add a label" />
|
|
23
|
+
</Field>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## API reference
|
|
29
|
+
|
|
30
|
+
### TagInput
|
|
31
|
+
|
|
32
|
+
A field of short values (tags, emails, labels): typing one and pressing
|
|
33
|
+
Enter or a comma adds it, pasting a list adds each, Backspace in the empty
|
|
34
|
+
field takes the last one back. Each is removed with its ×. Values are
|
|
35
|
+
trimmed and kept once. Past about three rows the tags scroll, under a
|
|
36
|
+
typing line that stays in view (a max-h-* class sets the field's height).
|
|
37
|
+
|
|
38
|
+
| Prop | Type | Default | Description |
|
|
39
|
+
| --- | --- | --- | --- |
|
|
40
|
+
| `defaultValue` | `string[]` | `[]` | |
|
|
41
|
+
| `name` | `string` | | Form field name: each tag is submitted under it. |
|
|
42
|
+
| `onValueChange` | `((value: string[]) => void)` | | |
|
|
43
|
+
| `removeLabel` | `((tag: string) => string)` | | The × button's label, for screen readers. |
|
|
44
|
+
| `validate` | `((entry: string) => boolean)` | | Whether an entry may be added (e.g. looks like an email). |
|
|
45
|
+
| `value` | `string[]` | | |
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Textarea
|
|
2
|
+
|
|
3
|
+
Multi-line text, styled like Input.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { Textarea } from '@rinzai/zen';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### Default
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Textarea } from '@rinzai/zen';
|
|
15
|
+
|
|
16
|
+
export default function Default() {
|
|
17
|
+
return <Textarea placeholder="Add a note…" className="w-80" />;
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## API reference
|
|
22
|
+
|
|
23
|
+
### Textarea
|
|
24
|
+
|
|
25
|
+
No props of its own; it accepts the props of the element it renders.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Theme Toggle
|
|
2
|
+
|
|
3
|
+
Switches between the dark and light themes, remembering the choice itself or leaving it to your app.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { ThemeToggle, ThemeScript } from '@rinzai/zen';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### Default
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { ThemeToggle } from '@rinzai/zen';
|
|
15
|
+
|
|
16
|
+
/** Flips the whole page between dark and light. Add <ThemeScript /> to <head> so the choice sticks on reload. */
|
|
17
|
+
export default function Default() {
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex items-center gap-3 text-sm">
|
|
20
|
+
<ThemeToggle />
|
|
21
|
+
<span className="text-muted-foreground">Switch the theme</span>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Your state
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { ThemeToggle, type Appearance } from '@rinzai/zen';
|
|
31
|
+
import { useRef, useState } from 'react';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The theme kept by your app (state here; an account setting, a cookie or a database in yours):
|
|
35
|
+
* the toggle shows `value`, and `onValueChange` applies the new one. This one themes just its panel,
|
|
36
|
+
* so `target` keeps the water inside it.
|
|
37
|
+
*/
|
|
38
|
+
export default function YourState() {
|
|
39
|
+
const [theme, setTheme] = useState<Appearance>('light');
|
|
40
|
+
const panel = useRef<HTMLDivElement>(null);
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
ref={panel}
|
|
44
|
+
className={`${theme} bg-background text-foreground flex h-96 w-full flex-col justify-between rounded-2xl border p-6 text-sm`}
|
|
45
|
+
>
|
|
46
|
+
<div className="flex items-center justify-between">
|
|
47
|
+
<span className="font-medium">Preview</span>
|
|
48
|
+
<ThemeToggle value={theme} onValueChange={setTheme} target={panel} />
|
|
49
|
+
</div>
|
|
50
|
+
<span className="text-muted-foreground">This panel is {theme}; the page around it stays as it is.</span>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## API reference
|
|
57
|
+
|
|
58
|
+
### ThemeToggle
|
|
59
|
+
|
|
60
|
+
Switches between the dark and light themes; the new theme spreads from the
|
|
61
|
+
button like a drop of water (themeWave).
|
|
62
|
+
|
|
63
|
+
On its own, it switches <html>'s class and remembers the choice (localStorage):
|
|
64
|
+
add ThemeScript to <head> to apply it on the next visit. Give it `value` and
|
|
65
|
+
`onValueChange` and the theme is yours: it shows `value` and leaves applying and
|
|
66
|
+
storing the choice to `onValueChange` (in your state, a database, a cookie…).
|
|
67
|
+
With `target` too, it switches the theme of one element (a panel, a preview),
|
|
68
|
+
and the water stays inside it.
|
|
69
|
+
|
|
70
|
+
| Prop | Type | Default | Description |
|
|
71
|
+
| --- | --- | --- | --- |
|
|
72
|
+
| `className` | `string` | | |
|
|
73
|
+
| `onValueChange` | `((theme: Appearance) => void)` | | Called with the theme switched to. |
|
|
74
|
+
| `storageKey` | `string` | `theme` | localStorage key for the choice, when the toggle keeps it. Use the same one in ThemeScript. |
|
|
75
|
+
| `target` | `RefObject<HTMLElement \| null>` | | The element whose theme `onValueChange` switches, when it's not the page (a panel with its own .dark or .light): the wave plays inside it. Needs `value`. |
|
|
76
|
+
| `value` | `Appearance` | | The theme, when you keep it: the toggle shows it, and `onValueChange` must apply the new one (by the end of a synchronous render and its effects). Without it, the toggle switches <html>'s class itself. |
|
|
77
|
+
|
|
78
|
+
### ThemeScript
|
|
79
|
+
|
|
80
|
+
Put in <head>: applies the remembered theme before the first paint, so a
|
|
81
|
+
light-theme visitor never sees a dark flash on page load.
|
|
82
|
+
|
|
83
|
+
| Prop | Type | Default | Description |
|
|
84
|
+
| --- | --- | --- | --- |
|
|
85
|
+
| `storageKey` | `string` | `theme` | |
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Theming & styling
|
|
2
|
+
|
|
3
|
+
Your own colours from a few settings, readable in dark and light for any hue; and className overrides that always win.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { applyTheme, applyPreset, customize, PRESETS, DEFAULT_THEME, themeVars, cx } from '@rinzai/zen';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### Theme editor
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import {
|
|
15
|
+
applyPreset,
|
|
16
|
+
applyTheme,
|
|
17
|
+
Badge,
|
|
18
|
+
Button,
|
|
19
|
+
customize,
|
|
20
|
+
DEFAULT_THEME,
|
|
21
|
+
INTENSITIES,
|
|
22
|
+
Input,
|
|
23
|
+
Meter,
|
|
24
|
+
type PresetId,
|
|
25
|
+
PRESETS,
|
|
26
|
+
Segmented,
|
|
27
|
+
SegmentedItem,
|
|
28
|
+
Select,
|
|
29
|
+
SelectItem,
|
|
30
|
+
Slider,
|
|
31
|
+
type ThemeSettings,
|
|
32
|
+
Toggle,
|
|
33
|
+
} from '@rinzai/zen';
|
|
34
|
+
import { useEffect, useRef, useState } from 'react';
|
|
35
|
+
|
|
36
|
+
const HUES = `linear-gradient(to right, ${Array.from({ length: 13 }, (_, i) => `oklch(0.72 0.13 ${i * 30})`).join(', ')})`;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A few settings make every colour token, readable in dark and light for any
|
|
40
|
+
* hue. Here the theme applies to the preview only (`target`); leave it out to
|
|
41
|
+
* theme the whole page.
|
|
42
|
+
*/
|
|
43
|
+
export default function ThemeEditor() {
|
|
44
|
+
const [theme, setTheme] = useState<ThemeSettings>(applyPreset(DEFAULT_THEME, 'ocean'));
|
|
45
|
+
const preview = useRef<HTMLDivElement>(null);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const root = document.documentElement;
|
|
49
|
+
const apply = () =>
|
|
50
|
+
preview.current &&
|
|
51
|
+
applyTheme(theme, {
|
|
52
|
+
appearance: root.classList.contains('light') ? 'light' : 'dark',
|
|
53
|
+
target: preview.current,
|
|
54
|
+
});
|
|
55
|
+
apply();
|
|
56
|
+
// Follow the page's dark/light switch.
|
|
57
|
+
const watch = new MutationObserver(apply);
|
|
58
|
+
watch.observe(root, { attributes: true, attributeFilter: ['class'] });
|
|
59
|
+
return () => watch.disconnect();
|
|
60
|
+
}, [theme]);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="grid w-full max-w-2xl gap-6 sm:grid-cols-2">
|
|
64
|
+
<div className="flex flex-col gap-4">
|
|
65
|
+
<Select
|
|
66
|
+
aria-label="Preset"
|
|
67
|
+
value={theme.preset === 'custom' ? null : theme.preset}
|
|
68
|
+
placeholder="Custom"
|
|
69
|
+
onValueChange={(id: PresetId) => setTheme((t) => applyPreset(t, id))}
|
|
70
|
+
>
|
|
71
|
+
{PRESETS.map((p) => (
|
|
72
|
+
<SelectItem key={p.id} value={p.id}>
|
|
73
|
+
{p.label}
|
|
74
|
+
</SelectItem>
|
|
75
|
+
))}
|
|
76
|
+
</Select>
|
|
77
|
+
<Slider
|
|
78
|
+
aria-label="Hue"
|
|
79
|
+
min={0}
|
|
80
|
+
max={359}
|
|
81
|
+
value={theme.hue}
|
|
82
|
+
onValueChange={(hue) => setTheme((t) => customize(t, { hue }))}
|
|
83
|
+
trackBackground={HUES}
|
|
84
|
+
thumbColor={`oklch(0.72 0.13 ${theme.hue})`}
|
|
85
|
+
valueText={(v) => `Hue ${v} degrees`}
|
|
86
|
+
/>
|
|
87
|
+
<Segmented
|
|
88
|
+
label="Intensity"
|
|
89
|
+
value={theme.intensity}
|
|
90
|
+
onValueChange={(intensity) => setTheme((t) => customize(t, { intensity }))}
|
|
91
|
+
>
|
|
92
|
+
{INTENSITIES.map((i) => (
|
|
93
|
+
<SegmentedItem key={i} value={i}>
|
|
94
|
+
{i[0].toUpperCase() + i.slice(1)}
|
|
95
|
+
</SegmentedItem>
|
|
96
|
+
))}
|
|
97
|
+
</Segmented>
|
|
98
|
+
<label className="flex items-center justify-between gap-4 text-sm">
|
|
99
|
+
Tinted greys
|
|
100
|
+
<Toggle
|
|
101
|
+
checked={theme.tint === 'tinted'}
|
|
102
|
+
onCheckedChange={(on) => setTheme((t) => customize(t, { tint: on ? 'tinted' : 'neutral' }))}
|
|
103
|
+
aria-label="Tinted greys"
|
|
104
|
+
/>
|
|
105
|
+
</label>
|
|
106
|
+
<label className="flex items-center justify-between gap-4 text-sm">
|
|
107
|
+
High contrast
|
|
108
|
+
<Toggle
|
|
109
|
+
checked={theme.contrast === 'high'}
|
|
110
|
+
onCheckedChange={(on) => setTheme((t) => ({ ...t, contrast: on ? 'high' : 'standard' }))}
|
|
111
|
+
aria-label="High contrast"
|
|
112
|
+
/>
|
|
113
|
+
</label>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div
|
|
117
|
+
ref={preview}
|
|
118
|
+
className="bg-background text-foreground border-tint/10 flex flex-col gap-4 rounded-xl border p-5"
|
|
119
|
+
>
|
|
120
|
+
<div className="flex items-center justify-between">
|
|
121
|
+
<span className="font-semibold">Groceries</span>
|
|
122
|
+
<Badge>On track</Badge>
|
|
123
|
+
</div>
|
|
124
|
+
<Meter label="September" value={420} max={600} detail="$420 / $600" hint="$180 left" />
|
|
125
|
+
<Input placeholder="Add a note" />
|
|
126
|
+
<div className="flex gap-2">
|
|
127
|
+
<Button>Save</Button>
|
|
128
|
+
<Button variant="outline">Cancel</Button>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Stylesheet
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
import { applyPreset, CodeBlock, DEFAULT_THEME, Segmented, SegmentedItem, themeVars } from '@rinzai/zen';
|
|
140
|
+
import { useState } from 'react';
|
|
141
|
+
|
|
142
|
+
const css = (vars: Record<string, string>) =>
|
|
143
|
+
Object.entries(vars)
|
|
144
|
+
.map(([k, v]) => ` --${k}: ${v};`)
|
|
145
|
+
.join('\n');
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* No script needed: themeVars gives the tokens as data, so a theme can be
|
|
149
|
+
* written into a stylesheet at build time or on the server.
|
|
150
|
+
*/
|
|
151
|
+
export default function Stylesheet() {
|
|
152
|
+
const [preset, setPreset] = useState<'teal' | 'amber'>('teal');
|
|
153
|
+
const theme = applyPreset(DEFAULT_THEME, preset);
|
|
154
|
+
const code = `:root, .dark {\n${css(themeVars(theme, 'dark'))}\n}\n.light {\n${css(themeVars(theme, 'light'))}\n}`;
|
|
155
|
+
return (
|
|
156
|
+
<div className="flex w-full max-w-2xl flex-col gap-3">
|
|
157
|
+
<Segmented label="Preset" value={preset} onValueChange={setPreset}>
|
|
158
|
+
<SegmentedItem value="teal">Teal</SegmentedItem>
|
|
159
|
+
<SegmentedItem value="amber">Amber</SegmentedItem>
|
|
160
|
+
</Segmented>
|
|
161
|
+
<CodeBlock code={code} language="css" className="max-h-72 overflow-auto" />
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Overrides
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
import { Avatar, Button, cx } from '@rinzai/zen';
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* className always wins: when it sets something a component already sets
|
|
174
|
+
* (size, padding, radius, colour…), the component's default is dropped. Your
|
|
175
|
+
* own components get the same with cx.
|
|
176
|
+
*/
|
|
177
|
+
export default function Overrides() {
|
|
178
|
+
return (
|
|
179
|
+
<div className="flex flex-col gap-4">
|
|
180
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
181
|
+
<Button>Default</Button>
|
|
182
|
+
<Button className="h-12 rounded-full px-8">Taller, rounder</Button>
|
|
183
|
+
<Avatar name="Sam" />
|
|
184
|
+
<Avatar name="Sam" className="size-14 text-xl" />
|
|
185
|
+
</div>
|
|
186
|
+
<code className="text-muted-foreground text-xs">
|
|
187
|
+
cx('px-3 py-1 rounded-lg', 'p-4 rounded-full') →{' '}
|
|
188
|
+
{cx('px-3 py-1 rounded-lg', 'p-4 rounded-full')}
|
|
189
|
+
</code>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
```
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Time Field
|
|
2
|
+
|
|
3
|
+
A time of day, typed or stepped a part at a time, in the locale’s order, digits and 12- or 24-hour clock.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { TimeField } from '@rinzai/zen';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### Default
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Field, TimeField } from '@rinzai/zen';
|
|
15
|
+
import { useState } from 'react';
|
|
16
|
+
|
|
17
|
+
/** Type the digits, or ↑↓ each part; minutes step by 5 here. */
|
|
18
|
+
export default function Default() {
|
|
19
|
+
const [time, setTime] = useState<string | null>('09:30');
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex flex-col items-start gap-2">
|
|
22
|
+
<Field label="Reminder at">
|
|
23
|
+
<TimeField value={time} onValueChange={setTime} step={5} />
|
|
24
|
+
</Field>
|
|
25
|
+
<p className="text-muted-foreground mt-0! text-sm tabular-nums">Value: {time ?? 'null'}</p>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### With date
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { DatePicker, Field, TimeField } from '@rinzai/zen';
|
|
35
|
+
|
|
36
|
+
/** A date and a time side by side: a DatePicker for the day, a TimeField for the time. */
|
|
37
|
+
export default function WithDate() {
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex items-end gap-2">
|
|
40
|
+
<Field label="Date" className="w-44">
|
|
41
|
+
<DatePicker defaultValue="2026-10-06" />
|
|
42
|
+
</Field>
|
|
43
|
+
<Field label="Time">
|
|
44
|
+
<TimeField defaultValue="18:45" step={15} locale="en-US" />
|
|
45
|
+
</Field>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## API reference
|
|
52
|
+
|
|
53
|
+
### TimeField
|
|
54
|
+
|
|
55
|
+
A time of day, typed or stepped a part at a time: hour, minute and, where
|
|
56
|
+
the locale uses a 12-hour clock, AM/PM, in the locale's order and digits.
|
|
57
|
+
Type digits (it moves on when a part is full), ↑↓ to step (minutes by
|
|
58
|
+
`step`), ←→ between parts, Backspace to clear one. The value is "HH:MM"
|
|
59
|
+
(24-hour) once every part is set, and null until then.
|
|
60
|
+
|
|
61
|
+
| Prop | Type | Default | Description |
|
|
62
|
+
| --- | --- | --- | --- |
|
|
63
|
+
| `defaultValue` | `string \| null` | `null` | The first time, when the field keeps its own. |
|
|
64
|
+
| `disabled` | `boolean` | | |
|
|
65
|
+
| `invalid` | `boolean` | | Show the field in its error state. |
|
|
66
|
+
| `locale` | `string` | | Defaults to the LocaleProvider's. |
|
|
67
|
+
| `name` | `string` | | Form field name: the time is submitted under it as "HH:MM". |
|
|
68
|
+
| `onValueChange` | `((value: string \| null) => void)` | | |
|
|
69
|
+
| `size` | `"default" \| "lg" \| "sm"` | `default` | Height, matching Button's. |
|
|
70
|
+
| `step` | `number` | `1` | Minutes ↑↓ move by, e.g. 5 or 15. Default 1. |
|
|
71
|
+
| `value` | `string \| null` | | "HH:MM" (24-hour), or null, when you keep it. |
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Timeline
|
|
2
|
+
|
|
3
|
+
Events in order, each on a dot joined by a line: an activity feed or a history.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { Timeline, TimelineItem } from '@rinzai/zen';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### Default
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Timeline, TimelineItem } from '@rinzai/zen';
|
|
15
|
+
|
|
16
|
+
/** An entry's history, newest first; the latest is lit. */
|
|
17
|
+
export default function Default() {
|
|
18
|
+
return (
|
|
19
|
+
<Timeline className="w-full max-w-sm">
|
|
20
|
+
<TimelineItem active title="Paid" time="Today, 09:12">
|
|
21
|
+
$1,850 to Northside Lettings.
|
|
22
|
+
</TimelineItem>
|
|
23
|
+
<TimelineItem title="Amount changed" time="3 Sep">
|
|
24
|
+
From $1,800 to $1,850.
|
|
25
|
+
</TimelineItem>
|
|
26
|
+
<TimelineItem title="Created" time="1 Aug" />
|
|
27
|
+
</Timeline>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## API reference
|
|
33
|
+
|
|
34
|
+
### Timeline
|
|
35
|
+
|
|
36
|
+
Events in order, each on a dot joined by a line: an activity feed, a history.
|
|
37
|
+
|
|
38
|
+
No props of its own; it accepts the props of the element it renders.
|
|
39
|
+
|
|
40
|
+
### TimelineItem
|
|
41
|
+
|
|
42
|
+
One event: its title, when (`time`), and anything more as children. `icon`
|
|
43
|
+
replaces the dot; `active` lights it in the accent, e.g. what's happening now.
|
|
44
|
+
|
|
45
|
+
| Prop | Type | Default | Description |
|
|
46
|
+
| --- | --- | --- | --- |
|
|
47
|
+
| `title` (required) | `ReactNode` | | |
|
|
48
|
+
| `active` | `boolean` | | Lit in the accent: the current or latest event. |
|
|
49
|
+
| `icon` | `ReactNode` | | An icon in place of the dot. |
|
|
50
|
+
| `time` | `ReactNode` | | When it happened, e.g. a <time> or "2 hours ago". |
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Toast
|
|
2
|
+
|
|
3
|
+
Short messages that confirm an action or report a problem, with an optional action like Undo. They stack into a deck that fans out when pointed at, and swipe away.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { ToastProvider } from '@rinzai/zen';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### Tones
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Button, useToast } from '@rinzai/zen';
|
|
15
|
+
|
|
16
|
+
/** Needs a <ToastProvider> above it, once near the root of your app. */
|
|
17
|
+
export default function Tones() {
|
|
18
|
+
const toast = useToast();
|
|
19
|
+
return (
|
|
20
|
+
<div className="flex flex-wrap gap-3">
|
|
21
|
+
<Button onClick={() => toast('Saved', { description: 'Your changes are stored.', tone: 'positive' })}>
|
|
22
|
+
Success
|
|
23
|
+
</Button>
|
|
24
|
+
<Button
|
|
25
|
+
variant="outline"
|
|
26
|
+
onClick={() => toast('Entry deleted', { action: { label: 'Undo', onClick: () => {} } })}
|
|
27
|
+
>
|
|
28
|
+
With action
|
|
29
|
+
</Button>
|
|
30
|
+
<Button
|
|
31
|
+
tone="negative"
|
|
32
|
+
onClick={() => toast("Couldn't save", { description: 'Check your connection.', tone: 'negative' })}
|
|
33
|
+
>
|
|
34
|
+
Error
|
|
35
|
+
</Button>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## API reference
|
|
42
|
+
|
|
43
|
+
### ToastProvider
|
|
44
|
+
|
|
45
|
+
Mount once near the root; `useToast()` works anywhere inside. Toasts stack in
|
|
46
|
+
a deck in the corner, newest in front with up to two peeking out behind; it
|
|
47
|
+
fans out while pointed at or focused, and the countdowns wait meanwhile.
|
|
48
|
+
Swipe a toast right to dismiss it.
|
|
49
|
+
|
|
50
|
+
| Prop | Type | Default | Description |
|
|
51
|
+
| --- | --- | --- | --- |
|
|
52
|
+
| `offset` | `string` | | Gap between the toasts and the bottom of the screen on phones (a CSS length, default 1rem), e.g. "5.5rem" to clear a bottom tab bar. |
|
|
53
|
+
| `viewportClassName` | `string` | | Classes for the column the toasts stack in (position, width…). |
|