@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.
- 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 +8 -2
- 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/number-field/index.d.ts +18 -2
- package/dist/number-field/index.d.ts.map +1 -1
- package/dist/number-field/index.js +65 -51
- 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,75 @@
|
|
|
1
|
+
# Side Nav
|
|
2
|
+
|
|
3
|
+
Vertical navigation in titled groups, for docs and settings sidebars. Works with any router.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { SideNav, SideNavGroup, SideNavLink } from '@rinzai/zen';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### Default
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { SideNav, SideNavGroup, SideNavLink } from '@rinzai/zen';
|
|
15
|
+
import { useState } from 'react';
|
|
16
|
+
|
|
17
|
+
const GROUPS = [
|
|
18
|
+
{ title: 'Account', links: ['Profile', 'Security', 'Passkeys'] },
|
|
19
|
+
{ title: 'Workspace', links: ['Members', 'Billing', 'Integrations'] },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/** With a router, pass its link as the child: <SideNavLink asChild><NavLink to="/billing">Billing</NavLink></SideNavLink>. */
|
|
23
|
+
export default function Default() {
|
|
24
|
+
const [current, setCurrent] = useState('Security');
|
|
25
|
+
return (
|
|
26
|
+
<SideNav aria-label="Settings" className="w-56">
|
|
27
|
+
{GROUPS.map((group) => (
|
|
28
|
+
<SideNavGroup key={group.title} title={group.title}>
|
|
29
|
+
{group.links.map((link) => (
|
|
30
|
+
<SideNavLink
|
|
31
|
+
key={link}
|
|
32
|
+
href={`#${link.toLowerCase()}`}
|
|
33
|
+
active={link === current}
|
|
34
|
+
onClick={(e) => {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
setCurrent(link);
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
{link}
|
|
40
|
+
</SideNavLink>
|
|
41
|
+
))}
|
|
42
|
+
</SideNavGroup>
|
|
43
|
+
))}
|
|
44
|
+
</SideNav>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## API reference
|
|
50
|
+
|
|
51
|
+
### SideNav
|
|
52
|
+
|
|
53
|
+
Vertical navigation, e.g. a docs or settings sidebar. Give it an aria-label.
|
|
54
|
+
|
|
55
|
+
No props of its own; it accepts the props of the element it renders.
|
|
56
|
+
|
|
57
|
+
### SideNavGroup
|
|
58
|
+
|
|
59
|
+
A titled group of links.
|
|
60
|
+
|
|
61
|
+
| Prop | Type | Default | Description |
|
|
62
|
+
| --- | --- | --- | --- |
|
|
63
|
+
| `className` | `string` | | |
|
|
64
|
+
| `title` | `ReactNode` | | |
|
|
65
|
+
|
|
66
|
+
### SideNavLink
|
|
67
|
+
|
|
68
|
+
One link. `active` marks the current page. With `asChild`, the styles go onto
|
|
69
|
+
your own link element instead (a router's Link or NavLink, which may set
|
|
70
|
+
aria-current itself).
|
|
71
|
+
|
|
72
|
+
| Prop | Type | Default | Description |
|
|
73
|
+
| --- | --- | --- | --- |
|
|
74
|
+
| `active` | `boolean` | | This link is the current page. |
|
|
75
|
+
| `asChild` | `boolean` | | Style the single child element (e.g. a router link) instead of rendering an <a>. |
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Skeleton & Spinner
|
|
2
|
+
|
|
3
|
+
Placeholders for content on its way.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { Skeleton, Spinner } from '@rinzai/zen';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### Loading card
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Skeleton } from '@rinzai/zen';
|
|
15
|
+
|
|
16
|
+
export default function LoadingCard() {
|
|
17
|
+
return (
|
|
18
|
+
<div className="glass glow-edge flex w-80 flex-col gap-3 rounded-xl p-4">
|
|
19
|
+
<Skeleton className="h-3 w-24" />
|
|
20
|
+
<Skeleton className="h-7 w-40" />
|
|
21
|
+
<Skeleton className="h-3 w-full" />
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Reveal
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { Button, Skeleton } from '@rinzai/zen';
|
|
31
|
+
import { useState } from 'react';
|
|
32
|
+
|
|
33
|
+
export default function Reveal() {
|
|
34
|
+
const [loading, setLoading] = useState(true);
|
|
35
|
+
return (
|
|
36
|
+
<div className="flex flex-col items-start gap-4">
|
|
37
|
+
<div className="glass glow-edge flex w-80 flex-col gap-3 rounded-xl p-4">
|
|
38
|
+
<Skeleton loading={loading} className="h-3 w-24">
|
|
39
|
+
<p className="text-muted-foreground text-xs">Balance</p>
|
|
40
|
+
</Skeleton>
|
|
41
|
+
<Skeleton loading={loading} className="h-7 w-40">
|
|
42
|
+
<p className="text-2xl font-semibold tabular-nums">$4,280.15</p>
|
|
43
|
+
</Skeleton>
|
|
44
|
+
</div>
|
|
45
|
+
<Button variant="outline" onClick={() => setLoading((l) => !l)}>
|
|
46
|
+
{loading ? 'Load' : 'Reload'}
|
|
47
|
+
</Button>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### With spinner
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { Spinner } from '@rinzai/zen';
|
|
57
|
+
|
|
58
|
+
export default function WithSpinner() {
|
|
59
|
+
return (
|
|
60
|
+
<div className="text-muted-foreground flex items-center gap-2 text-sm">
|
|
61
|
+
<Spinner /> Loading…
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API reference
|
|
68
|
+
|
|
69
|
+
### Skeleton
|
|
70
|
+
|
|
71
|
+
Shimmering placeholder in the shape of content that's on its way. Size it with className.
|
|
72
|
+
|
|
73
|
+
Give it the content as its child and `loading`: the placeholder shows while
|
|
74
|
+
loading, then the content fades in where it was.
|
|
75
|
+
|
|
76
|
+
| Prop | Type | Default | Description |
|
|
77
|
+
| --- | --- | --- | --- |
|
|
78
|
+
| `children` | `ReactElement<unknown, string \| JSXElementConstructor<any>>` | | The content this stands in for, a single element. |
|
|
79
|
+
| `loading` | `boolean` | | With children: show the placeholder instead of them while true. |
|
|
80
|
+
|
|
81
|
+
### Spinner
|
|
82
|
+
|
|
83
|
+
Small circular spinner in the current text colour; size it with className
|
|
84
|
+
(default size-4). Decorative by default (e.g. inside a loading Button). Give
|
|
85
|
+
it a `label` when it stands on its own, and screen readers announce it.
|
|
86
|
+
|
|
87
|
+
| Prop | Type | Default | Description |
|
|
88
|
+
| --- | --- | --- | --- |
|
|
89
|
+
| `label` | `string` | | What is loading, e.g. "Loading entries". Makes the spinner announce itself. |
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Slider
|
|
2
|
+
|
|
3
|
+
A native range input with a glowing thumb and any track you like.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { Slider } from '@rinzai/zen';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### Default
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Slider } from '@rinzai/zen';
|
|
15
|
+
import { useState } from 'react';
|
|
16
|
+
|
|
17
|
+
/** Filled up to the value; arrow keys and Home/End work as on any range input. */
|
|
18
|
+
export default function Default() {
|
|
19
|
+
const [savings, setSavings] = useState(30);
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex w-72 flex-col gap-2">
|
|
22
|
+
<div className="flex justify-between text-sm">
|
|
23
|
+
<span className="font-medium">Save each month</span>
|
|
24
|
+
<span className="text-muted-foreground tabular-nums">{savings}%</span>
|
|
25
|
+
</div>
|
|
26
|
+
<Slider
|
|
27
|
+
aria-label="Save each month"
|
|
28
|
+
value={savings}
|
|
29
|
+
onValueChange={setSavings}
|
|
30
|
+
valueText={(v) => `${v} percent`}
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Hue
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { Slider } from '@rinzai/zen';
|
|
41
|
+
import { useState } from 'react';
|
|
42
|
+
|
|
43
|
+
/** A custom track (every hue) and thumb (the chosen colour); onValueCommit fires once, when you let go. */
|
|
44
|
+
export default function Hue() {
|
|
45
|
+
const [hue, setHue] = useState(295);
|
|
46
|
+
const [saved, setSaved] = useState(295);
|
|
47
|
+
const gradient = `linear-gradient(to right, ${Array.from({ length: 13 }, (_, i) => `oklch(0.72 0.13 ${i * 30})`).join(', ')})`;
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex w-72 flex-col gap-2">
|
|
50
|
+
<Slider
|
|
51
|
+
aria-label="Accent hue"
|
|
52
|
+
min={0}
|
|
53
|
+
max={359}
|
|
54
|
+
value={hue}
|
|
55
|
+
onValueChange={setHue}
|
|
56
|
+
onValueCommit={setSaved}
|
|
57
|
+
trackBackground={gradient}
|
|
58
|
+
thumbColor={`oklch(0.72 0.13 ${hue})`}
|
|
59
|
+
valueText={(v) => `Hue ${v} degrees`}
|
|
60
|
+
/>
|
|
61
|
+
<p className="text-muted-foreground mt-0! text-xs">Saved: {saved}°</p>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API reference
|
|
68
|
+
|
|
69
|
+
### Slider
|
|
70
|
+
|
|
71
|
+
A range slider on the native <input type="range"> (keyboard, forms and
|
|
72
|
+
screen readers work as usual): a rounded track, filled up to the value in
|
|
73
|
+
the accent unless you give it your own `trackBackground` (e.g. a hue
|
|
74
|
+
gradient), and a glowing thumb.
|
|
75
|
+
|
|
76
|
+
| Prop | Type | Default | Description |
|
|
77
|
+
| --- | --- | --- | --- |
|
|
78
|
+
| `defaultValue` | `number` | | The first value, when the slider keeps its own (default `min`). |
|
|
79
|
+
| `max` | `number` | `100` | |
|
|
80
|
+
| `min` | `number` | `0` | |
|
|
81
|
+
| `onValueChange` | `((value: number) => void)` | | While dragging (every step). |
|
|
82
|
+
| `onValueCommit` | `((value: number) => void)` | | Once the user lets go: the moment to save. |
|
|
83
|
+
| `step` | `"any" \| number` | `1` | `'any'` for no snapping; the arrow keys then move by the browser's own amount. |
|
|
84
|
+
| `thumbColor` | `string` | | CSS colour for the thumb (default: the accent). |
|
|
85
|
+
| `trackBackground` | `string` | | CSS background for the track, e.g. a gradient of hues. |
|
|
86
|
+
| `value` | `number` | | The value, when you keep it. |
|
|
87
|
+
| `valueText` | `((value: number) => string)` | | Spoken value, e.g. (v) => `Hue ${v} degrees`. |
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Sortable List
|
|
2
|
+
|
|
3
|
+
Put a list in order, or a tree of groups (a table’s rows, say), by dragging items by a grip or with the keyboard, each step announced: into other groups, open sub-items, or under another item.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { SortableList, SortableTree, SortableItem, SortableHandle, flattenTree } from '@rinzai/zen';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### Default
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Meter, SortableHandle, SortableItem, SortableList } from '@rinzai/zen';
|
|
15
|
+
import { useState } from 'react';
|
|
16
|
+
|
|
17
|
+
const BUDGETS: Record<string, { name: string; spent: number; limit: number }> = {
|
|
18
|
+
rent: { name: 'Rent', spent: 1800, limit: 1800 },
|
|
19
|
+
groceries: { name: 'Groceries', spent: 412, limit: 500 },
|
|
20
|
+
transport: { name: 'Transport', spent: 64, limit: 120 },
|
|
21
|
+
dining: { name: 'Eating out', spent: 188, limit: 150 },
|
|
22
|
+
fun: { name: 'Fun', spent: 40, limit: 100 },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Drag a budget by its grip to put them in your order (or Tab to a grip, Space, arrows, Space). */
|
|
26
|
+
export default function Default() {
|
|
27
|
+
const [order, setOrder] = useState(Object.keys(BUDGETS));
|
|
28
|
+
return (
|
|
29
|
+
<SortableList value={order} onValueChange={setOrder} aria-label="Budgets" className="w-full max-w-md gap-2">
|
|
30
|
+
{order.map((id) => {
|
|
31
|
+
const b = BUDGETS[id];
|
|
32
|
+
return (
|
|
33
|
+
<SortableItem
|
|
34
|
+
key={id}
|
|
35
|
+
id={id}
|
|
36
|
+
className="glass glow-edge flex items-center gap-2 rounded-xl py-2 pr-4 pl-1.5"
|
|
37
|
+
>
|
|
38
|
+
<SortableHandle id={id} label={b.name} />
|
|
39
|
+
<Meter
|
|
40
|
+
label={b.name}
|
|
41
|
+
value={b.spent}
|
|
42
|
+
max={b.limit}
|
|
43
|
+
detail={`$${b.spent} / $${b.limit}`}
|
|
44
|
+
className="flex-1"
|
|
45
|
+
/>
|
|
46
|
+
</SortableItem>
|
|
47
|
+
);
|
|
48
|
+
})}
|
|
49
|
+
</SortableList>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Groups
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { flattenTree, SortableHandle, SortableItem, type SortableNode, SortableTree } from '@rinzai/zen';
|
|
58
|
+
import { useState } from 'react';
|
|
59
|
+
|
|
60
|
+
const NAMES: Record<string, string> = {
|
|
61
|
+
bills: 'Bills',
|
|
62
|
+
rent: 'Rent',
|
|
63
|
+
phone: 'Phone',
|
|
64
|
+
streaming: 'Streaming',
|
|
65
|
+
life: 'Lifestyle',
|
|
66
|
+
gym: 'Gym',
|
|
67
|
+
books: 'Books',
|
|
68
|
+
};
|
|
69
|
+
const GROUPS = new Set(['bills', 'life']);
|
|
70
|
+
const AMOUNTS: Record<string, string> = {
|
|
71
|
+
rent: '$1,800',
|
|
72
|
+
phone: '$45',
|
|
73
|
+
streaming: '$32',
|
|
74
|
+
gym: '$60',
|
|
75
|
+
books: '$25',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Budgets in groups, with SortableTree: drag one within its group, into the
|
|
80
|
+
* other, or sideways to tuck it under another budget. Groups stay at the top
|
|
81
|
+
* and take budgets; budgets never sit at the top on their own.
|
|
82
|
+
*/
|
|
83
|
+
export default function Groups() {
|
|
84
|
+
const [tree, setTree] = useState<SortableNode[]>([
|
|
85
|
+
{ id: 'bills', children: [{ id: 'rent' }, { id: 'phone' }, { id: 'streaming' }] },
|
|
86
|
+
{ id: 'life', children: [{ id: 'gym' }, { id: 'books' }] },
|
|
87
|
+
]);
|
|
88
|
+
return (
|
|
89
|
+
<SortableTree
|
|
90
|
+
aria-label="Budgets"
|
|
91
|
+
value={tree}
|
|
92
|
+
onValueChange={setTree}
|
|
93
|
+
accepts={(id, parent) => (GROUPS.has(id) ? parent === null : parent !== null)}
|
|
94
|
+
className="w-full max-w-sm gap-1.5"
|
|
95
|
+
>
|
|
96
|
+
{flattenTree(tree).map(({ id, depth }) => (
|
|
97
|
+
<SortableItem key={id} id={id} style={{ marginInlineStart: Math.max(0, depth - 1) * 24 }}>
|
|
98
|
+
{GROUPS.has(id) ? (
|
|
99
|
+
<div className="flex items-center gap-1 pt-2">
|
|
100
|
+
<SortableHandle id={id} label={NAMES[id]} />
|
|
101
|
+
<span className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
|
|
102
|
+
{NAMES[id]}
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
105
|
+
) : (
|
|
106
|
+
<div className="glass glow-edge flex items-center gap-1 rounded-xl py-1.5 ps-1.5 pe-4">
|
|
107
|
+
<SortableHandle id={id} label={NAMES[id]} />
|
|
108
|
+
<span className="flex-1 text-sm">{NAMES[id]}</span>
|
|
109
|
+
<span className="text-muted-foreground text-sm tabular-nums">{AMOUNTS[id]}</span>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</SortableItem>
|
|
113
|
+
))}
|
|
114
|
+
</SortableTree>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## API reference
|
|
120
|
+
|
|
121
|
+
### SortableList
|
|
122
|
+
|
|
123
|
+
A list put in order by dragging, e.g. budget categories: drag an item by its
|
|
124
|
+
SortableHandle and the others make way (a phone taps lightly at each new
|
|
125
|
+
place); it settles into place when let go.
|
|
126
|
+
From the keyboard, Space or Enter on a handle picks the item up, the arrow
|
|
127
|
+
keys move it, Space or Enter drops it and Escape puts it back; each step is
|
|
128
|
+
announced. `value` is the ids in order; render a SortableItem for each.
|
|
129
|
+
With `asChild` (and `asChild` on the items), your own elements instead: a
|
|
130
|
+
TableBody of TableRows, say, one per group, each sorted on its own.
|
|
131
|
+
|
|
132
|
+
| Prop | Type | Default | Description |
|
|
133
|
+
| --- | --- | --- | --- |
|
|
134
|
+
| `onValueChange` (required) | `(value: string[]) => void` | | |
|
|
135
|
+
| `value` (required) | `readonly string[]` | | The items' ids, in order. |
|
|
136
|
+
| `asChild` | `boolean` | | Put the list's props on your own element (the single child, e.g. a TableBody) instead of a ul. |
|
|
137
|
+
|
|
138
|
+
### SortableTree
|
|
139
|
+
|
|
140
|
+
Items in groups, and groups in groups, put in order by dragging: an item
|
|
141
|
+
can go anywhere in its group, into another group (or one open inside it),
|
|
142
|
+
or under another item. While dragging, a glowing line shows where it will
|
|
143
|
+
land, set in as deep as it'll go, and the group it would join is lit; drag
|
|
144
|
+
sideways to go a level in or out. From the keyboard, Space or Enter on a
|
|
145
|
+
handle picks it up, the up and down arrows move it through every place it
|
|
146
|
+
can go, left and right a level out or in, Space or Enter drops it and
|
|
147
|
+
Escape puts it back; each step is announced.
|
|
148
|
+
|
|
149
|
+
`value` is the tree of ids. Render a SortableItem for each row shown, in
|
|
150
|
+
order (flattenTree lists them, with their depths): what's under a closed
|
|
151
|
+
one isn't shown, and moves with it. `accepts` says which group takes which
|
|
152
|
+
item. With `asChild`, your own element (a Table, its rows SortableItems with
|
|
153
|
+
asChild on TableRows) instead of a ul.
|
|
154
|
+
|
|
155
|
+
| Prop | Type | Default | Description |
|
|
156
|
+
| --- | --- | --- | --- |
|
|
157
|
+
| `onValueChange` (required) | `(value: SortableNode[]) => void` | | |
|
|
158
|
+
| `value` (required) | `readonly SortableNode[]` | | The tree of ids, in order. |
|
|
159
|
+
| `accepts` | `((id: string, parentId: string \| null) => boolean)` | | Whether `parentId` (null: the top level) takes item `id`; by default, everything goes anywhere. |
|
|
160
|
+
| `asChild` | `boolean` | | Put its props on your own element (the single child, e.g. a Table) instead of a ul. |
|
|
161
|
+
| `indent` | `number` | `24` | How far in a level is set, in px: the drop line is set in this far, and dragging this far sideways goes a level in or out. |
|
|
162
|
+
|
|
163
|
+
### SortableItem
|
|
164
|
+
|
|
165
|
+
One item in a SortableList or SortableTree; give it the `id` it has in the list's value. With `asChild`, your own element (a TableRow).
|
|
166
|
+
|
|
167
|
+
| Prop | Type | Default | Description |
|
|
168
|
+
| --- | --- | --- | --- |
|
|
169
|
+
| `id` (required) | `string` | | |
|
|
170
|
+
| `asChild` | `boolean` | | Put the item's props on your own element (the single child, e.g. a TableRow) instead of an li. |
|
|
171
|
+
| `label` | `string` | | Its name, for one without a SortableHandle (a group others go into), to say "2 of 3 in Food". |
|
|
172
|
+
|
|
173
|
+
### SortableHandle
|
|
174
|
+
|
|
175
|
+
What an item is dragged by: a grip button. `label` names the item ("Rent");
|
|
176
|
+
the button reads "Reorder Rent". It takes no scroll gestures, so dragging it
|
|
177
|
+
never scrolls the page.
|
|
178
|
+
|
|
179
|
+
| Prop | Type | Default | Description |
|
|
180
|
+
| --- | --- | --- | --- |
|
|
181
|
+
| `id` (required) | `string` | | The item's id, as in the list's value. |
|
|
182
|
+
| `label` (required) | `string` | | The item's name, e.g. "Rent". |
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Sparkline
|
|
2
|
+
|
|
3
|
+
A small line of how a number has moved, with no axes, for beside a figure. It draws itself in.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { Sparkline } from '@rinzai/zen';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### Default
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Sparkline, Stat, StatRow } from '@rinzai/zen';
|
|
15
|
+
|
|
16
|
+
/** Beside the figures they explain: a balance over the month, and spending by week. */
|
|
17
|
+
export default function Default() {
|
|
18
|
+
return (
|
|
19
|
+
<StatRow className="w-full max-w-lg">
|
|
20
|
+
<Stat
|
|
21
|
+
label="Balance"
|
|
22
|
+
value={
|
|
23
|
+
<span className="flex items-center justify-between gap-3">
|
|
24
|
+
$6,908
|
|
25
|
+
<Sparkline values={[4100, 4300, 3900, 5200, 5600, 5400, 6100, 6908]} />
|
|
26
|
+
</span>
|
|
27
|
+
}
|
|
28
|
+
/>
|
|
29
|
+
<Stat
|
|
30
|
+
label="Spent"
|
|
31
|
+
tone="negative"
|
|
32
|
+
value={
|
|
33
|
+
<span className="flex items-center justify-between gap-3">
|
|
34
|
+
$2,742
|
|
35
|
+
<Sparkline tone="negative" values={[620, 540, 810, 772]} />
|
|
36
|
+
</span>
|
|
37
|
+
}
|
|
38
|
+
/>
|
|
39
|
+
</StatRow>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## API reference
|
|
45
|
+
|
|
46
|
+
### Sparkline
|
|
47
|
+
|
|
48
|
+
A small line of how a number has moved (a balance over a month, spending by
|
|
49
|
+
week), with no axes: for beside a figure in a Stat or a table row. It takes
|
|
50
|
+
its size from className (e.g. h-8 w-24) and draws itself in.
|
|
51
|
+
|
|
52
|
+
| Prop | Type | Default | Description |
|
|
53
|
+
| --- | --- | --- | --- |
|
|
54
|
+
| `values` (required) | `number[]` | | |
|
|
55
|
+
| `area` | `boolean` | `true` | A soft fill under the line. |
|
|
56
|
+
| `tone` | `"default" \| "muted" \| "negative"` | `default` | The accent (`primary`), red (`negative`), or grey (`muted`). |
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Spinner
|
|
2
|
+
|
|
3
|
+
A small loading spinner in the current text colour, announced to screen readers when labelled.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { Spinner } from '@rinzai/zen';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### Default
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Spinner } from '@rinzai/zen';
|
|
15
|
+
|
|
16
|
+
/** Sized and coloured with className; it follows the text colour. */
|
|
17
|
+
export default function Default() {
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex items-center gap-6">
|
|
20
|
+
<Spinner />
|
|
21
|
+
<Spinner className="size-6" />
|
|
22
|
+
<Spinner className="text-primary size-8 border-[3px]" />
|
|
23
|
+
<Spinner className="text-muted-foreground size-5" />
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Loading
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { Button, Card, CardHeader, CardTitle, Spinner } from '@rinzai/zen';
|
|
33
|
+
import { useState } from 'react';
|
|
34
|
+
|
|
35
|
+
/** Standing on its own, a spinner needs a label so screen readers announce what is loading. */
|
|
36
|
+
export default function Loading() {
|
|
37
|
+
const [loading, setLoading] = useState(true);
|
|
38
|
+
return (
|
|
39
|
+
<Card className="w-80">
|
|
40
|
+
<CardHeader>
|
|
41
|
+
<CardTitle>Entries</CardTitle>
|
|
42
|
+
</CardHeader>
|
|
43
|
+
{loading ? (
|
|
44
|
+
<div className="text-muted-foreground flex items-center gap-3 py-6 text-sm">
|
|
45
|
+
<Spinner label="Loading entries" />
|
|
46
|
+
Loading entries…
|
|
47
|
+
</div>
|
|
48
|
+
) : (
|
|
49
|
+
<p className="py-6 text-sm">12 entries this month.</p>
|
|
50
|
+
)}
|
|
51
|
+
<Button variant="outline" size="sm" onClick={() => setLoading((l) => !l)}>
|
|
52
|
+
{loading ? 'Finish loading' : 'Load again'}
|
|
53
|
+
</Button>
|
|
54
|
+
</Card>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## API reference
|
|
60
|
+
|
|
61
|
+
### Spinner
|
|
62
|
+
|
|
63
|
+
Small circular spinner in the current text colour; size it with className
|
|
64
|
+
(default size-4). Decorative by default (e.g. inside a loading Button). Give
|
|
65
|
+
it a `label` when it stands on its own, and screen readers announce it.
|
|
66
|
+
|
|
67
|
+
| Prop | Type | Default | Description |
|
|
68
|
+
| --- | --- | --- | --- |
|
|
69
|
+
| `label` | `string` | | What is loading, e.g. "Loading entries". Makes the spinner announce itself. |
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Status Pill
|
|
2
|
+
|
|
3
|
+
A small status that changes in place, such as Saving…, Saved or Not saved with a Retry, and fades back once the news is old.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { StatusPill, StatusPillAction } from '@rinzai/zen';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### Save
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Button, Spinner, StatusPill, StatusPillAction } from '@rinzai/zen';
|
|
15
|
+
import { useEffect, useState } from 'react';
|
|
16
|
+
// Icons from Heroicons (https://heroicons.com): AlertTriangle (exclamation-triangle), Check (check).
|
|
17
|
+
|
|
18
|
+
type Save = 'saving' | 'saved' | 'error';
|
|
19
|
+
|
|
20
|
+
/** A save indicator: saving spins, saved checks and then quietens, a failure is red and offers a retry. */
|
|
21
|
+
export default function Save() {
|
|
22
|
+
const [state, setState] = useState<Save>('saved');
|
|
23
|
+
const [quiet, setQuiet] = useState(false);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setQuiet(false);
|
|
26
|
+
if (state !== 'saved') return;
|
|
27
|
+
const t = window.setTimeout(() => setQuiet(true), 2500);
|
|
28
|
+
return () => window.clearTimeout(t);
|
|
29
|
+
}, [state]);
|
|
30
|
+
const save = (ok: boolean) => {
|
|
31
|
+
setState('saving');
|
|
32
|
+
window.setTimeout(() => setState(ok ? 'saved' : 'error'), 1200);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="flex flex-col items-center gap-4">
|
|
37
|
+
{state === 'error' ? (
|
|
38
|
+
<StatusPill tone="negative">
|
|
39
|
+
<AlertTriangle />
|
|
40
|
+
Not saved
|
|
41
|
+
<StatusPillAction onClick={() => save(true)}>Retry</StatusPillAction>
|
|
42
|
+
</StatusPill>
|
|
43
|
+
) : (
|
|
44
|
+
<StatusPill tone={state === 'saved' ? 'positive' : 'default'} quiet={state === 'saved' && quiet}>
|
|
45
|
+
{state === 'saving' ? <Spinner /> : <Check />}
|
|
46
|
+
{state === 'saving' ? 'Saving…' : 'Saved'}
|
|
47
|
+
</StatusPill>
|
|
48
|
+
)}
|
|
49
|
+
<div className="flex gap-2">
|
|
50
|
+
<Button variant="outline" size="sm" onClick={() => save(true)}>
|
|
51
|
+
Save
|
|
52
|
+
</Button>
|
|
53
|
+
<Button variant="outline" size="sm" onClick={() => save(false)}>
|
|
54
|
+
Save and fail
|
|
55
|
+
</Button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## API reference
|
|
63
|
+
|
|
64
|
+
### StatusPill
|
|
65
|
+
|
|
66
|
+
A small rounded status that changes in place, e.g. an app's "Saving…",
|
|
67
|
+
"Saved", "Not saved · Retry": an icon (a Spinner, a Check) and a few words.
|
|
68
|
+
`quiet` fades it back once the news is old (a save a few seconds ago).
|
|
69
|
+
Changes are read out politely (role="status"). For an action inside it,
|
|
70
|
+
such as Retry, use StatusPillAction. For a fixed label or count, use Badge.
|
|
71
|
+
|
|
72
|
+
| Prop | Type | Default | Description |
|
|
73
|
+
| --- | --- | --- | --- |
|
|
74
|
+
| `quiet` | `boolean` | `false` | Faded back into the page: the news is old. |
|
|
75
|
+
| `tone` | `"default" \| "negative" \| "positive"` | `default` | `positive` in the accent colour (done), `negative` in red (failed); `default` is muted (in progress). |
|
|
76
|
+
|
|
77
|
+
### StatusPillAction
|
|
78
|
+
|
|
79
|
+
A text button inside a StatusPill, e.g. Retry, in the pill's colour.
|
|
80
|
+
|
|
81
|
+
No props of its own; it accepts the props of the element it renders.
|