@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.
Files changed (169) 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 +7 -1
  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/player-bar/index.d.ts +11 -0
  128. package/dist/player-bar/index.d.ts.map +1 -0
  129. package/dist/player-bar/index.js +17 -0
  130. package/dist/radio-cards/index.d.ts +2 -1
  131. package/dist/radio-cards/index.d.ts.map +1 -1
  132. package/dist/radio-cards/index.js +3 -3
  133. package/dist/seek-bar/index.d.ts +24 -0
  134. package/dist/seek-bar/index.d.ts.map +1 -0
  135. package/dist/seek-bar/index.js +120 -0
  136. package/dist/segmented/index.d.ts +3 -2
  137. package/dist/segmented/index.d.ts.map +1 -1
  138. package/dist/segmented/index.js +19 -18
  139. package/dist/slider/index.d.ts +2 -1
  140. package/dist/slider/index.d.ts.map +1 -1
  141. package/dist/tab-bar/index.d.ts +4 -2
  142. package/dist/tab-bar/index.d.ts.map +1 -1
  143. package/dist/tab-bar/index.js +24 -19
  144. package/dist/textarea/index.d.ts +6 -2
  145. package/dist/textarea/index.d.ts.map +1 -1
  146. package/dist/textarea/index.js +2 -1
  147. package/dist/theme.css +40 -0
  148. package/dist/utils/duration.d.ts +3 -0
  149. package/dist/utils/duration.d.ts.map +1 -0
  150. package/dist/utils/duration.js +7 -0
  151. package/dist/utils/messages.d.ts +2 -0
  152. package/dist/utils/messages.d.ts.map +1 -1
  153. package/dist/utils/messages.js +3 -1
  154. package/dist/utils/slot.d.ts +5 -2
  155. package/dist/utils/slot.d.ts.map +1 -1
  156. package/dist/utils/slot.js +16 -11
  157. package/dist/utils/useAnchoredPopup.d.ts.map +1 -1
  158. package/dist/utils/useAnchoredPopup.js +3 -2
  159. package/dist/utils/useHeightVar.d.ts +7 -0
  160. package/dist/utils/useHeightVar.d.ts.map +1 -0
  161. package/dist/utils/useHeightVar.js +17 -0
  162. package/dist/utils/useMediaPlayer.d.ts +38 -0
  163. package/dist/utils/useMediaPlayer.d.ts.map +1 -0
  164. package/dist/utils/useMediaPlayer.js +91 -0
  165. package/dist/utils/useMediaSession.d.ts +24 -0
  166. package/dist/utils/useMediaSession.d.ts.map +1 -0
  167. package/dist/utils/useMediaSession.js +76 -0
  168. package/dist/zen.css +1 -1
  169. package/package.json +4 -2
@@ -0,0 +1,271 @@
1
+ # Dialog
2
+
3
+ Modal dialogs on the native <dialog> element, sheets that slide in from an edge, and a confirmation for destructive actions.
4
+
5
+ ```tsx
6
+ import { Dialog, DialogFooter, DialogClose, ConfirmDialog } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Form
12
+
13
+ ```tsx
14
+ import { Button, Dialog, DialogClose, DialogFooter, Input } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ export default function Form() {
18
+ const [open, setOpen] = useState(false);
19
+ return (
20
+ <>
21
+ <Button onClick={() => setOpen(true)}>Rename</Button>
22
+ <Dialog
23
+ open={open}
24
+ onOpenChange={setOpen}
25
+ title="Rename category"
26
+ description="Entries in this category keep their history."
27
+ >
28
+ <label className="flex flex-col gap-2">
29
+ Name
30
+ <Input defaultValue="Groceries" />
31
+ </label>
32
+ <DialogFooter>
33
+ <DialogClose variant="outline">Cancel</DialogClose>
34
+ <DialogClose>Save</DialogClose>
35
+ </DialogFooter>
36
+ </Dialog>
37
+ </>
38
+ );
39
+ }
40
+ ```
41
+
42
+ ### Sheets
43
+
44
+ ```tsx
45
+ import { Button, Checkbox, Dialog, DialogClose, DialogFooter } from '@rinzai/zen';
46
+ import { useState } from 'react';
47
+
48
+ /** With side, a dialog slides in from an edge: a bottom sheet (phones) or a side panel (filters, details). */
49
+ export default function Sheets() {
50
+ const [open, setOpen] = useState(false);
51
+ const [side, setSide] = useState<'bottom' | 'right'>('bottom');
52
+ const show = (from: 'bottom' | 'right') => {
53
+ setSide(from);
54
+ setOpen(true);
55
+ };
56
+ return (
57
+ <div className="flex flex-wrap gap-2">
58
+ <Button variant="outline" onClick={() => show('bottom')}>
59
+ Bottom sheet
60
+ </Button>
61
+ <Button variant="outline" onClick={() => show('right')}>
62
+ Side panel
63
+ </Button>
64
+ <Dialog
65
+ open={open}
66
+ onOpenChange={setOpen}
67
+ side={side}
68
+ title="Filters"
69
+ description="Show only what you need."
70
+ >
71
+ <div className="flex flex-col gap-3">
72
+ <Checkbox defaultChecked>Income</Checkbox>
73
+ <Checkbox defaultChecked>Spending</Checkbox>
74
+ <Checkbox>Transfers</Checkbox>
75
+ </div>
76
+ <DialogFooter className="mt-auto">
77
+ <DialogClose variant="ghost">Cancel</DialogClose>
78
+ <DialogClose>Apply</DialogClose>
79
+ </DialogFooter>
80
+ </Dialog>
81
+ </div>
82
+ );
83
+ }
84
+ ```
85
+
86
+ ### Pickers
87
+
88
+ ```tsx
89
+ import {
90
+ Button,
91
+ Dialog,
92
+ DialogClose,
93
+ DialogFooter,
94
+ Combobox,
95
+ DatePicker,
96
+ ComboboxEmpty,
97
+ ComboboxList,
98
+ ComboboxPopup,
99
+ ComboboxSearch,
100
+ ComboboxTrigger,
101
+ MonthPicker,
102
+ Select,
103
+ SelectItem,
104
+ useToast,
105
+ } from '@rinzai/zen';
106
+ import { useState } from 'react';
107
+
108
+ const CATEGORIES = [
109
+ { value: 'groceries', label: 'Groceries' },
110
+ { value: 'rent', label: 'Rent' },
111
+ { value: 'transport', label: 'Transport' },
112
+ { value: 'dining', label: 'Dining out' },
113
+ ] as const;
114
+ const TAGS = ['Essential', 'Shared', 'Work', 'Treat'];
115
+ const asText = (tag: string) => tag;
116
+
117
+ /** Selects, pickers and menus open above the dialog, and toasts show over it (try Save draft). */
118
+ export default function Pickers() {
119
+ const toast = useToast();
120
+ const [open, setOpen] = useState(false);
121
+ const [category, setCategory] = useState<(typeof CATEGORIES)[number]['value'] | null>('groceries');
122
+ const [month, setMonth] = useState<string | null>('2026-09');
123
+ const [start, setStart] = useState<string | null>('2026-09-30');
124
+ const [tags, setTags] = useState<string[]>([TAGS[0]]);
125
+ return (
126
+ <>
127
+ <Button onClick={() => setOpen(true)}>Add budget</Button>
128
+ <Dialog open={open} onOpenChange={setOpen} title="Add budget">
129
+ <div className="grid gap-3 sm:grid-cols-2">
130
+ <label className="flex flex-col gap-2 text-sm">
131
+ Category
132
+ <Select aria-label="Category" value={category} onValueChange={setCategory}>
133
+ {CATEGORIES.map((c) => (
134
+ <SelectItem key={c.value} value={c.value}>
135
+ {c.label}
136
+ </SelectItem>
137
+ ))}
138
+ </Select>
139
+ </label>
140
+ <label className="flex flex-col gap-2 text-sm">
141
+ From
142
+ <MonthPicker aria-label="From" value={month} onValueChange={setMonth} locale="en-IN" />
143
+ </label>
144
+ </div>
145
+ <label className="flex flex-col gap-2 text-sm">
146
+ Starts on
147
+ <DatePicker aria-label="Starts on" value={start} onValueChange={setStart} locale="en-IN" />
148
+ </label>
149
+ <div className="flex flex-col gap-2 text-sm">
150
+ <span>Tags</span>
151
+ <Combobox
152
+ multiple
153
+ items={TAGS}
154
+ itemKey={asText}
155
+ itemText={asText}
156
+ value={tags}
157
+ onValueChange={setTags}
158
+ >
159
+ <ComboboxTrigger className="w-full" />
160
+ <ComboboxPopup>
161
+ <ComboboxSearch />
162
+ <ComboboxList>
163
+ <ComboboxEmpty />
164
+ </ComboboxList>
165
+ </ComboboxPopup>
166
+ </Combobox>
167
+ </div>
168
+ <DialogFooter>
169
+ <Button
170
+ variant="ghost"
171
+ className="mr-auto"
172
+ onClick={() => toast('Draft saved', { tone: 'default' })}
173
+ >
174
+ Save draft
175
+ </Button>
176
+ <DialogClose variant="outline">Cancel</DialogClose>
177
+ <DialogClose onClick={() => toast('Budget added', { tone: 'positive' })}>Add</DialogClose>
178
+ </DialogFooter>
179
+ </Dialog>
180
+ </>
181
+ );
182
+ }
183
+ ```
184
+
185
+ ### Confirm
186
+
187
+ ```tsx
188
+ import { Button, ConfirmDialog } from '@rinzai/zen';
189
+ import { useState } from 'react';
190
+
191
+ export default function Confirm() {
192
+ const [open, setOpen] = useState(false);
193
+ return (
194
+ <>
195
+ <Button tone="negative" onClick={() => setOpen(true)}>
196
+ Delete goal
197
+ </Button>
198
+ <ConfirmDialog
199
+ open={open}
200
+ onOpenChange={setOpen}
201
+ title="Delete this goal?"
202
+ description="Its progress history is removed too. This can't be undone."
203
+ confirmLabel="Delete"
204
+ tone="negative"
205
+ onConfirm={() => {}}
206
+ />
207
+ </>
208
+ );
209
+ }
210
+ ```
211
+
212
+ ## API reference
213
+
214
+ ### Dialog
215
+
216
+ Modal dialog on the native <dialog> element: focus is trapped and restored,
217
+ the page behind is inert, and Escape closes it. `dismissible={false}` is for
218
+ flows the user must finish (no Escape, outside-click or close button). With `side`,
219
+ it's a sheet that slides in from that edge: a bottom sheet on phones, a side
220
+ panel for filters or details. On touch, a sheet swipes back to its edge to close.
221
+
222
+ | Prop | Type | Default | Description |
223
+ | --- | --- | --- | --- |
224
+ | `open` (required) | `boolean` | | |
225
+ | `title` (required) | `ReactNode` | | |
226
+ | `closeLabel` | `string` | | The close button's accessible name. |
227
+ | `description` | `ReactNode` | | |
228
+ | `dismissible` | `boolean` | `true` | |
229
+ | `initialFocus` | `RefObject<HTMLElement \| null>` | | Element to focus when the dialog opens (default: the first focusable one). |
230
+ | `onOpenChange` | `((open: boolean) => void)` | | |
231
+ | `side` | `"bottom" \| "left" \| "right"` | | Slide in from this edge as a sheet, instead of appearing in the middle. |
232
+ | `size` | `"lg" \| "md"` | `md` | "lg" for dialogs holding a whole tool rather than a short form. |
233
+
234
+ ### DialogFooter
235
+
236
+ The row of actions at the end of a dialog, right-aligned.
237
+
238
+ No props of its own; it accepts the props of the element it renders.
239
+
240
+ ### DialogClose
241
+
242
+ A button that closes its dialog (after its own onClick, unless that calls
243
+ preventDefault). Takes every Button prop: variant, size, asChild…
244
+
245
+ | Prop | Type | Default | Description |
246
+ | --- | --- | --- | --- |
247
+ | `asChild` | `boolean` | | Put the button's styles on the single child element (e.g. a link) instead of a <button>. |
248
+ | `loading` | `boolean` | | Shows a spinner and disables the button while an action runs. |
249
+ | `size` | `"default" \| "icon-lg" \| "icon-sm" \| "icon-xs" \| "icon" \| "lg" \| "sm" \| null` | | |
250
+ | `tone` | `"default" \| "inherit" \| "muted" \| "negative" \| null` | | |
251
+ | `variant` | `"default" \| "ghost" \| "link" \| "outline" \| "secondary" \| null` | | |
252
+
253
+ ### ConfirmDialog
254
+
255
+ Confirmation for destructive or irreversible actions. Cancel has focus, so Enter is the safe choice.
256
+
257
+ | Prop | Type | Default | Description |
258
+ | --- | --- | --- | --- |
259
+ | `confirmLabel` (required) | `string` | | |
260
+ | `onConfirm` (required) | `() => void` | | |
261
+ | `onOpenChange` (required) | `(open: boolean) => void` | | |
262
+ | `open` (required) | `boolean` | | |
263
+ | `title` (required) | `ReactNode` | | |
264
+ | `cancelLabel` | `string` | | |
265
+ | `closeLabel` | `string` | | The close button's accessible name. |
266
+ | `description` | `ReactNode` | | |
267
+ | `dismissible` | `boolean` | | |
268
+ | `initialFocus` | `RefObject<HTMLElement \| null>` | | Element to focus when the dialog opens (default: the first focusable one). |
269
+ | `side` | `"bottom" \| "left" \| "right"` | | Slide in from this edge as a sheet, instead of appearing in the middle. |
270
+ | `size` | `"lg" \| "md"` | | "lg" for dialogs holding a whole tool rather than a short form. |
271
+ | `tone` | `"default" \| "negative"` | `default` | `negative` for a destructive action: the confirm button is red. |
@@ -0,0 +1,88 @@
1
+ # Disclosure
2
+
3
+ A button that shows and hides a section, like finished goals under a list.
4
+
5
+ ```tsx
6
+ import { Disclosure, DisclosureTrigger, DisclosureContent } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Card, CardHeader, CardTitle, Disclosure, DisclosureContent, DisclosureTrigger, Meter } from '@rinzai/zen';
15
+
16
+ const ACTIVE = [
17
+ { name: 'New phone', saved: 42000, target: 60000 },
18
+ { name: 'Emergency fund', saved: 180000, target: 300000 },
19
+ ];
20
+ const PAST = ['Laptop', 'Trip to Goa', 'Wedding gift'];
21
+
22
+ /** Finished goals folded away under the active ones; the section grows open and folds shut. */
23
+ export default function Default() {
24
+ return (
25
+ <Card className="w-full max-w-sm gap-4">
26
+ <CardHeader>
27
+ <CardTitle>Goals</CardTitle>
28
+ </CardHeader>
29
+ <ul className="flex flex-col gap-3 text-sm">
30
+ {ACTIVE.map((g) => (
31
+ <li key={g.name} className="flex flex-col gap-1.5">
32
+ <div className="flex justify-between">
33
+ <span>{g.name}</span>
34
+ <span className="text-muted-foreground tabular-nums">
35
+ {Math.round((g.saved / g.target) * 100)}%
36
+ </span>
37
+ </div>
38
+ <Meter value={g.saved} max={g.target} aria-label={g.name} />
39
+ </li>
40
+ ))}
41
+ </ul>
42
+ <Disclosure>
43
+ <DisclosureTrigger className="mt-2">Past ({PAST.length})</DisclosureTrigger>
44
+ <DisclosureContent>
45
+ <ul className="border-tint/[0.07] flex flex-col gap-2 border-l pl-4 text-sm">
46
+ {PAST.map((g) => (
47
+ <li key={g} className="text-muted-foreground flex items-center justify-between">
48
+ {g}
49
+ <span className="text-xs">Done</span>
50
+ </li>
51
+ ))}
52
+ </ul>
53
+ </DisclosureContent>
54
+ </Disclosure>
55
+ </Card>
56
+ );
57
+ }
58
+ ```
59
+
60
+ ## API reference
61
+
62
+ ### Disclosure
63
+
64
+ A section that a button shows and hides, e.g. "Past (3)" under a list. Put
65
+ a DisclosureTrigger and a DisclosureContent inside (it adds no element of its
66
+ own). Opens and closes on its own, or follows `open`/`onOpenChange`.
67
+
68
+ | Prop | Type | Default | Description |
69
+ | --- | --- | --- | --- |
70
+ | `defaultOpen` | `boolean` | `false` | |
71
+ | `onOpenChange` | `((open: boolean) => void)` | | |
72
+ | `open` | `boolean` | | Show or hide it from your state; leave it out to let the trigger do it. |
73
+
74
+ ### DisclosureTrigger
75
+
76
+ The button: your label after a chevron that turns down as the section opens. Brightens on hover and while open.
77
+
78
+ No props of its own; it accepts the props of the element it renders.
79
+
80
+ ### DisclosureContent
81
+
82
+ What the trigger shows and hides. It grows open and folds shut (the height
83
+ animates, the content fades and settles). While closed it isn't rendered at
84
+ all, so what's inside costs nothing and can't be found, focused or read; it
85
+ mounts as it opens and unmounts once it has folded shut. className goes on
86
+ the content itself.
87
+
88
+ No props of its own; it accepts the props of the element it renders.
@@ -0,0 +1,90 @@
1
+ # Editable Cell
2
+
3
+ A value edited in place: shows the value, swaps in any editor on click or Enter.
4
+
5
+ ```tsx
6
+ import { EditableCell } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { EditableCell, formatMoney, Input, MoneyInput } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ /** Click a value (or Tab to it and press Enter) to edit; Enter or blur saves, Escape cancels. */
18
+ export default function Default() {
19
+ const [amount, setAmount] = useState<number | null>(1850000);
20
+ const [note, setNote] = useState('Due on the 5th');
21
+ return (
22
+ <div className="grid w-80 grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2 text-sm">
23
+ <span className="text-muted-foreground">Rent</span>
24
+ <EditableCell
25
+ label={`Rent: ${amount === null ? 'none' : formatMoney(amount, 'INR', 'en-IN')}`}
26
+ editor={(close) => (
27
+ <MoneyInput
28
+ compact
29
+ autoFocus
30
+ allowEmpty
31
+ aria-label="Rent"
32
+ value={amount}
33
+ currency="INR"
34
+ locale="en-IN"
35
+ onValueChange={(v) => {
36
+ setAmount(v);
37
+ close();
38
+ }}
39
+ onCancel={close}
40
+ />
41
+ )}
42
+ >
43
+ {amount === null ? '–' : formatMoney(amount, 'INR', 'en-IN', { showDecimals: false })}
44
+ </EditableCell>
45
+ <span className="text-muted-foreground">Note</span>
46
+ <EditableCell
47
+ label={`Note: ${note}`}
48
+ className="text-left"
49
+ editor={(close) => (
50
+ <Input
51
+ autoFocus
52
+ aria-label="Note"
53
+ defaultValue={note}
54
+ className="h-7"
55
+ onBlur={(e) => {
56
+ setNote(e.target.value);
57
+ close();
58
+ }}
59
+ onKeyDown={(e) => {
60
+ if (e.key === 'Enter') e.currentTarget.blur();
61
+ if (e.key === 'Escape') close();
62
+ }}
63
+ />
64
+ )}
65
+ >
66
+ {note}
67
+ </EditableCell>
68
+ </div>
69
+ );
70
+ }
71
+ ```
72
+
73
+ ## API reference
74
+
75
+ ### EditableCell
76
+
77
+ A value edited in place, e.g. in a table cell: shows `children` as a button,
78
+ and on click (or Enter) swaps in your editor. Call `close()` from the editor
79
+ when it's done (committed or cancelled). Works with any editor: a compact
80
+ MoneyInput, an Input, a Select. A value changed by an edit glows briefly
81
+ when the editor closes.
82
+
83
+ | Prop | Type | Default | Description |
84
+ | --- | --- | --- | --- |
85
+ | `children` (required) | `ReactNode` | | What the cell shows when not editing. |
86
+ | `editor` (required) | `(close: () => void) => ReactNode` | | The editor to show while editing; call `close` when it's done. |
87
+ | `editing` | `boolean` | | Control the editing state yourself (with onEditingChange). |
88
+ | `label` | `string` | | Accessible name of the value, e.g. "Rent, Oct 2026: ₹18,500"; " Edit" is added for the button. |
89
+ | `onEditingChange` | `((editing: boolean) => void)` | | |
90
+ | `readOnly` | `boolean` | `false` | Show the value without making it editable. |
@@ -0,0 +1,46 @@
1
+ # Empty State
2
+
3
+ What a list or page shows when there's nothing in it yet, and the action that fills it.
4
+
5
+ ```tsx
6
+ import { EmptyState } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Button, Card, EmptyState } from '@rinzai/zen';
15
+ // Icons from Heroicons (https://heroicons.com): ListBullet (list-bullet).
16
+
17
+ /** A month with no entries yet: what's missing, and the action that adds the first one. */
18
+ export default function Default() {
19
+ return (
20
+ <Card className="w-full max-w-md">
21
+ <EmptyState
22
+ icon={<ListBullet />}
23
+ title="No entries this month"
24
+ description="Add your income and spending, or bring them in from a bank statement."
25
+ >
26
+ <Button>Add entry</Button>
27
+ <Button variant="outline">Import</Button>
28
+ </EmptyState>
29
+ </Card>
30
+ );
31
+ }
32
+ ```
33
+
34
+ ## API reference
35
+
36
+ ### EmptyState
37
+
38
+ What a list, table or page shows when there's nothing in it yet: an icon on
39
+ a soft glow, a title, a line on what to do, and the action that does it
40
+ (children, e.g. a Button).
41
+
42
+ | Prop | Type | Default | Description |
43
+ | --- | --- | --- | --- |
44
+ | `title` (required) | `ReactNode` | | |
45
+ | `description` | `ReactNode` | | |
46
+ | `icon` | `ReactNode` | | An icon, shown on the accent's glow. |
@@ -0,0 +1,82 @@
1
+ # Field
2
+
3
+ A label, hint and error wired to their control for screen readers, and messages for a whole form.
4
+
5
+ ```tsx
6
+ import { Field, FormMessage } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Button, Field, FormMessage, Input } from '@rinzai/zen';
15
+ import { FormEvent, useState } from 'react';
16
+
17
+ /** Field links the label, hint and error to its control; FormMessage speaks for the whole form. */
18
+ export default function Default() {
19
+ const [email, setEmail] = useState('');
20
+ const [submitted, setSubmitted] = useState(false);
21
+ const invalid = submitted && !email.includes('@');
22
+ return (
23
+ <form
24
+ className="flex w-80 flex-col gap-4"
25
+ noValidate
26
+ onSubmit={(e: FormEvent) => {
27
+ e.preventDefault();
28
+ setSubmitted(true);
29
+ }}
30
+ >
31
+ <Field
32
+ label="Email"
33
+ hint="We'll send a sign-in link."
34
+ error={invalid ? 'Enter an email address.' : undefined}
35
+ >
36
+ <Input
37
+ type="email"
38
+ value={email}
39
+ onChange={(e) => setEmail(e.target.value)}
40
+ placeholder="you@example.com"
41
+ />
42
+ </Field>
43
+ {submitted && !invalid && <FormMessage tone="positive">Check your inbox for the link.</FormMessage>}
44
+ <Button type="submit" className="self-start">
45
+ Send link
46
+ </Button>
47
+ </form>
48
+ );
49
+ }
50
+ ```
51
+
52
+ ## API reference
53
+
54
+ ### Field
55
+
56
+ A labelled form control with an optional hint and error. Links them for
57
+ screen readers: the control gets an id (if it has none), aria-describedby
58
+ for the hint or error, and aria-invalid while there's an error. The error
59
+ shows in place of the hint until it's fixed.
60
+
61
+ The direct child gets these as props. When the control sits inside other
62
+ markup (e.g. an amount beside a unit select), give it an id and pass the
63
+ same id as `htmlFor`: Zen's controls with that id pick them up themselves.
64
+
65
+ | Prop | Type | Default | Description |
66
+ | --- | --- | --- | --- |
67
+ | `children` (required) | `ReactNode` | | One control (Input, Select, MoneyInput…), or anything else (then link it yourself). |
68
+ | `label` (required) | `ReactNode` | | |
69
+ | `className` | `string` | | |
70
+ | `error` | `ReactNode` | | Error under the control, in place of the hint; marks it invalid. |
71
+ | `hint` | `ReactNode` | | Help text under the control. |
72
+ | `htmlFor` | `string` | | The control's id, when it isn't the direct child (it's inside other markup). |
73
+
74
+ ### FormMessage
75
+
76
+ A message for a form as a whole: an error or success in a tinted box, or a quiet note. An Alert, set for forms.
77
+
78
+ | Prop | Type | Default | Description |
79
+ | --- | --- | --- | --- |
80
+ | `icon` | `ReactNode` | | Replaces the tone's icon (info, check, warning); `null` for none. |
81
+ | `title` | `ReactNode` | | A first line in the tone's colour; the text under it is quieter. |
82
+ | `tone` | `"default" \| "negative" \| "positive"` | `default` | |
@@ -0,0 +1,44 @@
1
+ # File Drop
2
+
3
+ Somewhere to drop files, or click to choose them, that lights up as files are dragged over it.
4
+
5
+ ```tsx
6
+ import { FileDrop } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { FileDrop } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ /** A bank statement to import: CSV or Excel, one at a time. */
18
+ export default function Default() {
19
+ const [file, setFile] = useState<File>();
20
+ return (
21
+ <FileDrop accept=".csv,.xlsx" onFiles={([f]) => setFile(f)} className="w-full max-w-md">
22
+ <span className="text-foreground font-medium">{file ? file.name : 'Drop a statement here'}</span>
23
+ <span className="text-xs">
24
+ {file ? `${Math.ceil(file.size / 1024)} KB · choose another` : 'CSV or Excel, or click to choose'}
25
+ </span>
26
+ </FileDrop>
27
+ );
28
+ }
29
+ ```
30
+
31
+ ## API reference
32
+
33
+ ### FileDrop
34
+
35
+ Somewhere to drop files, or click (or press Enter) to choose them: it lights
36
+ up while files are dragged over it. `onFiles` gets those that `accept` allows
37
+ (one, unless `multiple`). Children are what it says, e.g. "Drop a CSV here".
38
+
39
+ | Prop | Type | Default | Description |
40
+ | --- | --- | --- | --- |
41
+ | `onFiles` (required) | `(files: File[]) => void` | | |
42
+ | `accept` | `string` | | As an <input type="file">'s accept: ".csv,.xlsx", "image/*". Dropped files are held to it too. |
43
+ | `disabled` | `boolean` | `false` | |
44
+ | `multiple` | `boolean` | `false` | |
@@ -0,0 +1,66 @@
1
+ # Filter Bar
2
+
3
+ The filters in effect, each removed with its ×, and Clear all.
4
+
5
+ ```tsx
6
+ import { FilterBar, FilterChip } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Button, FilterBar, FilterChip } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ const ALL = { amount: 'Amount: $50–$500', category: 'Category: Dining out', tag: 'Tag: Shared' };
18
+ type Key = keyof typeof ALL;
19
+
20
+ /** The filters on a list of entries; remove one, or all at once. */
21
+ export default function Default() {
22
+ const [on, setOn] = useState<Key[]>(['amount', 'category', 'tag']);
23
+ return (
24
+ <div className="flex min-h-16 w-full max-w-md flex-col items-start gap-3">
25
+ <FilterBar onClear={() => setOn([])}>
26
+ {on.map((key) => (
27
+ <FilterChip
28
+ key={key}
29
+ onRemove={() => setOn((keys) => keys.filter((k) => k !== key))}
30
+ removeLabel={`Remove ${key} filter`}
31
+ >
32
+ {ALL[key]}
33
+ </FilterChip>
34
+ ))}
35
+ </FilterBar>
36
+ {on.length === 0 && (
37
+ <Button variant="outline" size="sm" onClick={() => setOn(['amount', 'category', 'tag'])}>
38
+ Filter again
39
+ </Button>
40
+ )}
41
+ </div>
42
+ );
43
+ }
44
+ ```
45
+
46
+ ## API reference
47
+
48
+ ### FilterBar
49
+
50
+ The filters in effect, as chips each removed with its ×, and "Clear all"
51
+ once there are two or more. Renders nothing when no filter is set.
52
+
53
+ | Prop | Type | Default | Description |
54
+ | --- | --- | --- | --- |
55
+ | `clearLabel` | `ReactNode` | | |
56
+ | `onClear` | `(() => void)` | | Removes every filter; shown as "Clear all" once there are two or more. |
57
+
58
+ ### FilterChip
59
+
60
+ One filter in a FilterBar: what it is (e.g. "Amount: $50–$500"), and its × to remove it.
61
+
62
+ | Prop | Type | Default | Description |
63
+ | --- | --- | --- | --- |
64
+ | `onRemove` (required) | `() => void` | | |
65
+ | `className` | `string` | | |
66
+ | `removeLabel` | `string` | | The × button's name for screen readers, e.g. "Remove amount filter". |