@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,86 @@
|
|
|
1
|
+
# Marquee
|
|
2
|
+
|
|
3
|
+
One line of text cut off with an ellipsis, that scrolls to show the rest on hover or focus (of itself or of its row), or all the time. Only text that doesn't fit moves.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { Marquee } from '@rinzai/zen';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### Default
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Marquee } from '@rinzai/zen';
|
|
15
|
+
|
|
16
|
+
const TRACKS = [
|
|
17
|
+
{ title: 'Glass Aurora', artist: 'Halcyon Drift' },
|
|
18
|
+
{
|
|
19
|
+
title: 'The Long Way Home Through the Harbour Lights (Extended Night Mix)',
|
|
20
|
+
artist: 'Mira Vale and the Quiet Hours Orchestra',
|
|
21
|
+
},
|
|
22
|
+
{ title: 'Paper Moons', artist: 'Mira Vale' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Hover a row, or Tab to it: names too long for it scroll, the rest stay put.
|
|
27
|
+
* `data-zen-marquee-host` makes the whole row the trigger.
|
|
28
|
+
*/
|
|
29
|
+
export default function Default() {
|
|
30
|
+
return (
|
|
31
|
+
<ul className="flex w-72 flex-col gap-1">
|
|
32
|
+
{TRACKS.map((t) => (
|
|
33
|
+
<li key={t.title}>
|
|
34
|
+
<a
|
|
35
|
+
href="#play"
|
|
36
|
+
data-zen-marquee-host
|
|
37
|
+
onClick={(e) => e.preventDefault()}
|
|
38
|
+
className="focus-ring hover:bg-tint/[0.06] flex flex-col rounded-lg px-3 py-2"
|
|
39
|
+
>
|
|
40
|
+
<Marquee className="text-sm font-medium">{t.title}</Marquee>
|
|
41
|
+
<Marquee className="text-muted-foreground text-xs">{t.artist}</Marquee>
|
|
42
|
+
</a>
|
|
43
|
+
</li>
|
|
44
|
+
))}
|
|
45
|
+
</ul>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Always
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import { Marquee } from '@rinzai/zen';
|
|
54
|
+
|
|
55
|
+
/** `trigger="always"` keeps it moving, for what's playing: to the end, a pause, and back. */
|
|
56
|
+
export default function Always() {
|
|
57
|
+
return (
|
|
58
|
+
<div className="border-tint/10 flex w-56 items-center gap-3 rounded-xl border p-3">
|
|
59
|
+
<div className="from-primary size-10 shrink-0 rounded-md bg-linear-135 to-fuchsia-400" />
|
|
60
|
+
<div className="min-w-0">
|
|
61
|
+
<Marquee trigger="always" className="text-sm font-medium">
|
|
62
|
+
The Long Way Home Through the Harbour Lights
|
|
63
|
+
</Marquee>
|
|
64
|
+
<Marquee trigger="always" className="text-muted-foreground text-xs">
|
|
65
|
+
Mira Vale
|
|
66
|
+
</Marquee>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API reference
|
|
74
|
+
|
|
75
|
+
### Marquee
|
|
76
|
+
|
|
77
|
+
One line of text that's cut off with an ellipsis, and scrolls to show the
|
|
78
|
+
rest: to the end, a pause, and back. Only text that doesn't fit moves.
|
|
79
|
+
With `trigger="hover"` (the default) it moves while it, or the closest
|
|
80
|
+
element marked `data-zen-marquee-host` (a row, a tile, a link), is hovered
|
|
81
|
+
or holds focus; `"always"` keeps it moving, e.g. for what's playing. Stays
|
|
82
|
+
an ellipsis for reduced motion. Set the type and colour with className.
|
|
83
|
+
|
|
84
|
+
| Prop | Type | Default | Description |
|
|
85
|
+
| --- | --- | --- | --- |
|
|
86
|
+
| `trigger` | `"always" \| "hover"` | `hover` | `'hover'` (default): moves while it or its `data-zen-marquee-host` is hovered or focused. `'always'`: keeps moving. |
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# Menu
|
|
2
|
+
|
|
3
|
+
A button that opens a menu, or a context menu from a right-click or long press, with your own trigger, a header, icons, groups, destructive items, checkbox and radio items, submenus, and a menubar of menus.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { Menu, MenuTrigger, MenuContextTrigger, MenuContent, MenuItem, MenuCheckboxItem, MenuRadioGroup, MenuRadioItem, MenuSub, MenuSubTrigger, MenuSubContent, MenuSeparator, MenuHeader, Menubar } from '@rinzai/zen';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### Account
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Avatar, Menu, MenuContent, MenuHeader, MenuItem, MenuSeparator, MenuTrigger, useToast } from '@rinzai/zen';
|
|
15
|
+
// Icons from Heroicons (https://heroicons.com): Lock (lock-closed), Logout (arrow-right-start-on-rectangle), Settings (cog-6-tooth).
|
|
16
|
+
|
|
17
|
+
/** Your own trigger (an avatar), a header that isn't a choice, icons, groups, and a destructive item. */
|
|
18
|
+
export default function Account() {
|
|
19
|
+
const toast = useToast();
|
|
20
|
+
const email = 'rin@example.com';
|
|
21
|
+
return (
|
|
22
|
+
<Menu>
|
|
23
|
+
<MenuTrigger
|
|
24
|
+
aria-label="Account menu"
|
|
25
|
+
className="focus-visible:ring-ring/50 data-popup-open:ring-ring/40 rounded-full outline-hidden transition-transform duration-200 hover:scale-105 focus-visible:ring-2 data-popup-open:ring-2"
|
|
26
|
+
>
|
|
27
|
+
<Avatar name={email} />
|
|
28
|
+
</MenuTrigger>
|
|
29
|
+
<MenuContent offset={10} className="w-64 shadow-[0_20px_60px_-20px_oklch(0_0_0/0.8)]">
|
|
30
|
+
<MenuHeader>
|
|
31
|
+
<Avatar name={email} className="size-10" />
|
|
32
|
+
<div className="min-w-0">
|
|
33
|
+
<div className="text-muted-foreground text-xs">Signed in as</div>
|
|
34
|
+
<div className="truncate text-sm font-medium" title={email}>
|
|
35
|
+
{email}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</MenuHeader>
|
|
39
|
+
<MenuSeparator />
|
|
40
|
+
<MenuItem icon={<Settings className="size-4" />} onSelect={() => toast('Settings')}>
|
|
41
|
+
Settings
|
|
42
|
+
</MenuItem>
|
|
43
|
+
<MenuItem icon={<Lock className="size-4" />} onSelect={() => toast('Locked', { tone: 'positive' })}>
|
|
44
|
+
Lock now
|
|
45
|
+
</MenuItem>
|
|
46
|
+
<MenuSeparator />
|
|
47
|
+
<MenuItem tone="negative" icon={<Logout className="size-4" />} onSelect={() => toast('Logged out')}>
|
|
48
|
+
Log out
|
|
49
|
+
</MenuItem>
|
|
50
|
+
</MenuContent>
|
|
51
|
+
</Menu>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Actions
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { buttonVariants, Menu, MenuContent, MenuItem, MenuTrigger, useToast } from '@rinzai/zen';
|
|
60
|
+
// Icons from Heroicons (https://heroicons.com): Ellipsis (ellipsis-horizontal).
|
|
61
|
+
|
|
62
|
+
/** The usual "⋯" menu of actions for one thing: an icon button as the trigger, and a destructive item last. */
|
|
63
|
+
export default function Actions() {
|
|
64
|
+
const toast = useToast();
|
|
65
|
+
return (
|
|
66
|
+
<Menu>
|
|
67
|
+
<MenuTrigger aria-label="Goal actions" className={buttonVariants({ variant: 'ghost', size: 'icon' })}>
|
|
68
|
+
<Ellipsis />
|
|
69
|
+
</MenuTrigger>
|
|
70
|
+
<MenuContent>
|
|
71
|
+
<MenuItem onSelect={() => toast('Edit')}>Edit</MenuItem>
|
|
72
|
+
<MenuItem onSelect={() => toast('Duplicated', { tone: 'positive' })}>Duplicate</MenuItem>
|
|
73
|
+
<MenuItem tone="negative" onSelect={() => toast('Deleted', { tone: 'negative' })}>
|
|
74
|
+
Delete
|
|
75
|
+
</MenuItem>
|
|
76
|
+
</MenuContent>
|
|
77
|
+
</Menu>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Context
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { Menu, MenuContent, MenuContextTrigger, MenuItem, MenuSeparator, useToast } from '@rinzai/zen';
|
|
86
|
+
import { useState } from 'react';
|
|
87
|
+
|
|
88
|
+
const ENTRIES = [
|
|
89
|
+
{ id: 1, label: 'Weekly shop', amount: '−$165' },
|
|
90
|
+
{ id: 2, label: 'Train pass', amount: '−$92' },
|
|
91
|
+
{ id: 3, label: 'Salary', amount: '+$4,200' },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
/** Right-click an entry (or press and hold on a phone) for its actions. One menu serves the whole list. */
|
|
95
|
+
export default function Context() {
|
|
96
|
+
const toast = useToast();
|
|
97
|
+
const [entry, setEntry] = useState(ENTRIES[0]);
|
|
98
|
+
return (
|
|
99
|
+
<Menu>
|
|
100
|
+
<ul className="glass glow-edge divide-tint/[0.06] w-full max-w-sm divide-y rounded-xl">
|
|
101
|
+
{ENTRIES.map((e) => (
|
|
102
|
+
<MenuContextTrigger
|
|
103
|
+
key={e.id}
|
|
104
|
+
asChild
|
|
105
|
+
onContextMenu={() => setEntry(e)}
|
|
106
|
+
onPointerDown={() => setEntry(e)}
|
|
107
|
+
>
|
|
108
|
+
<li
|
|
109
|
+
tabIndex={0}
|
|
110
|
+
className="focus-visible:ring-ring/40 data-popup-open:bg-primary/12 flex justify-between px-4 py-3 text-sm outline-hidden transition-colors select-none first:rounded-t-xl last:rounded-b-xl focus-visible:ring-2"
|
|
111
|
+
>
|
|
112
|
+
<span>{e.label}</span>
|
|
113
|
+
<span className="tabular-nums">{e.amount}</span>
|
|
114
|
+
</li>
|
|
115
|
+
</MenuContextTrigger>
|
|
116
|
+
))}
|
|
117
|
+
</ul>
|
|
118
|
+
<MenuContent aria-label={`${entry.label} actions`}>
|
|
119
|
+
<MenuItem onSelect={() => toast(`Editing ${entry.label}`)}>Edit</MenuItem>
|
|
120
|
+
<MenuItem onSelect={() => toast(`Duplicated ${entry.label}`, { tone: 'positive' })}>Duplicate</MenuItem>
|
|
121
|
+
<MenuSeparator />
|
|
122
|
+
<MenuItem tone="negative" onSelect={() => toast(`Deleted ${entry.label}`, { tone: 'negative' })}>
|
|
123
|
+
Delete
|
|
124
|
+
</MenuItem>
|
|
125
|
+
</MenuContent>
|
|
126
|
+
</Menu>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Options
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
import {
|
|
135
|
+
buttonVariants,
|
|
136
|
+
Menu,
|
|
137
|
+
MenuCheckboxItem,
|
|
138
|
+
MenuContent,
|
|
139
|
+
MenuItem,
|
|
140
|
+
MenuRadioGroup,
|
|
141
|
+
MenuRadioItem,
|
|
142
|
+
MenuSeparator,
|
|
143
|
+
MenuSub,
|
|
144
|
+
MenuSubContent,
|
|
145
|
+
MenuSubTrigger,
|
|
146
|
+
MenuTrigger,
|
|
147
|
+
useToast,
|
|
148
|
+
} from '@rinzai/zen';
|
|
149
|
+
import { useState } from 'react';
|
|
150
|
+
// Icons from Heroicons (https://heroicons.com): Settings (cog-6-tooth).
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Settings that stay put while you change them: checkbox items turn columns on
|
|
154
|
+
* and off, radio items pick the order, and a submenu holds the less used
|
|
155
|
+
* exports. Hover, or use the arrow keys, to open it.
|
|
156
|
+
*/
|
|
157
|
+
export default function Options() {
|
|
158
|
+
const toast = useToast();
|
|
159
|
+
const [sort, setSort] = useState('date');
|
|
160
|
+
return (
|
|
161
|
+
<Menu>
|
|
162
|
+
<MenuTrigger className={buttonVariants({ variant: 'outline' })}>
|
|
163
|
+
<Settings className="size-4" />
|
|
164
|
+
View
|
|
165
|
+
</MenuTrigger>
|
|
166
|
+
<MenuContent className="w-56">
|
|
167
|
+
<MenuCheckboxItem defaultChecked>Category</MenuCheckboxItem>
|
|
168
|
+
<MenuCheckboxItem defaultChecked>Account</MenuCheckboxItem>
|
|
169
|
+
<MenuCheckboxItem>Notes</MenuCheckboxItem>
|
|
170
|
+
<MenuSeparator />
|
|
171
|
+
<MenuRadioGroup aria-label="Sort by" value={sort} onValueChange={setSort}>
|
|
172
|
+
<MenuRadioItem value="date">Newest first</MenuRadioItem>
|
|
173
|
+
<MenuRadioItem value="amount">Largest first</MenuRadioItem>
|
|
174
|
+
</MenuRadioGroup>
|
|
175
|
+
<MenuSeparator />
|
|
176
|
+
<MenuSub>
|
|
177
|
+
<MenuSubTrigger>Export</MenuSubTrigger>
|
|
178
|
+
<MenuSubContent>
|
|
179
|
+
<MenuItem onSelect={() => toast('Exported as CSV')}>CSV</MenuItem>
|
|
180
|
+
<MenuItem onSelect={() => toast('Exported as PDF')}>PDF</MenuItem>
|
|
181
|
+
<MenuItem onSelect={() => toast('Exported as JSON')}>JSON</MenuItem>
|
|
182
|
+
</MenuSubContent>
|
|
183
|
+
</MenuSub>
|
|
184
|
+
</MenuContent>
|
|
185
|
+
</Menu>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Bar
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
import {
|
|
194
|
+
Menu,
|
|
195
|
+
Menubar,
|
|
196
|
+
MenuCheckboxItem,
|
|
197
|
+
MenuContent,
|
|
198
|
+
MenuItem,
|
|
199
|
+
MenuRadioGroup,
|
|
200
|
+
MenuRadioItem,
|
|
201
|
+
MenuSeparator,
|
|
202
|
+
MenuSub,
|
|
203
|
+
MenuSubContent,
|
|
204
|
+
MenuSubTrigger,
|
|
205
|
+
MenuTrigger,
|
|
206
|
+
useToast,
|
|
207
|
+
} from '@rinzai/zen';
|
|
208
|
+
import { useState } from 'react';
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* A row of menus, as at the top of an app. Tab to it once: ←→ move between
|
|
212
|
+
* the menus and ↓ opens one; with one open, ←→ go to the next and pointing at
|
|
213
|
+
* another label switches to it.
|
|
214
|
+
*/
|
|
215
|
+
export default function Bar() {
|
|
216
|
+
const toast = useToast();
|
|
217
|
+
const [period, setPeriod] = useState('month');
|
|
218
|
+
return (
|
|
219
|
+
<Menubar aria-label="Ledger">
|
|
220
|
+
<Menu>
|
|
221
|
+
<MenuTrigger>File</MenuTrigger>
|
|
222
|
+
<MenuContent>
|
|
223
|
+
<MenuItem onSelect={() => toast('New entry')}>New entry</MenuItem>
|
|
224
|
+
<MenuItem onSelect={() => toast('Imported')}>Import…</MenuItem>
|
|
225
|
+
<MenuSub>
|
|
226
|
+
<MenuSubTrigger>Export</MenuSubTrigger>
|
|
227
|
+
<MenuSubContent>
|
|
228
|
+
<MenuItem onSelect={() => toast('Exported as CSV')}>CSV</MenuItem>
|
|
229
|
+
<MenuItem onSelect={() => toast('Exported as PDF')}>PDF</MenuItem>
|
|
230
|
+
</MenuSubContent>
|
|
231
|
+
</MenuSub>
|
|
232
|
+
</MenuContent>
|
|
233
|
+
</Menu>
|
|
234
|
+
<Menu>
|
|
235
|
+
<MenuTrigger>Edit</MenuTrigger>
|
|
236
|
+
<MenuContent>
|
|
237
|
+
<MenuItem onSelect={() => toast('Undone')}>Undo</MenuItem>
|
|
238
|
+
<MenuItem onSelect={() => toast('Redone')}>Redo</MenuItem>
|
|
239
|
+
<MenuSeparator />
|
|
240
|
+
<MenuItem tone="negative" onSelect={() => toast('Deleted', { tone: 'negative' })}>
|
|
241
|
+
Delete entry
|
|
242
|
+
</MenuItem>
|
|
243
|
+
</MenuContent>
|
|
244
|
+
</Menu>
|
|
245
|
+
<Menu>
|
|
246
|
+
<MenuTrigger>View</MenuTrigger>
|
|
247
|
+
<MenuContent className="w-52">
|
|
248
|
+
<MenuRadioGroup aria-label="Period" value={period} onValueChange={setPeriod}>
|
|
249
|
+
<MenuRadioItem value="week">Week</MenuRadioItem>
|
|
250
|
+
<MenuRadioItem value="month">Month</MenuRadioItem>
|
|
251
|
+
<MenuRadioItem value="year">Year</MenuRadioItem>
|
|
252
|
+
</MenuRadioGroup>
|
|
253
|
+
<MenuSeparator />
|
|
254
|
+
<MenuCheckboxItem defaultChecked>Show categories</MenuCheckboxItem>
|
|
255
|
+
</MenuContent>
|
|
256
|
+
</Menu>
|
|
257
|
+
</Menubar>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## API reference
|
|
263
|
+
|
|
264
|
+
### Menu
|
|
265
|
+
|
|
266
|
+
A list of actions that opens from a button: a native popover, so outside
|
|
267
|
+
clicks and Escape close it and focus returns to the button. Arrow keys, Home
|
|
268
|
+
and End move between items. Put a MenuTrigger (its content is yours: an icon,
|
|
269
|
+
an avatar, a label) and a MenuContent inside; fill the content with MenuItem,
|
|
270
|
+
MenuSeparator and MenuHeader. For a context menu, put a MenuContextTrigger
|
|
271
|
+
around what it's for instead of a MenuTrigger.
|
|
272
|
+
|
|
273
|
+
| Prop | Type | Default | Description |
|
|
274
|
+
| --- | --- | --- | --- |
|
|
275
|
+
| `onOpenChange` | `((open: boolean) => void)` | | Every open and close: the trigger, choosing an item, outside clicks, Escape. |
|
|
276
|
+
|
|
277
|
+
### MenuTrigger
|
|
278
|
+
|
|
279
|
+
The button that opens the menu (↓ or ↑ opens it at its first or last item).
|
|
280
|
+
Give it an `aria-label` when it shows only an icon or avatar; the menu is
|
|
281
|
+
named after it. In a Menubar it's one of the bar's items, and looks it. With
|
|
282
|
+
`asChild`, your own button instead.
|
|
283
|
+
|
|
284
|
+
| Prop | Type | Default | Description |
|
|
285
|
+
| --- | --- | --- | --- |
|
|
286
|
+
| `asChild` | `boolean` | | Put the trigger's props on your own button element instead of rendering one. |
|
|
287
|
+
|
|
288
|
+
### MenuContextTrigger
|
|
289
|
+
|
|
290
|
+
What a context menu is for, e.g. a row or a card: right-click (or the
|
|
291
|
+
keyboard's menu key, or Shift+F10) opens the Menu where the pointer is, and
|
|
292
|
+
so does a long press on a phone. While its menu is open it has
|
|
293
|
+
`data-popup-open`, to show what the menu is for. With `asChild`, your own
|
|
294
|
+
element instead of a div.
|
|
295
|
+
|
|
296
|
+
| Prop | Type | Default | Description |
|
|
297
|
+
| --- | --- | --- | --- |
|
|
298
|
+
| `asChild` | `boolean` | | Put its props on your own element (the single child) instead of a div. |
|
|
299
|
+
| `disabled` | `boolean` | | Leave the browser's own context menu. |
|
|
300
|
+
|
|
301
|
+
### MenuContent
|
|
302
|
+
|
|
303
|
+
The menu panel: glass, below the button (above it if there's no room), lined
|
|
304
|
+
up with its end edge by default (its start edge in a Menubar). A context menu opens from the pointer
|
|
305
|
+
instead; name it with `aria-label`.
|
|
306
|
+
|
|
307
|
+
| Prop | Type | Default | Description |
|
|
308
|
+
| --- | --- | --- | --- |
|
|
309
|
+
| `align` | `"center" \| "end" \| "start"` | | Which edge of the button the menu lines up with (default end; a context menu starts at the pointer), or centred on it. |
|
|
310
|
+
| `offset` | `number` | | Gap between the button and the menu, in px. |
|
|
311
|
+
|
|
312
|
+
### MenuItem
|
|
313
|
+
|
|
314
|
+
One choice in a Menu: closes the menu, then runs `onSelect`. An `icon` sits
|
|
315
|
+
before the label (muted, or red with the label when `tone` is negative). With
|
|
316
|
+
`asChild`, the item's role and styles go onto your own element instead, such
|
|
317
|
+
as a router's Link.
|
|
318
|
+
|
|
319
|
+
| Prop | Type | Default | Description |
|
|
320
|
+
| --- | --- | --- | --- |
|
|
321
|
+
| `asChild` | `boolean` | | Put the item's role and styles on the single child element (a link) instead of a button. |
|
|
322
|
+
| `icon` | `ReactNode` | | Shown before the label; size it yourself (e.g. size-4). |
|
|
323
|
+
| `onSelect` | `(() => void)` | | Runs after the menu closes. |
|
|
324
|
+
| `tone` | `"default" \| "negative"` | `default` | `negative` for a destructive choice: red. |
|
|
325
|
+
|
|
326
|
+
### MenuCheckboxItem
|
|
327
|
+
|
|
328
|
+
A choice that's on or off, e.g. a column to show: choosing it flips
|
|
329
|
+
`checked` and leaves the menu open, so several can be set at once. A tick
|
|
330
|
+
shows while it's on.
|
|
331
|
+
|
|
332
|
+
| Prop | Type | Default | Description |
|
|
333
|
+
| --- | --- | --- | --- |
|
|
334
|
+
| `checked` | `boolean` | | On (ticked); give it to control the item, or `defaultChecked` to start it. |
|
|
335
|
+
| `defaultChecked` | `boolean` | `false` | |
|
|
336
|
+
| `onCheckedChange` | `((checked: boolean) => void)` | | |
|
|
337
|
+
|
|
338
|
+
### MenuRadioGroup
|
|
339
|
+
|
|
340
|
+
One choice out of several, e.g. how to sort: put MenuRadioItems inside. Choosing one leaves the menu open.
|
|
341
|
+
|
|
342
|
+
| Prop | Type | Default | Description |
|
|
343
|
+
| --- | --- | --- | --- |
|
|
344
|
+
| `defaultValue` | `string \| null` | `null` | |
|
|
345
|
+
| `onValueChange` | `((value: string) => void)` | | |
|
|
346
|
+
| `value` | `string \| null` | | The chosen item's `value`; give it to control the group, or `defaultValue` to start it. |
|
|
347
|
+
|
|
348
|
+
### MenuRadioItem
|
|
349
|
+
|
|
350
|
+
One of a MenuRadioGroup's choices; a dot marks the chosen one.
|
|
351
|
+
|
|
352
|
+
| Prop | Type | Default | Description |
|
|
353
|
+
| --- | --- | --- | --- |
|
|
354
|
+
| `value` (required) | `string` | | |
|
|
355
|
+
|
|
356
|
+
### MenuSub
|
|
357
|
+
|
|
358
|
+
A menu inside a menu, opening beside its item: on hover, or with Enter,
|
|
359
|
+
Space or the arrow towards it; the arrow back (or Escape) closes it. Put a
|
|
360
|
+
MenuSubTrigger and a MenuSubContent inside, in a MenuContent.
|
|
361
|
+
|
|
362
|
+
| Prop | Type | Default | Description |
|
|
363
|
+
| --- | --- | --- | --- |
|
|
364
|
+
| `onOpenChange` | `((open: boolean) => void)` | | Every open and close: the trigger, choosing an item, outside clicks, Escape. |
|
|
365
|
+
|
|
366
|
+
### MenuSubTrigger
|
|
367
|
+
|
|
368
|
+
The item a submenu opens from; a chevron after its label points the way.
|
|
369
|
+
|
|
370
|
+
| Prop | Type | Default | Description |
|
|
371
|
+
| --- | --- | --- | --- |
|
|
372
|
+
| `icon` | `ReactNode` | | Shown before the label; size it yourself (e.g. size-4). |
|
|
373
|
+
|
|
374
|
+
### MenuSubContent
|
|
375
|
+
|
|
376
|
+
The submenu's panel, beside its item (on the other side if there's no room), its first item level with it.
|
|
377
|
+
|
|
378
|
+
| Prop | Type | Default | Description |
|
|
379
|
+
| --- | --- | --- | --- |
|
|
380
|
+
| `offset` | `number` | `8` | Gap between the item and the submenu, in px (counting the menu's own padding). |
|
|
381
|
+
|
|
382
|
+
### MenuSeparator
|
|
383
|
+
|
|
384
|
+
A hairline between groups of items.
|
|
385
|
+
|
|
386
|
+
| Prop | Type | Default | Description |
|
|
387
|
+
| --- | --- | --- | --- |
|
|
388
|
+
| `className` | `string` | | |
|
|
389
|
+
|
|
390
|
+
### MenuHeader
|
|
391
|
+
|
|
392
|
+
Content at the top of a menu that isn't a choice, e.g. who is signed in.
|
|
393
|
+
|
|
394
|
+
No props of its own; it accepts the props of the element it renders.
|
|
395
|
+
|
|
396
|
+
### Menubar
|
|
397
|
+
|
|
398
|
+
A row of menus, as at the top of an app (File, Edit, View): put a Menu for
|
|
399
|
+
each inside, its MenuTrigger a label. One Tab stop: ←→ move between the
|
|
400
|
+
menus, ↓ or Enter opens one; while one is open, ←→ open the one beside it
|
|
401
|
+
and pointing at another switches to it. Name it with `aria-label`.
|
|
402
|
+
|
|
403
|
+
No props of its own; it accepts the props of the element it renders.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Meter
|
|
2
|
+
|
|
3
|
+
A measure against a limit, like spend against a budget: warm near it, red past it.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { Meter } from '@rinzai/zen';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### Budget
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { formatMoney, Meter } from '@rinzai/zen';
|
|
15
|
+
|
|
16
|
+
const inr = (paise: number) => formatMoney(paise, 'INR', 'en-IN', { showDecimals: false });
|
|
17
|
+
|
|
18
|
+
/** Spend against a budget: the bar warms near the limit and turns red past it. */
|
|
19
|
+
export default function Budget() {
|
|
20
|
+
const budgets = [
|
|
21
|
+
{ label: 'Groceries', spent: 620000, budget: 1000000 },
|
|
22
|
+
{ label: 'Dining out', spent: 270000, budget: 300000 },
|
|
23
|
+
{ label: 'Transport', spent: 410000, budget: 350000 },
|
|
24
|
+
];
|
|
25
|
+
return (
|
|
26
|
+
<div className="flex w-80 flex-col gap-5">
|
|
27
|
+
{budgets.map(({ label, spent, budget }) => {
|
|
28
|
+
const over = spent > budget;
|
|
29
|
+
const status = over ? `${inr(spent - budget)} over` : `${inr(budget - spent)} left`;
|
|
30
|
+
return (
|
|
31
|
+
<Meter
|
|
32
|
+
key={label}
|
|
33
|
+
label={label}
|
|
34
|
+
value={spent}
|
|
35
|
+
max={budget}
|
|
36
|
+
detail={`${inr(spent)} / ${inr(budget)}`}
|
|
37
|
+
hint={status}
|
|
38
|
+
valueText={`${inr(spent)} of ${inr(budget)}, ${status}`}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
})}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Milestones
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { Button, formatMoney, Meter } from '@rinzai/zen';
|
|
51
|
+
import { useState } from 'react';
|
|
52
|
+
|
|
53
|
+
const money = (cents: number) => formatMoney(cents, 'USD', 'en-US', { showDecimals: false });
|
|
54
|
+
|
|
55
|
+
/** A savings goal with milestones at each quarter: the ones passed are notched into the fill. */
|
|
56
|
+
export default function Milestones() {
|
|
57
|
+
const [saved, setSaved] = useState(420000);
|
|
58
|
+
const goal = 1000000;
|
|
59
|
+
return (
|
|
60
|
+
<div className="flex w-full max-w-sm flex-col gap-4">
|
|
61
|
+
<Meter
|
|
62
|
+
label="Emergency fund"
|
|
63
|
+
value={saved}
|
|
64
|
+
max={goal}
|
|
65
|
+
marks={[goal / 4, goal / 2, (goal * 3) / 4]}
|
|
66
|
+
tone="default"
|
|
67
|
+
detail={`${money(saved)} / ${money(goal)}`}
|
|
68
|
+
hint={`${Math.floor((saved / goal) * 4)} of 4 milestones`}
|
|
69
|
+
valueText={`${money(saved)} of ${money(goal)}`}
|
|
70
|
+
/>
|
|
71
|
+
<Button
|
|
72
|
+
variant="outline"
|
|
73
|
+
className="self-start"
|
|
74
|
+
onClick={() => setSaved((s) => Math.min(goal, s + 100000))}
|
|
75
|
+
>
|
|
76
|
+
Add $1,000
|
|
77
|
+
</Button>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## API reference
|
|
84
|
+
|
|
85
|
+
### Meter
|
|
86
|
+
|
|
87
|
+
A measure against a limit, e.g. spend against a budget: a bar with a label,
|
|
88
|
+
a figure and a note. Over the limit it turns red; near it, warm. Announced
|
|
89
|
+
as a meter, with `valueText` read out instead of the bare number.
|
|
90
|
+
|
|
91
|
+
| Prop | Type | Default | Description |
|
|
92
|
+
| --- | --- | --- | --- |
|
|
93
|
+
| `value` (required) | `number` | | |
|
|
94
|
+
| `className` | `string` | | |
|
|
95
|
+
| `detail` | `ReactNode` | | Top right, e.g. "₹8,200 / ₹10,000". |
|
|
96
|
+
| `hint` | `ReactNode` | | Below the bar, e.g. "₹1,800 left". |
|
|
97
|
+
| `label` | `ReactNode` | | |
|
|
98
|
+
| `marks` | `number[]` | | Milestones along the bar, in the same units as value: e.g. [2500, 5000, 7500] of 10000. |
|
|
99
|
+
| `max` | `number` | `100` | The limit (default 100). Values past it fill the bar and turn it red. |
|
|
100
|
+
| `tone` | `"auto" \| "default" \| "negative" \| "warning"` | `auto` | `auto` picks warning past `warnAt` of max and negative past max. |
|
|
101
|
+
| `valueText` | `string` | | What screen readers say for the value, e.g. "₹8,200 of ₹10,000, ₹1,800 left". |
|
|
102
|
+
| `warnAt` | `number` | `0.85` | Fraction of max where `auto` turns to warning (default 0.85). |
|