@rinzai/zen 0.15.0 → 0.16.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 (78) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/animated-number/index.js +14 -14
  3. package/dist/breadcrumbs/index.js +14 -14
  4. package/dist/calendar/index.js +29 -29
  5. package/dist/calendar-heatmap/index.js +12 -12
  6. package/dist/canvas/index.js +19 -19
  7. package/dist/carousel/index.js +24 -24
  8. package/dist/chart/index.js +7 -7
  9. package/dist/code-block/index.js +5 -5
  10. package/dist/collapse/index.js +6 -6
  11. package/dist/color-picker/index.js +10 -10
  12. package/dist/combobox/index.js +61 -61
  13. package/dist/command-palette/index.js +5 -5
  14. package/dist/confirm-dialog/index.js +4 -4
  15. package/dist/date-picker/index.js +15 -15
  16. package/dist/dialog/index.d.ts.map +1 -1
  17. package/dist/dialog/index.js +45 -65
  18. package/dist/docs/README.md +4 -4
  19. package/dist/docs/locale-provider.md +1 -1
  20. package/dist/docs/menu.md +54 -1
  21. package/dist/docs/seek-bar.md +30 -1
  22. package/dist/docs/sortable.md +63 -1
  23. package/dist/docs/tooltip.md +45 -3
  24. package/dist/filter-bar/index.js +6 -6
  25. package/dist/header/index.js +6 -6
  26. package/dist/icon-picker/index.js +5 -5
  27. package/dist/keypad/index.js +5 -5
  28. package/dist/lib.js +7 -7
  29. package/dist/load-more/index.js +5 -5
  30. package/dist/menu/index.d.ts +7 -1
  31. package/dist/menu/index.d.ts.map +1 -1
  32. package/dist/menu/index.js +217 -174
  33. package/dist/money-input/index.js +54 -54
  34. package/dist/month-picker/index.js +11 -11
  35. package/dist/number-field/index.js +5 -5
  36. package/dist/pagination/index.js +12 -12
  37. package/dist/range-slider/index.js +11 -11
  38. package/dist/repeat-picker/index.js +5 -5
  39. package/dist/resizable/index.js +22 -22
  40. package/dist/seek-bar/index.d.ts +3 -1
  41. package/dist/seek-bar/index.d.ts.map +1 -1
  42. package/dist/seek-bar/index.js +41 -40
  43. package/dist/select/index.js +21 -21
  44. package/dist/sortable/index.d.ts +13 -1
  45. package/dist/sortable/index.d.ts.map +1 -1
  46. package/dist/sortable/index.js +74 -55
  47. package/dist/sortable/shared.js +14 -14
  48. package/dist/sortable/tree.js +17 -17
  49. package/dist/sparkline/index.js +4 -4
  50. package/dist/stepper/index.js +13 -13
  51. package/dist/table/index.js +26 -26
  52. package/dist/table/selection.js +9 -9
  53. package/dist/table-of-contents/index.js +11 -11
  54. package/dist/tag-input/index.js +5 -5
  55. package/dist/theme-toggle/index.js +4 -4
  56. package/dist/theme.css +103 -0
  57. package/dist/time-field/index.js +21 -21
  58. package/dist/toast/index.d.ts.map +1 -1
  59. package/dist/toast/index.js +41 -41
  60. package/dist/tooltip/index.d.ts +9 -3
  61. package/dist/tooltip/index.d.ts.map +1 -1
  62. package/dist/tooltip/index.js +19 -16
  63. package/dist/tree/index.js +34 -34
  64. package/dist/trend/index.js +5 -5
  65. package/dist/utils/messages.d.ts +1 -0
  66. package/dist/utils/messages.d.ts.map +1 -1
  67. package/dist/utils/messages.js +1 -0
  68. package/dist/utils/swipe.d.ts +7 -0
  69. package/dist/utils/swipe.d.ts.map +1 -1
  70. package/dist/utils/swipe.js +45 -25
  71. package/dist/utils/useAnchoredPopup.d.ts +2 -1
  72. package/dist/utils/useAnchoredPopup.d.ts.map +1 -1
  73. package/dist/utils/useAnchoredPopup.js +9 -5
  74. package/dist/utils/useVirtualList.d.ts +6 -1
  75. package/dist/utils/useVirtualList.d.ts.map +1 -1
  76. package/dist/utils/useVirtualList.js +12 -12
  77. package/dist/zen.css +1 -1
  78. package/package.json +1 -1
@@ -1,115 +1,95 @@
1
1
  "use client";
2
- import { cx as e } from "../utils/cx.js";
3
- import { useGraphicsMode as t } from "../utils/graphics.js";
4
- import { useMergedRefs as n } from "../utils/refs.js";
5
- import { useMessages as r } from "../locale-provider/index.js";
6
- import i from "../button/index.js";
7
- import a from "../icons/micro/x-mark.js";
8
- import { useToastHost as o } from "../toast/index.js";
9
- import { useSwipe as s } from "../utils/swipe.js";
2
+ import { useMessages as e } from "../locale-provider/index.js";
3
+ import { cx as t } from "../utils/cx.js";
4
+ import { useGraphicsMode as n } from "../utils/graphics.js";
5
+ import { useMergedRefs as r } from "../utils/refs.js";
6
+ import { useSheetSwipe as i } from "../utils/swipe.js";
7
+ import a from "../button/index.js";
8
+ import o from "../icons/micro/x-mark.js";
9
+ import { useToastHost as s } from "../toast/index.js";
10
10
  import { createContext as c, useContext as l, useEffect as u, useId as d, useRef as f } from "react";
11
11
  import { jsx as p, jsxs as m } from "react/jsx-runtime";
12
12
  //#region packages/dialog/index.tsx
13
13
  var h = c(() => {});
14
- function g({ open: t, onOpenChange: i, title: o, description: s, children: c, dismissible: l = !0, initialFocus: u, size: g = "md", side: v, className: b, role: x, closeLabel: S, ref: C, ...w }) {
15
- let T = r(), E = S ?? T.close, D = f(null), O = n(D, C), k = d(), A = d();
16
- return y(D, l && t ? v : void 0, () => i?.(!1)), _(D, t, u), /* @__PURE__ */ m("dialog", {
14
+ function g({ open: n, onOpenChange: a, title: s, description: c, children: l, dismissible: u = !0, initialFocus: g, size: v = "md", side: y, className: b, role: x, closeLabel: S, ref: C, ...w }) {
15
+ let T = e(), E = S ?? T.close, D = f(null), O = r(D, C), k = d(), A = d();
16
+ return i(D, u && n ? y : void 0, () => a?.(!1)), _(D, n, g), /* @__PURE__ */ m("dialog", {
17
17
  ...w,
18
18
  ref: O,
19
19
  role: x,
20
20
  "aria-labelledby": k,
21
- "aria-describedby": s ? A : void 0,
22
- "data-side": v,
23
- className: e("zen__dialog glass glass-blur glow-edge text-card-foreground bg-card/90! overflow-visible p-0", !v && "m-auto max-h-[calc(100dvh-2rem)] max-w-[calc(100vw-2rem)] rounded-xl", !v && (g === "lg" ? "w-[60rem]" : "w-[28rem]"), v === "bottom" && "mx-auto mt-auto mb-0 max-h-[85dvh] w-full max-w-2xl rounded-t-2xl", (v === "left" || v === "right") && "my-0 h-dvh max-h-dvh w-[24rem] max-w-[calc(100vw-2.5rem)]", v === "right" && "mr-0 ml-auto rounded-l-2xl", v === "left" && "mr-auto ml-0 rounded-r-2xl", "backdrop:bg-[color:var(--backdrop-scrim,oklch(0_0_0/0.5))] backdrop:backdrop-blur-sm pointer-coarse:backdrop:backdrop-blur-none", b),
21
+ "aria-describedby": c ? A : void 0,
22
+ "data-side": y,
23
+ className: t("zen__dialog glass glass-blur glow-edge text-card-foreground bg-card/90! overflow-visible p-0", !y && "m-auto max-h-[calc(100dvh-2rem)] max-w-[calc(100vw-2rem)] rounded-xl", !y && (v === "lg" ? "w-[60rem]" : "w-[28rem]"), y === "bottom" && "mx-auto mt-auto mb-0 max-h-[85dvh] w-full max-w-2xl rounded-t-2xl", (y === "left" || y === "right") && "my-0 h-dvh max-h-dvh w-[24rem] max-w-[calc(100vw-2.5rem)]", y === "right" && "mr-0 ml-auto rounded-l-2xl", y === "left" && "mr-auto ml-0 rounded-r-2xl", "backdrop:bg-[color:var(--backdrop-scrim,oklch(0_0_0/0.5))] backdrop:backdrop-blur-sm pointer-coarse:backdrop:backdrop-blur-none", b),
24
24
  onCancel: (e) => {
25
- e.preventDefault(), l && i?.(!1);
25
+ e.preventDefault(), u && a?.(!1);
26
26
  },
27
27
  onClick: (e) => {
28
- e.target === e.currentTarget && l && i?.(!1);
28
+ e.target === e.currentTarget && u && a?.(!1);
29
29
  },
30
30
  children: [/* @__PURE__ */ m("div", {
31
- className: e("flex flex-col gap-4 overflow-x-hidden overflow-y-auto p-6", v ? v === "bottom" ? "max-h-[85dvh] pt-3" : "h-full" : "max-h-[calc(100dvh-2rem)]"),
31
+ className: t("flex flex-col gap-4 overflow-x-hidden overflow-y-auto p-6", y ? y === "bottom" ? "max-h-[85dvh] pt-3" : "h-full" : "max-h-[calc(100dvh-2rem)]"),
32
32
  children: [
33
- v === "bottom" && /* @__PURE__ */ p("div", {
33
+ y === "bottom" && /* @__PURE__ */ p("div", {
34
34
  "aria-hidden": !0,
35
35
  className: "bg-tint/20 mx-auto h-1 w-10 shrink-0 rounded-full"
36
36
  }),
37
37
  /* @__PURE__ */ m("div", {
38
- className: e("flex flex-col gap-2", l && "pe-8"),
38
+ className: t("flex flex-col gap-2", u && "pe-8"),
39
39
  children: [/* @__PURE__ */ p("h2", {
40
40
  id: k,
41
41
  className: "text-lg font-semibold tracking-tight text-balance",
42
- children: o
43
- }), s && /* @__PURE__ */ p("p", {
42
+ children: s
43
+ }), c && /* @__PURE__ */ p("p", {
44
44
  id: A,
45
45
  className: "text-muted-foreground mt-0! text-sm leading-5",
46
- children: s
46
+ children: c
47
47
  })]
48
48
  }),
49
49
  /* @__PURE__ */ p(h.Provider, {
50
- value: () => i?.(!1),
51
- children: c
50
+ value: () => a?.(!1),
51
+ children: l
52
52
  })
53
53
  ]
54
- }), l && /* @__PURE__ */ p("button", {
54
+ }), u && /* @__PURE__ */ p("button", {
55
55
  type: "button",
56
56
  "aria-label": E,
57
- onClick: () => i?.(!1),
58
- className: e("text-muted-foreground hover:bg-tint/[0.07] hover:text-foreground absolute end-4 grid size-8 cursor-pointer place-items-center rounded-lg outline-hidden transition-colors focus-ring", v === "bottom" ? "top-7.5" : "top-5.5"),
59
- children: /* @__PURE__ */ p(a, { className: "size-4" })
57
+ onClick: () => a?.(!1),
58
+ className: t("text-muted-foreground hover:bg-tint/[0.07] hover:text-foreground absolute end-4 grid size-8 cursor-pointer place-items-center rounded-lg outline-hidden transition-colors focus-ring", y === "bottom" ? "top-7.5" : "top-5.5"),
59
+ children: /* @__PURE__ */ p(o, { className: "size-4" })
60
60
  })]
61
61
  });
62
62
  }
63
- function _(e, n, r) {
64
- t(), u(() => {
65
- let t = e.current;
66
- if (t) {
67
- if (n && !t.open) {
63
+ function _(e, t, r) {
64
+ n(), u(() => {
65
+ let n = e.current;
66
+ if (n) {
67
+ if (t && !n.open) {
68
68
  let e = document.documentElement;
69
69
  e.toggleAttribute("data-zen-scrollbar", window.innerWidth > e.clientWidth);
70
- let n = document.activeElement === document.body ? null : document.activeElement?.getBoundingClientRect(), i = !!n && !t.dataset.side;
71
- if (t.toggleAttribute("data-zen-from", i), t.showModal(), i) {
72
- let e = n.left + n.width / 2 - t.offsetLeft, r = n.top + n.height / 2 - t.offsetTop;
73
- t.style.transformOrigin = `${Math.round(e)}px ${Math.round(r)}px`;
70
+ let t = document.activeElement === document.body ? null : document.activeElement?.getBoundingClientRect(), i = !!t && !n.dataset.side;
71
+ if (n.toggleAttribute("data-zen-from", i), n.showModal(), i) {
72
+ let e = t.left + t.width / 2 - n.offsetLeft, r = t.top + t.height / 2 - n.offsetTop;
73
+ n.style.transformOrigin = `${Math.round(e)}px ${Math.round(r)}px`;
74
74
  }
75
75
  r?.current?.focus();
76
- } else !n && t.open && t.close();
76
+ } else !t && n.open && n.close();
77
77
  }
78
78
  }, [
79
79
  e,
80
- n,
80
+ t,
81
81
  r
82
- ]), o(e, n);
83
- }
84
- var v = .3;
85
- function y(e, t, n) {
86
- let r = t === "bottom" ? "y" : "x", i = t === "left" ? -1 : 1, a = () => r === "y" ? e.current.offsetHeight : e.current.offsetWidth, o = () => {
87
- e.current?.style.removeProperty("--zen-swipe"), e.current?.style.removeProperty("--zen-swipe-progress");
88
- };
89
- u(o, [e, t]), s(e, t ? {
90
- axis: r,
91
- sign: i
92
- } : void 0, {
93
- move: (t) => {
94
- let n = t > 0 ? t : t / 6;
95
- e.current.style.setProperty("--zen-swipe", r === "y" ? `0 ${n}px` : `${n * i}px 0`), e.current.style.setProperty("--zen-swipe-progress", String(Math.max(0, Math.min(1, t / a()))));
96
- },
97
- release: (e, t) => {
98
- e > a() * v || e > 0 && t > .5 ? n() : o();
99
- },
100
- cancel: o,
101
- threshold: () => a() * v
102
- });
82
+ ]), s(e, t);
103
83
  }
104
- function b({ className: t, ...n }) {
84
+ function v({ className: e, ...n }) {
105
85
  return /* @__PURE__ */ p("div", {
106
- className: e("flex justify-end gap-2", t),
86
+ className: t("flex justify-end gap-2", e),
107
87
  ...n
108
88
  });
109
89
  }
110
- function x({ onClick: e, ...t }) {
90
+ function y({ onClick: e, ...t }) {
111
91
  let n = l(h);
112
- return /* @__PURE__ */ p(i, {
92
+ return /* @__PURE__ */ p(a, {
113
93
  ...t,
114
94
  onClick: (t) => {
115
95
  e?.(t), t.defaultPrevented || n();
@@ -117,4 +97,4 @@ function x({ onClick: e, ...t }) {
117
97
  });
118
98
  }
119
99
  //#endregion
120
- export { x as DialogClose, b as DialogFooter, g as default, _ as useModal };
100
+ export { y as DialogClose, v as DialogFooter, g as default, _ as useModal };
@@ -156,7 +156,7 @@ Breaking changes and additions by version are in ../../CHANGELOG.md.
156
156
  - [Load More](./load-more.md): The end of a long list that has more: loads the next page when it scrolls into view, or on a press.
157
157
  - [Locale Provider](./locale-provider.md): The language for everything inside: how dates, numbers and money read, and every word components show or announce. Without it, "en-US" and English.
158
158
  - [Marquee](./marquee.md): One line of text cut off with an ellipsis, that scrolls to show the rest on hover or focus (of itself or of its row), or all the time. Only text that doesn't fit moves.
159
- - [Menu](./menu.md): A button that opens a menu, or a context menu from a right-click or long press, with your own trigger, a header, icons, groups, destructive items, checkbox and radio items, submenus, and a menubar of menus.
159
+ - [Menu](./menu.md): A button that opens a menu, or a context menu from a right-click or long press, with your own trigger, a header, icons, groups, destructive items, checkbox and radio items, submenus, a bottom sheet on phones, and a menubar of menus.
160
160
  - [Meter](./meter.md): A measure against a limit, like spend against a budget: warm near it, red past it.
161
161
  - [Money Input](./money-input.md): 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.
162
162
  - [Month Picker](./month-picker.md): A field showing a month that opens a year and a 12-month grid, with optional limits and clearing.
@@ -174,14 +174,14 @@ Breaking changes and additions by version are in ../../CHANGELOG.md.
174
174
  - [Range Slider](./range-slider.md): Two thumbs on one track, for a span like an amount from $50 to $500.
175
175
  - [Repeat Picker](./repeat-picker.md): How often something recurs, like a bill, said in words, with the next dates it falls on.
176
176
  - [Resizable](./resizable.md): Panels with handles between them to drag, or to move with the arrow keys, within limits you set; groups nest, and the layout is reported to keep.
177
- - [Seek Bar](./seek-bar.md): Where playback is in a track, and the way to move it: played and loaded on one track, between the time played and the length.
177
+ - [Seek Bar](./seek-bar.md): Where playback is in a track, and the way to move it: played and loaded on one track, the time played and the length beside it or under its ends.
178
178
  - [Segmented](./segmented.md): A pill row for a few mutually exclusive options.
179
179
  - [Select](./select.md): Pick one value from a short list, fully keyboard-driven. For long or searchable lists, use Combobox.
180
180
  - [Separator](./separator.md): A hairline between groups of content, across or upright.
181
181
  - [Side Nav](./side-nav.md): Vertical navigation in titled groups, for docs and settings sidebars. Works with any router.
182
182
  - [Skeleton & Spinner](./skeleton.md): Placeholders for content on its way.
183
183
  - [Slider](./slider.md): A native range input with a glowing thumb and any track you like.
184
- - [Sortable List](./sortable.md): 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.
184
+ - [Sortable List](./sortable.md): 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. Also a virtualized list of hundreds.
185
185
  - [Sparkline](./sparkline.md): A small line of how a number has moved, with no axes, for beside a figure. It draws itself in.
186
186
  - [Spinner](./spinner.md): A small loading spinner in the current text colour, announced to screen readers when labelled.
187
187
  - [Status Pill](./status-pill.md): 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.
@@ -200,6 +200,6 @@ Breaking changes and additions by version are in ../../CHANGELOG.md.
200
200
  - [Toggle](./toggle.md): An on/off switch.
201
201
  - [Toggle Group](./toggle-group.md): 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.
202
202
  - [Toolbar](./toolbar.md): 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.
203
- - [Tooltip](./tooltip.md): A short glass hint on hover or keyboard focus, describing its trigger to screen readers.
203
+ - [Tooltip](./tooltip.md): A short glass hint on hover or keyboard focus, describing its trigger to screen readers; below its trigger or beside it.
204
204
  - [Tree Table](./tree.md): 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.
205
205
  - [Trend](./trend.md): A change, like +12% with an arrow, coloured by whether it is good news.
@@ -96,4 +96,4 @@ part of the page.
96
96
  | Prop | Type | Default | Description |
97
97
  | --- | --- | --- | --- |
98
98
  | `locale` | `string` | | e.g. "en-IN", "fr-FR". Defaults to the enclosing provider's, or "en-US". |
99
- | `messages` | `Partial<{ clear: string; cancel: string; close: string; dismiss: string; delete: string; remove: (item: string) => string; noResults: string; search: string; expand: string; collapse: string; minimum: string; ... 109 more ...; timeOf: (time: string, total: string) => string; }>` | | Any of the words to replace; the others are kept. |
99
+ | `messages` | `Partial<{ clear: string; cancel: string; close: string; back: string; dismiss: string; delete: string; remove: (item: string) => string; noResults: string; search: string; expand: string; collapse: string; minimum: string; ... 109 more ...; timeOf: (time: string, total: string) => string; }>` | | Any of the words to replace; the others are kept. |
package/dist/docs/menu.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Menu
2
2
 
3
- A button that opens a menu, or a context menu from a right-click or long press, with your own trigger, a header, icons, groups, destructive items, checkbox and radio items, submenus, and a menubar of menus.
3
+ A button that opens a menu, or a context menu from a right-click or long press, with your own trigger, a header, icons, groups, destructive items, checkbox and radio items, submenus, a bottom sheet on phones, and a menubar of menus.
4
4
 
5
5
  ```tsx
6
6
  import { Menu, MenuTrigger, MenuContextTrigger, MenuContent, MenuItem, MenuCheckboxItem, MenuRadioGroup, MenuRadioItem, MenuSub, MenuSubTrigger, MenuSubContent, MenuSeparator, MenuHeader, Menubar } from '@rinzai/zen';
@@ -187,6 +187,58 @@ export default function Options() {
187
187
  }
188
188
  ```
189
189
 
190
+ ### Sheet
191
+
192
+ ```tsx
193
+ import {
194
+ buttonVariants,
195
+ Menu,
196
+ MenuContent,
197
+ MenuItem,
198
+ MenuSeparator,
199
+ MenuSub,
200
+ MenuSubContent,
201
+ MenuSubTrigger,
202
+ MenuTrigger,
203
+ useToast,
204
+ } from '@rinzai/zen';
205
+ // Icons from Heroicons (https://heroicons.com): Ellipsis (ellipsis-horizontal), Heart (heart), MusicalNote (musical-note), QueueList (queue-list).
206
+
207
+ /** A song's menu: on a phone (narrow the window), a bottom sheet, its playlists sliding in over it. */
208
+ export default function Sheet() {
209
+ const toast = useToast();
210
+ return (
211
+ <Menu>
212
+ <MenuTrigger aria-label="Song actions" className={buttonVariants({ variant: 'ghost', size: 'icon' })}>
213
+ <Ellipsis />
214
+ </MenuTrigger>
215
+ <MenuContent sheet>
216
+ <MenuItem icon={<QueueList className="size-4" />} onSelect={() => toast('Playing next')}>
217
+ Play next
218
+ </MenuItem>
219
+ <MenuItem icon={<Heart className="size-4" />} onSelect={() => toast('Liked', { tone: 'positive' })}>
220
+ Like
221
+ </MenuItem>
222
+ <MenuSub>
223
+ <MenuSubTrigger icon={<MusicalNote className="size-4" />}>Add to playlist</MenuSubTrigger>
224
+ <MenuSubContent>
225
+ {['Late night', 'Focus', 'Running', 'Sunday morning'].map((name) => (
226
+ <MenuItem key={name} onSelect={() => toast(`Added to ${name}`, { tone: 'positive' })}>
227
+ {name}
228
+ </MenuItem>
229
+ ))}
230
+ </MenuSubContent>
231
+ </MenuSub>
232
+ <MenuSeparator />
233
+ <MenuItem tone="negative" onSelect={() => toast('Removed from queue', { tone: 'negative' })}>
234
+ Remove from queue
235
+ </MenuItem>
236
+ </MenuContent>
237
+ </Menu>
238
+ );
239
+ }
240
+ ```
241
+
190
242
  ### Bar
191
243
 
192
244
  ```tsx
@@ -308,6 +360,7 @@ instead; name it with `aria-label`.
308
360
  | --- | --- | --- | --- |
309
361
  | `align` | `"center" \| "end" \| "start"` | | Which edge of the button the menu lines up with (default end; a context menu starts at the pointer), or centred on it. |
310
362
  | `offset` | `number` | | Gap between the button and the menu, in px. |
363
+ | `sheet` | `boolean` | | On phones (below `md`), a bottom sheet instead, as music apps show a song's menu: across the bottom of the screen over a scrim, swiped down to close; its submenus slide in over it, with a way back at the top. |
311
364
 
312
365
  ### MenuItem
313
366
 
@@ -1,6 +1,6 @@
1
1
  # Seek Bar
2
2
 
3
- Where playback is in a track, and the way to move it: played and loaded on one track, between the time played and the length.
3
+ Where playback is in a track, and the way to move it: played and loaded on one track, the time played and the length beside it or under its ends.
4
4
 
5
5
  ```tsx
6
6
  import { SeekBar, formatDuration } from '@rinzai/zen';
@@ -46,6 +46,34 @@ export default function Default() {
46
46
  }
47
47
  ```
48
48
 
49
+ ### Times below
50
+
51
+ ```tsx
52
+ import { SeekBar, useMediaPlayer } from '@rinzai/zen';
53
+
54
+ import { useDemoTrack } from '../../player-bar/examples/demo-track';
55
+
56
+ /** The times under the bar's ends, as on a phone's Now Playing screen. */
57
+ export default function TimesBelow() {
58
+ const src = useDemoTrack();
59
+ const [audioRef, player] = useMediaPlayer();
60
+ return (
61
+ <div className="w-80">
62
+ <audio ref={audioRef} src={src} preload="metadata" />
63
+ <SeekBar
64
+ times="below"
65
+ currentTime={player.currentTime}
66
+ playing={player.playing && !player.waiting}
67
+ playbackRate={player.playbackRate}
68
+ duration={player.duration}
69
+ buffered={player.buffered}
70
+ onSeek={player.seek}
71
+ />
72
+ </div>
73
+ );
74
+ }
75
+ ```
76
+
49
77
  ## API reference
50
78
 
51
79
  ### SeekBar
@@ -65,4 +93,5 @@ the track keeps playing; letting go seeks. The arrow keys seek 5 seconds.
65
93
  | `playbackRate` | `number` | `1` | How fast it moves on while playing (default 1). |
66
94
  | `playing` | `boolean` | `false` | While the time is running (playing, not waiting for data): the bar glides on between time updates. |
67
95
  | `thumbColor` | `string` | | CSS colour for the thumb (default: the accent). |
96
+ | `times` | `"below" \| "beside"` | `beside` | Where the time played and the length go: either side of the bar (default), or under its ends, as on a phone's Now Playing screen. |
68
97
  | `valueText` | `((value: number) => string)` | | Spoken value, e.g. (v) => `Hue ${v} degrees`. |
@@ -1,6 +1,6 @@
1
1
  # Sortable List
2
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.
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. Also a virtualized list of hundreds.
4
4
 
5
5
  ```tsx
6
6
  import { SortableList, SortableTree, SortableItem, SortableHandle, flattenTree } from '@rinzai/zen';
@@ -116,6 +116,66 @@ export default function Groups() {
116
116
  }
117
117
  ```
118
118
 
119
+ ### Virtual
120
+
121
+ ```tsx
122
+ import { SortableHandle, SortableItem, SortableList, useVirtualList } from '@rinzai/zen';
123
+ import { useRef, useState } from 'react';
124
+
125
+ const ROW = 48;
126
+ const ARTISTS = ['Nils Frahm', 'Ólafur Arnalds', 'Hania Rani', 'Max Richter', 'Bonobo', 'Tycho', 'Jon Hopkins'];
127
+ const SONGS = Object.fromEntries(
128
+ Array.from({ length: 500 }, (_, i) => [`s${i}`, { title: `Track ${i + 1}`, artist: ARTISTS[(i * 3) % 7] }]),
129
+ );
130
+
131
+ /**
132
+ * A queue of 500, rendering only the rows in view: `itemHeight` on the list,
133
+ * and the dragged row kept rendered (`keep`) as it scrolls the list along.
134
+ */
135
+ export default function Virtual() {
136
+ const [order, setOrder] = useState(Object.keys(SONGS));
137
+ const [lifted, setLifted] = useState<string | null>(null);
138
+ const scrollRef = useRef<HTMLDivElement>(null);
139
+ const rows = useVirtualList({
140
+ count: order.length,
141
+ itemHeight: ROW,
142
+ scrollRef,
143
+ keep: lifted ? order.indexOf(lifted) : undefined,
144
+ });
145
+ return (
146
+ <div ref={scrollRef} className="glass h-96 w-full max-w-md overflow-y-auto rounded-xl">
147
+ <SortableList
148
+ value={order}
149
+ onValueChange={setOrder}
150
+ itemHeight={ROW}
151
+ onLiftedChange={setLifted}
152
+ aria-label="Up next"
153
+ style={{ height: rows.totalSize }}
154
+ >
155
+ {rows.items.map(({ index, start }) => {
156
+ const id = order[index];
157
+ const song = SONGS[id];
158
+ return (
159
+ <SortableItem
160
+ key={id}
161
+ id={id}
162
+ className="absolute! inset-x-0 flex items-center gap-2 px-1.5"
163
+ style={{ top: start, height: ROW }}
164
+ >
165
+ <SortableHandle id={id} label={song.title} />
166
+ <div className="min-w-0">
167
+ <div className="truncate text-sm font-medium">{song.title}</div>
168
+ <div className="text-muted-foreground truncate text-xs">{song.artist}</div>
169
+ </div>
170
+ </SortableItem>
171
+ );
172
+ })}
173
+ </SortableList>
174
+ </div>
175
+ );
176
+ }
177
+ ```
178
+
119
179
  ## API reference
120
180
 
121
181
  ### SortableList
@@ -134,6 +194,8 @@ TableBody of TableRows, say, one per group, each sorted on its own.
134
194
  | `onValueChange` (required) | `(value: string[]) => void` | | |
135
195
  | `value` (required) | `readonly string[]` | | The items' ids, in order. |
136
196
  | `asChild` | `boolean` | | Put the list's props on your own element (the single child, e.g. a TableBody) instead of a ul. |
197
+ | `itemHeight` | `number` | | For a virtualized list (useVirtualList), its rows' height, gap included: render just the items in view, each placed at its place in `value` times this, and the drag works from `value` instead of the rows on screen. |
198
+ | `onLiftedChange` | `((id: string \| null) => void)` | | When an item is picked up (by its handle, or from the keyboard), and when it's put down (null). A virtualized list keeps that row rendered meanwhile (useVirtualList's `keep`), so it stays under the pointer as the list scrolls. |
137
199
 
138
200
  ### SortableTree
139
201
 
@@ -1,6 +1,6 @@
1
1
  # Tooltip
2
2
 
3
- A short glass hint on hover or keyboard focus, describing its trigger to screen readers.
3
+ A short glass hint on hover or keyboard focus, describing its trigger to screen readers; below its trigger or beside it.
4
4
 
5
5
  ```tsx
6
6
  import { Tooltip, TooltipTrigger, TooltipContent } from '@rinzai/zen';
@@ -31,6 +31,45 @@ export default function Default() {
31
31
  }
32
32
  ```
33
33
 
34
+ ### Beside
35
+
36
+ ```tsx
37
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@rinzai/zen';
38
+ // Icons from Heroicons (https://heroicons.com): MusicalNote (musical-note), QueueList (queue-list), Search (magnifying-glass), Squares (squares-2x2).
39
+
40
+ const ITEMS = [
41
+ { label: 'Home', icon: Squares },
42
+ { label: 'Search', icon: Search },
43
+ { label: 'Library', icon: MusicalNote },
44
+ { label: 'Up next', icon: QueueList },
45
+ ];
46
+
47
+ /**
48
+ * An icon rail, each icon's name in a tooltip beside it (`side="beside"`), clear
49
+ * of the icons below. The name is the button's own label, so it isn't also its description.
50
+ */
51
+ export default function Beside() {
52
+ return (
53
+ <nav aria-label="Main" className="glass flex flex-col gap-1 rounded-xl p-1.5">
54
+ {ITEMS.map(({ label, icon: Icon }) => (
55
+ <Tooltip key={label}>
56
+ <TooltipTrigger
57
+ aria-label={label}
58
+ aria-describedby={undefined}
59
+ className="text-muted-foreground hover:text-foreground hover:bg-tint/[0.07] focus-ring grid size-10 cursor-pointer place-items-center rounded-lg transition-colors"
60
+ >
61
+ <Icon className="size-5" />
62
+ </TooltipTrigger>
63
+ <TooltipContent side="beside" offset={8}>
64
+ <p className="px-2.5 py-1.5 text-sm">{label}</p>
65
+ </TooltipContent>
66
+ </Tooltip>
67
+ ))}
68
+ </nav>
69
+ );
70
+ }
71
+ ```
72
+
34
73
  ## API reference
35
74
 
36
75
  ### Tooltip
@@ -52,9 +91,12 @@ What the tooltip describes: a button by default. With `asChild`, your own focusa
52
91
 
53
92
  ### TooltipContent
54
93
 
55
- The hint: glass, below the trigger (above it if there's no room), centred on it by default.
94
+ The hint: glass, below the trigger (above it if there's no room), centred on
95
+ it by default. `side="beside"` puts it after the trigger instead (before it
96
+ if there's no room), as for the icons of a collapsed sidebar.
56
97
 
57
98
  | Prop | Type | Default | Description |
58
99
  | --- | --- | --- | --- |
59
- | `align` | `"center" \| "end" \| "start"` | `center` | Centred on the trigger (default), or lined up with its start or end edge. |
100
+ | `align` | `"center" \| "end" \| "start"` | `center` | Centred on the trigger (default), or lined up with its start or end edge (beside: its top or bottom). |
60
101
  | `offset` | `number` | `5` | Gap from the trigger, in px. |
102
+ | `side` | `"below" \| "beside"` | `below` | Below the trigger (default; above if there's no room), or beside it (after it; before if there's no room). |
@@ -1,6 +1,6 @@
1
1
  "use client";
2
- import { cx as e } from "../utils/cx.js";
3
- import { useMessages as t } from "../locale-provider/index.js";
2
+ import { useMessages as e } from "../locale-provider/index.js";
3
+ import { cx as t } from "../utils/cx.js";
4
4
  import { reducedMotion as n } from "../utils/motion.js";
5
5
  import r from "../icons/micro/x-mark.js";
6
6
  import { Children as i, useEffect as a, useLayoutEffect as o, useRef as s } from "react";
@@ -14,7 +14,7 @@ var u = typeof window > "u" ? a : o, d = {
14
14
  easing: "ease-in"
15
15
  };
16
16
  function p({ onClear: n, clearLabel: r, "aria-label": a, className: o, children: u, ref: d, ...f }) {
17
- let p = t(), h = r ?? p.clearAll, g = a ?? p.activeFilters, _ = i.toArray(u).length, v = s(null);
17
+ let p = e(), h = r ?? p.clearAll, g = a ?? p.activeFilters, _ = i.toArray(u).length, v = s(null);
18
18
  return m(v), _ === 0 ? null : /* @__PURE__ */ l("div", {
19
19
  ref: (e) => {
20
20
  if (v.current = e, typeof d == "function") return d(e);
@@ -22,7 +22,7 @@ function p({ onClear: n, clearLabel: r, "aria-label": a, className: o, children:
22
22
  },
23
23
  role: "group",
24
24
  "aria-label": g,
25
- className: e("zen__filter-bar relative flex flex-wrap items-center gap-1.5", o),
25
+ className: t("zen__filter-bar relative flex flex-wrap items-center gap-1.5", o),
26
26
  ...f,
27
27
  children: [u, _ > 1 && n && /* @__PURE__ */ c("button", {
28
28
  type: "button",
@@ -76,9 +76,9 @@ function m(e) {
76
76
  });
77
77
  }
78
78
  function h({ onRemove: n, removeLabel: i, className: a, children: o }) {
79
- let s = t(), u = i ?? s.removeFilter;
79
+ let s = e(), u = i ?? s.removeFilter;
80
80
  return /* @__PURE__ */ l("span", {
81
- className: e("zen__filter-chip bg-primary/12 text-foreground inline-flex items-center gap-1 rounded-full py-1 ps-3 pe-1 text-xs shadow-[inset_0_0_0_1px_oklch(var(--primary)/0.25)]", a),
81
+ className: t("zen__filter-chip bg-primary/12 text-foreground inline-flex items-center gap-1 rounded-full py-1 ps-3 pe-1 text-xs shadow-[inset_0_0_0_1px_oklch(var(--primary)/0.25)]", a),
82
82
  children: [o, /* @__PURE__ */ c("button", {
83
83
  type: "button",
84
84
  "aria-label": u,
@@ -1,13 +1,13 @@
1
1
  "use client";
2
- import { cx as e } from "../utils/cx.js";
3
- import { useGraphicsMode as t } from "../utils/graphics.js";
4
- import { useMessages as n } from "../locale-provider/index.js";
2
+ import { useMessages as e } from "../locale-provider/index.js";
3
+ import { cx as t } from "../utils/cx.js";
4
+ import { useGraphicsMode as n } from "../utils/graphics.js";
5
5
  import { jsx as r, jsxs as i } from "react/jsx-runtime";
6
6
  //#region packages/header/index.tsx
7
7
  function a({ skipLabel: a, className: o, children: s, ...c }) {
8
- let l = n(), u = a === void 0 ? l.skipToContent : a;
9
- return t(), /* @__PURE__ */ i("header", {
10
- className: e("zen__header glow-edge border-tint/5 bg-background/60 sticky top-0 z-40 border-b pt-[env(safe-area-inset-top)] backdrop-blur-xl", "pointer-coarse:bg-background/95 pointer-coarse:backdrop-blur-none", o),
8
+ let l = e(), u = a === void 0 ? l.skipToContent : a;
9
+ return n(), /* @__PURE__ */ i("header", {
10
+ className: t("zen__header glow-edge border-tint/5 bg-background/60 sticky top-0 z-40 border-b pt-[env(safe-area-inset-top)] backdrop-blur-xl", "pointer-coarse:bg-background/95 pointer-coarse:backdrop-blur-none", o),
11
11
  ...c,
12
12
  children: [u !== null && /* @__PURE__ */ r("a", {
13
13
  href: "#main",
@@ -1,7 +1,7 @@
1
1
  "use client";
2
- import { cx as e } from "../utils/cx.js";
3
- import { useControllable as t } from "../utils/useControllable.js";
4
- import { useMessages as n } from "../locale-provider/index.js";
2
+ import { useMessages as e } from "../locale-provider/index.js";
3
+ import { cx as t } from "../utils/cx.js";
4
+ import { useControllable as n } from "../utils/useControllable.js";
5
5
  import r, { PopoverContent as i, PopoverTrigger as a } from "../popover/index.js";
6
6
  import { useId as o } from "react";
7
7
  import { jsx as s, jsxs as c } from "react/jsx-runtime";
@@ -41,12 +41,12 @@ var l = [
41
41
  }
42
42
  ];
43
43
  function u({ icons: u, colors: d = l, value: f, defaultValue: p = { icon: "" }, onValueChange: m, label: h, name: g, className: _, ...v }) {
44
- let [y, b] = t(f, p, m), x = n(), S = (e) => e.label ?? x.colourNames[e.value] ?? e.value, C = o(), w = g ?? C, T = u.find((e) => e.value === y.icon), E = d.find((e) => e.value === y.color), D = (e) => ({ "--icon-color": e ?? "var(--color-muted-foreground)" });
44
+ let [y, b] = n(f, p, m), x = e(), S = (e) => e.label ?? x.colourNames[e.value] ?? e.value, C = o(), w = g ?? C, T = u.find((e) => e.value === y.icon), E = d.find((e) => e.value === y.color), D = (e) => ({ "--icon-color": e ?? "var(--color-muted-foreground)" });
45
45
  return /* @__PURE__ */ c(r, { children: [/* @__PURE__ */ s(a, {
46
46
  ...v,
47
47
  "aria-label": `${h}: ${[T?.label, E && S(E)].filter(Boolean).join(", ") || x.none}`,
48
48
  style: D(E?.color),
49
- className: e("zen__icon-picker grid size-10 shrink-0 place-items-center rounded-xl bg-[color-mix(in_oklch,var(--icon-color)_18%,transparent)] text-lg text-(--icon-color) outline-hidden transition-colors focus-ring", _),
49
+ className: t("zen__icon-picker grid size-10 shrink-0 place-items-center rounded-xl bg-[color-mix(in_oklch,var(--icon-color)_18%,transparent)] text-lg text-(--icon-color) outline-hidden transition-colors focus-ring", _),
50
50
  children: T?.icon
51
51
  }), /* @__PURE__ */ c(i, {
52
52
  "aria-label": h,
@@ -1,7 +1,7 @@
1
1
  "use client";
2
- import { cx as e } from "../utils/cx.js";
3
- import { useControllable as t } from "../utils/useControllable.js";
4
- import { useMessages as n } from "../locale-provider/index.js";
2
+ import { useMessages as e } from "../locale-provider/index.js";
3
+ import { cx as t } from "../utils/cx.js";
4
+ import { useControllable as n } from "../utils/useControllable.js";
5
5
  import r from "../icons/arrow-left.js";
6
6
  import { jsx as i } from "react/jsx-runtime";
7
7
  //#region packages/keypad/index.tsx
@@ -20,7 +20,7 @@ var a = [
20
20
  "back"
21
21
  ];
22
22
  function o({ value: o, defaultValue: s = "", onValueChange: c, decimals: l = 2, maxLength: u = 12, deleteLabel: d, "aria-label": f, className: p, ...m }) {
23
- let h = n(), g = d ?? h.delete, _ = f ?? h.keypad, [v, y] = t(o, s, c), b = (e) => {
23
+ let h = e(), g = d ?? h.delete, _ = f ?? h.keypad, [v, y] = n(o, s, c), b = (e) => {
24
24
  if (e === "back") return y(v.slice(0, -1));
25
25
  let t = v.indexOf(".");
26
26
  if (e === ".") return l === 0 || t !== -1 ? void 0 : y((v || "0") + ".");
@@ -29,7 +29,7 @@ function o({ value: o, defaultValue: s = "", onValueChange: c, decimals: l = 2,
29
29
  return /* @__PURE__ */ i("div", {
30
30
  role: "group",
31
31
  "aria-label": _,
32
- className: e("zen__keypad grid grid-cols-3 gap-2", p),
32
+ className: t("zen__keypad grid grid-cols-3 gap-2", p),
33
33
  ...m,
34
34
  children: a.map((e) => /* @__PURE__ */ i("button", {
35
35
  type: "button",