@rinzai/zen 0.13.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.
Files changed (172) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +16 -1
  3. package/dist/carousel/index.d.ts.map +1 -1
  4. package/dist/carousel/index.js +5 -5
  5. package/dist/docs/README.md +205 -0
  6. package/dist/docs/accordion.md +122 -0
  7. package/dist/docs/alert.md +69 -0
  8. package/dist/docs/animated-number.md +105 -0
  9. package/dist/docs/avatar.md +91 -0
  10. package/dist/docs/backdrop.md +94 -0
  11. package/dist/docs/badge.md +37 -0
  12. package/dist/docs/breadcrumbs.md +44 -0
  13. package/dist/docs/button-group.md +76 -0
  14. package/dist/docs/button.md +131 -0
  15. package/dist/docs/calendar-heatmap.md +73 -0
  16. package/dist/docs/calendar.md +101 -0
  17. package/dist/docs/canvas.md +584 -0
  18. package/dist/docs/card.md +180 -0
  19. package/dist/docs/carousel.md +162 -0
  20. package/dist/docs/chart.md +325 -0
  21. package/dist/docs/checkbox.md +88 -0
  22. package/dist/docs/chip.md +52 -0
  23. package/dist/docs/code-block.md +116 -0
  24. package/dist/docs/code-input.md +52 -0
  25. package/dist/docs/collapse.md +46 -0
  26. package/dist/docs/color-picker.md +145 -0
  27. package/dist/docs/combobox.md +208 -0
  28. package/dist/docs/command-palette.md +65 -0
  29. package/dist/docs/date-picker.md +167 -0
  30. package/dist/docs/dialog.md +271 -0
  31. package/dist/docs/disclosure.md +88 -0
  32. package/dist/docs/editable-cell.md +90 -0
  33. package/dist/docs/empty-state.md +46 -0
  34. package/dist/docs/field.md +82 -0
  35. package/dist/docs/file-drop.md +44 -0
  36. package/dist/docs/filter-bar.md +66 -0
  37. package/dist/docs/header.md +51 -0
  38. package/dist/docs/hover-card.md +110 -0
  39. package/dist/docs/icon-picker.md +65 -0
  40. package/dist/docs/input-group.md +89 -0
  41. package/dist/docs/input.md +33 -0
  42. package/dist/docs/inset.md +57 -0
  43. package/dist/docs/kbd.md +34 -0
  44. package/dist/docs/keypad.md +51 -0
  45. package/dist/docs/load-more.md +66 -0
  46. package/dist/docs/locale-provider.md +99 -0
  47. package/dist/docs/marquee.md +86 -0
  48. package/dist/docs/menu.md +403 -0
  49. package/dist/docs/meter.md +102 -0
  50. package/dist/docs/money-input.md +169 -0
  51. package/dist/docs/month-picker.md +80 -0
  52. package/dist/docs/navigation-menu.md +157 -0
  53. package/dist/docs/number-field.md +129 -0
  54. package/dist/docs/page-header.md +44 -0
  55. package/dist/docs/page-transition.md +93 -0
  56. package/dist/docs/pagination.md +42 -0
  57. package/dist/docs/pills.md +85 -0
  58. package/dist/docs/player-bar.md +158 -0
  59. package/dist/docs/popover.md +200 -0
  60. package/dist/docs/progress-ring.md +70 -0
  61. package/dist/docs/radio-cards.md +82 -0
  62. package/dist/docs/radio-group.md +86 -0
  63. package/dist/docs/range-slider.md +64 -0
  64. package/dist/docs/repeat-picker.md +44 -0
  65. package/dist/docs/resizable.md +107 -0
  66. package/dist/docs/seek-bar.md +68 -0
  67. package/dist/docs/segmented.md +89 -0
  68. package/dist/docs/select.md +148 -0
  69. package/dist/docs/separator.md +76 -0
  70. package/dist/docs/side-nav.md +75 -0
  71. package/dist/docs/skeleton.md +89 -0
  72. package/dist/docs/slider.md +87 -0
  73. package/dist/docs/sortable.md +182 -0
  74. package/dist/docs/sparkline.md +56 -0
  75. package/dist/docs/spinner.md +69 -0
  76. package/dist/docs/status-pill.md +81 -0
  77. package/dist/docs/stepper.md +59 -0
  78. package/dist/docs/tab-bar.md +78 -0
  79. package/dist/docs/table-of-contents.md +43 -0
  80. package/dist/docs/table.md +714 -0
  81. package/dist/docs/tabs.md +140 -0
  82. package/dist/docs/tag-input.md +45 -0
  83. package/dist/docs/textarea.md +25 -0
  84. package/dist/docs/theme-toggle.md +85 -0
  85. package/dist/docs/theming.md +193 -0
  86. package/dist/docs/time-field.md +71 -0
  87. package/dist/docs/timeline.md +50 -0
  88. package/dist/docs/toast.md +53 -0
  89. package/dist/docs/toggle-group.md +83 -0
  90. package/dist/docs/toggle.md +38 -0
  91. package/dist/docs/toolbar.md +97 -0
  92. package/dist/docs/tooltip.md +60 -0
  93. package/dist/docs/tree.md +295 -0
  94. package/dist/docs/trend.md +50 -0
  95. package/dist/icons/arrow-path.d.ts +3 -0
  96. package/dist/icons/arrow-path.d.ts.map +1 -0
  97. package/dist/icons/arrows-right-left.d.ts +3 -0
  98. package/dist/icons/arrows-right-left.d.ts.map +1 -0
  99. package/dist/icons/backward.d.ts +3 -0
  100. package/dist/icons/backward.d.ts.map +1 -0
  101. package/dist/icons/forward.d.ts +3 -0
  102. package/dist/icons/forward.d.ts.map +1 -0
  103. package/dist/icons/heart-solid.d.ts +3 -0
  104. package/dist/icons/heart-solid.d.ts.map +1 -0
  105. package/dist/icons/heart.d.ts +3 -0
  106. package/dist/icons/heart.d.ts.map +1 -0
  107. package/dist/icons/musical-note.d.ts +3 -0
  108. package/dist/icons/musical-note.d.ts.map +1 -0
  109. package/dist/icons/pause.d.ts +3 -0
  110. package/dist/icons/pause.d.ts.map +1 -0
  111. package/dist/icons/play.d.ts +3 -0
  112. package/dist/icons/play.d.ts.map +1 -0
  113. package/dist/icons/queue-list.d.ts +3 -0
  114. package/dist/icons/queue-list.d.ts.map +1 -0
  115. package/dist/icons/speaker-wave.d.ts +3 -0
  116. package/dist/icons/speaker-wave.d.ts.map +1 -0
  117. package/dist/icons/speaker-x-mark.d.ts +3 -0
  118. package/dist/icons/speaker-x-mark.d.ts.map +1 -0
  119. package/dist/index.d.ts +8 -2
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/lib.js +80 -74
  122. package/dist/marquee/index.d.ts +15 -0
  123. package/dist/marquee/index.d.ts.map +1 -0
  124. package/dist/marquee/index.js +90 -0
  125. package/dist/menu/index.d.ts.map +1 -1
  126. package/dist/menu/index.js +7 -6
  127. package/dist/number-field/index.d.ts +18 -2
  128. package/dist/number-field/index.d.ts.map +1 -1
  129. package/dist/number-field/index.js +65 -51
  130. package/dist/player-bar/index.d.ts +11 -0
  131. package/dist/player-bar/index.d.ts.map +1 -0
  132. package/dist/player-bar/index.js +17 -0
  133. package/dist/radio-cards/index.d.ts +2 -1
  134. package/dist/radio-cards/index.d.ts.map +1 -1
  135. package/dist/radio-cards/index.js +3 -3
  136. package/dist/seek-bar/index.d.ts +24 -0
  137. package/dist/seek-bar/index.d.ts.map +1 -0
  138. package/dist/seek-bar/index.js +120 -0
  139. package/dist/segmented/index.d.ts +3 -2
  140. package/dist/segmented/index.d.ts.map +1 -1
  141. package/dist/segmented/index.js +19 -18
  142. package/dist/slider/index.d.ts +2 -1
  143. package/dist/slider/index.d.ts.map +1 -1
  144. package/dist/tab-bar/index.d.ts +4 -2
  145. package/dist/tab-bar/index.d.ts.map +1 -1
  146. package/dist/tab-bar/index.js +24 -19
  147. package/dist/textarea/index.d.ts +6 -2
  148. package/dist/textarea/index.d.ts.map +1 -1
  149. package/dist/textarea/index.js +2 -1
  150. package/dist/theme.css +40 -0
  151. package/dist/utils/duration.d.ts +3 -0
  152. package/dist/utils/duration.d.ts.map +1 -0
  153. package/dist/utils/duration.js +7 -0
  154. package/dist/utils/messages.d.ts +2 -0
  155. package/dist/utils/messages.d.ts.map +1 -1
  156. package/dist/utils/messages.js +3 -1
  157. package/dist/utils/slot.d.ts +5 -2
  158. package/dist/utils/slot.d.ts.map +1 -1
  159. package/dist/utils/slot.js +16 -11
  160. package/dist/utils/useAnchoredPopup.d.ts.map +1 -1
  161. package/dist/utils/useAnchoredPopup.js +3 -2
  162. package/dist/utils/useHeightVar.d.ts +7 -0
  163. package/dist/utils/useHeightVar.d.ts.map +1 -0
  164. package/dist/utils/useHeightVar.js +17 -0
  165. package/dist/utils/useMediaPlayer.d.ts +38 -0
  166. package/dist/utils/useMediaPlayer.d.ts.map +1 -0
  167. package/dist/utils/useMediaPlayer.js +91 -0
  168. package/dist/utils/useMediaSession.d.ts +24 -0
  169. package/dist/utils/useMediaSession.d.ts.map +1 -0
  170. package/dist/utils/useMediaSession.js +76 -0
  171. package/dist/zen.css +1 -1
  172. 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(&apos;px-3 py-1 rounded-lg&apos;, &apos;p-4 rounded-full&apos;) →{' '}
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…). |