@rinzai/zen 0.14.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 (169) 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 +7 -1
  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/player-bar/index.d.ts +11 -0
  128. package/dist/player-bar/index.d.ts.map +1 -0
  129. package/dist/player-bar/index.js +17 -0
  130. package/dist/radio-cards/index.d.ts +2 -1
  131. package/dist/radio-cards/index.d.ts.map +1 -1
  132. package/dist/radio-cards/index.js +3 -3
  133. package/dist/seek-bar/index.d.ts +24 -0
  134. package/dist/seek-bar/index.d.ts.map +1 -0
  135. package/dist/seek-bar/index.js +120 -0
  136. package/dist/segmented/index.d.ts +3 -2
  137. package/dist/segmented/index.d.ts.map +1 -1
  138. package/dist/segmented/index.js +19 -18
  139. package/dist/slider/index.d.ts +2 -1
  140. package/dist/slider/index.d.ts.map +1 -1
  141. package/dist/tab-bar/index.d.ts +4 -2
  142. package/dist/tab-bar/index.d.ts.map +1 -1
  143. package/dist/tab-bar/index.js +24 -19
  144. package/dist/textarea/index.d.ts +6 -2
  145. package/dist/textarea/index.d.ts.map +1 -1
  146. package/dist/textarea/index.js +2 -1
  147. package/dist/theme.css +40 -0
  148. package/dist/utils/duration.d.ts +3 -0
  149. package/dist/utils/duration.d.ts.map +1 -0
  150. package/dist/utils/duration.js +7 -0
  151. package/dist/utils/messages.d.ts +2 -0
  152. package/dist/utils/messages.d.ts.map +1 -1
  153. package/dist/utils/messages.js +3 -1
  154. package/dist/utils/slot.d.ts +5 -2
  155. package/dist/utils/slot.d.ts.map +1 -1
  156. package/dist/utils/slot.js +16 -11
  157. package/dist/utils/useAnchoredPopup.d.ts.map +1 -1
  158. package/dist/utils/useAnchoredPopup.js +3 -2
  159. package/dist/utils/useHeightVar.d.ts +7 -0
  160. package/dist/utils/useHeightVar.d.ts.map +1 -0
  161. package/dist/utils/useHeightVar.js +17 -0
  162. package/dist/utils/useMediaPlayer.d.ts +38 -0
  163. package/dist/utils/useMediaPlayer.d.ts.map +1 -0
  164. package/dist/utils/useMediaPlayer.js +91 -0
  165. package/dist/utils/useMediaSession.d.ts +24 -0
  166. package/dist/utils/useMediaSession.d.ts.map +1 -0
  167. package/dist/utils/useMediaSession.js +76 -0
  168. package/dist/zen.css +1 -1
  169. package/package.json +4 -2
@@ -0,0 +1,75 @@
1
+ # Side Nav
2
+
3
+ Vertical navigation in titled groups, for docs and settings sidebars. Works with any router.
4
+
5
+ ```tsx
6
+ import { SideNav, SideNavGroup, SideNavLink } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { SideNav, SideNavGroup, SideNavLink } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ const GROUPS = [
18
+ { title: 'Account', links: ['Profile', 'Security', 'Passkeys'] },
19
+ { title: 'Workspace', links: ['Members', 'Billing', 'Integrations'] },
20
+ ];
21
+
22
+ /** With a router, pass its link as the child: <SideNavLink asChild><NavLink to="/billing">Billing</NavLink></SideNavLink>. */
23
+ export default function Default() {
24
+ const [current, setCurrent] = useState('Security');
25
+ return (
26
+ <SideNav aria-label="Settings" className="w-56">
27
+ {GROUPS.map((group) => (
28
+ <SideNavGroup key={group.title} title={group.title}>
29
+ {group.links.map((link) => (
30
+ <SideNavLink
31
+ key={link}
32
+ href={`#${link.toLowerCase()}`}
33
+ active={link === current}
34
+ onClick={(e) => {
35
+ e.preventDefault();
36
+ setCurrent(link);
37
+ }}
38
+ >
39
+ {link}
40
+ </SideNavLink>
41
+ ))}
42
+ </SideNavGroup>
43
+ ))}
44
+ </SideNav>
45
+ );
46
+ }
47
+ ```
48
+
49
+ ## API reference
50
+
51
+ ### SideNav
52
+
53
+ Vertical navigation, e.g. a docs or settings sidebar. Give it an aria-label.
54
+
55
+ No props of its own; it accepts the props of the element it renders.
56
+
57
+ ### SideNavGroup
58
+
59
+ A titled group of links.
60
+
61
+ | Prop | Type | Default | Description |
62
+ | --- | --- | --- | --- |
63
+ | `className` | `string` | | |
64
+ | `title` | `ReactNode` | | |
65
+
66
+ ### SideNavLink
67
+
68
+ One link. `active` marks the current page. With `asChild`, the styles go onto
69
+ your own link element instead (a router's Link or NavLink, which may set
70
+ aria-current itself).
71
+
72
+ | Prop | Type | Default | Description |
73
+ | --- | --- | --- | --- |
74
+ | `active` | `boolean` | | This link is the current page. |
75
+ | `asChild` | `boolean` | | Style the single child element (e.g. a router link) instead of rendering an <a>. |
@@ -0,0 +1,89 @@
1
+ # Skeleton & Spinner
2
+
3
+ Placeholders for content on its way.
4
+
5
+ ```tsx
6
+ import { Skeleton, Spinner } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Loading card
12
+
13
+ ```tsx
14
+ import { Skeleton } from '@rinzai/zen';
15
+
16
+ export default function LoadingCard() {
17
+ return (
18
+ <div className="glass glow-edge flex w-80 flex-col gap-3 rounded-xl p-4">
19
+ <Skeleton className="h-3 w-24" />
20
+ <Skeleton className="h-7 w-40" />
21
+ <Skeleton className="h-3 w-full" />
22
+ </div>
23
+ );
24
+ }
25
+ ```
26
+
27
+ ### Reveal
28
+
29
+ ```tsx
30
+ import { Button, Skeleton } from '@rinzai/zen';
31
+ import { useState } from 'react';
32
+
33
+ export default function Reveal() {
34
+ const [loading, setLoading] = useState(true);
35
+ return (
36
+ <div className="flex flex-col items-start gap-4">
37
+ <div className="glass glow-edge flex w-80 flex-col gap-3 rounded-xl p-4">
38
+ <Skeleton loading={loading} className="h-3 w-24">
39
+ <p className="text-muted-foreground text-xs">Balance</p>
40
+ </Skeleton>
41
+ <Skeleton loading={loading} className="h-7 w-40">
42
+ <p className="text-2xl font-semibold tabular-nums">$4,280.15</p>
43
+ </Skeleton>
44
+ </div>
45
+ <Button variant="outline" onClick={() => setLoading((l) => !l)}>
46
+ {loading ? 'Load' : 'Reload'}
47
+ </Button>
48
+ </div>
49
+ );
50
+ }
51
+ ```
52
+
53
+ ### With spinner
54
+
55
+ ```tsx
56
+ import { Spinner } from '@rinzai/zen';
57
+
58
+ export default function WithSpinner() {
59
+ return (
60
+ <div className="text-muted-foreground flex items-center gap-2 text-sm">
61
+ <Spinner /> Loading…
62
+ </div>
63
+ );
64
+ }
65
+ ```
66
+
67
+ ## API reference
68
+
69
+ ### Skeleton
70
+
71
+ Shimmering placeholder in the shape of content that's on its way. Size it with className.
72
+
73
+ Give it the content as its child and `loading`: the placeholder shows while
74
+ loading, then the content fades in where it was.
75
+
76
+ | Prop | Type | Default | Description |
77
+ | --- | --- | --- | --- |
78
+ | `children` | `ReactElement<unknown, string \| JSXElementConstructor<any>>` | | The content this stands in for, a single element. |
79
+ | `loading` | `boolean` | | With children: show the placeholder instead of them while true. |
80
+
81
+ ### Spinner
82
+
83
+ Small circular spinner in the current text colour; size it with className
84
+ (default size-4). Decorative by default (e.g. inside a loading Button). Give
85
+ it a `label` when it stands on its own, and screen readers announce it.
86
+
87
+ | Prop | Type | Default | Description |
88
+ | --- | --- | --- | --- |
89
+ | `label` | `string` | | What is loading, e.g. "Loading entries". Makes the spinner announce itself. |
@@ -0,0 +1,87 @@
1
+ # Slider
2
+
3
+ A native range input with a glowing thumb and any track you like.
4
+
5
+ ```tsx
6
+ import { Slider } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Slider } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ /** Filled up to the value; arrow keys and Home/End work as on any range input. */
18
+ export default function Default() {
19
+ const [savings, setSavings] = useState(30);
20
+ return (
21
+ <div className="flex w-72 flex-col gap-2">
22
+ <div className="flex justify-between text-sm">
23
+ <span className="font-medium">Save each month</span>
24
+ <span className="text-muted-foreground tabular-nums">{savings}%</span>
25
+ </div>
26
+ <Slider
27
+ aria-label="Save each month"
28
+ value={savings}
29
+ onValueChange={setSavings}
30
+ valueText={(v) => `${v} percent`}
31
+ />
32
+ </div>
33
+ );
34
+ }
35
+ ```
36
+
37
+ ### Hue
38
+
39
+ ```tsx
40
+ import { Slider } from '@rinzai/zen';
41
+ import { useState } from 'react';
42
+
43
+ /** A custom track (every hue) and thumb (the chosen colour); onValueCommit fires once, when you let go. */
44
+ export default function Hue() {
45
+ const [hue, setHue] = useState(295);
46
+ const [saved, setSaved] = useState(295);
47
+ const gradient = `linear-gradient(to right, ${Array.from({ length: 13 }, (_, i) => `oklch(0.72 0.13 ${i * 30})`).join(', ')})`;
48
+ return (
49
+ <div className="flex w-72 flex-col gap-2">
50
+ <Slider
51
+ aria-label="Accent hue"
52
+ min={0}
53
+ max={359}
54
+ value={hue}
55
+ onValueChange={setHue}
56
+ onValueCommit={setSaved}
57
+ trackBackground={gradient}
58
+ thumbColor={`oklch(0.72 0.13 ${hue})`}
59
+ valueText={(v) => `Hue ${v} degrees`}
60
+ />
61
+ <p className="text-muted-foreground mt-0! text-xs">Saved: {saved}°</p>
62
+ </div>
63
+ );
64
+ }
65
+ ```
66
+
67
+ ## API reference
68
+
69
+ ### Slider
70
+
71
+ A range slider on the native <input type="range"> (keyboard, forms and
72
+ screen readers work as usual): a rounded track, filled up to the value in
73
+ the accent unless you give it your own `trackBackground` (e.g. a hue
74
+ gradient), and a glowing thumb.
75
+
76
+ | Prop | Type | Default | Description |
77
+ | --- | --- | --- | --- |
78
+ | `defaultValue` | `number` | | The first value, when the slider keeps its own (default `min`). |
79
+ | `max` | `number` | `100` | |
80
+ | `min` | `number` | `0` | |
81
+ | `onValueChange` | `((value: number) => void)` | | While dragging (every step). |
82
+ | `onValueCommit` | `((value: number) => void)` | | Once the user lets go: the moment to save. |
83
+ | `step` | `"any" \| number` | `1` | `'any'` for no snapping; the arrow keys then move by the browser's own amount. |
84
+ | `thumbColor` | `string` | | CSS colour for the thumb (default: the accent). |
85
+ | `trackBackground` | `string` | | CSS background for the track, e.g. a gradient of hues. |
86
+ | `value` | `number` | | The value, when you keep it. |
87
+ | `valueText` | `((value: number) => string)` | | Spoken value, e.g. (v) => `Hue ${v} degrees`. |
@@ -0,0 +1,182 @@
1
+ # Sortable List
2
+
3
+ Put a list in order, or a tree of groups (a table’s rows, say), by dragging items by a grip or with the keyboard, each step announced: into other groups, open sub-items, or under another item.
4
+
5
+ ```tsx
6
+ import { SortableList, SortableTree, SortableItem, SortableHandle, flattenTree } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Meter, SortableHandle, SortableItem, SortableList } from '@rinzai/zen';
15
+ import { useState } from 'react';
16
+
17
+ const BUDGETS: Record<string, { name: string; spent: number; limit: number }> = {
18
+ rent: { name: 'Rent', spent: 1800, limit: 1800 },
19
+ groceries: { name: 'Groceries', spent: 412, limit: 500 },
20
+ transport: { name: 'Transport', spent: 64, limit: 120 },
21
+ dining: { name: 'Eating out', spent: 188, limit: 150 },
22
+ fun: { name: 'Fun', spent: 40, limit: 100 },
23
+ };
24
+
25
+ /** Drag a budget by its grip to put them in your order (or Tab to a grip, Space, arrows, Space). */
26
+ export default function Default() {
27
+ const [order, setOrder] = useState(Object.keys(BUDGETS));
28
+ return (
29
+ <SortableList value={order} onValueChange={setOrder} aria-label="Budgets" className="w-full max-w-md gap-2">
30
+ {order.map((id) => {
31
+ const b = BUDGETS[id];
32
+ return (
33
+ <SortableItem
34
+ key={id}
35
+ id={id}
36
+ className="glass glow-edge flex items-center gap-2 rounded-xl py-2 pr-4 pl-1.5"
37
+ >
38
+ <SortableHandle id={id} label={b.name} />
39
+ <Meter
40
+ label={b.name}
41
+ value={b.spent}
42
+ max={b.limit}
43
+ detail={`$${b.spent} / $${b.limit}`}
44
+ className="flex-1"
45
+ />
46
+ </SortableItem>
47
+ );
48
+ })}
49
+ </SortableList>
50
+ );
51
+ }
52
+ ```
53
+
54
+ ### Groups
55
+
56
+ ```tsx
57
+ import { flattenTree, SortableHandle, SortableItem, type SortableNode, SortableTree } from '@rinzai/zen';
58
+ import { useState } from 'react';
59
+
60
+ const NAMES: Record<string, string> = {
61
+ bills: 'Bills',
62
+ rent: 'Rent',
63
+ phone: 'Phone',
64
+ streaming: 'Streaming',
65
+ life: 'Lifestyle',
66
+ gym: 'Gym',
67
+ books: 'Books',
68
+ };
69
+ const GROUPS = new Set(['bills', 'life']);
70
+ const AMOUNTS: Record<string, string> = {
71
+ rent: '$1,800',
72
+ phone: '$45',
73
+ streaming: '$32',
74
+ gym: '$60',
75
+ books: '$25',
76
+ };
77
+
78
+ /**
79
+ * Budgets in groups, with SortableTree: drag one within its group, into the
80
+ * other, or sideways to tuck it under another budget. Groups stay at the top
81
+ * and take budgets; budgets never sit at the top on their own.
82
+ */
83
+ export default function Groups() {
84
+ const [tree, setTree] = useState<SortableNode[]>([
85
+ { id: 'bills', children: [{ id: 'rent' }, { id: 'phone' }, { id: 'streaming' }] },
86
+ { id: 'life', children: [{ id: 'gym' }, { id: 'books' }] },
87
+ ]);
88
+ return (
89
+ <SortableTree
90
+ aria-label="Budgets"
91
+ value={tree}
92
+ onValueChange={setTree}
93
+ accepts={(id, parent) => (GROUPS.has(id) ? parent === null : parent !== null)}
94
+ className="w-full max-w-sm gap-1.5"
95
+ >
96
+ {flattenTree(tree).map(({ id, depth }) => (
97
+ <SortableItem key={id} id={id} style={{ marginInlineStart: Math.max(0, depth - 1) * 24 }}>
98
+ {GROUPS.has(id) ? (
99
+ <div className="flex items-center gap-1 pt-2">
100
+ <SortableHandle id={id} label={NAMES[id]} />
101
+ <span className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
102
+ {NAMES[id]}
103
+ </span>
104
+ </div>
105
+ ) : (
106
+ <div className="glass glow-edge flex items-center gap-1 rounded-xl py-1.5 ps-1.5 pe-4">
107
+ <SortableHandle id={id} label={NAMES[id]} />
108
+ <span className="flex-1 text-sm">{NAMES[id]}</span>
109
+ <span className="text-muted-foreground text-sm tabular-nums">{AMOUNTS[id]}</span>
110
+ </div>
111
+ )}
112
+ </SortableItem>
113
+ ))}
114
+ </SortableTree>
115
+ );
116
+ }
117
+ ```
118
+
119
+ ## API reference
120
+
121
+ ### SortableList
122
+
123
+ A list put in order by dragging, e.g. budget categories: drag an item by its
124
+ SortableHandle and the others make way (a phone taps lightly at each new
125
+ place); it settles into place when let go.
126
+ From the keyboard, Space or Enter on a handle picks the item up, the arrow
127
+ keys move it, Space or Enter drops it and Escape puts it back; each step is
128
+ announced. `value` is the ids in order; render a SortableItem for each.
129
+ With `asChild` (and `asChild` on the items), your own elements instead: a
130
+ TableBody of TableRows, say, one per group, each sorted on its own.
131
+
132
+ | Prop | Type | Default | Description |
133
+ | --- | --- | --- | --- |
134
+ | `onValueChange` (required) | `(value: string[]) => void` | | |
135
+ | `value` (required) | `readonly string[]` | | The items' ids, in order. |
136
+ | `asChild` | `boolean` | | Put the list's props on your own element (the single child, e.g. a TableBody) instead of a ul. |
137
+
138
+ ### SortableTree
139
+
140
+ Items in groups, and groups in groups, put in order by dragging: an item
141
+ can go anywhere in its group, into another group (or one open inside it),
142
+ or under another item. While dragging, a glowing line shows where it will
143
+ land, set in as deep as it'll go, and the group it would join is lit; drag
144
+ sideways to go a level in or out. From the keyboard, Space or Enter on a
145
+ handle picks it up, the up and down arrows move it through every place it
146
+ can go, left and right a level out or in, Space or Enter drops it and
147
+ Escape puts it back; each step is announced.
148
+
149
+ `value` is the tree of ids. Render a SortableItem for each row shown, in
150
+ order (flattenTree lists them, with their depths): what's under a closed
151
+ one isn't shown, and moves with it. `accepts` says which group takes which
152
+ item. With `asChild`, your own element (a Table, its rows SortableItems with
153
+ asChild on TableRows) instead of a ul.
154
+
155
+ | Prop | Type | Default | Description |
156
+ | --- | --- | --- | --- |
157
+ | `onValueChange` (required) | `(value: SortableNode[]) => void` | | |
158
+ | `value` (required) | `readonly SortableNode[]` | | The tree of ids, in order. |
159
+ | `accepts` | `((id: string, parentId: string \| null) => boolean)` | | Whether `parentId` (null: the top level) takes item `id`; by default, everything goes anywhere. |
160
+ | `asChild` | `boolean` | | Put its props on your own element (the single child, e.g. a Table) instead of a ul. |
161
+ | `indent` | `number` | `24` | How far in a level is set, in px: the drop line is set in this far, and dragging this far sideways goes a level in or out. |
162
+
163
+ ### SortableItem
164
+
165
+ One item in a SortableList or SortableTree; give it the `id` it has in the list's value. With `asChild`, your own element (a TableRow).
166
+
167
+ | Prop | Type | Default | Description |
168
+ | --- | --- | --- | --- |
169
+ | `id` (required) | `string` | | |
170
+ | `asChild` | `boolean` | | Put the item's props on your own element (the single child, e.g. a TableRow) instead of an li. |
171
+ | `label` | `string` | | Its name, for one without a SortableHandle (a group others go into), to say "2 of 3 in Food". |
172
+
173
+ ### SortableHandle
174
+
175
+ What an item is dragged by: a grip button. `label` names the item ("Rent");
176
+ the button reads "Reorder Rent". It takes no scroll gestures, so dragging it
177
+ never scrolls the page.
178
+
179
+ | Prop | Type | Default | Description |
180
+ | --- | --- | --- | --- |
181
+ | `id` (required) | `string` | | The item's id, as in the list's value. |
182
+ | `label` (required) | `string` | | The item's name, e.g. "Rent". |
@@ -0,0 +1,56 @@
1
+ # Sparkline
2
+
3
+ A small line of how a number has moved, with no axes, for beside a figure. It draws itself in.
4
+
5
+ ```tsx
6
+ import { Sparkline } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Sparkline, Stat, StatRow } from '@rinzai/zen';
15
+
16
+ /** Beside the figures they explain: a balance over the month, and spending by week. */
17
+ export default function Default() {
18
+ return (
19
+ <StatRow className="w-full max-w-lg">
20
+ <Stat
21
+ label="Balance"
22
+ value={
23
+ <span className="flex items-center justify-between gap-3">
24
+ $6,908
25
+ <Sparkline values={[4100, 4300, 3900, 5200, 5600, 5400, 6100, 6908]} />
26
+ </span>
27
+ }
28
+ />
29
+ <Stat
30
+ label="Spent"
31
+ tone="negative"
32
+ value={
33
+ <span className="flex items-center justify-between gap-3">
34
+ $2,742
35
+ <Sparkline tone="negative" values={[620, 540, 810, 772]} />
36
+ </span>
37
+ }
38
+ />
39
+ </StatRow>
40
+ );
41
+ }
42
+ ```
43
+
44
+ ## API reference
45
+
46
+ ### Sparkline
47
+
48
+ A small line of how a number has moved (a balance over a month, spending by
49
+ week), with no axes: for beside a figure in a Stat or a table row. It takes
50
+ its size from className (e.g. h-8 w-24) and draws itself in.
51
+
52
+ | Prop | Type | Default | Description |
53
+ | --- | --- | --- | --- |
54
+ | `values` (required) | `number[]` | | |
55
+ | `area` | `boolean` | `true` | A soft fill under the line. |
56
+ | `tone` | `"default" \| "muted" \| "negative"` | `default` | The accent (`primary`), red (`negative`), or grey (`muted`). |
@@ -0,0 +1,69 @@
1
+ # Spinner
2
+
3
+ A small loading spinner in the current text colour, announced to screen readers when labelled.
4
+
5
+ ```tsx
6
+ import { Spinner } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Default
12
+
13
+ ```tsx
14
+ import { Spinner } from '@rinzai/zen';
15
+
16
+ /** Sized and coloured with className; it follows the text colour. */
17
+ export default function Default() {
18
+ return (
19
+ <div className="flex items-center gap-6">
20
+ <Spinner />
21
+ <Spinner className="size-6" />
22
+ <Spinner className="text-primary size-8 border-[3px]" />
23
+ <Spinner className="text-muted-foreground size-5" />
24
+ </div>
25
+ );
26
+ }
27
+ ```
28
+
29
+ ### Loading
30
+
31
+ ```tsx
32
+ import { Button, Card, CardHeader, CardTitle, Spinner } from '@rinzai/zen';
33
+ import { useState } from 'react';
34
+
35
+ /** Standing on its own, a spinner needs a label so screen readers announce what is loading. */
36
+ export default function Loading() {
37
+ const [loading, setLoading] = useState(true);
38
+ return (
39
+ <Card className="w-80">
40
+ <CardHeader>
41
+ <CardTitle>Entries</CardTitle>
42
+ </CardHeader>
43
+ {loading ? (
44
+ <div className="text-muted-foreground flex items-center gap-3 py-6 text-sm">
45
+ <Spinner label="Loading entries" />
46
+ Loading entries…
47
+ </div>
48
+ ) : (
49
+ <p className="py-6 text-sm">12 entries this month.</p>
50
+ )}
51
+ <Button variant="outline" size="sm" onClick={() => setLoading((l) => !l)}>
52
+ {loading ? 'Finish loading' : 'Load again'}
53
+ </Button>
54
+ </Card>
55
+ );
56
+ }
57
+ ```
58
+
59
+ ## API reference
60
+
61
+ ### Spinner
62
+
63
+ Small circular spinner in the current text colour; size it with className
64
+ (default size-4). Decorative by default (e.g. inside a loading Button). Give
65
+ it a `label` when it stands on its own, and screen readers announce it.
66
+
67
+ | Prop | Type | Default | Description |
68
+ | --- | --- | --- | --- |
69
+ | `label` | `string` | | What is loading, e.g. "Loading entries". Makes the spinner announce itself. |
@@ -0,0 +1,81 @@
1
+ # Status Pill
2
+
3
+ A small status that changes in place, such as Saving…, Saved or Not saved with a Retry, and fades back once the news is old.
4
+
5
+ ```tsx
6
+ import { StatusPill, StatusPillAction } from '@rinzai/zen';
7
+ ```
8
+
9
+ ## Examples
10
+
11
+ ### Save
12
+
13
+ ```tsx
14
+ import { Button, Spinner, StatusPill, StatusPillAction } from '@rinzai/zen';
15
+ import { useEffect, useState } from 'react';
16
+ // Icons from Heroicons (https://heroicons.com): AlertTriangle (exclamation-triangle), Check (check).
17
+
18
+ type Save = 'saving' | 'saved' | 'error';
19
+
20
+ /** A save indicator: saving spins, saved checks and then quietens, a failure is red and offers a retry. */
21
+ export default function Save() {
22
+ const [state, setState] = useState<Save>('saved');
23
+ const [quiet, setQuiet] = useState(false);
24
+ useEffect(() => {
25
+ setQuiet(false);
26
+ if (state !== 'saved') return;
27
+ const t = window.setTimeout(() => setQuiet(true), 2500);
28
+ return () => window.clearTimeout(t);
29
+ }, [state]);
30
+ const save = (ok: boolean) => {
31
+ setState('saving');
32
+ window.setTimeout(() => setState(ok ? 'saved' : 'error'), 1200);
33
+ };
34
+
35
+ return (
36
+ <div className="flex flex-col items-center gap-4">
37
+ {state === 'error' ? (
38
+ <StatusPill tone="negative">
39
+ <AlertTriangle />
40
+ Not saved
41
+ <StatusPillAction onClick={() => save(true)}>Retry</StatusPillAction>
42
+ </StatusPill>
43
+ ) : (
44
+ <StatusPill tone={state === 'saved' ? 'positive' : 'default'} quiet={state === 'saved' && quiet}>
45
+ {state === 'saving' ? <Spinner /> : <Check />}
46
+ {state === 'saving' ? 'Saving…' : 'Saved'}
47
+ </StatusPill>
48
+ )}
49
+ <div className="flex gap-2">
50
+ <Button variant="outline" size="sm" onClick={() => save(true)}>
51
+ Save
52
+ </Button>
53
+ <Button variant="outline" size="sm" onClick={() => save(false)}>
54
+ Save and fail
55
+ </Button>
56
+ </div>
57
+ </div>
58
+ );
59
+ }
60
+ ```
61
+
62
+ ## API reference
63
+
64
+ ### StatusPill
65
+
66
+ A small rounded status that changes in place, e.g. an app's "Saving…",
67
+ "Saved", "Not saved · Retry": an icon (a Spinner, a Check) and a few words.
68
+ `quiet` fades it back once the news is old (a save a few seconds ago).
69
+ Changes are read out politely (role="status"). For an action inside it,
70
+ such as Retry, use StatusPillAction. For a fixed label or count, use Badge.
71
+
72
+ | Prop | Type | Default | Description |
73
+ | --- | --- | --- | --- |
74
+ | `quiet` | `boolean` | `false` | Faded back into the page: the news is old. |
75
+ | `tone` | `"default" \| "negative" \| "positive"` | `default` | `positive` in the accent colour (done), `negative` in red (failed); `default` is muted (in progress). |
76
+
77
+ ### StatusPillAction
78
+
79
+ A text button inside a StatusPill, e.g. Retry, in the pill's colour.
80
+
81
+ No props of its own; it accepts the props of the element it renders.