@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,169 @@
1
+ # Money Input
2
+
3
+ Amounts in any currency and locale, with shorthand like 1.5L or 10k and sums like 120+45.50, exact to the paisa, and typed in other currencies, converted at your rates. Built on Input Group.
4
+
5
+ ```tsx
6
+ import { MoneyInput, MoneyCurrencyMenu, MoneyConversionHint } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { formatMoney, MoneyInput } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ /** Try 1.5L, 2cr, 10k or 1,52,000. Commits on blur or Enter; Escape reverts. */
18
+ export default function Default() {
19
+ const [amount, setAmount] = useState<number | null>(15200000);
20
+ return (
21
+ <div className="flex w-64 flex-col gap-2">
22
+ <MoneyInput aria-label="Amount" value={amount} onValueChange={setAmount} currency="INR" locale="en-IN" />
23
+ <p className="text-muted-foreground mt-0! text-xs">
24
+ Value: {amount === null ? 'empty' : `${amount} paise (${formatMoney(amount, 'INR', 'en-IN')})`}
25
+ </p>
26
+ </div>
27
+ );
28
+ }
29
+ ```
30
+
31
+ ### Dollars
32
+
33
+ ```tsx
34
+ import { MoneyInput } from '@rinzai/zen';
35
+ import { useState } from 'react';
36
+
37
+ /** Any currency and locale; `allowEmpty` makes an empty field mean null. */
38
+ export default function Dollars() {
39
+ const [amount, setAmount] = useState<number | null>(null);
40
+ return (
41
+ <MoneyInput
42
+ aria-label="Budget"
43
+ value={amount}
44
+ onValueChange={setAmount}
45
+ currency="USD"
46
+ locale="en-US"
47
+ allowEmpty
48
+ className="w-64"
49
+ />
50
+ );
51
+ }
52
+ ```
53
+
54
+ ### Convert
55
+
56
+ ```tsx
57
+ import { type ForeignAmount, formatMoney, MoneyInput, type RateTable } from '@rinzai/zen';
58
+ import { useState } from 'react';
59
+
60
+ // Your app fetches these (e.g. the ECB's daily rates, from your server); here, a fixed sample.
61
+ const SAMPLE: RateTable = {
62
+ base: 'EUR',
63
+ date: '2026-09-29',
64
+ rates: { INR: 98.4, USD: 1.17, GBP: 0.87, AED: 4.3, SGD: 1.5, JPY: 172.6 },
65
+ };
66
+ const loadRates = () => new Promise<RateTable>((done) => setTimeout(() => done(SAMPLE), 600));
67
+
68
+ /** Pick another currency from the symbol and type in it: it's converted into rupees at the loaded rate, and the original is kept. */
69
+ export default function Convert() {
70
+ const [amount, setAmount] = useState<number | null>(3274000);
71
+ const [original, setOriginal] = useState<ForeignAmount | null>(null);
72
+ return (
73
+ <div className="flex w-72 flex-col gap-2">
74
+ <MoneyInput
75
+ aria-label="Amount"
76
+ value={amount}
77
+ onValueChange={setAmount}
78
+ currency="INR"
79
+ locale="en-IN"
80
+ convert={{
81
+ currencies: ['USD', 'EUR', 'GBP', 'AED', 'SGD', 'JPY'],
82
+ loadRates,
83
+ source: 'ECB',
84
+ onForeign: setOriginal,
85
+ }}
86
+ />
87
+ <p className="text-muted-foreground mt-0! text-xs">
88
+ Saved: {amount === null ? 'empty' : formatMoney(amount, 'INR', 'en-IN')}
89
+ {original && `, typed as ${formatMoney(original.amount, original.currency, 'en-IN')}`}
90
+ </p>
91
+ </div>
92
+ );
93
+ }
94
+ ```
95
+
96
+ ### Compact
97
+
98
+ ```tsx
99
+ import { MoneyInput } from '@rinzai/zen';
100
+ import { useState } from 'react';
101
+
102
+ /** `compact`: shorter and right-aligned, for editing inside table rows. */
103
+ export default function Compact() {
104
+ const [amount, setAmount] = useState<number | null>(1640000);
105
+ return (
106
+ <MoneyInput
107
+ compact
108
+ aria-label="Groceries"
109
+ value={amount}
110
+ onValueChange={setAmount}
111
+ currency="INR"
112
+ locale="en-IN"
113
+ className="w-36"
114
+ />
115
+ );
116
+ }
117
+ ```
118
+
119
+ ## API reference
120
+
121
+ ### MoneyInput
122
+
123
+ Amount input in one currency. Shows grouped digits for the locale (1,52,000
124
+ for en-IN) and accepts shorthand: 1.5L, 2cr, 10k, 1.2m, and sums: 120+45.50,
125
+ 3*12.99. Commits on blur or Enter; Escape reverts. Values are integer minor
126
+ units (paise, cents), or null when empty.
127
+
128
+ With `convert`, the symbol becomes a currency switch: type an amount in e.g.
129
+ USD and it's converted at the rates you load (in the browser; the amount is
130
+ never sent anywhere). Replace the symbol with `start`, or build your own field
131
+ from useMoneyInput, MoneyCurrencyMenu and MoneyConversionHint.
132
+
133
+ | Prop | Type | Default | Description |
134
+ | --- | --- | --- | --- |
135
+ | `currency` (required) | `string` | | ISO 4217 code, e.g. "INR", "USD". |
136
+ | `allowEmpty` | `boolean` | | |
137
+ | `allowNegative` | `boolean` | | |
138
+ | `compact` | `boolean` | `false` | Smaller still and right-aligned, for editing inside table rows. |
139
+ | `convert` | `CurrencyConversion` | | Let people type in another currency, converted at the rates you load. |
140
+ | `defaultValue` | `number \| null` | | The first amount, when the field keeps its own. |
141
+ | `end` | `ReactNode` | | Content after the input, e.g. a unit or a clear button. |
142
+ | `live` | `boolean` | | For forms: report the amount (converted, when typed in another currency) on every keystroke instead of on blur, so a Save button can never miss it. Invalid text reports null. |
143
+ | `locale` | `string` | | Decides digit grouping and the symbol, e.g. "en-IN" (1,52,000) or "en-US" (152,000). Defaults to the LocaleProvider's. |
144
+ | `onCancel` | `(() => void)` | | Called on Escape, after reverting. |
145
+ | `onValueChange` | `((value: number \| null) => void)` | | |
146
+ | `size` | `"default" \| "lg" \| "sm"` | | Height, matching Button's: sm (32px), default (40px) or lg (44px). |
147
+ | `start` | `ReactNode` | | Replaces the currency symbol (or the currency switch), e.g. with your own. |
148
+ | `value` | `number \| null` | | Amount in minor units (paise, cents), or null when empty, when you keep it. |
149
+
150
+ ### MoneyCurrencyMenu
151
+
152
+ The currency switch for a converting field (`convert`): shows the symbol, or
153
+ the code of the foreign currency being typed in, and opens a menu of your
154
+ currency and the ones with a rate. Put it in an InputGroup's start slot.
155
+
156
+ | Prop | Type | Default | Description |
157
+ | --- | --- | --- | --- |
158
+ | `money` (required) | `{ inputProps: { readonly type: "text"; readonly inputMode: "decimal"; readonly autoComplete: "off"; readonly spellCheck: false; readonly value: string; readonly 'aria-invalid': true \| undefined; ... 4 more ...; readonly onKeyDown: (e: KeyboardEvent<...>) => void; }; ... 10 more ...; conversion: { ...; } \| null; }` | | |
159
+ | `className` | `string` | | |
160
+
161
+ ### MoneyConversionHint
162
+
163
+ The line under a converting field: getting the rate, the rate and what the
164
+ typed amount comes to, or what the last amount was converted from.
165
+
166
+ | Prop | Type | Default | Description |
167
+ | --- | --- | --- | --- |
168
+ | `money` (required) | `{ inputProps: { readonly type: "text"; readonly inputMode: "decimal"; readonly autoComplete: "off"; readonly spellCheck: false; readonly value: string; readonly 'aria-invalid': true \| undefined; ... 4 more ...; readonly onKeyDown: (e: KeyboardEvent<...>) => void; }; ... 10 more ...; conversion: { ...; } \| null; }` | | |
169
+ | `className` | `string` | | |
@@ -0,0 +1,80 @@
1
+ # Month Picker
2
+
3
+ A field showing a month that opens a year and a 12-month grid, with optional limits and clearing.
4
+
5
+ ```tsx
6
+ import { MonthPicker } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { MonthPicker } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ export default function Default() {
18
+ const [month, setMonth] = useState<string | null>('2026-09');
19
+ return <MonthPicker aria-label="Month" value={month} onValueChange={setMonth} locale="en-IN" className="w-48" />;
20
+ }
21
+ ```
22
+
23
+ ### Range
24
+
25
+ ```tsx
26
+ import { addMonths, MonthPicker } from '@rinzai/zen';
27
+ import { useState } from 'react';
28
+
29
+ /** A start and an optional end: the end can't come before the start, and can be cleared. */
30
+ export default function Range() {
31
+ const [start, setStart] = useState<string | null>('2026-10');
32
+ const [end, setEnd] = useState<string | null>('2027-09');
33
+ return (
34
+ <div className="flex items-end gap-3">
35
+ <label className="flex flex-col gap-2 text-sm font-medium">
36
+ Starts
37
+ <MonthPicker value={start} onValueChange={setStart} locale="en-US" className="w-40" />
38
+ </label>
39
+ <label className="flex flex-col gap-2 text-sm font-medium">
40
+ Ends
41
+ <MonthPicker
42
+ value={end}
43
+ onValueChange={setEnd}
44
+ locale="en-US"
45
+ min={start ? addMonths(start, 1) : undefined}
46
+ placeholder="No end"
47
+ clearable
48
+ clearLabel="No end"
49
+ className="w-40"
50
+ />
51
+ </label>
52
+ </div>
53
+ );
54
+ }
55
+ ```
56
+
57
+ ## API reference
58
+
59
+ ### MonthPicker
60
+
61
+ Month selector: a field showing "Sep 2026" that opens a year and a 12-month
62
+ grid. Months are "YYYY-MM" strings. When `clearable`, the popup also offers
63
+ `clearLabel` (e.g. "No end") to empty it. With `name`, it's submitted with a form.
64
+
65
+ | Prop | Type | Default | Description |
66
+ | --- | --- | --- | --- |
67
+ | `className` | `string` | | Classes for the trigger (e.g. a width, instead of the default full width). |
68
+ | `clearable` | `boolean` | `false` | Offers a clear option in the popup (shown when a month is set); clearing reports null. |
69
+ | `clearLabel` | `string` | | |
70
+ | `defaultValue` | `string \| null` | `null` | The first month, when the picker keeps its own. |
71
+ | `disabled` | `boolean` | `false` | |
72
+ | `id` | `string` | | |
73
+ | `locale` | `string` | | Decides month names and format, e.g. "en-IN". Defaults to the LocaleProvider's. |
74
+ | `max` | `string` | | Latest month that can be picked. |
75
+ | `min` | `string` | | Earliest month that can be picked ("YYYY-MM"). |
76
+ | `name` | `string` | | Form field name: the month ("YYYY-MM") is submitted with the form. |
77
+ | `onValueChange` | `((month: string \| null) => void)` | | |
78
+ | `placeholder` | `string` | | |
79
+ | `size` | `"default" \| "lg" \| "sm"` | `default` | Height, matching Button's: sm (32px), default (40px) or lg (44px). |
80
+ | `value` | `string \| null` | | "YYYY-MM", or null for none, when you keep it. |
@@ -0,0 +1,157 @@
1
+ # Navigation Menu
2
+
3
+ A site’s navigation: links, and labels that open glass panels of links on hover or click, switching at once between them; works with any router.
4
+
5
+ ```tsx
6
+ import { NavigationMenu, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import {
15
+ NavigationMenu,
16
+ NavigationMenuContent,
17
+ NavigationMenuItem,
18
+ NavigationMenuLink,
19
+ NavigationMenuTrigger,
20
+ } from '@rinzai/zen';
21
+
22
+ /**
23
+ * A site's top navigation: rest the pointer on Plan or Reports (or click, or
24
+ * Tab to one and press Enter) for its panel; once one is open, moving to the
25
+ * other switches at once.
26
+ */
27
+ export default function Default() {
28
+ return (
29
+ <NavigationMenu aria-label="Site">
30
+ <NavigationMenuItem>
31
+ <NavigationMenuLink href="#overview" active>
32
+ Overview
33
+ </NavigationMenuLink>
34
+ </NavigationMenuItem>
35
+ <NavigationMenuItem>
36
+ <NavigationMenuTrigger>Plan</NavigationMenuTrigger>
37
+ <NavigationMenuContent className="grid w-[26rem] grid-cols-2 gap-1">
38
+ <NavigationMenuLink href="#budgets" description="Limits for each category, month by month.">
39
+ Budgets
40
+ </NavigationMenuLink>
41
+ <NavigationMenuLink href="#goals" description="Savings with a target and a date.">
42
+ Goals
43
+ </NavigationMenuLink>
44
+ <NavigationMenuLink href="#bills" description="What's due, and when it repeats.">
45
+ Bills
46
+ </NavigationMenuLink>
47
+ <NavigationMenuLink href="#planner" description="The year ahead, item by item.">
48
+ Planner
49
+ </NavigationMenuLink>
50
+ </NavigationMenuContent>
51
+ </NavigationMenuItem>
52
+ <NavigationMenuItem>
53
+ <NavigationMenuTrigger>Reports</NavigationMenuTrigger>
54
+ <NavigationMenuContent className="flex w-60 flex-col">
55
+ <NavigationMenuLink href="#spending">Spending</NavigationMenuLink>
56
+ <NavigationMenuLink href="#income">Income</NavigationMenuLink>
57
+ <NavigationMenuLink href="#net-worth">Net worth</NavigationMenuLink>
58
+ </NavigationMenuContent>
59
+ </NavigationMenuItem>
60
+ <NavigationMenuItem>
61
+ <NavigationMenuLink href="#help">Help</NavigationMenuLink>
62
+ </NavigationMenuItem>
63
+ </NavigationMenu>
64
+ );
65
+ }
66
+ ```
67
+
68
+ ### Router
69
+
70
+ ```tsx
71
+ import {
72
+ NavigationMenu,
73
+ NavigationMenuContent,
74
+ NavigationMenuItem,
75
+ NavigationMenuLink,
76
+ NavigationMenuTrigger,
77
+ } from '@rinzai/zen';
78
+ import { useState } from 'react';
79
+
80
+ const ACCOUNTS = ['Everyday', 'Savings', 'Credit card'];
81
+
82
+ /**
83
+ * With your router: `asChild` puts each link's look and wiring on your own
84
+ * Link (a button stands in for one here), and `active` follows the page.
85
+ */
86
+ export default function Router() {
87
+ const [page, setPage] = useState('Everyday');
88
+ return (
89
+ <div className="flex flex-col items-center gap-4">
90
+ <NavigationMenu aria-label="Accounts">
91
+ <NavigationMenuItem>
92
+ <NavigationMenuTrigger>Accounts</NavigationMenuTrigger>
93
+ <NavigationMenuContent className="flex w-48 flex-col">
94
+ {ACCOUNTS.map((name) => (
95
+ <NavigationMenuLink key={name} asChild active={page === name}>
96
+ <button type="button" className="text-start" onClick={() => setPage(name)}>
97
+ {name}
98
+ </button>
99
+ </NavigationMenuLink>
100
+ ))}
101
+ </NavigationMenuContent>
102
+ </NavigationMenuItem>
103
+ </NavigationMenu>
104
+ <p className="text-muted-foreground mt-0! text-sm">Showing {page}</p>
105
+ </div>
106
+ );
107
+ }
108
+ ```
109
+
110
+ ## API reference
111
+
112
+ ### NavigationMenu
113
+
114
+ A site's navigation: links, and labels that open panels of links below
115
+ them, on hover (after `openDelay`; at once while another is open) or with a
116
+ click or Enter. Escape, a click outside or Tab moving on closes it. Fill it
117
+ with NavigationMenuItems, each a NavigationMenuLink, or a
118
+ NavigationMenuTrigger and a NavigationMenuContent. Name it with
119
+ `aria-label` when there's more than one nav on the page.
120
+
121
+ | Prop | Type | Default | Description |
122
+ | --- | --- | --- | --- |
123
+ | `closeDelay` | `number` | `200` | How long a panel opened by the pointer stays after it leaves, in ms (time to reach the panel). |
124
+ | `openDelay` | `number` | `150` | How long the pointer rests on a label before its panel opens, in ms. |
125
+
126
+ ### NavigationMenuItem
127
+
128
+ One place in the menu: a link, or a label with its panel.
129
+
130
+ No props of its own; it accepts the props of the element it renders.
131
+
132
+ ### NavigationMenuTrigger
133
+
134
+ The label that opens its item's panel; a chevron after it turns over while it's open.
135
+
136
+ No props of its own; it accepts the props of the element it renders.
137
+
138
+ ### NavigationMenuContent
139
+
140
+ The panel: glass, below its label (above it if there's no room), lined up with its start. Lay out its links as you like.
141
+
142
+ | Prop | Type | Default | Description |
143
+ | --- | --- | --- | --- |
144
+ | `align` | `"center" \| "end" \| "start"` | `start` | |
145
+ | `offset` | `number` | `8` | Gap below the label, in px. |
146
+
147
+ ### NavigationMenuLink
148
+
149
+ A link: one of the bar's labels at the top, or a row in a panel (with an
150
+ optional `description` under its text). `active` marks the page you're on.
151
+ With `asChild`, your router's link instead of an `<a>`.
152
+
153
+ | Prop | Type | Default | Description |
154
+ | --- | --- | --- | --- |
155
+ | `active` | `boolean` | | The page it leads to is the one you're on. |
156
+ | `asChild` | `boolean` | | Put its props on your own link (the single child), e.g. a router's Link. |
157
+ | `description` | `string` | | In a panel, a line under the link's text. |
@@ -0,0 +1,129 @@
1
+ # Number Field
2
+
3
+ A number to type or step with − and +, the arrow keys and Page Up and Down: shown the locale’s way, as a percentage or a unit, and kept within bounds. Each change says whether it was a step (and by how much) or typed, and onValueCommit fires once it’s finished.
4
+
5
+ ```tsx
6
+ import { NumberField } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Field, NumberField } from '@rinzai/zen';
15
+
16
+ /** Type, use − and +, or ↑↓: kept between 1 and 12. */
17
+ export default function Default() {
18
+ return (
19
+ <Field label="Split between" hint="People sharing the bill" className="w-56">
20
+ <NumberField defaultValue={2} min={1} max={12} />
21
+ </Field>
22
+ );
23
+ }
24
+ ```
25
+
26
+ ### Formats
27
+
28
+ ```tsx
29
+ import { Field, NumberField } from '@rinzai/zen';
30
+ import { useState } from 'react';
31
+
32
+ /** `formatOptions` shows it as a percentage, a unit or with set decimals, and reads it back typed that way. */
33
+ export default function Formats() {
34
+ const [rate, setRate] = useState<number | null>(0.065);
35
+ return (
36
+ <div className="flex w-56 flex-col gap-4">
37
+ <Field label="Interest rate">
38
+ <NumberField
39
+ value={rate}
40
+ onValueChange={setRate}
41
+ step={0.0025}
42
+ min={0}
43
+ max={0.3}
44
+ formatOptions={{ style: 'percent', minimumFractionDigits: 2 }}
45
+ />
46
+ </Field>
47
+ <Field label="Distance">
48
+ <NumberField
49
+ defaultValue={12.5}
50
+ step={0.5}
51
+ min={0}
52
+ formatOptions={{ style: 'unit', unit: 'kilometer' }}
53
+ />
54
+ </Field>
55
+ </div>
56
+ );
57
+ }
58
+ ```
59
+
60
+ ### Steps
61
+
62
+ ```tsx
63
+ import { Field, NumberField } from '@rinzai/zen';
64
+ import { useState } from 'react';
65
+
66
+ const clamp = (n: number) => Math.min(10, Math.max(0, n));
67
+
68
+ /**
69
+ * A count kept somewhere else, such as a server another device also changes.
70
+ * `onValueCommit` says how each change came about: − + and ↑↓ send their step
71
+ * (`delta`), added to whatever the count is by then; a typed number is sent
72
+ * once, on Enter or on leaving the field.
73
+ */
74
+ export default function Steps() {
75
+ const [count, setCount] = useState(3);
76
+ const [sent, setSent] = useState<string | null>(null);
77
+ return (
78
+ <Field label="Episodes watched" hint={sent ? `Sent ${sent}` : 'Out of 10'} className="w-56">
79
+ <NumberField
80
+ value={count}
81
+ min={0}
82
+ max={10}
83
+ size="sm"
84
+ onValueCommit={(n, change) => {
85
+ if (change.reason === 'step') {
86
+ setCount((now) => clamp(now + change.delta));
87
+ setSent(change.delta > 0 ? `+${change.delta}` : `−${-change.delta}`);
88
+ } else {
89
+ setCount(n ?? 0);
90
+ setSent(String(n ?? 0));
91
+ }
92
+ }}
93
+ />
94
+ </Field>
95
+ );
96
+ }
97
+ ```
98
+
99
+ ## API reference
100
+
101
+ ### NumberField
102
+
103
+ A number to type or step: − and + on either side, ↑↓ by `step`, Page Up and
104
+ Page Down by `largeStep`, Home and End to `min` and `max`. Shown in the
105
+ locale's way (`formatOptions`: a percentage, a unit, decimals) and read back
106
+ from what's typed in it, digits, separators and all. Kept within `min` and
107
+ `max` once you leave the field; empty is null.
108
+
109
+ Each change says how it came about: `{ reason: 'step', delta }` for − +,
110
+ ↑↓ and Page Up/Down, `'input'` for typing, `'limit'` for Home and End. So a
111
+ step can be sent on as a step, and the number kept by whoever owns it.
112
+ `onValueCommit` fires once per finished change: each step, and what's typed
113
+ on Enter or on leaving the field.
114
+
115
+ | Prop | Type | Default | Description |
116
+ | --- | --- | --- | --- |
117
+ | `defaultValue` | `number \| null` | `null` | The first number, when the field keeps its own. |
118
+ | `formatOptions` | `NumberFormatOptions` | | How it's shown, as for Intl.NumberFormat: { style: 'percent' }, { maximumFractionDigits: 2 }, a unit. |
119
+ | `invalid` | `boolean` | | Show the field in its error state. |
120
+ | `largeStep` | `number` | `step * 10` | What Page Up and Page Down do. Default ten steps. |
121
+ | `locale` | `string` | | Defaults to the LocaleProvider's. |
122
+ | `max` | `number` | | |
123
+ | `min` | `number` | | |
124
+ | `name` | `string` | | Form field name: the number is submitted under it. |
125
+ | `onValueChange` | `((value: number \| null, details: NumberFieldChange) => void)` | | Every change, typing included; `details` says how it came about. |
126
+ | `onValueCommit` | `((value: number \| null, details: NumberFieldChange) => void)` | | Each finished change: a step, Home or End, or what's typed, on Enter or on leaving the field. |
127
+ | `size` | `"default" \| "lg" \| "sm"` | `default` | Height, matching Button's. |
128
+ | `step` | `number` | `1` | What ↑↓ and − + add or take away. Default 1. |
129
+ | `value` | `number \| null` | | The number, or null for empty, when you keep it. |
@@ -0,0 +1,44 @@
1
+ # Page Header
2
+
3
+ Eyebrow, aurora title and lead paragraph for the top of a page.
4
+
5
+ ```tsx
6
+ import { PageHeader } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { buttonVariants, PageHeader } from '@rinzai/zen';
15
+
16
+ export default function Default() {
17
+ return (
18
+ <PageHeader eyebrow="Budget" title="Month overview" lead="Where your money went in September, and what's left.">
19
+ <div className="flex gap-3">
20
+ <a href="#add" className={buttonVariants()}>
21
+ Add entry
22
+ </a>
23
+ <a href="#export" className={buttonVariants({ variant: 'outline' })}>
24
+ Export
25
+ </a>
26
+ </div>
27
+ </PageHeader>
28
+ );
29
+ }
30
+ ```
31
+
32
+ ## API reference
33
+
34
+ ### PageHeader
35
+
36
+ Top of a page: a small eyebrow label, the title in the aurora gradient, and a lead paragraph.
37
+
38
+ | Prop | Type | Default | Description |
39
+ | --- | --- | --- | --- |
40
+ | `title` (required) | `ReactNode` | | |
41
+ | `children` | `ReactNode` | | Anything after the lead, e.g. a row of buttons. |
42
+ | `className` | `string` | | |
43
+ | `eyebrow` | `ReactNode` | | |
44
+ | `lead` | `ReactNode` | | |
@@ -0,0 +1,93 @@
1
+ # Page Transition
2
+
3
+ The old page fades out as the new one rises in, when your router changes page in a view transition.
4
+
5
+ ```tsx
6
+ import { PageTransition } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { PageTransition, Pill, PillIndicator, Pills, Stat, StatRow } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+ import { flushSync } from 'react-dom';
17
+
18
+ const PAGES = {
19
+ overview: {
20
+ title: 'Overview',
21
+ stats: [
22
+ ['Net', '$2,410'],
23
+ ['Spent', '$1,845'],
24
+ ],
25
+ },
26
+ budgets: {
27
+ title: 'Budgets',
28
+ stats: [
29
+ ['On track', '4 of 5'],
30
+ ['Left', '$655'],
31
+ ],
32
+ },
33
+ goals: {
34
+ title: 'Goals',
35
+ stats: [
36
+ ['Saved', '$12,940'],
37
+ ['Next', 'Holiday'],
38
+ ],
39
+ },
40
+ } as const;
41
+ type Page = keyof typeof PAGES;
42
+
43
+ /** A router does this for you (React Router: `viewTransition` on a Link); here, by hand. */
44
+ export default function Default() {
45
+ const [page, setPage] = useState<Page>('overview');
46
+ const go = (next: Page) => {
47
+ if (!document.startViewTransition) return setPage(next);
48
+ document.startViewTransition(() => flushSync(() => setPage(next)));
49
+ };
50
+ return (
51
+ <div className="flex w-full max-w-md flex-col gap-4">
52
+ <nav aria-label="Pages">
53
+ <Pills>
54
+ <PillIndicator />
55
+ {(Object.keys(PAGES) as Page[]).map((p) => (
56
+ <Pill
57
+ key={p}
58
+ href={`#${p}`}
59
+ active={p === page}
60
+ onClick={(e) => {
61
+ e.preventDefault();
62
+ go(p);
63
+ }}
64
+ >
65
+ {PAGES[p].title}
66
+ </Pill>
67
+ ))}
68
+ </Pills>
69
+ </nav>
70
+ <PageTransition>
71
+ <h3 className="mb-3 text-lg font-semibold">{PAGES[page].title}</h3>
72
+ <StatRow className="sm:grid-cols-2!">
73
+ {PAGES[page].stats.map(([label, value]) => (
74
+ <Stat key={label} label={label} value={value} />
75
+ ))}
76
+ </StatRow>
77
+ </PageTransition>
78
+ </div>
79
+ );
80
+ }
81
+ ```
82
+
83
+ ## API reference
84
+
85
+ ### PageTransition
86
+
87
+ The page's content, which changes between pages: wrap your route's outlet in
88
+ it, and when the router changes page in a view transition (React Router's
89
+ `viewTransition`, or document.startViewTransition yourself) the old page
90
+ fades out as the new one rises in, while everything outside (header, nav)
91
+ stays still. One per page.
92
+
93
+ No props of its own; it accepts the props of the element it renders.