@rinzai/zen 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +16 -1
  3. package/dist/carousel/index.d.ts.map +1 -1
  4. package/dist/carousel/index.js +5 -5
  5. package/dist/docs/README.md +205 -0
  6. package/dist/docs/accordion.md +122 -0
  7. package/dist/docs/alert.md +69 -0
  8. package/dist/docs/animated-number.md +105 -0
  9. package/dist/docs/avatar.md +91 -0
  10. package/dist/docs/backdrop.md +94 -0
  11. package/dist/docs/badge.md +37 -0
  12. package/dist/docs/breadcrumbs.md +44 -0
  13. package/dist/docs/button-group.md +76 -0
  14. package/dist/docs/button.md +131 -0
  15. package/dist/docs/calendar-heatmap.md +73 -0
  16. package/dist/docs/calendar.md +101 -0
  17. package/dist/docs/canvas.md +584 -0
  18. package/dist/docs/card.md +180 -0
  19. package/dist/docs/carousel.md +162 -0
  20. package/dist/docs/chart.md +325 -0
  21. package/dist/docs/checkbox.md +88 -0
  22. package/dist/docs/chip.md +52 -0
  23. package/dist/docs/code-block.md +116 -0
  24. package/dist/docs/code-input.md +52 -0
  25. package/dist/docs/collapse.md +46 -0
  26. package/dist/docs/color-picker.md +145 -0
  27. package/dist/docs/combobox.md +208 -0
  28. package/dist/docs/command-palette.md +65 -0
  29. package/dist/docs/date-picker.md +167 -0
  30. package/dist/docs/dialog.md +271 -0
  31. package/dist/docs/disclosure.md +88 -0
  32. package/dist/docs/editable-cell.md +90 -0
  33. package/dist/docs/empty-state.md +46 -0
  34. package/dist/docs/field.md +82 -0
  35. package/dist/docs/file-drop.md +44 -0
  36. package/dist/docs/filter-bar.md +66 -0
  37. package/dist/docs/header.md +51 -0
  38. package/dist/docs/hover-card.md +110 -0
  39. package/dist/docs/icon-picker.md +65 -0
  40. package/dist/docs/input-group.md +89 -0
  41. package/dist/docs/input.md +33 -0
  42. package/dist/docs/inset.md +57 -0
  43. package/dist/docs/kbd.md +34 -0
  44. package/dist/docs/keypad.md +51 -0
  45. package/dist/docs/load-more.md +66 -0
  46. package/dist/docs/locale-provider.md +99 -0
  47. package/dist/docs/marquee.md +86 -0
  48. package/dist/docs/menu.md +403 -0
  49. package/dist/docs/meter.md +102 -0
  50. package/dist/docs/money-input.md +169 -0
  51. package/dist/docs/month-picker.md +80 -0
  52. package/dist/docs/navigation-menu.md +157 -0
  53. package/dist/docs/number-field.md +129 -0
  54. package/dist/docs/page-header.md +44 -0
  55. package/dist/docs/page-transition.md +93 -0
  56. package/dist/docs/pagination.md +42 -0
  57. package/dist/docs/pills.md +85 -0
  58. package/dist/docs/player-bar.md +158 -0
  59. package/dist/docs/popover.md +200 -0
  60. package/dist/docs/progress-ring.md +70 -0
  61. package/dist/docs/radio-cards.md +82 -0
  62. package/dist/docs/radio-group.md +86 -0
  63. package/dist/docs/range-slider.md +64 -0
  64. package/dist/docs/repeat-picker.md +44 -0
  65. package/dist/docs/resizable.md +107 -0
  66. package/dist/docs/seek-bar.md +68 -0
  67. package/dist/docs/segmented.md +89 -0
  68. package/dist/docs/select.md +148 -0
  69. package/dist/docs/separator.md +76 -0
  70. package/dist/docs/side-nav.md +75 -0
  71. package/dist/docs/skeleton.md +89 -0
  72. package/dist/docs/slider.md +87 -0
  73. package/dist/docs/sortable.md +182 -0
  74. package/dist/docs/sparkline.md +56 -0
  75. package/dist/docs/spinner.md +69 -0
  76. package/dist/docs/status-pill.md +81 -0
  77. package/dist/docs/stepper.md +59 -0
  78. package/dist/docs/tab-bar.md +78 -0
  79. package/dist/docs/table-of-contents.md +43 -0
  80. package/dist/docs/table.md +714 -0
  81. package/dist/docs/tabs.md +140 -0
  82. package/dist/docs/tag-input.md +45 -0
  83. package/dist/docs/textarea.md +25 -0
  84. package/dist/docs/theme-toggle.md +85 -0
  85. package/dist/docs/theming.md +193 -0
  86. package/dist/docs/time-field.md +71 -0
  87. package/dist/docs/timeline.md +50 -0
  88. package/dist/docs/toast.md +53 -0
  89. package/dist/docs/toggle-group.md +83 -0
  90. package/dist/docs/toggle.md +38 -0
  91. package/dist/docs/toolbar.md +97 -0
  92. package/dist/docs/tooltip.md +60 -0
  93. package/dist/docs/tree.md +295 -0
  94. package/dist/docs/trend.md +50 -0
  95. package/dist/icons/arrow-path.d.ts +3 -0
  96. package/dist/icons/arrow-path.d.ts.map +1 -0
  97. package/dist/icons/arrows-right-left.d.ts +3 -0
  98. package/dist/icons/arrows-right-left.d.ts.map +1 -0
  99. package/dist/icons/backward.d.ts +3 -0
  100. package/dist/icons/backward.d.ts.map +1 -0
  101. package/dist/icons/forward.d.ts +3 -0
  102. package/dist/icons/forward.d.ts.map +1 -0
  103. package/dist/icons/heart-solid.d.ts +3 -0
  104. package/dist/icons/heart-solid.d.ts.map +1 -0
  105. package/dist/icons/heart.d.ts +3 -0
  106. package/dist/icons/heart.d.ts.map +1 -0
  107. package/dist/icons/musical-note.d.ts +3 -0
  108. package/dist/icons/musical-note.d.ts.map +1 -0
  109. package/dist/icons/pause.d.ts +3 -0
  110. package/dist/icons/pause.d.ts.map +1 -0
  111. package/dist/icons/play.d.ts +3 -0
  112. package/dist/icons/play.d.ts.map +1 -0
  113. package/dist/icons/queue-list.d.ts +3 -0
  114. package/dist/icons/queue-list.d.ts.map +1 -0
  115. package/dist/icons/speaker-wave.d.ts +3 -0
  116. package/dist/icons/speaker-wave.d.ts.map +1 -0
  117. package/dist/icons/speaker-x-mark.d.ts +3 -0
  118. package/dist/icons/speaker-x-mark.d.ts.map +1 -0
  119. package/dist/index.d.ts +8 -2
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/lib.js +80 -74
  122. package/dist/marquee/index.d.ts +15 -0
  123. package/dist/marquee/index.d.ts.map +1 -0
  124. package/dist/marquee/index.js +90 -0
  125. package/dist/menu/index.d.ts.map +1 -1
  126. package/dist/menu/index.js +7 -6
  127. package/dist/number-field/index.d.ts +18 -2
  128. package/dist/number-field/index.d.ts.map +1 -1
  129. package/dist/number-field/index.js +65 -51
  130. package/dist/player-bar/index.d.ts +11 -0
  131. package/dist/player-bar/index.d.ts.map +1 -0
  132. package/dist/player-bar/index.js +17 -0
  133. package/dist/radio-cards/index.d.ts +2 -1
  134. package/dist/radio-cards/index.d.ts.map +1 -1
  135. package/dist/radio-cards/index.js +3 -3
  136. package/dist/seek-bar/index.d.ts +24 -0
  137. package/dist/seek-bar/index.d.ts.map +1 -0
  138. package/dist/seek-bar/index.js +120 -0
  139. package/dist/segmented/index.d.ts +3 -2
  140. package/dist/segmented/index.d.ts.map +1 -1
  141. package/dist/segmented/index.js +19 -18
  142. package/dist/slider/index.d.ts +2 -1
  143. package/dist/slider/index.d.ts.map +1 -1
  144. package/dist/tab-bar/index.d.ts +4 -2
  145. package/dist/tab-bar/index.d.ts.map +1 -1
  146. package/dist/tab-bar/index.js +24 -19
  147. package/dist/textarea/index.d.ts +6 -2
  148. package/dist/textarea/index.d.ts.map +1 -1
  149. package/dist/textarea/index.js +2 -1
  150. package/dist/theme.css +40 -0
  151. package/dist/utils/duration.d.ts +3 -0
  152. package/dist/utils/duration.d.ts.map +1 -0
  153. package/dist/utils/duration.js +7 -0
  154. package/dist/utils/messages.d.ts +2 -0
  155. package/dist/utils/messages.d.ts.map +1 -1
  156. package/dist/utils/messages.js +3 -1
  157. package/dist/utils/slot.d.ts +5 -2
  158. package/dist/utils/slot.d.ts.map +1 -1
  159. package/dist/utils/slot.js +16 -11
  160. package/dist/utils/useAnchoredPopup.d.ts.map +1 -1
  161. package/dist/utils/useAnchoredPopup.js +3 -2
  162. package/dist/utils/useHeightVar.d.ts +7 -0
  163. package/dist/utils/useHeightVar.d.ts.map +1 -0
  164. package/dist/utils/useHeightVar.js +17 -0
  165. package/dist/utils/useMediaPlayer.d.ts +38 -0
  166. package/dist/utils/useMediaPlayer.d.ts.map +1 -0
  167. package/dist/utils/useMediaPlayer.js +91 -0
  168. package/dist/utils/useMediaSession.d.ts +24 -0
  169. package/dist/utils/useMediaSession.d.ts.map +1 -0
  170. package/dist/utils/useMediaSession.js +76 -0
  171. package/dist/zen.css +1 -1
  172. package/package.json +4 -2
@@ -0,0 +1,42 @@
1
+ # Pagination
2
+
3
+ The pages a long list is split into, as buttons or links for your router.
4
+
5
+ ```tsx
6
+ import { Pagination } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Pagination } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ /** Pages as buttons. With a router, give `href` and render each with its Link through `renderLink`. */
18
+ export default function Default() {
19
+ const [page, setPage] = useState(6);
20
+ return <Pagination page={page} count={12} onPageChange={setPage} />;
21
+ }
22
+ ```
23
+
24
+ ## API reference
25
+
26
+ ### Pagination
27
+
28
+ The pages a long list is split into: previous and next, the first and last
29
+ pages, and those around the current one (`page`, from 1). Pages are buttons
30
+ calling `onPageChange`, or with `href`, links (render them with your router's
31
+ Link through `renderLink`).
32
+
33
+ | Prop | Type | Default | Description |
34
+ | --- | --- | --- | --- |
35
+ | `count` (required) | `number` | | How many pages there are. With fewer than 2, nothing is shown. |
36
+ | `page` (required) | `number` | | The current page, from 1. |
37
+ | `aria-label` | `string` | | |
38
+ | `className` | `string` | | |
39
+ | `href` | `((page: number) => string)` | | Pages as links: the URL of each. |
40
+ | `onPageChange` | `((page: number) => void)` | | |
41
+ | `renderLink` | `((props: DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, page: number) => ReactNode)` | `(props) => <a {...props} />` | Renders a page link, e.g. with your router's Link. |
42
+ | `siblings` | `number` | `1` | Pages shown either side of the current one. |
@@ -0,0 +1,85 @@
1
+ # Pills
2
+
3
+ A glass track with a glowing pill that slides to the current page. In a nav, it is site navigation; works with any router.
4
+
5
+ ```tsx
6
+ import { Pills, Pill, PillIndicator } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Navigation
12
+
13
+ ```tsx
14
+ import { Pill, PillIndicator, Pills } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ const PAGES = ['Month', 'Planner', 'Loans', 'Goals', 'Trends'];
18
+
19
+ /**
20
+ * Site navigation: Pills in a <nav>. With a router, pass its link as the child:
21
+ * <Pill asChild><NavLink to="/goals">Goals</NavLink></Pill>.
22
+ */
23
+ export default function Navigation() {
24
+ const [current, setCurrent] = useState('Month');
25
+ return (
26
+ <nav aria-label="Main">
27
+ <Pills>
28
+ <PillIndicator />
29
+ {PAGES.map((page) => (
30
+ <Pill
31
+ key={page}
32
+ href={`#${page.toLowerCase()}`}
33
+ active={page === current}
34
+ onClick={(e) => {
35
+ e.preventDefault();
36
+ setCurrent(page);
37
+ }}
38
+ >
39
+ {page}
40
+ </Pill>
41
+ ))}
42
+ </Pills>
43
+ </nav>
44
+ );
45
+ }
46
+ ```
47
+
48
+ ## API reference
49
+
50
+ ### Pills
51
+
52
+ A glass track of pills, with an optional glowing pill that slides to the
53
+ active one: whichever has aria-current="page" (router links such as React
54
+ Router's NavLink set it themselves) or aria-selected="true". It adds no role
55
+ of its own; what you wrap it in says what it is. Site navigation:
56
+
57
+ <nav aria-label="Main">
58
+ <Pills>
59
+ <PillIndicator />
60
+ <Pill href="/month" active>Month</Pill>
61
+ <Pill asChild><NavLink to="/goals">Goals</NavLink></Pill>
62
+ </Pills>
63
+ </nav>
64
+
65
+ For tabs that switch panels in place, use Tabs with `<TabList variant="pills">`.
66
+
67
+ No props of its own; it accepts the props of the element it renders.
68
+
69
+ ### Pill
70
+
71
+ One pill: a link. `active` marks the current page. With `asChild`, the
72
+ styles go onto your own element instead (a router's Link or NavLink).
73
+
74
+ | Prop | Type | Default | Description |
75
+ | --- | --- | --- | --- |
76
+ | `active` | `boolean` | | This pill is the current page (aria-current="page"). |
77
+ | `asChild` | `boolean` | | Style the single child element (e.g. a router link) instead of rendering an <a>. |
78
+
79
+ ### PillIndicator
80
+
81
+ The glowing pill under the active item. Place it inside Pills; hidden while none is active.
82
+
83
+ | Prop | Type | Default | Description |
84
+ | --- | --- | --- | --- |
85
+ | `className` | `string` | | |
@@ -0,0 +1,158 @@
1
+ # Player Bar
2
+
3
+ What's playing, along the bottom of the screen, sitting on a TabBar on phones. useMediaPlayer drives your own <audio> or <video>; useMediaSession puts it on the lock screen and the media keys.
4
+
5
+ ```tsx
6
+ import { PlayerBar, useMediaPlayer, useMediaSession } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Button, PlayerBar, SeekBar, Slider, useMediaPlayer, useMediaSession } from '@rinzai/zen';
15
+
16
+ import { useDemoTrack } from './demo-track';
17
+ // Icons from Heroicons (https://heroicons.com): Backward (backward), Pause (pause), Play (play), SpeakerWave (speaker-wave), SpeakerXMark (speaker-x-mark).
18
+
19
+ /**
20
+ * Your own <audio>, driven by useMediaPlayer: the cover and title, the controls
21
+ * over a SeekBar, and the volume. useMediaSession puts it on the lock screen
22
+ * and the media keys. Fixed to the bottom of the screen; here it sits in a frame.
23
+ */
24
+ export default function Default() {
25
+ const src = useDemoTrack();
26
+ const [audioRef, player] = useMediaPlayer();
27
+ useMediaSession(player, { title: 'Glass Aurora', artist: 'Zen' });
28
+ return (
29
+ <div className="border-tint/10 relative h-48 w-full [transform:translateZ(0)] overflow-hidden rounded-2xl border">
30
+ <audio ref={audioRef} src={src} preload="metadata" />
31
+ <PlayerBar aria-label="Now playing" className="absolute! justify-between rounded-b-[15px]">
32
+ <div className="flex min-w-0 flex-1 items-center gap-3">
33
+ <div className="from-primary size-11 shrink-0 rounded-lg bg-linear-135 to-fuchsia-400" />
34
+ <div className="min-w-0">
35
+ <p className="truncate text-sm font-medium">Glass Aurora</p>
36
+ <p className="text-muted-foreground truncate text-xs">Zen</p>
37
+ </div>
38
+ </div>
39
+ <div className="flex flex-[2] flex-col items-center gap-1">
40
+ <div className="flex items-center gap-2">
41
+ <Button variant="ghost" size="icon-sm" aria-label="Restart" onClick={() => player.seek(0)}>
42
+ <Backward className="size-4" />
43
+ </Button>
44
+ <Button
45
+ size="icon"
46
+ className="rounded-full"
47
+ aria-label={player.playing ? 'Pause' : 'Play'}
48
+ disabled={!src}
49
+ onClick={() => player.toggle()}
50
+ >
51
+ {player.playing ? <Pause className="size-5" /> : <Play className="size-5" />}
52
+ </Button>
53
+ </div>
54
+ <SeekBar
55
+ className="w-full"
56
+ currentTime={player.currentTime}
57
+ playing={player.playing && !player.waiting}
58
+ playbackRate={player.playbackRate}
59
+ duration={player.duration}
60
+ buffered={player.buffered}
61
+ onSeek={player.seek}
62
+ />
63
+ </div>
64
+ <div className="hidden flex-1 items-center justify-end gap-2 sm:flex">
65
+ <Button
66
+ variant="ghost"
67
+ size="icon-sm"
68
+ aria-label={player.muted ? 'Unmute' : 'Mute'}
69
+ onClick={() => player.setMuted(!player.muted)}
70
+ >
71
+ {player.muted ? <SpeakerXMark className="size-4" /> : <SpeakerWave className="size-4" />}
72
+ </Button>
73
+ <Slider
74
+ aria-label="Volume"
75
+ className="w-24"
76
+ value={player.muted ? 0 : Math.round(player.volume * 100)}
77
+ onValueChange={(v) => {
78
+ player.setVolume(v / 100);
79
+ if (v > 0 && player.muted) player.setMuted(false);
80
+ }}
81
+ />
82
+ </div>
83
+ </PlayerBar>
84
+ </div>
85
+ );
86
+ }
87
+ ```
88
+
89
+ ### On tab bar
90
+
91
+ ```tsx
92
+ import { Button, PlayerBar, TabBar, TabBarItem, useMediaPlayer } from '@rinzai/zen';
93
+
94
+ import { useDemoTrack } from './demo-track';
95
+ // Icons from Heroicons (https://heroicons.com): Pause (pause), Play (play), Search (magnifying-glass), Squares (squares-2x2), ListBullet (list-bullet).
96
+
97
+ /**
98
+ * On a phone, it sits on the TabBar beside it, and the page pads its end by
99
+ * both bars' heights (--zen-tab-bar-height and --zen-player-bar-height).
100
+ */
101
+ export default function OnTabBar() {
102
+ const src = useDemoTrack();
103
+ const [audioRef, player] = useMediaPlayer();
104
+ const progress = player.duration ? (player.currentTime / player.duration) * 100 : 0;
105
+ return (
106
+ <div className="border-tint/10 relative h-96 w-80 [transform:translateZ(0)] overflow-hidden rounded-2xl border">
107
+ <audio ref={audioRef} src={src} preload="metadata" />
108
+ <div className="h-full overflow-y-auto p-4 pb-[calc(var(--zen-tab-bar-height,0px)+var(--zen-player-bar-height,0px))]">
109
+ <p className="text-muted-foreground text-sm">Home</p>
110
+ </div>
111
+ <PlayerBar aria-label="Now playing" className="absolute! gap-3 px-3 pt-2 pb-2">
112
+ <div className="from-primary size-9 shrink-0 rounded-md bg-linear-135 to-fuchsia-400" />
113
+ <div className="min-w-0 flex-1">
114
+ <p className="truncate text-sm font-medium">Glass Aurora</p>
115
+ <p className="text-muted-foreground truncate text-xs">Zen</p>
116
+ </div>
117
+ <Button
118
+ variant="ghost"
119
+ size="icon-sm"
120
+ aria-label={player.playing ? 'Pause' : 'Play'}
121
+ disabled={!src}
122
+ onClick={() => player.toggle()}
123
+ >
124
+ {player.playing ? <Pause className="size-5" /> : <Play className="size-5" />}
125
+ </Button>
126
+ <span
127
+ aria-hidden
128
+ className="bg-primary absolute start-0 bottom-0 h-0.5 shadow-[0_0_8px_oklch(var(--glow)/calc(0.8*var(--glow-k)))]"
129
+ style={{ width: `${progress}%` }}
130
+ />
131
+ </PlayerBar>
132
+ <TabBar aria-label="Main" hideFrom={false} className="absolute! rounded-b-[15px]">
133
+ <TabBarItem href="#home" icon={<Squares />} active onClick={(e) => e.preventDefault()}>
134
+ Home
135
+ </TabBarItem>
136
+ <TabBarItem href="#search" icon={<Search />} onClick={(e) => e.preventDefault()}>
137
+ Search
138
+ </TabBarItem>
139
+ <TabBarItem href="#library" icon={<ListBullet />} onClick={(e) => e.preventDefault()}>
140
+ Library
141
+ </TabBarItem>
142
+ </TabBar>
143
+ </div>
144
+ );
145
+ }
146
+ ```
147
+
148
+ ## API reference
149
+
150
+ ### PlayerBar
151
+
152
+ What's playing, along the bottom of the screen: a glass bar for the cover,
153
+ the title, the controls and a SeekBar, laid out as you like. It sits on a
154
+ TabBar beside it (in the same parent), and clears the home indicator where
155
+ there's none. Its height is kept in `--zen-player-bar-height` on that
156
+ parent, for the page to pad its end by. Name it with an aria-label ("Now playing"). Drive it with useMediaPlayer.
157
+
158
+ No props of its own; it accepts the props of the element it renders.
@@ -0,0 +1,200 @@
1
+ # Popover
2
+
3
+ A glass panel that opens from a button, on the native Popover API: small forms, cards and settings. For hints use Tooltip; for actions, Menu.
4
+
5
+ ```tsx
6
+ import { Popover, PopoverTrigger, PopoverContent, PopoverClose } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Profile card
12
+
13
+ ```tsx
14
+ import { Badge, Button, Popover, PopoverContent, PopoverTrigger } from '@rinzai/zen';
15
+
16
+ /** Your own trigger: here an avatar button. */
17
+ export default function ProfileCard() {
18
+ return (
19
+ <Popover>
20
+ <PopoverTrigger
21
+ aria-label="Alex Johnson"
22
+ className="bg-primary text-primary-foreground flex size-9 items-center justify-center rounded-full text-sm font-bold"
23
+ >
24
+ AJ
25
+ </PopoverTrigger>
26
+ <PopoverContent aria-label="Alex Johnson">
27
+ <div className="flex w-64 flex-col gap-3 p-4">
28
+ <div className="flex items-center gap-3">
29
+ <div className="bg-primary text-primary-foreground flex size-10 items-center justify-center rounded-full text-sm font-bold">
30
+ AJ
31
+ </div>
32
+ <div className="flex flex-col">
33
+ <span className="text-sm font-semibold">Alex Johnson</span>
34
+ <span className="text-muted-foreground text-xs">alex@example.com</span>
35
+ </div>
36
+ </div>
37
+ <div className="flex gap-2">
38
+ <Badge>Admin</Badge>
39
+ <Badge variant="outline">Pro</Badge>
40
+ </div>
41
+ <div className="border-tint/10 flex flex-col gap-1 border-t pt-3">
42
+ <Button variant="ghost" size="sm" className="justify-start">
43
+ View profile
44
+ </Button>
45
+ <Button variant="ghost" size="sm" className="justify-start">
46
+ Settings
47
+ </Button>
48
+ <Button variant="ghost" size="sm" className="text-destructive justify-start">
49
+ Sign out
50
+ </Button>
51
+ </div>
52
+ </div>
53
+ </PopoverContent>
54
+ </Popover>
55
+ );
56
+ }
57
+ ```
58
+
59
+ ### Feedback form
60
+
61
+ ```tsx
62
+ import { Button, Input, Popover, PopoverClose, PopoverContent, PopoverTrigger, Textarea } from '@rinzai/zen';
63
+
64
+ export default function FeedbackForm() {
65
+ return (
66
+ <Popover>
67
+ <PopoverTrigger asChild>
68
+ <Button variant="outline">Give feedback</Button>
69
+ </PopoverTrigger>
70
+ <PopoverContent aria-labelledby="feedback-title">
71
+ <div className="flex w-72 flex-col gap-3 p-4">
72
+ <p id="feedback-title" className="text-sm font-semibold">
73
+ Send feedback
74
+ </p>
75
+ <Input placeholder="Subject" />
76
+ <Textarea placeholder="Describe your feedback…" className="resize-none" />
77
+ <div className="flex justify-end gap-2">
78
+ <PopoverClose variant="outline" size="sm">
79
+ Cancel
80
+ </PopoverClose>
81
+ <Button size="sm">Submit</Button>
82
+ </div>
83
+ </div>
84
+ </PopoverContent>
85
+ </Popover>
86
+ );
87
+ }
88
+ ```
89
+
90
+ ### Settings
91
+
92
+ ```tsx
93
+ import { Button, Popover, PopoverContent, PopoverTrigger, Toggle } from '@rinzai/zen';
94
+ import { useState } from 'react';
95
+
96
+ export default function Settings() {
97
+ const [settings, setSettings] = useState({ notifications: true, sounds: false, autoSave: true });
98
+ const rows = [
99
+ { key: 'notifications', label: 'Notifications' },
100
+ { key: 'sounds', label: 'Sounds' },
101
+ { key: 'autoSave', label: 'Auto-save' },
102
+ ] as const;
103
+ return (
104
+ <Popover>
105
+ <PopoverTrigger asChild>
106
+ <Button variant="outline">Preferences</Button>
107
+ </PopoverTrigger>
108
+ <PopoverContent aria-label="Preferences">
109
+ <div className="flex w-60 flex-col gap-3 p-4">
110
+ <p className="text-sm font-semibold">Preferences</p>
111
+ {rows.map(({ key, label }) => (
112
+ <label key={key} className="flex items-center justify-between font-normal">
113
+ {label}
114
+ <Toggle
115
+ checked={settings[key]}
116
+ onCheckedChange={(on) => setSettings((s) => ({ ...s, [key]: on }))}
117
+ />
118
+ </label>
119
+ ))}
120
+ </div>
121
+ </PopoverContent>
122
+ </Popover>
123
+ );
124
+ }
125
+ ```
126
+
127
+ ### Controlled
128
+
129
+ ```tsx
130
+ import { Button, Popover, PopoverClose, PopoverContent, PopoverTrigger } from '@rinzai/zen';
131
+ import { useState } from 'react';
132
+
133
+ /** Follow your own state with `open`; `onOpenChange` hears every open and close (outside clicks, Escape…). */
134
+ export default function Controlled() {
135
+ const [open, setOpen] = useState(false);
136
+ return (
137
+ <Popover open={open} onOpenChange={setOpen}>
138
+ <PopoverTrigger asChild>
139
+ <Button>{open ? 'Hide' : 'Show'} popover</Button>
140
+ </PopoverTrigger>
141
+ <PopoverContent aria-label="Controlled">
142
+ <div className="flex flex-col gap-2 p-3 text-sm">
143
+ <p className="font-medium">Manually controlled</p>
144
+ <PopoverClose size="sm" variant="secondary">
145
+ Close
146
+ </PopoverClose>
147
+ </div>
148
+ </PopoverContent>
149
+ </Popover>
150
+ );
151
+ }
152
+ ```
153
+
154
+ ## API reference
155
+
156
+ ### Popover
157
+
158
+ A panel that opens from a button: a small form, a profile card, settings. A
159
+ native popover, so outside clicks and Escape close it and focus returns to
160
+ the button. Put a PopoverTrigger and a PopoverContent inside. Opens and closes
161
+ on its own, or follows `open` (with `onOpenChange` to hear the user close it).
162
+ For a hint on hover or focus, use Tooltip; for a list of actions, Menu.
163
+
164
+ | Prop | Type | Default | Description |
165
+ | --- | --- | --- | --- |
166
+ | `defaultOpen` | `boolean` | `false` | Open when it first appears. |
167
+ | `onOpenChange` | `((open: boolean) => void)` | | Every open and close: the trigger, outside clicks, Escape, PopoverClose. |
168
+ | `open` | `boolean` | | Open or close it from your state; leave it out to let the trigger do it. |
169
+
170
+ ### PopoverTrigger
171
+
172
+ The button that opens and closes the popover. With `asChild`, your own button (e.g. a Button) instead.
173
+
174
+ | Prop | Type | Default | Description |
175
+ | --- | --- | --- | --- |
176
+ | `asChild` | `boolean` | | Put the trigger's props on your own button element instead of rendering one. |
177
+
178
+ ### PopoverContent
179
+
180
+ The panel: glass, below the trigger (above it if there's no room), centred on
181
+ it by default. A dialog to assistive tech; give it `aria-label` or a heading
182
+ it's labelled by when it holds a form.
183
+
184
+ | Prop | Type | Default | Description |
185
+ | --- | --- | --- | --- |
186
+ | `align` | `"center" \| "end" \| "start"` | `center` | Centred on the trigger (default), or lined up with its start or end edge. |
187
+ | `offset` | `number` | `5` | Gap from the trigger, in px. |
188
+
189
+ ### PopoverClose
190
+
191
+ A button that closes its popover (after its own onClick, unless that calls
192
+ preventDefault). Takes every Button prop: variant, size, asChild…
193
+
194
+ | Prop | Type | Default | Description |
195
+ | --- | --- | --- | --- |
196
+ | `asChild` | `boolean` | | Put the button's styles on the single child element (e.g. a link) instead of a <button>. |
197
+ | `loading` | `boolean` | | Shows a spinner and disables the button while an action runs. |
198
+ | `size` | `"default" \| "icon-lg" \| "icon-sm" \| "icon-xs" \| "icon" \| "lg" \| "sm" \| null` | | |
199
+ | `tone` | `"default" \| "inherit" \| "muted" \| "negative" \| null` | | |
200
+ | `variant` | `"default" \| "ghost" \| "link" \| "outline" \| "secondary" \| null` | | |
@@ -0,0 +1,70 @@
1
+ # Progress Ring
2
+
3
+ Circular progress with room for a label in the middle.
4
+
5
+ ```tsx
6
+ import { ProgressRing } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { ProgressRing } from '@rinzai/zen';
15
+
16
+ export default function Default() {
17
+ return (
18
+ <div className="flex items-center gap-6">
19
+ {[0.28, 0.64, 1].map((value) => (
20
+ <ProgressRing key={value} value={value} label={`${Math.round(value * 100)}% saved`}>
21
+ <span className="text-sm font-semibold tabular-nums">{Math.round(value * 100)}%</span>
22
+ </ProgressRing>
23
+ ))}
24
+ </div>
25
+ );
26
+ }
27
+ ```
28
+
29
+ ### Goal
30
+
31
+ ```tsx
32
+ import { Button, ProgressRing } from '@rinzai/zen';
33
+ import { useState } from 'react';
34
+
35
+ /** Reaching the goal sends out a ring of glow. */
36
+ export default function Goal() {
37
+ const [saved, setSaved] = useState(120);
38
+ const goal = 200;
39
+ return (
40
+ <div className="flex items-center gap-6">
41
+ <ProgressRing value={saved / goal} label="Holiday fund">
42
+ <span className="text-sm font-semibold tabular-nums">${saved}</span>
43
+ </ProgressRing>
44
+ <div className="flex flex-col gap-2">
45
+ <Button variant="outline" onClick={() => setSaved((s) => Math.min(s + 40, goal))}>
46
+ Save $40
47
+ </Button>
48
+ <Button variant="ghost" onClick={() => setSaved(120)}>
49
+ Reset
50
+ </Button>
51
+ </div>
52
+ </div>
53
+ );
54
+ }
55
+ ```
56
+
57
+ ## API reference
58
+
59
+ ### ProgressRing
60
+
61
+ Circular progress, 0–1, with optional content in the middle. Reaching 1
62
+ while shown sends out a ring of glow, once.
63
+
64
+ | Prop | Type | Default | Description |
65
+ | --- | --- | --- | --- |
66
+ | `label` (required) | `string` | | Accessible name, e.g. "Savings goal". |
67
+ | `value` (required) | `number` | | Progress from 0 to 1; values outside are clamped. |
68
+ | `className` | `string` | | |
69
+ | `size` | `number` | `84` | |
70
+ | `stroke` | `number` | `7` | |
@@ -0,0 +1,82 @@
1
+ # Radio Cards
2
+
3
+ Choose one of a few cards that each show their option, like a colour palette.
4
+
5
+ ```tsx
6
+ import { RadioCards, RadioCard } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { RadioCard, RadioCards } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ const PALETTES = [
18
+ { id: 'violet', label: 'Violet', hue: 295 },
19
+ { id: 'ocean', label: 'Ocean', hue: 250 },
20
+ { id: 'teal', label: 'Teal', hue: 195 },
21
+ { id: 'amber', label: 'Amber', hue: 75 },
22
+ ];
23
+
24
+ /** Each card shows what it picks; the whole card is the click target, and arrow keys move the choice. */
25
+ export default function Default() {
26
+ const [palette, setPalette] = useState('violet');
27
+ return (
28
+ <RadioCards
29
+ label="Palette"
30
+ value={palette}
31
+ onValueChange={setPalette}
32
+ className="grid grid-cols-2 gap-2 sm:grid-cols-4"
33
+ >
34
+ {PALETTES.map((p) => (
35
+ <RadioCard key={p.id} value={p.id}>
36
+ <span
37
+ aria-hidden
38
+ className="size-7 shrink-0 rounded-full"
39
+ style={{
40
+ background: `oklch(0.72 0.13 ${p.hue})`,
41
+ boxShadow: `0 0 14px -2px oklch(0.7 0.13 ${p.hue})`,
42
+ }}
43
+ />
44
+ <span className="font-medium">{p.label}</span>
45
+ </RadioCard>
46
+ ))}
47
+ </RadioCards>
48
+ );
49
+ }
50
+ ```
51
+
52
+ ## API reference
53
+
54
+ ### RadioCards
55
+
56
+ A choice of one among a few cards, each showing what it picks (a palette, a
57
+ plan). Native radio inputs underneath, so arrow keys and forms work as for any
58
+ radio group. Lay the cards out with className (e.g. a grid); fill it with
59
+ RadioCard. For short text options, use Segmented.
60
+
61
+ | Prop | Type | Default | Description |
62
+ | --- | --- | --- | --- |
63
+ | `children` (required) | `ReactNode` | | The RadioCards. |
64
+ | `className` | `string` | | Classes for the cards' container: its layout (e.g. a grid). |
65
+ | `defaultValue` | `string` | | The first chosen card, when the group keeps its own. |
66
+ | `label` | `string` | | Shown above the cards, naming the group. Without it, give the group an aria-label or aria-labelledby. |
67
+ | `name` | `string` | | Form field name; defaults to a generated one. |
68
+ | `onValueChange` | `((value: V) => void)` | | |
69
+ | `value` | `string` | | The chosen card, when you keep it. |
70
+
71
+ ### RadioCard
72
+
73
+ One card: a native radio covering whatever you put inside, so the whole card
74
+ is the click target. Outlined, lit when chosen; style it further with
75
+ className and style (e.g. a background showing the choice).
76
+
77
+ | Prop | Type | Default | Description |
78
+ | --- | --- | --- | --- |
79
+ | `value` (required) | `string` | | |
80
+ | `className` | `string` | | |
81
+ | `disabled` | `boolean` | | Can't be chosen (and the arrow keys skip it). |
82
+ | `style` | `CSSProperties` | | |