@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,180 @@
1
+ # Card
2
+
3
+ Glass panels for grouping content, and stats for single figures.
4
+
5
+ ```tsx
6
+ import { Card, CardHeader, CardTitle, CardDescription, Stat, StatRow } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Goal
12
+
13
+ ```tsx
14
+ import {
15
+ Button,
16
+ buttonVariants,
17
+ Card,
18
+ CardHeader,
19
+ CardTitle,
20
+ Menu,
21
+ MenuContent,
22
+ MenuItem,
23
+ MenuTrigger,
24
+ ProgressRing,
25
+ } from '@rinzai/zen';
26
+ // Icons from Heroicons (https://heroicons.com): Ellipsis (ellipsis-horizontal).
27
+
28
+ export default function Goal() {
29
+ return (
30
+ <Card className="w-full max-w-sm">
31
+ <CardHeader>
32
+ <CardTitle>Emergency fund</CardTitle>
33
+ <Menu>
34
+ <MenuTrigger
35
+ aria-label="Goal actions"
36
+ className={buttonVariants({ variant: 'ghost', size: 'icon' })}
37
+ >
38
+ <Ellipsis />
39
+ </MenuTrigger>
40
+ <MenuContent>
41
+ <MenuItem>Edit</MenuItem>
42
+ </MenuContent>
43
+ </Menu>
44
+ </CardHeader>
45
+ <div className="flex items-center gap-4">
46
+ <ProgressRing value={0.64} label="Emergency fund">
47
+ <span className="text-sm font-semibold tabular-nums">64%</span>
48
+ </ProgressRing>
49
+ <div className="flex flex-col gap-1">
50
+ <span className="text-muted-foreground text-sm">$6,400 of $10,000</span>
51
+ <Button size="sm" variant="secondary" className="self-start">
52
+ Add money
53
+ </Button>
54
+ </div>
55
+ </div>
56
+ </Card>
57
+ );
58
+ }
59
+ ```
60
+
61
+ ### Stats
62
+
63
+ ```tsx
64
+ import { Stat, StatRow } from '@rinzai/zen';
65
+
66
+ export default function Stats() {
67
+ return (
68
+ <StatRow className="w-full max-w-3xl">
69
+ <Stat label="Income" value="$8,450" hint="+4% on last month" />
70
+ <Stat label="Spent" value="$5,120" tone="negative" />
71
+ <Stat label="Net this month" value="$3,330" tone="positive" />
72
+ </StatRow>
73
+ );
74
+ }
75
+ ```
76
+
77
+ ### Morph
78
+
79
+ ```tsx
80
+ import { Button, Card, CardDescription, CardHeader, CardTitle, Sparkline } from '@rinzai/zen';
81
+ import { startTransition, useState, ViewTransition } from 'react';
82
+
83
+ const accounts = [
84
+ { id: 'checking', name: 'Checking', balance: '$4,280.15', trend: [31, 34, 30, 38, 36, 42, 40, 45] },
85
+ { id: 'savings', name: 'Savings', balance: '$12,940.00', trend: [80, 82, 83, 85, 86, 88, 91, 94] },
86
+ { id: 'card', name: 'Credit card', balance: '−$612.40', trend: [12, 18, 15, 22, 27, 21, 30, 26] },
87
+ ];
88
+
89
+ export default function Morph() {
90
+ const [open, setOpen] = useState<string | null>(null);
91
+ const go = (id: string | null) => startTransition(() => setOpen(id));
92
+ const account = accounts.find((a) => a.id === open);
93
+
94
+ if (account)
95
+ return (
96
+ <ViewTransition name={`account-${account.id}`} share="zen-morph">
97
+ <Card className="w-full max-w-md">
98
+ <CardHeader>
99
+ <CardTitle>{account.name}</CardTitle>
100
+ <Button size="sm" variant="ghost" onClick={() => go(null)}>
101
+ Back
102
+ </Button>
103
+ </CardHeader>
104
+ <p className="text-3xl font-semibold tabular-nums">{account.balance}</p>
105
+ <CardDescription className="mt-1">Last 8 weeks</CardDescription>
106
+ <Sparkline values={account.trend} className="mt-4 h-24 w-full" />
107
+ </Card>
108
+ </ViewTransition>
109
+ );
110
+
111
+ return (
112
+ <div className="grid w-full max-w-md gap-3">
113
+ {accounts.map((a) => (
114
+ <ViewTransition key={a.id} name={`account-${a.id}`} share="zen-morph">
115
+ <Card className="p-0 md:p-0">
116
+ <button
117
+ type="button"
118
+ onClick={() => go(a.id)}
119
+ className="flex w-full items-center justify-between rounded-xl p-4 text-left"
120
+ >
121
+ <span className="font-medium">{a.name}</span>
122
+ <span className="tabular-nums">{a.balance}</span>
123
+ </button>
124
+ </Card>
125
+ </ViewTransition>
126
+ ))}
127
+ </div>
128
+ );
129
+ }
130
+ ```
131
+
132
+ ## API reference
133
+
134
+ ### Card
135
+
136
+ Glass panel whose border catches the pointer light. For a title with an
137
+ action beside it, start it with a CardHeader holding a CardTitle and the action.
138
+
139
+ No props of its own; it accepts the props of the element it renders.
140
+
141
+ ### CardHeader
142
+
143
+ The top row of a Card: its title, and an action (a button, a menu) at the end.
144
+
145
+ No props of its own; it accepts the props of the element it renders.
146
+
147
+ ### CardTitle
148
+
149
+ The card's title: an h4 by default; with `asChild`, your own heading (an h2, say) gets its look.
150
+
151
+ | Prop | Type | Default | Description |
152
+ | --- | --- | --- | --- |
153
+ | `asChild` | `boolean` | | Put the title's look on your own heading element instead of an h4. |
154
+
155
+ ### CardDescription
156
+
157
+ A line under the CardTitle: what the card is about, or a figure's context ("₹12L at 8.5% · 20 years").
158
+
159
+ No props of its own; it accepts the props of the element it renders.
160
+
161
+ ### Stat
162
+
163
+ A single figure with a label, e.g. "Revenue $12,400".
164
+
165
+ | Prop | Type | Default | Description |
166
+ | --- | --- | --- | --- |
167
+ | `label` (required) | `string` | | |
168
+ | `value` (required) | `ReactNode` | | |
169
+ | `children` | `ReactNode` | | Under the figure: how it got there, like a Sparkline. |
170
+ | `className` | `string` | | |
171
+ | `hint` | `ReactNode` | | |
172
+ | `tone` | `"default" \| "negative" \| "positive"` | `default` | |
173
+
174
+ ### StatRow
175
+
176
+ Row of stats: two columns on phones (an odd last one spans both), three from `sm` up.
177
+
178
+ | Prop | Type | Default | Description |
179
+ | --- | --- | --- | --- |
180
+ | `className` | `string` | | |
@@ -0,0 +1,162 @@
1
+ # Carousel
2
+
3
+ Slides in a row that snap as you swipe or scroll, with glass buttons and dots that light the current one beneath, and the arrow keys. Native scrolling, so it follows the finger exactly.
4
+
5
+ ```tsx
6
+ import { Carousel, CarouselContent, CarouselItem, CarouselControls, CarouselPrevious, CarouselNext, CarouselDots } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import {
15
+ Card,
16
+ Carousel,
17
+ CarouselContent,
18
+ CarouselControls,
19
+ CarouselDots,
20
+ CarouselItem,
21
+ CarouselNext,
22
+ CarouselPrevious,
23
+ ProgressRing,
24
+ } from '@rinzai/zen';
25
+
26
+ const GOALS = [
27
+ { name: 'Emergency fund', saved: 6400, target: 10000 },
28
+ { name: 'New laptop', saved: 1150, target: 1800 },
29
+ { name: 'Trip to Kyoto', saved: 2100, target: 4500 },
30
+ { name: 'Wedding gift', saved: 300, target: 300 },
31
+ ];
32
+
33
+ const usd = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });
34
+
35
+ /** One slide at a time, with buttons and dots below. Swipe or scroll it too, or focus it and use the arrow keys. */
36
+ export default function Default() {
37
+ return (
38
+ <Carousel aria-label="Savings goals" className="w-full max-w-sm">
39
+ <CarouselContent>
40
+ {GOALS.map((goal) => (
41
+ <CarouselItem key={goal.name}>
42
+ <Card className="flex items-center gap-5">
43
+ <ProgressRing value={goal.saved / goal.target} label={goal.name}>
44
+ <span className="text-sm font-semibold tabular-nums">
45
+ {Math.round((goal.saved / goal.target) * 100)}%
46
+ </span>
47
+ </ProgressRing>
48
+ <div className="flex min-w-0 flex-col gap-1">
49
+ <span className="truncate font-medium">{goal.name}</span>
50
+ <span className="text-muted-foreground text-sm tabular-nums">
51
+ {usd.format(goal.saved)} of {usd.format(goal.target)}
52
+ </span>
53
+ </div>
54
+ </Card>
55
+ </CarouselItem>
56
+ ))}
57
+ </CarouselContent>
58
+ <CarouselControls>
59
+ <CarouselPrevious />
60
+ <CarouselDots />
61
+ <CarouselNext />
62
+ </CarouselControls>
63
+ </Carousel>
64
+ );
65
+ }
66
+ ```
67
+
68
+ ### Several
69
+
70
+ ```tsx
71
+ import { Carousel, CarouselContent, CarouselControls, CarouselItem, CarouselNext, CarouselPrevious } from '@rinzai/zen';
72
+
73
+ const CATEGORIES = [
74
+ { name: 'Groceries', spent: '$412', chart: 6 },
75
+ { name: 'Rent', spent: '$1,600', chart: 7 },
76
+ { name: 'Transport', spent: '$96', chart: 1 },
77
+ { name: 'Eating out', spent: '$238', chart: 2 },
78
+ { name: 'Subscriptions', spent: '$54', chart: 5 },
79
+ { name: 'Health', spent: '$120', chart: 3 },
80
+ ];
81
+
82
+ /**
83
+ * Several slides in view: size the items (here a half, then a third on wider
84
+ * screens). It snaps to each; the buttons sit together at the end, each gone when there's no further to go.
85
+ */
86
+ export default function Several() {
87
+ return (
88
+ <Carousel aria-label="Spending by category" className="w-full max-w-xl">
89
+ <CarouselContent className="gap-3">
90
+ {CATEGORIES.map((c) => (
91
+ <CarouselItem key={c.name} className="basis-[calc(50%-0.375rem)] sm:basis-[calc(33.333%-0.5rem)]">
92
+ <div
93
+ className="border-tint/10 flex h-28 flex-col justify-end gap-0.5 rounded-xl border p-4"
94
+ style={{
95
+ // The chart palette, so each category looks as it does in charts.
96
+ background: `linear-gradient(160deg, color-mix(in oklch, var(--chart-${c.chart}) 35%, transparent), color-mix(in oklch, var(--chart-${c.chart}) 6%, transparent))`,
97
+ }}
98
+ >
99
+ <span className="text-muted-foreground text-xs">{c.name}</span>
100
+ <span className="text-lg font-semibold tabular-nums">{c.spent}</span>
101
+ </div>
102
+ </CarouselItem>
103
+ ))}
104
+ </CarouselContent>
105
+ <CarouselControls className="justify-end">
106
+ <CarouselPrevious />
107
+ <CarouselNext />
108
+ </CarouselControls>
109
+ </Carousel>
110
+ );
111
+ }
112
+ ```
113
+
114
+ ## API reference
115
+
116
+ ### Carousel
117
+
118
+ Slides in a row that scrolls sideways and snaps to each one: swipe, scroll,
119
+ use the arrow keys once it has focus, or the CarouselPrevious and
120
+ CarouselNext buttons. Put a CarouselContent of CarouselItems inside, then a
121
+ CarouselControls of the buttons and CarouselDots if you want them. A slide is as wide as the
122
+ carousel unless you size it (e.g. `basis-1/3` for three in view).
123
+
124
+ No props of its own; it accepts the props of the element it renders.
125
+
126
+ ### CarouselContent
127
+
128
+ The scrolling row of slides.
129
+
130
+ No props of its own; it accepts the props of the element it renders.
131
+
132
+ ### CarouselItem
133
+
134
+ One slide: it's announced as which of how many.
135
+
136
+ No props of its own; it accepts the props of the element it renders.
137
+
138
+ ### CarouselControls
139
+
140
+ The row under the slides: CarouselPrevious at the start, CarouselNext at the end, CarouselDots between.
141
+
142
+ No props of its own; it accepts the props of the element it renders.
143
+
144
+ ### CarouselPrevious
145
+
146
+ Scroll back one slide: a round glass button, gone at the first (keeping its
147
+ place, so the controls don't shift). Put it in CarouselControls, under the
148
+ slides, where it never covers them.
149
+
150
+ No props of its own; it accepts the props of the element it renders.
151
+
152
+ ### CarouselNext
153
+
154
+ Scroll on one slide: a round glass button, gone at the last.
155
+
156
+ No props of its own; it accepts the props of the element it renders.
157
+
158
+ ### CarouselDots
159
+
160
+ A dot for each place it can scroll to, the current one drawn out long and lit; each goes there.
161
+
162
+ No props of its own; it accepts the props of the element it renders.
@@ -0,0 +1,325 @@
1
+ # Chart
2
+
3
+ Areas, lines and bars on one axis, a donut and an allocation bar: plain SVG that draws itself in, with a crosshair tooltip, keyboard reading, a legend and a table for screen readers.
4
+
5
+ ```tsx
6
+ import { Chart, ChartArea, ChartLine, ChartBar, ChartReference, ChartTooltipCard, DonutChart, AllocationBar } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Balance
12
+
13
+ ```tsx
14
+ import { Chart, ChartArea, ChartReference } from '@rinzai/zen';
15
+
16
+ const MONTHS = [
17
+ '2026-04',
18
+ '2026-05',
19
+ '2026-06',
20
+ '2026-07',
21
+ '2026-08',
22
+ '2026-09',
23
+ '2026-10',
24
+ '2026-11',
25
+ '2026-12',
26
+ '2027-01',
27
+ '2027-02',
28
+ '2027-03',
29
+ ];
30
+ const BALANCE = [4.2, 5.1, 3.8, 4.6, 6.2, 7.1, 6.4, 7.9, 8.8, 7.6, 9.4, 10.2].map((l) => l * 100000);
31
+ const TODAY = '2026-09';
32
+ // One series split in two at today: solid where it happened, dashed where it's projected.
33
+ const DATA = MONTHS.map((month, i) => ({
34
+ month,
35
+ balance: month <= TODAY ? BALANCE[i] : null,
36
+ projected: month >= TODAY ? BALANCE[i] : null,
37
+ }));
38
+ const label = (m: string) => new Date(`${m}-01T00:00Z`).toLocaleString('en-IN', { month: 'short', timeZone: 'UTC' });
39
+ const money = new Intl.NumberFormat('en-IN', {
40
+ style: 'currency',
41
+ currency: 'INR',
42
+ notation: 'compact',
43
+ maximumFractionDigits: 1,
44
+ });
45
+
46
+ /** An area in the theme's glow: solid up to today, dashed ahead, with a "Today" marker. Point at it, or focus it and use the arrow keys. */
47
+ export default function Balance() {
48
+ return (
49
+ <Chart
50
+ data={DATA}
51
+ x="month"
52
+ label="Balance over time"
53
+ summary="Balance grows from ₹4.2L in April to a projected ₹10.2L next March."
54
+ formatX={label}
55
+ formatY={(v) => money.format(v)}
56
+ className="w-full max-w-2xl"
57
+ >
58
+ <ChartArea dataKey="balance" label="Balance" color="oklch(var(--glow))" />
59
+ <ChartArea dataKey="projected" label="Projected" color="oklch(var(--glow-2))" dashed />
60
+ <ChartReference x={TODAY} label="Today" />
61
+ </Chart>
62
+ );
63
+ }
64
+ ```
65
+
66
+ ### Cashflow
67
+
68
+ ```tsx
69
+ import { Chart, ChartBar, ChartLine, ChartReference } from '@rinzai/zen';
70
+
71
+ const DATA = [
72
+ { month: 'Apr', income: 92000, spending: 61000 },
73
+ { month: 'May', income: 92000, spending: 74000 },
74
+ { month: 'Jun', income: 98000, spending: 58000 },
75
+ { month: 'Jul', income: 98000, spending: 103000 },
76
+ { month: 'Aug', income: 98000, spending: 66000 },
77
+ { month: 'Sep', income: 112000, spending: 71000 },
78
+ { month: 'Oct', income: 112000, spending: 69000 },
79
+ { month: 'Nov', income: 112000, spending: 80000 },
80
+ ].map((r) => ({ ...r, net: r.income - r.spending }));
81
+ const inr = new Intl.NumberFormat('en-IN', {
82
+ style: 'currency',
83
+ currency: 'INR',
84
+ notation: 'compact',
85
+ maximumFractionDigits: 1,
86
+ });
87
+
88
+ /** Bars grouped by month with a line on the same axis; months still to come are faded. */
89
+ export default function Cashflow() {
90
+ return (
91
+ <Chart
92
+ data={DATA}
93
+ x="month"
94
+ label="Income and spending by month"
95
+ formatY={(v) => inr.format(v)}
96
+ className="w-full max-w-2xl"
97
+ >
98
+ <ChartBar dataKey="income" label="Income" dim={(_, i) => i > 5} />
99
+ <ChartBar dataKey="spending" label="Spending" dim={(_, i) => i > 5} />
100
+ <ChartLine dataKey="net" label="Net" color="var(--chart-3)" />
101
+ <ChartReference y={0} tone="negative" />
102
+ </Chart>
103
+ );
104
+ }
105
+ ```
106
+
107
+ ### Lines
108
+
109
+ ```tsx
110
+ import { Chart, ChartLine } from '@rinzai/zen';
111
+
112
+ const DATA = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map((day, i) => ({
113
+ day,
114
+ web: [420, 480, 510, 470, 560, 690, 640][i],
115
+ ios: [310, 330, 360, 390, 380, 450, 470][i],
116
+ android: [220, 260, 250, 300, 330, 310, 360][i],
117
+ }));
118
+
119
+ /** Several series in the chart palette, in the order they're written; the legend appears for two or more. */
120
+ export default function Lines() {
121
+ return (
122
+ <Chart data={DATA} x="day" label="Sessions by platform" height={220} className="w-full max-w-2xl">
123
+ <ChartLine dataKey="web" label="Web" />
124
+ <ChartLine dataKey="ios" label="iOS" />
125
+ <ChartLine dataKey="android" label="Android" />
126
+ </Chart>
127
+ );
128
+ }
129
+ ```
130
+
131
+ ### Donut
132
+
133
+ ```tsx
134
+ import { DonutChart, paletteColor } from '@rinzai/zen';
135
+
136
+ const SPENDING = [
137
+ { key: 'home', label: 'Home', value: 28000 },
138
+ { key: 'food', label: 'Food', value: 14500 },
139
+ { key: 'travel', label: 'Travel', value: 9200 },
140
+ { key: 'fun', label: 'Fun', value: 6100 },
141
+ { key: 'other', label: 'Other', value: 3400 },
142
+ ];
143
+ const total = SPENDING.reduce((s, x) => s + x.value, 0);
144
+ const inr = (v: number) =>
145
+ new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 0 }).format(v);
146
+
147
+ /** Parts of a whole with the total in the middle, and a legend of your own beside it, in the chart's colours (paletteColor). */
148
+ export default function Donut() {
149
+ return (
150
+ <div className="flex flex-wrap items-center gap-8">
151
+ <DonutChart items={SPENDING} label="Spending by category" formatValue={inr}>
152
+ <div>
153
+ <div className="text-muted-foreground text-[0.68rem] tracking-[0.1em] uppercase">Spent</div>
154
+ <div className="text-lg font-semibold tracking-tight">{inr(total)}</div>
155
+ </div>
156
+ </DonutChart>
157
+ <ul className="flex min-w-48 flex-col gap-2 text-sm">
158
+ {SPENDING.map((s, i) => (
159
+ <li key={s.key} className="flex items-center gap-2.5">
160
+ <span
161
+ className="size-2.5 rounded-[3px]"
162
+ style={{ background: paletteColor('chart', i, SPENDING.length) }}
163
+ />
164
+ <span className="flex-1">{s.label}</span>
165
+ <span className="font-medium tabular-nums">{inr(s.value)}</span>
166
+ </li>
167
+ ))}
168
+ </ul>
169
+ </div>
170
+ );
171
+ }
172
+ ```
173
+
174
+ ### Allocation
175
+
176
+ ```tsx
177
+ import { AllocationBar, formatMoney } from '@rinzai/zen';
178
+
179
+ /** Where September's income went. */
180
+ export default function Allocation() {
181
+ return (
182
+ <AllocationBar
183
+ label="Income in September"
184
+ className="w-full max-w-md"
185
+ locale="en-US"
186
+ formatValue={(v) => formatMoney(v, 'USD', 'en-US', { showDecimals: false })}
187
+ items={[
188
+ { key: 'bills', label: 'Bills', value: 182000 },
189
+ { key: 'spending', label: 'Spending', value: 145600 },
190
+ { key: 'savings', label: 'Savings', value: 96000 },
191
+ { key: 'investing', label: 'Investing', value: 50000 },
192
+ { key: 'left', label: 'Left over', value: 26400 },
193
+ ]}
194
+ />
195
+ );
196
+ }
197
+ ```
198
+
199
+ ## API reference
200
+
201
+ ### Chart
202
+
203
+ A chart on one axis: areas, lines and bars over rows of data (e.g. months),
204
+ with a recessive grid, a crosshair and a tooltip listing every series at the
205
+ pointer (or keyboard: focus it and use the arrow keys), a legend for two or
206
+ more series, and a table of the values for screen readers. Series draw in
207
+ as they appear. Colours default to the chart palette (--chart-1…8) in the
208
+ order the series are written; give each its own `color` for meaning (e.g.
209
+ the theme's glow).
210
+
211
+ | Prop | Type | Default | Description |
212
+ | --- | --- | --- | --- |
213
+ | `children` (required) | `ReactNode` | | ChartArea, ChartLine, ChartBar and ChartReference parts. |
214
+ | `data` (required) | `readonly T[]` | | One row per point on the x axis (e.g. a month). |
215
+ | `label` (required) | `string` | | Names the chart for screen readers. |
216
+ | `x` (required) | `string` | | The row field placed along the x axis. |
217
+ | `className` | `string` | | |
218
+ | `formatTooltipX` | `((value: T[X]) => string)` | | The tooltip's title for an x value (defaults to formatX). |
219
+ | `formatX` | `((value: T[X]) => string)` | | Axis labels for x values. |
220
+ | `formatY` | `((value: number) => string)` | `(n: number) => compactFormat.format(n)` | Axis ticks and tooltip values (defaults to compact numbers: 1.2K). |
221
+ | `height` | `number` | `240` | Height in px, including the axis labels. |
222
+ | `legend` | `boolean` | | Show the legend. Defaults to on for two or more series, or a bar's `dimLabel`. |
223
+ | `palette` | `ChartPalette` | `chart` | Colours for series that don't give their own: the categorical palette (default), or shades of the theme's glow. |
224
+ | `renderTooltip` | `((row: T, index: number) => ReactNode)` | | Your own tooltip for a row (build it from ChartTooltipCard). |
225
+ | `summary` | `string` | | A sentence on what the chart shows, for screen readers (e.g. its highest and lowest points). |
226
+
227
+ ### ChartArea
228
+
229
+ A line with a soft fill down to the baseline.
230
+
231
+ | Prop | Type | Default | Description |
232
+ | --- | --- | --- | --- |
233
+ | `dataKey` (required) | `string` | | The row field holding this series' values (numbers; null for none). |
234
+ | `label` (required) | `string` | | Its name in the legend, the tooltip and the table. |
235
+ | `color` | `string` | | Any CSS colour. Defaults to the next of --chart-1…8. |
236
+ | `curve` | `"linear" \| "monotone"` | | Smooth (default) or straight between points. |
237
+ | `dashed` | `boolean` | | Dashed (e.g. a projection). |
238
+
239
+ ### ChartLine
240
+
241
+ A 2px line. Gaps (null values) break it, so one series can be split into solid and dashed parts.
242
+
243
+ | Prop | Type | Default | Description |
244
+ | --- | --- | --- | --- |
245
+ | `dataKey` (required) | `string` | | The row field holding this series' values (numbers; null for none). |
246
+ | `label` (required) | `string` | | Its name in the legend, the tooltip and the table. |
247
+ | `color` | `string` | | Any CSS colour. Defaults to the next of --chart-1…8. |
248
+ | `curve` | `"linear" \| "monotone"` | | Smooth (default) or straight between points. |
249
+ | `dashed` | `boolean` | | Dashed (e.g. a projection). |
250
+
251
+ ### ChartBar
252
+
253
+ Bars, grouped with the other bar series, growing from the baseline.
254
+
255
+ | Prop | Type | Default | Description |
256
+ | --- | --- | --- | --- |
257
+ | `dataKey` (required) | `string` | | The row field holding this series' values (numbers; null for none). |
258
+ | `label` (required) | `string` | | Its name in the legend, the tooltip and the table. |
259
+ | `color` | `string` | | Any CSS colour. Defaults to the next of --chart-1…8. |
260
+ | `dim` | `((row: Row, index: number) => boolean)` | | Rows whose bar is faded, e.g. months still to come. |
261
+ | `dimLabel` | `string` | | What the faded bars mean, e.g. "Planned": the legend gets a faded key for it (and shows, even for one series). |
262
+
263
+ ### ChartReference
264
+
265
+ A marker across the plot: a horizontal line at `y` (e.g. zero), or a vertical one at the row whose x is `x` (e.g. today).
266
+
267
+ | Prop | Type | Default | Description |
268
+ | --- | --- | --- | --- |
269
+ | `dashed` | `boolean` | | Vertical lines are dashed unless `dashed={false}`; horizontal ones solid unless `dashed`. |
270
+ | `label` | `string` | | Text beside a vertical line, e.g. "Today". |
271
+ | `tone` | `"muted" \| "negative"` | | Destructive for a line that warns (e.g. below zero). |
272
+ | `x` | `unknown` | | A vertical line at the row whose x field equals this. |
273
+ | `y` | `number` | | A horizontal line at this value. |
274
+
275
+ ### ChartTooltipCard
276
+
277
+ The tooltip's card: a title and one row per series, the value first and the
278
+ series keyed by a short stroke of its colour. Use it in `renderTooltip` to
279
+ show your own rows.
280
+
281
+ | Prop | Type | Default | Description |
282
+ | --- | --- | --- | --- |
283
+ | `rows` (required) | `{ label: string; value: string; color?: string \| undefined; dashed?: boolean \| undefined; }[]` | | |
284
+ | `title` (required) | `ReactNode` | | |
285
+ | `className` | `string` | | |
286
+
287
+ ### DonutChart
288
+
289
+ A ring of parts of a whole (e.g. spending by category), with your content in
290
+ the middle (e.g. the total). Segments are parted by a small gap, with
291
+ rounded corners; pointing at one (or focusing it) lifts it and shows its
292
+ value. A part too small to show at its share is drawn as a pill, the
293
+ narrowest segment, taking the room from the larger parts in proportion. Best
294
+ for a handful of parts: past about six, use bars. Colours default to the
295
+ chart palette in order.
296
+
297
+ | Prop | Type | Default | Description |
298
+ | --- | --- | --- | --- |
299
+ | `items` (required) | `{ key: string; label: string; value: number; color?: string \| undefined; }[]` | | The parts, in order round the ring from 12 o'clock. |
300
+ | `label` (required) | `string` | | Names the chart for screen readers. |
301
+ | `children` | `ReactNode` | | Content in the middle, e.g. the total. |
302
+ | `className` | `string` | | |
303
+ | `formatValue` | `((value: number) => string)` | | Values in the tooltip and for screen readers. |
304
+ | `palette` | `ChartPalette` | `chart` | Colours for items that don't give their own: the categorical palette (default), or shades of the theme's glow. |
305
+ | `size` | `number` | `200` | Diameter in px. |
306
+ | `thickness` | `number` | `0.32` | Ring thickness as a share of the radius. |
307
+
308
+ ### AllocationBar
309
+
310
+ Where a whole went, as one bar split into its parts (e.g. income across
311
+ spending, savings and bills), with a legend of each part's value and share.
312
+ Pointing at a part, in the bar or the legend, picks it out; screen readers
313
+ hear every part's value and share from the bar's label. Sweeps in from
314
+ the start the first time it's seen. Colours default to the chart palette in
315
+ order, like DonutChart's.
316
+
317
+ | Prop | Type | Default | Description |
318
+ | --- | --- | --- | --- |
319
+ | `items` (required) | `AllocationItem[]` | | |
320
+ | `label` (required) | `string` | | What the whole is, e.g. "Income in September", for screen readers. |
321
+ | `className` | `string` | | |
322
+ | `formatValue` | `((value: number) => string)` | | |
323
+ | `legend` | `boolean` | `true` | The list of parts under the bar (default on). |
324
+ | `locale` | `string` | | For the shares' percent format. |
325
+ | `palette` | `ChartPalette` | `chart` | Colours for items that don't give their own: the categorical palette (default), or shades of the theme's glow. |