@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,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". |
|