@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,86 @@
1
+ # Radio Group
2
+
3
+ Choose one of a list, each option with a label and a description; native radios, so arrow keys work.
4
+
5
+ ```tsx
6
+ import { RadioGroup, Radio } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Radio, RadioGroup } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ /** Arrow keys move between options and pick them, as for any radio group. */
18
+ export default function Default() {
19
+ const [repay, setRepay] = useState<'emi' | 'tenure'>('emi');
20
+ return (
21
+ <RadioGroup label="After a prepayment" value={repay} onValueChange={setRepay}>
22
+ <Radio value="emi" description="Keep the end date; pay less each month.">
23
+ Lower the EMI
24
+ </Radio>
25
+ <Radio value="tenure" description="Keep the EMI; finish sooner and save more interest.">
26
+ Shorten the tenure
27
+ </Radio>
28
+ </RadioGroup>
29
+ );
30
+ }
31
+ ```
32
+
33
+ ### Horizontal
34
+
35
+ ```tsx
36
+ import { Radio, RadioGroup } from '@rinzai/zen';
37
+ import { useState } from 'react';
38
+
39
+ /** In a row, with one option disabled. */
40
+ export default function Horizontal() {
41
+ const [every, setEvery] = useState<string | null>(null);
42
+ return (
43
+ <RadioGroup label="Repeat" value={every} onValueChange={setEvery} orientation="horizontal">
44
+ <Radio value="week">Weekly</Radio>
45
+ <Radio value="month">Monthly</Radio>
46
+ <Radio value="year">Yearly</Radio>
47
+ <Radio value="custom" disabled>
48
+ Custom
49
+ </Radio>
50
+ </RadioGroup>
51
+ );
52
+ }
53
+ ```
54
+
55
+ ## API reference
56
+
57
+ ### RadioGroup
58
+
59
+ Choose one of a list: native radios underneath, so arrow keys move and pick
60
+ within the group, and forms work as usual. Fill it with Radio. For a few
61
+ short options side by side use Segmented; for options that show themselves,
62
+ RadioCards.
63
+
64
+ | Prop | Type | Default | Description |
65
+ | --- | --- | --- | --- |
66
+ | `children` (required) | `ReactNode` | | The Radios. |
67
+ | `className` | `string` | | Classes for the options' container. |
68
+ | `defaultValue` | `string \| null` | `null` | The first chosen option, when the group keeps its own. |
69
+ | `disabled` | `boolean` | `false` | |
70
+ | `label` | `ReactNode` | | Shown above the options, naming the group. Without it, give the group an aria-label. |
71
+ | `name` | `string` | | Form field name; defaults to a generated one. |
72
+ | `onValueChange` | `((value: V) => void)` | | |
73
+ | `orientation` | `"horizontal" \| "vertical"` | `vertical` | Options in a column (default) or wrapping in a row. |
74
+ | `value` | `string \| null` | | The chosen option when you keep it, or null for none yet. |
75
+
76
+ ### Radio
77
+
78
+ One option: a round radio that fills with a glowing dot when chosen, its label, and an optional description.
79
+
80
+ | Prop | Type | Default | Description |
81
+ | --- | --- | --- | --- |
82
+ | `children` (required) | `ReactNode` | | The label. |
83
+ | `value` (required) | `string` | | |
84
+ | `className` | `string` | | |
85
+ | `description` | `ReactNode` | | A muted line under the label. |
86
+ | `disabled` | `boolean` | | Can't be chosen (and the arrow keys skip it). |
@@ -0,0 +1,64 @@
1
+ # Range Slider
2
+
3
+ Two thumbs on one track, for a span like an amount from $50 to $500.
4
+
5
+ ```tsx
6
+ import { RangeSlider } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { RangeSlider } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ /** Entries between two amounts. */
18
+ export default function Default() {
19
+ const [span, setSpan] = useState<[number, number]>([50, 500]);
20
+ return (
21
+ <div className="flex w-full max-w-sm flex-col gap-3">
22
+ <div className="flex justify-between text-sm">
23
+ <span className="font-medium">Amount</span>
24
+ <span className="text-muted-foreground tabular-nums">
25
+ ${span[0]} – ${span[1]}
26
+ </span>
27
+ </div>
28
+ <RangeSlider
29
+ label="Amount"
30
+ min={0}
31
+ max={1000}
32
+ step={10}
33
+ value={span}
34
+ onValueChange={setSpan}
35
+ valueText={(v) => `$${v}`}
36
+ />
37
+ </div>
38
+ );
39
+ }
40
+ ```
41
+
42
+ ## API reference
43
+
44
+ ### RangeSlider
45
+
46
+ Two thumbs on one track, for a span such as an amount from $50 to $500:
47
+ two native range inputs (keyboard, forms and screen readers work as on
48
+ Slider), the span between them filled. The thumbs can meet but not cross.
49
+
50
+ | Prop | Type | Default | Description |
51
+ | --- | --- | --- | --- |
52
+ | `label` (required) | `string` | | Accessible name of the pair, e.g. "Amount". |
53
+ | `className` | `string` | | |
54
+ | `defaultValue` | `readonly [number, number]` | | The first span, when the slider keeps its own (default the whole range). |
55
+ | `disabled` | `boolean` | | |
56
+ | `max` | `number` | `100` | |
57
+ | `min` | `number` | `0` | |
58
+ | `name` | `string` | | Form field name: low and high are submitted under it, in that order. |
59
+ | `onValueChange` | `((value: [number, number]) => void)` | | While dragging (every step). |
60
+ | `onValueCommit` | `((value: [number, number]) => void)` | | Once the user lets go: the moment to save or filter. |
61
+ | `step` | `number` | `1` | |
62
+ | `thumbLabels` | `readonly [string, string]` | | Each thumb's name (default "Minimum", "Maximum"). |
63
+ | `value` | `readonly [number, number]` | | The span, low then high, when you keep it. |
64
+ | `valueText` | `((value: number) => string)` | | Spoken value of a thumb, e.g. (v) => `$${v}`. |
@@ -0,0 +1,44 @@
1
+ # Repeat Picker
2
+
3
+ How often something recurs, like a bill, said in words, with the next dates it falls on.
4
+
5
+ ```tsx
6
+ import { RepeatPicker } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { type Repeat, RepeatPicker } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ /** Rent: monthly on the 1st, from November. */
18
+ export default function Default() {
19
+ const [repeat, setRepeat] = useState<Repeat>({ every: 1, unit: 'month', day: 1 });
20
+ return (
21
+ <RepeatPicker value={repeat} onValueChange={setRepeat} locale="en-US" start="2026-11-01" className="max-w-sm" />
22
+ );
23
+ }
24
+ ```
25
+
26
+ ## API reference
27
+
28
+ ### RepeatPicker
29
+
30
+ How often something recurs, e.g. a bill: every n days, weeks, months or
31
+ years, on chosen weekdays or a day of the month. Says what it means in
32
+ words, and with a `start` date, the next few dates it falls on.
33
+
34
+ | Prop | Type | Default | Description |
35
+ | --- | --- | --- | --- |
36
+ | `className` | `string` | | |
37
+ | `defaultValue` | `Repeat` | `{ every: 1, unit: 'month' }` | The first rule, when the picker keeps its own. Every month by default. |
38
+ | `label` | `string` | | The group's name for screen readers (default "Repeats"). |
39
+ | `locale` | `string` | | For weekday and date names, and the week's first day. Defaults to the LocaleProvider's. |
40
+ | `name` | `string` | | Form field name: the rule is submitted as JSON. |
41
+ | `onValueChange` | `((value: Repeat) => void)` | | |
42
+ | `start` | `string` | | When it starts: its weekday and day of the month are the defaults, and the next dates are shown. |
43
+ | `value` | `Repeat` | | The rule, when you keep it. |
44
+ | `weekStartsOn` | `number` | | 0 for Sunday … 6. Defaults to the locale's. |
@@ -0,0 +1,107 @@
1
+ # Resizable
2
+
3
+ Panels with handles between them to drag, or to move with the arrow keys, within limits you set; groups nest, and the layout is reported to keep.
4
+
5
+ ```tsx
6
+ import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@rinzai/zen';
15
+
16
+ /** A sidebar beside the main view, and a detail pane under it: drag a line, or focus it and use the arrow keys. */
17
+ export default function Default() {
18
+ return (
19
+ <ResizablePanelGroup className="border-tint/10 h-72 w-full max-w-2xl rounded-xl border">
20
+ <ResizablePanel defaultSize={30} minSize={20} maxSize={50}>
21
+ <div className="flex h-full flex-col gap-1 p-4">
22
+ <span className="text-muted-foreground mb-2 text-xs font-medium tracking-wide uppercase">
23
+ Accounts
24
+ </span>
25
+ <span className="text-sm">Everyday</span>
26
+ <span className="text-sm">Savings</span>
27
+ <span className="text-sm">Credit card</span>
28
+ </div>
29
+ </ResizablePanel>
30
+ <ResizableHandle aria-label="Resize accounts" />
31
+ <ResizablePanel>
32
+ <ResizablePanelGroup orientation="vertical">
33
+ <ResizablePanel defaultSize={65} minSize={30}>
34
+ <div className="flex h-full items-center justify-center text-sm">Transactions</div>
35
+ </ResizablePanel>
36
+ <ResizableHandle aria-label="Resize details" />
37
+ <ResizablePanel minSize={20}>
38
+ <div className="text-muted-foreground flex h-full items-center justify-center text-sm">
39
+ Details
40
+ </div>
41
+ </ResizablePanel>
42
+ </ResizablePanelGroup>
43
+ </ResizablePanel>
44
+ </ResizablePanelGroup>
45
+ );
46
+ }
47
+ ```
48
+
49
+ ### Stacked
50
+
51
+ ```tsx
52
+ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@rinzai/zen';
53
+
54
+ /** Three panels stacked, each with a smallest size: drag either line, or focus one and use ↑↓. */
55
+ export default function Stacked() {
56
+ return (
57
+ <ResizablePanelGroup orientation="vertical" className="border-tint/10 h-80 w-full max-w-md rounded-xl border">
58
+ <ResizablePanel defaultSize={25} minSize={15}>
59
+ <div className="flex h-full flex-col justify-center gap-0.5 px-4">
60
+ <span className="text-muted-foreground text-xs">Balance</span>
61
+ <span className="text-lg font-semibold tabular-nums">$12,480</span>
62
+ </div>
63
+ </ResizablePanel>
64
+ <ResizableHandle aria-label="Resize balance" />
65
+ <ResizablePanel defaultSize={45} minSize={20}>
66
+ <div className="text-muted-foreground flex h-full items-center justify-center text-sm">This month</div>
67
+ </ResizablePanel>
68
+ <ResizableHandle aria-label="Resize this month" />
69
+ <ResizablePanel minSize={15}>
70
+ <div className="text-muted-foreground flex h-full items-center justify-center text-sm">Upcoming</div>
71
+ </ResizablePanel>
72
+ </ResizablePanelGroup>
73
+ );
74
+ }
75
+ ```
76
+
77
+ ## API reference
78
+
79
+ ### ResizablePanelGroup
80
+
81
+ Panels side by side (or stacked, `orientation="vertical"`) with handles
82
+ between them to drag, or to focus and move with the arrow keys. Sizes are
83
+ percentages of the group; `onLayout` reports them all after each change,
84
+ e.g. to keep for next time as the panels' `defaultSize`. Groups nest.
85
+
86
+ | Prop | Type | Default | Description |
87
+ | --- | --- | --- | --- |
88
+ | `onLayout` | `((sizes: number[]) => void)` | | Every panel's size, in percent and in order, after each change. |
89
+ | `orientation` | `"horizontal" \| "vertical"` | `horizontal` | Side by side (default), or stacked. |
90
+
91
+ ### ResizablePanel
92
+
93
+ One panel: `defaultSize` is a percentage of the group (those without one share what's left); `minSize` and `maxSize` bound it.
94
+
95
+ | Prop | Type | Default | Description |
96
+ | --- | --- | --- | --- |
97
+ | `defaultSize` | `number` | | Its share of the group, in percent. |
98
+ | `maxSize` | `number` | `100` | |
99
+ | `minSize` | `number` | `0` | |
100
+
101
+ ### ResizableHandle
102
+
103
+ The boundary between two panels: a hairline that lights up while it's
104
+ dragged, hovered or focused, with a wider grip than it looks. Arrow keys move
105
+ it, Home and End take the panel before it to its smallest and largest.
106
+
107
+ No props of its own; it accepts the props of the element it renders.
@@ -0,0 +1,68 @@
1
+ # Seek Bar
2
+
3
+ Where playback is in a track, and the way to move it: played and loaded on one track, between the time played and the length.
4
+
5
+ ```tsx
6
+ import { SeekBar, formatDuration } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Button, SeekBar, useMediaPlayer } from '@rinzai/zen';
15
+
16
+ import { useDemoTrack } from '../../player-bar/examples/demo-track';
17
+ // Icons from Heroicons (https://heroicons.com): Pause (pause), Play (play).
18
+
19
+ /** Filled to the time played, shaded on to what's loaded. Drag it: the track plays on, and seeks when you let go. */
20
+ export default function Default() {
21
+ const src = useDemoTrack();
22
+ const [audioRef, player] = useMediaPlayer();
23
+ return (
24
+ <div className="flex w-96 items-center gap-3">
25
+ <audio ref={audioRef} src={src} preload="metadata" />
26
+ <Button
27
+ size="icon-sm"
28
+ className="rounded-full"
29
+ aria-label={player.playing ? 'Pause' : 'Play'}
30
+ disabled={!src}
31
+ onClick={() => player.toggle()}
32
+ >
33
+ {player.playing ? <Pause className="size-4" /> : <Play className="size-4" />}
34
+ </Button>
35
+ <SeekBar
36
+ className="flex-1"
37
+ currentTime={player.currentTime}
38
+ playing={player.playing && !player.waiting}
39
+ playbackRate={player.playbackRate}
40
+ duration={player.duration}
41
+ buffered={player.buffered}
42
+ onSeek={player.seek}
43
+ />
44
+ </div>
45
+ );
46
+ }
47
+ ```
48
+
49
+ ## API reference
50
+
51
+ ### SeekBar
52
+
53
+ Where playback is in a track, and the way to move it: filled up to the
54
+ current time, shaded on to what's loaded, between the time played and the
55
+ length. While playing it glides, run by the browser's compositor between
56
+ the player's time updates. While dragging, the time follows the thumb and
57
+ the track keeps playing; letting go seeks. The arrow keys seek 5 seconds.
58
+
59
+ | Prop | Type | Default | Description |
60
+ | --- | --- | --- | --- |
61
+ | `currentTime` (required) | `number` | | Seconds played. |
62
+ | `duration` (required) | `number` | | Seconds long; 0 while unknown (the bar is disabled). |
63
+ | `onSeek` (required) | `(time: number) => void` | | When the user lets go, or steps with the keys: the time to play from. |
64
+ | `buffered` | `number` | `0` | Seconds loaded, shaded on the track past the time played. |
65
+ | `playbackRate` | `number` | `1` | How fast it moves on while playing (default 1). |
66
+ | `playing` | `boolean` | `false` | While the time is running (playing, not waiting for data): the bar glides on between time updates. |
67
+ | `thumbColor` | `string` | | CSS colour for the thumb (default: the accent). |
68
+ | `valueText` | `((value: number) => string)` | | Spoken value, e.g. (v) => `Hue ${v} degrees`. |
@@ -0,0 +1,89 @@
1
+ # Segmented
2
+
3
+ A pill row for a few mutually exclusive options.
4
+
5
+ ```tsx
6
+ import { Segmented, SegmentedItem } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Segmented, SegmentedItem } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ export default function Default() {
18
+ const [glow, setGlow] = useState<'off' | 'soft' | 'bright'>('soft');
19
+ return (
20
+ <Segmented label="Glow" value={glow} onValueChange={setGlow}>
21
+ <SegmentedItem value="off">Off</SegmentedItem>
22
+ <SegmentedItem value="soft">Soft</SegmentedItem>
23
+ <SegmentedItem value="bright">Bright</SegmentedItem>
24
+ </Segmented>
25
+ );
26
+ }
27
+ ```
28
+
29
+ ### With icons
30
+
31
+ ```tsx
32
+ import { Segmented, SegmentedItem } from '@rinzai/zen';
33
+ import { useState } from 'react';
34
+ // Icons from Heroicons (https://heroicons.com): ChartBarIcon (chart-bar), ListBullet (list-bullet), Squares (squares-2x2).
35
+
36
+ /** SegmentedItem holds anything (here an icon and text), and an option can be disabled. */
37
+ export default function WithIcons() {
38
+ const [view, setView] = useState('list');
39
+ return (
40
+ <Segmented label="View" value={view} onValueChange={setView}>
41
+ <SegmentedItem value="list">
42
+ <span className="flex items-center gap-1.5">
43
+ <ListBullet className="size-4" /> List
44
+ </span>
45
+ </SegmentedItem>
46
+ <SegmentedItem value="grid">
47
+ <span className="flex items-center gap-1.5">
48
+ <Squares className="size-4" /> Grid
49
+ </span>
50
+ </SegmentedItem>
51
+ <SegmentedItem value="chart" disabled>
52
+ <span className="flex items-center gap-1.5">
53
+ <ChartBarIcon className="size-4" /> Chart
54
+ </span>
55
+ </SegmentedItem>
56
+ </Segmented>
57
+ );
58
+ }
59
+ ```
60
+
61
+ ## API reference
62
+
63
+ ### Segmented
64
+
65
+ A small set of mutually exclusive options, shown as a pill row. Native radio
66
+ inputs underneath, so arrow keys and forms work as for any radio group. The
67
+ selection slides between options, like Pills. Fill it with
68
+ SegmentedItem (any content: text, icons, a disabled option).
69
+
70
+ | Prop | Type | Default | Description |
71
+ | --- | --- | --- | --- |
72
+ | `children` (required) | `ReactNode` | | The SegmentedItems. |
73
+ | `className` | `string` | | |
74
+ | `defaultValue` | `string` | | The first chosen option, when the control keeps its own. |
75
+ | `label` | `ReactNode` | | Shown above the options, naming the group. Without it, give the group an aria-label or aria-labelledby. |
76
+ | `name` | `string` | | Form field name; defaults to a generated one. |
77
+ | `onValueChange` | `((value: V) => void)` | | |
78
+ | `value` | `string` | | The chosen option, when you keep it. |
79
+
80
+ ### SegmentedItem
81
+
82
+ One option of a Segmented: a native radio covering whatever you put inside.
83
+
84
+ | Prop | Type | Default | Description |
85
+ | --- | --- | --- | --- |
86
+ | `children` (required) | `ReactNode` | | What the option shows: text, an icon, both. |
87
+ | `value` (required) | `string` | | |
88
+ | `className` | `string` | | |
89
+ | `disabled` | `boolean` | | Skipped by the arrow keys and can't be chosen. |
@@ -0,0 +1,148 @@
1
+ # Select
2
+
3
+ Pick one value from a short list, fully keyboard-driven. For long or searchable lists, use Combobox.
4
+
5
+ ```tsx
6
+ import { Select, SelectItem, SelectGroup, SelectSeparator } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Select, SelectItem } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ const CATEGORIES = ['Groceries', 'Rent', 'Transport', 'Dining out', 'Subscriptions', 'Health', 'Travel'];
18
+
19
+ /** Keyboard: arrows, Home/End, type a letter to jump, Enter to choose, Escape to close. */
20
+ export default function Default() {
21
+ const [category, setCategory] = useState<string | null>('rent');
22
+ return (
23
+ <Select aria-label="Category" value={category} onValueChange={setCategory} className="w-56">
24
+ {CATEGORIES.map((label) => (
25
+ <SelectItem key={label} value={label.toLowerCase().replace(/\s+/g, '-')}>
26
+ {label}
27
+ </SelectItem>
28
+ ))}
29
+ </Select>
30
+ );
31
+ }
32
+ ```
33
+
34
+ ### Custom options
35
+
36
+ ```tsx
37
+ import { currencySymbol, Select, SelectItem } from '@rinzai/zen';
38
+ import { useState } from 'react';
39
+
40
+ const CURRENCIES = [
41
+ { value: 'INR', label: 'Indian rupee' },
42
+ { value: 'USD', label: 'US dollar' },
43
+ { value: 'EUR', label: 'Euro' },
44
+ { value: 'JPY', label: 'Japanese yen', disabled: true },
45
+ ];
46
+
47
+ /** An item can show anything; its `label` is what the field shows and typing matches. Disabled items are skipped by the keyboard. */
48
+ export default function CustomOptions() {
49
+ const [currency, setCurrency] = useState<string | null>(null);
50
+ return (
51
+ <Select
52
+ aria-label="Currency"
53
+ placeholder="Choose a currency"
54
+ value={currency}
55
+ onValueChange={setCurrency}
56
+ className="w-64"
57
+ >
58
+ {CURRENCIES.map((c) => (
59
+ <SelectItem key={c.value} value={c.value} label={c.label} disabled={c.disabled}>
60
+ <span className="flex items-center justify-between gap-3">
61
+ {c.label}
62
+ <span className="text-muted-foreground tabular-nums">
63
+ {c.value} {currencySymbol(c.value, 'en')}
64
+ </span>
65
+ </span>
66
+ </SelectItem>
67
+ ))}
68
+ </Select>
69
+ );
70
+ }
71
+ ```
72
+
73
+ ### Groups
74
+
75
+ ```tsx
76
+ import { Select, SelectGroup, SelectItem, SelectSeparator } from '@rinzai/zen';
77
+ import { useState } from 'react';
78
+
79
+ /** SelectItem children, organised with SelectGroup and SelectSeparator; the keyboard skips the disabled one. */
80
+ export default function Groups() {
81
+ const [category, setCategory] = useState<string | null>('groceries');
82
+ return (
83
+ <Select aria-label="Category" value={category} onValueChange={setCategory} className="w-64">
84
+ <SelectGroup label="Essentials">
85
+ <SelectItem value="rent">Rent</SelectItem>
86
+ <SelectItem value="groceries">Groceries</SelectItem>
87
+ <SelectItem value="transport">Transport</SelectItem>
88
+ </SelectGroup>
89
+ <SelectSeparator />
90
+ <SelectGroup label="Lifestyle">
91
+ <SelectItem value="dining">Dining out</SelectItem>
92
+ <SelectItem value="travel" disabled>
93
+ Travel (over budget)
94
+ </SelectItem>
95
+ </SelectGroup>
96
+ </Select>
97
+ );
98
+ }
99
+ ```
100
+
101
+ ## API reference
102
+
103
+ ### Select
104
+
105
+ Pick one value from a short list: a field-styled button that opens a list
106
+ below it. Keyboard: arrows, Home/End, type to jump, Enter to choose, Escape
107
+ to close. Fill it with SelectItem, organised with SelectGroup and
108
+ SelectSeparator. For long or searchable lists, use Combobox.
109
+
110
+ | Prop | Type | Default | Description |
111
+ | --- | --- | --- | --- |
112
+ | `children` (required) | `ReactNode` | | SelectItem, SelectGroup and SelectSeparator. |
113
+ | `className` | `string` | | Classes for the trigger (e.g. a width, instead of the default full width). |
114
+ | `defaultValue` | `string \| null` | `null` | The first value, when the select keeps its own. |
115
+ | `disabled` | `boolean` | `false` | |
116
+ | `id` | `string` | | |
117
+ | `listClassName` | `string` | | Classes for the popup list. |
118
+ | `name` | `string` | | Form field name: adds a hidden input with the value. |
119
+ | `onValueChange` | `((value: V) => void)` | | |
120
+ | `placeholder` | `string` | | Shown while nothing is chosen. |
121
+ | `size` | `"default" \| "lg" \| "sm"` | `default` | Height, matching Button's: sm (32px), default (40px) or lg (44px). |
122
+ | `value` | `string \| null` | | The chosen value, when you keep it; null for none. |
123
+
124
+ ### SelectItem
125
+
126
+ One choice in a Select. Its `label` (or its text, when that's all it holds)
127
+ shows in the field once chosen and is matched when typing to jump.
128
+
129
+ | Prop | Type | Default | Description |
130
+ | --- | --- | --- | --- |
131
+ | `value` (required) | `string` | | |
132
+ | `className` | `string` | | |
133
+ | `disabled` | `boolean` | | |
134
+ | `label` | `string` | | Text for the field and for typing to jump (defaults to the item's text). |
135
+
136
+ ### SelectGroup
137
+
138
+ A labelled group of SelectItems.
139
+
140
+ | Prop | Type | Default | Description |
141
+ | --- | --- | --- | --- |
142
+ | `label` (required) | `ReactNode` | | |
143
+
144
+ ### SelectSeparator
145
+
146
+ A hairline between items or groups.
147
+
148
+ No props of its own; it accepts the props of the element it renders.
@@ -0,0 +1,76 @@
1
+ # Separator
2
+
3
+ A hairline between groups of content, across or upright.
4
+
5
+ ```tsx
6
+ import { Separator } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Separator } from '@rinzai/zen';
15
+
16
+ /** Across between sections, and upright between items in a row. */
17
+ export default function Default() {
18
+ return (
19
+ <div className="flex w-full max-w-xs flex-col gap-3 text-sm">
20
+ <div>
21
+ <p className="mt-0! font-medium">September 2026</p>
22
+ <p className="text-muted-foreground mt-0!">Budgets, entries and goals</p>
23
+ </div>
24
+ <Separator />
25
+ <div className="text-muted-foreground flex h-5 items-center gap-3">
26
+ <span>Spent $2,742</span>
27
+ <Separator orientation="vertical" />
28
+ <span>Left $1,258</span>
29
+ <Separator orientation="vertical" />
30
+ <span>12 entries</span>
31
+ </div>
32
+ </div>
33
+ );
34
+ }
35
+ ```
36
+
37
+ ### Stats
38
+
39
+ ```tsx
40
+ import { Card, Separator } from '@rinzai/zen';
41
+
42
+ /** Upright between figures in a card: it takes the row's height, so it spans each label and value. */
43
+ export default function Stats() {
44
+ return (
45
+ <Card className="flex w-full max-w-md items-stretch justify-between gap-4 py-4">
46
+ {[
47
+ ['Income', '$6,200'],
48
+ ['Spent', '$4,180'],
49
+ ['Saved', '$2,020'],
50
+ ].map(([label, value], i) => (
51
+ <div key={label} className="contents">
52
+ {i > 0 && <Separator orientation="vertical" decorative />}
53
+ <div className="flex flex-1 flex-col gap-1">
54
+ <span className="text-muted-foreground text-xs">{label}</span>
55
+ <span className="text-lg font-semibold tabular-nums">{value}</span>
56
+ </div>
57
+ </div>
58
+ ))}
59
+ </Card>
60
+ );
61
+ }
62
+ ```
63
+
64
+ ## API reference
65
+
66
+ ### Separator
67
+
68
+ A hairline between groups of content, across (the default) or upright
69
+ (`orientation="vertical"`, e.g. between toolbar buttons; it takes its
70
+ container's height). Screen readers hear it only when it divides something
71
+ meaningful: pass `decorative` for one that's just visual.
72
+
73
+ | Prop | Type | Default | Description |
74
+ | --- | --- | --- | --- |
75
+ | `decorative` | `boolean` | | Only visual: hidden from screen readers. |
76
+ | `orientation` | `"horizontal" \| "vertical"` | `horizontal` | |