@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,83 @@
1
+ # Toggle Group
2
+
3
+ Buttons that stay pressed, in a row: any number of them, like filters, or at most one. One stop in the tab order; the arrow keys go through them.
4
+
5
+ ```tsx
6
+ import { ToggleGroup, ToggleGroupItem } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { ToggleGroup, ToggleGroupItem } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ /** Filters: any number on at once. */
18
+ export default function Default() {
19
+ const [kinds, setKinds] = useState<readonly string[]>(['expense', 'income']);
20
+ return (
21
+ <div className="flex flex-col items-center gap-3">
22
+ <ToggleGroup multiple aria-label="Show" value={kinds} onValueChange={setKinds}>
23
+ <ToggleGroupItem value="expense">Expenses</ToggleGroupItem>
24
+ <ToggleGroupItem value="income">Income</ToggleGroupItem>
25
+ <ToggleGroupItem value="transfer">Transfers</ToggleGroupItem>
26
+ </ToggleGroup>
27
+ <p className="text-muted-foreground mt-0! text-sm">
28
+ {kinds.length ? `Showing ${kinds.length} of 3 kinds` : 'Showing everything'}
29
+ </p>
30
+ </div>
31
+ );
32
+ }
33
+ ```
34
+
35
+ ### Single
36
+
37
+ ```tsx
38
+ import { ToggleGroup, ToggleGroupItem } from '@rinzai/zen';
39
+ // Icons from Heroicons (https://heroicons.com): ChartBar (chart-bar), ListBullet (list-bullet), Squares (squares-2x2).
40
+
41
+ /** At most one: pressing the pressed one releases it. Icon-only items carry an aria-label. */
42
+ export default function Single() {
43
+ return (
44
+ <ToggleGroup aria-label="Group by" defaultValue="list">
45
+ <ToggleGroupItem value="list" aria-label="As a list" className="px-2">
46
+ <ListBullet className="size-4" />
47
+ </ToggleGroupItem>
48
+ <ToggleGroupItem value="cards" aria-label="As cards" className="px-2">
49
+ <Squares className="size-4" />
50
+ </ToggleGroupItem>
51
+ <ToggleGroupItem value="chart" aria-label="As a chart" className="px-2">
52
+ <ChartBar className="size-4" />
53
+ </ToggleGroupItem>
54
+ </ToggleGroup>
55
+ );
56
+ }
57
+ ```
58
+
59
+ ## API reference
60
+
61
+ ### ToggleGroup
62
+
63
+ Buttons that stay pressed, in a row: any number of them (`multiple`, e.g.
64
+ filters), or at most one, which can be released again. For exactly one
65
+ always chosen, use Segmented. ←→ move between them, Home and End jump to
66
+ the ends; Space or Enter presses.
67
+
68
+ | Prop | Type | Default | Description |
69
+ | --- | --- | --- | --- |
70
+ | `children` | `ReactNode` | | The ToggleGroupItems. |
71
+ | `defaultValue` | `string \| readonly string[] \| null` | | |
72
+ | `multiple` | `boolean` | | At most one pressed (the default); pressing another releases it. Any number pressed. |
73
+ | `name` | `string` | | Form field name: each pressed item's value is submitted under it. |
74
+ | `onValueChange` | `((value: string \| null) => void) \| ((value: readonly string[]) => void)` | | |
75
+ | `value` | `string \| readonly string[] \| null` | | The pressed item's value, or null for none, when you keep it. |
76
+
77
+ ### ToggleGroupItem
78
+
79
+ One button of a ToggleGroup: text, an icon (give it an aria-label), or both.
80
+
81
+ | Prop | Type | Default | Description |
82
+ | --- | --- | --- | --- |
83
+ | `value` (required) | `string` | | |
@@ -0,0 +1,38 @@
1
+ # Toggle
2
+
3
+ An on/off switch.
4
+
5
+ ```tsx
6
+ import { Toggle } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Toggle } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ export default function Default() {
18
+ const [on, setOn] = useState(true);
19
+ return (
20
+ <label className="flex w-64 items-center justify-between">
21
+ Repeats every month
22
+ <Toggle checked={on} onCheckedChange={setOn} />
23
+ </label>
24
+ );
25
+ }
26
+ ```
27
+
28
+ ## API reference
29
+
30
+ ### Toggle
31
+
32
+ On/off switch. A native checkbox with the switch role underneath, so forms
33
+ and screen readers work as they do for any checkbox. Space and Enter both
34
+ flip it (Enter doesn't submit a surrounding form), as for a switch button.
35
+
36
+ | Prop | Type | Default | Description |
37
+ | --- | --- | --- | --- |
38
+ | `onCheckedChange` | `((checked: boolean) => void)` | | |
@@ -0,0 +1,97 @@
1
+ # Toolbar
2
+
3
+ A row of controls for one thing, like a table’s: one stop for Tab, and the arrow keys through its buttons, toggles and fields.
4
+
5
+ ```tsx
6
+ import { Toolbar, ToolbarSeparator } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Button, Toolbar, ToolbarSeparator, ToggleGroup, ToggleGroupItem } from '@rinzai/zen';
15
+ // Icons from Heroicons (https://heroicons.com): ChartBar (chart-bar), ListBullet (list-bullet), Settings (cog-6-tooth), Table (table-cells).
16
+
17
+ /** A table's controls: one stop for Tab, and ←→ through them all, the toggle group's items too. */
18
+ export default function Default() {
19
+ return (
20
+ <Toolbar aria-label="Entries">
21
+ <ToggleGroup aria-label="View" defaultValue="table">
22
+ <ToggleGroupItem value="table" aria-label="Table" className="px-2">
23
+ <Table className="size-4" />
24
+ </ToggleGroupItem>
25
+ <ToggleGroupItem value="list" aria-label="List" className="px-2">
26
+ <ListBullet className="size-4" />
27
+ </ToggleGroupItem>
28
+ <ToggleGroupItem value="chart" aria-label="Chart" className="px-2">
29
+ <ChartBar className="size-4" />
30
+ </ToggleGroupItem>
31
+ </ToggleGroup>
32
+ <ToolbarSeparator />
33
+ <Button variant="ghost" size="sm">
34
+ Filter
35
+ </Button>
36
+ <Button variant="ghost" size="sm">
37
+ Sort
38
+ </Button>
39
+ <ToolbarSeparator />
40
+ <Button variant="ghost" size="icon-sm" aria-label="Table settings">
41
+ <Settings className="size-4" />
42
+ </Button>
43
+ </Toolbar>
44
+ );
45
+ }
46
+ ```
47
+
48
+ ### Vertical
49
+
50
+ ```tsx
51
+ import { Button, Toolbar, ToolbarSeparator, ToggleGroup, ToggleGroupItem } from '@rinzai/zen';
52
+ // Icons from Heroicons (https://heroicons.com): ChartBar (chart-bar), MinusMicro (minus), PlusMicro (plus), Squares (squares-2x2).
53
+
54
+ /** Upright beside what it controls, e.g. a chart: ↑↓ move through it, and its toggle group stacks too. */
55
+ export default function Vertical() {
56
+ return (
57
+ <Toolbar aria-label="Chart" orientation="vertical">
58
+ <Button variant="ghost" size="icon-sm" aria-label="Zoom in">
59
+ <PlusMicro />
60
+ </Button>
61
+ <Button variant="ghost" size="icon-sm" aria-label="Zoom out">
62
+ <MinusMicro />
63
+ </Button>
64
+ <ToolbarSeparator />
65
+ <ToggleGroup aria-label="Chart type" defaultValue="bars">
66
+ <ToggleGroupItem value="bars" aria-label="Bars" className="px-2">
67
+ <ChartBar className="size-4" />
68
+ </ToggleGroupItem>
69
+ <ToggleGroupItem value="tiles" aria-label="Tiles" className="px-2">
70
+ <Squares className="size-4" />
71
+ </ToggleGroupItem>
72
+ </ToggleGroup>
73
+ </Toolbar>
74
+ );
75
+ }
76
+ ```
77
+
78
+ ## API reference
79
+
80
+ ### Toolbar
81
+
82
+ A row of controls for one thing, e.g. a table's or an editor's: buttons,
83
+ ToggleGroups, Selects, a search field. Tab enters it once; ←→ (↑↓ when
84
+ `orientation="vertical"`) move between its controls, Home and End jump to
85
+ the ends. Divide it with ToolbarSeparator. Give it an aria-label.
86
+
87
+ | Prop | Type | Default | Description |
88
+ | --- | --- | --- | --- |
89
+ | `orientation` | `"horizontal" \| "vertical"` | `horizontal` | |
90
+
91
+ ### ToolbarSeparator
92
+
93
+ A hairline between groups of a Toolbar's controls, across its direction.
94
+
95
+ | Prop | Type | Default | Description |
96
+ | --- | --- | --- | --- |
97
+ | `decorative` | `boolean` | | Only visual: hidden from screen readers. |
@@ -0,0 +1,60 @@
1
+ # Tooltip
2
+
3
+ A short glass hint on hover or keyboard focus, describing its trigger to screen readers.
4
+
5
+ ```tsx
6
+ import { Tooltip, TooltipTrigger, TooltipContent } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Badge, Tooltip, TooltipContent, TooltipTrigger } from '@rinzai/zen';
15
+
16
+ /** Shows on hover and on keyboard focus; the trigger is described by it for screen readers. */
17
+ export default function Default() {
18
+ return (
19
+ <Tooltip>
20
+ <TooltipTrigger className="rounded-md">
21
+ <Badge variant="secondary">Beta</Badge>
22
+ </TooltipTrigger>
23
+ <TooltipContent>
24
+ <div className="p-3 text-sm">
25
+ <p className="font-medium">Beta feature</p>
26
+ <p className="text-muted-foreground mt-0.5!">This feature is still in preview and may change.</p>
27
+ </div>
28
+ </TooltipContent>
29
+ </Tooltip>
30
+ );
31
+ }
32
+ ```
33
+
34
+ ## API reference
35
+
36
+ ### Tooltip
37
+
38
+ A short hint shown while the pointer is over its trigger or the trigger has
39
+ keyboard focus; Escape hides it. The trigger is described by it for screen
40
+ readers. Put a TooltipTrigger and a TooltipContent inside. For anything to
41
+ click or fill in, use Popover.
42
+
43
+ No props of its own; it accepts the props of the element it renders.
44
+
45
+ ### TooltipTrigger
46
+
47
+ What the tooltip describes: a button by default. With `asChild`, your own focusable element instead.
48
+
49
+ | Prop | Type | Default | Description |
50
+ | --- | --- | --- | --- |
51
+ | `asChild` | `boolean` | | Put the trigger's props on your own focusable element instead of rendering a button. |
52
+
53
+ ### TooltipContent
54
+
55
+ The hint: glass, below the trigger (above it if there's no room), centred on it by default.
56
+
57
+ | Prop | Type | Default | Description |
58
+ | --- | --- | --- | --- |
59
+ | `align` | `"center" \| "end" \| "start"` | `center` | Centred on the trigger (default), or lined up with its start or end edge. |
60
+ | `offset` | `number` | `5` | Gap from the trigger, in px. |
@@ -0,0 +1,295 @@
1
+ # Tree Table
2
+
3
+ Rows that open into child rows, animated, built from useTree and the Table parts. With virtual rows and editable cells, enough for a budget planner.
4
+
5
+ ```tsx
6
+ import { TreeRow, TreeCell, TreeLabel, TreeToggle } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Basic
12
+
13
+ ```tsx
14
+ import {
15
+ Table,
16
+ TableBody,
17
+ TableContainer,
18
+ TableHead,
19
+ TableHeader,
20
+ TableRow,
21
+ TreeCell,
22
+ TreeLabel,
23
+ TreeRow,
24
+ useTree,
25
+ } from '@rinzai/zen';
26
+
27
+ interface Node {
28
+ name: string;
29
+ size: string;
30
+ children?: Node[];
31
+ }
32
+
33
+ const FILES: Node[] = [
34
+ {
35
+ name: 'packages',
36
+ size: '412 kB',
37
+ children: [
38
+ {
39
+ name: 'button',
40
+ size: '6 kB',
41
+ children: [
42
+ { name: 'index.tsx', size: '2 kB' },
43
+ { name: 'index.test.tsx', size: '4 kB' },
44
+ ],
45
+ },
46
+ { name: 'table', size: '14 kB', children: [{ name: 'index.tsx', size: '9 kB' }] },
47
+ ],
48
+ },
49
+ { name: 'docs', size: '88 kB', children: [{ name: 'app', size: '80 kB' }] },
50
+ { name: 'package.json', size: '3 kB' },
51
+ ];
52
+
53
+ /** Nested data in, visible rows out: useTree handles opening, closing and the animation. Start collapsed with defaultExpanded={false}. */
54
+ export default function Basic() {
55
+ const tree = useTree({ items: FILES, getKey: (n) => n.name + n.size, getChildren: (n) => n.children });
56
+ return (
57
+ <TableContainer className="w-full max-w-md">
58
+ <Table {...tree.tableProps}>
59
+ <TableHeader>
60
+ <TableRow>
61
+ <TableHead>Name</TableHead>
62
+ <TableHead numeric>Size</TableHead>
63
+ </TableRow>
64
+ </TableHeader>
65
+ <TableBody>
66
+ {tree.rows.map((row) => (
67
+ <TreeRow key={row.key} row={row} tree={tree}>
68
+ <TreeCell className={row.hasChildren ? 'font-medium' : undefined}>
69
+ <TreeLabel row={row} tree={tree}>
70
+ {row.item.name}
71
+ </TreeLabel>
72
+ </TreeCell>
73
+ <TreeCell numeric className="text-muted-foreground">
74
+ {row.item.size}
75
+ </TreeCell>
76
+ </TreeRow>
77
+ ))}
78
+ </TableBody>
79
+ </Table>
80
+ </TableContainer>
81
+ );
82
+ }
83
+ ```
84
+
85
+ ### Planner
86
+
87
+ ```tsx
88
+ import {
89
+ EditableCell,
90
+ formatMoney,
91
+ MoneyInput,
92
+ Table,
93
+ TableBody,
94
+ TableContainer,
95
+ TableFooter,
96
+ TableFooterCell,
97
+ TableHead,
98
+ TableHeader,
99
+ TableRow,
100
+ TableSpacerRow,
101
+ TreeCell,
102
+ TreeLabel,
103
+ TreeRow,
104
+ useTree,
105
+ useVirtualList,
106
+ } from '@rinzai/zen';
107
+ import { useRef, useState } from 'react';
108
+
109
+ const MONTHS = ['Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar'];
110
+ const ROW_HEIGHT = 36;
111
+
112
+ interface Line {
113
+ key: string;
114
+ label: string;
115
+ /** Amount per month, in paise; groups add up their items. */
116
+ amounts?: number[];
117
+ items?: Line[];
118
+ }
119
+
120
+ const item = (label: string, rupees: number[]): Line => ({
121
+ key: label.toLowerCase().replace(/\W+/g, '-'),
122
+ label,
123
+ amounts: rupees.map((r) => r * 100),
124
+ });
125
+
126
+ const PLAN: Line[] = [
127
+ {
128
+ key: 'home',
129
+ label: 'Home',
130
+ items: [
131
+ item('Rent', [18500, 18500, 18500, 18500, 18500, 18500]),
132
+ item('Electricity', [1400, 1250, 1600, 1900, 1700, 1300]),
133
+ item('Internet', [799, 799, 799, 799, 799, 799]),
134
+ ],
135
+ },
136
+ {
137
+ key: 'food',
138
+ label: 'Food',
139
+ items: [
140
+ item('Groceries', [9000, 9500, 12000, 9000, 9000, 9500]),
141
+ item('Dining out', [3000, 2500, 6000, 2000, 2500, 3000]),
142
+ ],
143
+ },
144
+ {
145
+ key: 'transport',
146
+ label: 'Transport',
147
+ items: [
148
+ item('Metro card', [1200, 1200, 1200, 1200, 1200, 1200]),
149
+ item('Cabs', [1500, 1800, 2500, 1500, 1200, 1600]),
150
+ ],
151
+ },
152
+ ];
153
+
154
+ const total = (line: Line, month: number): number =>
155
+ line.items ? line.items.reduce((sum, i) => sum + total(i, month), 0) : (line.amounts?.[month] ?? 0);
156
+
157
+ /**
158
+ * A budget grid like a planner: categories open into items, months run across,
159
+ * and the header, first column and totals stay put while it scrolls either way.
160
+ * Click an amount to change it. Rows are virtualized, so long plans stay fast.
161
+ */
162
+ export default function Planner() {
163
+ const [plan, setPlan] = useState(PLAN);
164
+ const tree = useTree({ items: plan, getKey: (l) => l.key, getChildren: (l) => l.items, rowHeight: ROW_HEIGHT });
165
+ const scrollRef = useRef<HTMLDivElement>(null);
166
+ const rows = useVirtualList({ count: tree.rows.length, itemHeight: ROW_HEIGHT, scrollRef, overscan: 12 });
167
+ const inr = (paise: number) => formatMoney(paise, 'INR', 'en-IN', { showDecimals: false });
168
+
169
+ const setAmount = (key: string, month: number, paise: number | null) =>
170
+ setPlan((p) =>
171
+ p.map((group) => ({
172
+ ...group,
173
+ items: group.items?.map((i) =>
174
+ i.key === key ? { ...i, amounts: i.amounts!.map((a, m) => (m === month ? (paise ?? 0) : a)) } : i,
175
+ ),
176
+ })),
177
+ );
178
+
179
+ return (
180
+ <TableContainer ref={scrollRef} label="Budget plan" className="max-h-80 w-full">
181
+ <Table {...tree.tableProps} className="w-max min-w-full">
182
+ <TableHeader>
183
+ <TableRow>
184
+ <TableHead sticky="start" className="min-w-44">
185
+ Budget
186
+ </TableHead>
187
+ {MONTHS.map((m) => (
188
+ <TableHead key={m} numeric className="min-w-28">
189
+ {m}
190
+ </TableHead>
191
+ ))}
192
+ </TableRow>
193
+ </TableHeader>
194
+ <TableBody>
195
+ <TableSpacerRow height={rows.paddingTop} colSpan={MONTHS.length + 1} />
196
+ {rows.items.map(({ index }) => {
197
+ const row = tree.rows[index];
198
+ const line = row.item;
199
+ return (
200
+ <TreeRow key={row.key} row={row} tree={tree}>
201
+ <TreeCell sticky="start" className={row.hasChildren ? 'font-medium' : undefined}>
202
+ <TreeLabel row={row} tree={tree}>
203
+ <span className="truncate">{line.label}</span>
204
+ </TreeLabel>
205
+ </TreeCell>
206
+ {MONTHS.map((month, m) => (
207
+ <TreeCell key={month} numeric>
208
+ {row.hasChildren ? (
209
+ <span className="text-muted-foreground px-1.5">{inr(total(line, m))}</span>
210
+ ) : (
211
+ <EditableCell
212
+ label={`${line.label}, ${month}: ${inr(total(line, m))}`}
213
+ editor={(close) => (
214
+ <MoneyInput
215
+ compact
216
+ autoFocus
217
+ allowEmpty
218
+ aria-label={`${line.label}, ${month}`}
219
+ value={total(line, m)}
220
+ currency="INR"
221
+ locale="en-IN"
222
+ className="w-28"
223
+ onValueChange={(v) => {
224
+ setAmount(line.key, m, v);
225
+ close();
226
+ }}
227
+ onCancel={close}
228
+ />
229
+ )}
230
+ >
231
+ {inr(total(line, m))}
232
+ </EditableCell>
233
+ )}
234
+ </TreeCell>
235
+ ))}
236
+ </TreeRow>
237
+ );
238
+ })}
239
+ <TableSpacerRow height={rows.paddingBottom} colSpan={MONTHS.length + 1} />
240
+ </TableBody>
241
+ <TableFooter>
242
+ <TableRow>
243
+ <TableFooterCell sticky="start">Total</TableFooterCell>
244
+ {MONTHS.map((month, m) => (
245
+ <TableFooterCell key={month} numeric>
246
+ {inr(plan.reduce((sum, g) => sum + total(g, m), 0))}
247
+ </TableFooterCell>
248
+ ))}
249
+ </TableRow>
250
+ </TableFooter>
251
+ </Table>
252
+ </TableContainer>
253
+ );
254
+ }
255
+ ```
256
+
257
+ ## API reference
258
+
259
+ ### TreeRow
260
+
261
+ A table row wired to useTree: tree semantics, the keyboard and the open/close animation.
262
+
263
+ | Prop | Type | Default | Description |
264
+ | --- | --- | --- | --- |
265
+ | `row` (required) | `TreeRowData<T>` | | |
266
+ | `tree` (required) | `{ rows: TreeRowData<T>[]; toggle: (key: string) => void; isExpanded: (key: string) => boolean; expand: (key: string) => void; collapse: (key: string) => void; expandAll: () => void; ... 4 more ...; rowHeight: number; }` | | |
267
+
268
+ ### TreeCell
269
+
270
+ A cell in a TreeRow: its content sits in a box whose height animates as groups open and close.
271
+
272
+ | Prop | Type | Default | Description |
273
+ | --- | --- | --- | --- |
274
+ | `numeric` | `boolean` | | Aligned to the end, tabular digits. |
275
+ | `sticky` | `"start"` | | Stay in view when the table scrolls sideways. |
276
+
277
+ ### TreeLabel
278
+
279
+ The first column's content: indented by depth, with the row's toggle (or a spacer, so labels line up).
280
+
281
+ | Prop | Type | Default | Description |
282
+ | --- | --- | --- | --- |
283
+ | `row` (required) | `TreeRowData<T>` | | |
284
+ | `tree` (required) | `{ rows: TreeRowData<T>[]; toggle: (key: string) => void; isExpanded: (key: string) => boolean; expand: (key: string) => void; collapse: (key: string) => void; expandAll: () => void; ... 4 more ...; rowHeight: number; }` | | |
285
+ | `className` | `string` | | |
286
+ | `indent` | `number` | `20` | Indent per level, in px. |
287
+
288
+ ### TreeToggle
289
+
290
+ Chevron button that opens and closes a row: points right when closed, down when open.
291
+
292
+ | Prop | Type | Default | Description |
293
+ | --- | --- | --- | --- |
294
+ | `expanded` (required) | `boolean` | | |
295
+ | `onToggle` (required) | `() => void` | | |
@@ -0,0 +1,50 @@
1
+ # Trend
2
+
3
+ A change, like +12% with an arrow, coloured by whether it is good news.
4
+
5
+ ```tsx
6
+ import { Trend } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Stat, StatRow, Trend } from '@rinzai/zen';
15
+
16
+ /** Spending going up is bad news (`good="down"`); savings going up is good. */
17
+ export default function Default() {
18
+ return (
19
+ <StatRow className="w-full max-w-xl">
20
+ <Stat
21
+ label="Spent"
22
+ value="$1,845"
23
+ hint={
24
+ <Trend value={0.12} good="down">
25
+ vs last month
26
+ </Trend>
27
+ }
28
+ />
29
+ <Stat label="Saved" value="$640" hint={<Trend value={0.084}>vs last month</Trend>} />
30
+ <Stat label="Bills" value="$1,210" hint={<Trend value={0}>vs last month</Trend>} />
31
+ </StatRow>
32
+ );
33
+ }
34
+ ```
35
+
36
+ ## API reference
37
+
38
+ ### Trend
39
+
40
+ A change, e.g. "+12%" with an arrow, coloured by whether it's good news:
41
+ savings going up is, spending going up isn't (`good="down"`). `value` is
42
+ the change as a fraction (0.12 for 12%); children follow it, muted, e.g.
43
+ "vs last month".
44
+
45
+ | Prop | Type | Default | Description |
46
+ | --- | --- | --- | --- |
47
+ | `value` (required) | `number` | | The change as a fraction: 0.12 is 12% up, -0.05 is 5% down. |
48
+ | `format` | `((value: number) => string)` | | How the change reads, e.g. as money instead of a percent. |
49
+ | `good` | `"down" \| "up"` | `up` | Which way is good news (default `up`); spending would be `down`. |
50
+ | `locale` | `string` | | For the default percent format. Defaults to the LocaleProvider's. |
@@ -0,0 +1,3 @@
1
+ import { default as IconProps } from './icon.types';
2
+ export default function ArrowPath(props: IconProps): import('react').JSX.Element;
3
+ //# sourceMappingURL=arrow-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"arrow-path.d.ts","sourceRoot":"","sources":["../../packages/icons/arrow-path.tsx"],"names":[],"mappings":"AAEA,OAAO,SAAS,MAAM,cAAc,CAAC;AAErC,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,KAAK,EAAE,SAAS,+BAoBjD"}
@@ -0,0 +1,3 @@
1
+ import { default as IconProps } from './icon.types';
2
+ export default function ArrowsRightLeft(props: IconProps): import('react').JSX.Element;
3
+ //# sourceMappingURL=arrows-right-left.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"arrows-right-left.d.ts","sourceRoot":"","sources":["../../packages/icons/arrows-right-left.tsx"],"names":[],"mappings":"AAEA,OAAO,SAAS,MAAM,cAAc,CAAC;AAErC,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,KAAK,EAAE,SAAS,+BAoBvD"}
@@ -0,0 +1,3 @@
1
+ import { default as IconProps } from './icon.types';
2
+ export default function Backward(props: IconProps): import('react').JSX.Element;
3
+ //# sourceMappingURL=backward.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backward.d.ts","sourceRoot":"","sources":["../../packages/icons/backward.tsx"],"names":[],"mappings":"AAEA,OAAO,SAAS,MAAM,cAAc,CAAC;AAErC,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,KAAK,EAAE,SAAS,+BAchD"}
@@ -0,0 +1,3 @@
1
+ import { default as IconProps } from './icon.types';
2
+ export default function Forward(props: IconProps): import('react').JSX.Element;
3
+ //# sourceMappingURL=forward.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forward.d.ts","sourceRoot":"","sources":["../../packages/icons/forward.tsx"],"names":[],"mappings":"AAEA,OAAO,SAAS,MAAM,cAAc,CAAC;AAErC,MAAM,CAAC,OAAO,UAAU,OAAO,CAAC,KAAK,EAAE,SAAS,+BAc/C"}
@@ -0,0 +1,3 @@
1
+ import { default as IconProps } from './icon.types';
2
+ export default function HeartSolid(props: IconProps): import('react').JSX.Element;
3
+ //# sourceMappingURL=heart-solid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heart-solid.d.ts","sourceRoot":"","sources":["../../packages/icons/heart-solid.tsx"],"names":[],"mappings":"AAEA,OAAO,SAAS,MAAM,cAAc,CAAC;AAErC,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,KAAK,EAAE,SAAS,+BAclD"}
@@ -0,0 +1,3 @@
1
+ import { default as IconProps } from './icon.types';
2
+ export default function Heart(props: IconProps): import('react').JSX.Element;
3
+ //# sourceMappingURL=heart.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heart.d.ts","sourceRoot":"","sources":["../../packages/icons/heart.tsx"],"names":[],"mappings":"AAEA,OAAO,SAAS,MAAM,cAAc,CAAC;AAErC,MAAM,CAAC,OAAO,UAAU,KAAK,CAAC,KAAK,EAAE,SAAS,+BAoB7C"}