@zyncat/ui 0.8.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 (334) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +133 -0
  3. package/dist/alert.css +132 -0
  4. package/dist/alert.d.ts +46 -0
  5. package/dist/alert.js +80 -0
  6. package/dist/alert.js.map +1 -0
  7. package/dist/avatar-group.d.ts +24 -0
  8. package/dist/avatar-group.js +23 -0
  9. package/dist/avatar-group.js.map +1 -0
  10. package/dist/avatar.css +200 -0
  11. package/dist/avatar.d.ts +35 -0
  12. package/dist/avatar.js +67 -0
  13. package/dist/avatar.js.map +1 -0
  14. package/dist/badge.css +182 -0
  15. package/dist/badge.d.ts +34 -0
  16. package/dist/badge.js +4 -0
  17. package/dist/badge.js.map +1 -0
  18. package/dist/button.css +269 -0
  19. package/dist/button.d.ts +44 -0
  20. package/dist/button.js +4 -0
  21. package/dist/button.js.map +1 -0
  22. package/dist/checkbox.css +177 -0
  23. package/dist/checkbox.d.ts +35 -0
  24. package/dist/checkbox.js +5 -0
  25. package/dist/checkbox.js.map +1 -0
  26. package/dist/chunk-2ERBBFZS.js +12 -0
  27. package/dist/chunk-2ERBBFZS.js.map +1 -0
  28. package/dist/chunk-2HORSLXJ.js +191 -0
  29. package/dist/chunk-2HORSLXJ.js.map +1 -0
  30. package/dist/chunk-36WVAQKL.js +40 -0
  31. package/dist/chunk-36WVAQKL.js.map +1 -0
  32. package/dist/chunk-3SBCBYB7.js +575 -0
  33. package/dist/chunk-3SBCBYB7.js.map +1 -0
  34. package/dist/chunk-4R2GQO43.js +125 -0
  35. package/dist/chunk-4R2GQO43.js.map +1 -0
  36. package/dist/chunk-64UU4LGU.js +258 -0
  37. package/dist/chunk-64UU4LGU.js.map +1 -0
  38. package/dist/chunk-7SZCJ7WI.js +35 -0
  39. package/dist/chunk-7SZCJ7WI.js.map +1 -0
  40. package/dist/chunk-BIDVNLJO.js +38 -0
  41. package/dist/chunk-BIDVNLJO.js.map +1 -0
  42. package/dist/chunk-CONSWNIV.js +17 -0
  43. package/dist/chunk-CONSWNIV.js.map +1 -0
  44. package/dist/chunk-EQ32F2QJ.js +75 -0
  45. package/dist/chunk-EQ32F2QJ.js.map +1 -0
  46. package/dist/chunk-GY7JOPUQ.js +111 -0
  47. package/dist/chunk-GY7JOPUQ.js.map +1 -0
  48. package/dist/chunk-I4XQ4PV2.js +30 -0
  49. package/dist/chunk-I4XQ4PV2.js.map +1 -0
  50. package/dist/chunk-JSKXZMLH.js +67 -0
  51. package/dist/chunk-JSKXZMLH.js.map +1 -0
  52. package/dist/chunk-L3Q3VDIW.js +295 -0
  53. package/dist/chunk-L3Q3VDIW.js.map +1 -0
  54. package/dist/chunk-LDC3RIYA.js +62 -0
  55. package/dist/chunk-LDC3RIYA.js.map +1 -0
  56. package/dist/chunk-MKIQFK7S.js +43 -0
  57. package/dist/chunk-MKIQFK7S.js.map +1 -0
  58. package/dist/chunk-MMRIQNEN.js +20 -0
  59. package/dist/chunk-MMRIQNEN.js.map +1 -0
  60. package/dist/chunk-MXOPCFWR.js +59 -0
  61. package/dist/chunk-MXOPCFWR.js.map +1 -0
  62. package/dist/chunk-NEF5XAHG.js +17 -0
  63. package/dist/chunk-NEF5XAHG.js.map +1 -0
  64. package/dist/chunk-NNXJCLTV.js +22 -0
  65. package/dist/chunk-NNXJCLTV.js.map +1 -0
  66. package/dist/chunk-O4N2OBTR.js +94 -0
  67. package/dist/chunk-O4N2OBTR.js.map +1 -0
  68. package/dist/chunk-PMECHR3A.js +38 -0
  69. package/dist/chunk-PMECHR3A.js.map +1 -0
  70. package/dist/chunk-QDK66S4G.js +44 -0
  71. package/dist/chunk-QDK66S4G.js.map +1 -0
  72. package/dist/chunk-QGNC7LH5.js +8 -0
  73. package/dist/chunk-QGNC7LH5.js.map +1 -0
  74. package/dist/chunk-QMSJZCK3.js +398 -0
  75. package/dist/chunk-QMSJZCK3.js.map +1 -0
  76. package/dist/chunk-S5ZTZANS.js +57 -0
  77. package/dist/chunk-S5ZTZANS.js.map +1 -0
  78. package/dist/chunk-S7B34VGN.js +14 -0
  79. package/dist/chunk-S7B34VGN.js.map +1 -0
  80. package/dist/chunk-TCUCYFT7.js +25 -0
  81. package/dist/chunk-TCUCYFT7.js.map +1 -0
  82. package/dist/chunk-TYHR4VNM.js +193 -0
  83. package/dist/chunk-TYHR4VNM.js.map +1 -0
  84. package/dist/chunk-U5QQZC3U.js +56 -0
  85. package/dist/chunk-U5QQZC3U.js.map +1 -0
  86. package/dist/chunk-UE63OOVG.js +13 -0
  87. package/dist/chunk-UE63OOVG.js.map +1 -0
  88. package/dist/chunk-VTJTGVZZ.js +22 -0
  89. package/dist/chunk-VTJTGVZZ.js.map +1 -0
  90. package/dist/chunk-W3SHPKSB.js +49 -0
  91. package/dist/chunk-W3SHPKSB.js.map +1 -0
  92. package/dist/chunk-XZAEIQDR.js +188 -0
  93. package/dist/chunk-XZAEIQDR.js.map +1 -0
  94. package/dist/chunk-YGGCQNNU.js +92 -0
  95. package/dist/chunk-YGGCQNNU.js.map +1 -0
  96. package/dist/collapse.css +88 -0
  97. package/dist/collapse.d.ts +46 -0
  98. package/dist/collapse.js +5 -0
  99. package/dist/collapse.js.map +1 -0
  100. package/dist/count-badge.d.ts +13 -0
  101. package/dist/count-badge.js +13 -0
  102. package/dist/count-badge.js.map +1 -0
  103. package/dist/date-field.d.ts +25 -0
  104. package/dist/date-field.js +87 -0
  105. package/dist/date-field.js.map +1 -0
  106. package/dist/date-picker.css +595 -0
  107. package/dist/date-range-field.d.ts +33 -0
  108. package/dist/date-range-field.js +404 -0
  109. package/dist/date-range-field.js.map +1 -0
  110. package/dist/datetime-field.d.ts +29 -0
  111. package/dist/datetime-field.js +155 -0
  112. package/dist/datetime-field.js.map +1 -0
  113. package/dist/dialog.css +180 -0
  114. package/dist/dialog.d.ts +42 -0
  115. package/dist/dialog.js +128 -0
  116. package/dist/dialog.js.map +1 -0
  117. package/dist/dom-props-C0jrllBw.d.ts +5 -0
  118. package/dist/field-chrome-CMPOQSLs.d.ts +28 -0
  119. package/dist/field-shell-Cuda-feL.d.ts +24 -0
  120. package/dist/glide.d.ts +15 -0
  121. package/dist/glide.js +5 -0
  122. package/dist/glide.js.map +1 -0
  123. package/dist/icon.css +33 -0
  124. package/dist/input.css +333 -0
  125. package/dist/mcp.js +359 -0
  126. package/dist/motion-devtools.css +265 -0
  127. package/dist/motion-devtools.d.ts +51 -0
  128. package/dist/motion-devtools.js +354 -0
  129. package/dist/motion-devtools.js.map +1 -0
  130. package/dist/motion-scale-Canq4stZ.d.ts +18 -0
  131. package/dist/motion-tokens.d.ts +28 -0
  132. package/dist/motion-tokens.js +3 -0
  133. package/dist/motion-tokens.js.map +1 -0
  134. package/dist/multi-select.d.ts +44 -0
  135. package/dist/multi-select.js +109 -0
  136. package/dist/multi-select.js.map +1 -0
  137. package/dist/number-field.d.ts +36 -0
  138. package/dist/number-field.js +112 -0
  139. package/dist/number-field.js.map +1 -0
  140. package/dist/odometer.css +62 -0
  141. package/dist/otp-field.d.ts +31 -0
  142. package/dist/otp-field.js +98 -0
  143. package/dist/otp-field.js.map +1 -0
  144. package/dist/overlay.css +23 -0
  145. package/dist/pagination.css +43 -0
  146. package/dist/pagination.d.ts +29 -0
  147. package/dist/pagination.js +107 -0
  148. package/dist/pagination.js.map +1 -0
  149. package/dist/popover.css +69 -0
  150. package/dist/popover.d.ts +35 -0
  151. package/dist/popover.js +15 -0
  152. package/dist/popover.js.map +1 -0
  153. package/dist/radio-group.css +300 -0
  154. package/dist/radio-group.d.ts +52 -0
  155. package/dist/radio-group.js +115 -0
  156. package/dist/radio-group.js.map +1 -0
  157. package/dist/select.css +416 -0
  158. package/dist/select.d.ts +49 -0
  159. package/dist/select.js +99 -0
  160. package/dist/select.js.map +1 -0
  161. package/dist/sheet.css +26 -0
  162. package/dist/sheet.d.ts +31 -0
  163. package/dist/sheet.js +14 -0
  164. package/dist/sheet.js.map +1 -0
  165. package/dist/status-badge.d.ts +19 -0
  166. package/dist/status-badge.js +72 -0
  167. package/dist/status-badge.js.map +1 -0
  168. package/dist/table.css +348 -0
  169. package/dist/table.d.ts +75 -0
  170. package/dist/table.js +262 -0
  171. package/dist/table.js.map +1 -0
  172. package/dist/tabs.css +208 -0
  173. package/dist/tabs.d.ts +71 -0
  174. package/dist/tabs.js +181 -0
  175. package/dist/tabs.js.map +1 -0
  176. package/dist/tag.css +248 -0
  177. package/dist/tag.d.ts +52 -0
  178. package/dist/tag.js +72 -0
  179. package/dist/tag.js.map +1 -0
  180. package/dist/text-field.d.ts +28 -0
  181. package/dist/text-field.js +69 -0
  182. package/dist/text-field.js.map +1 -0
  183. package/dist/textarea.css +219 -0
  184. package/dist/textarea.d.ts +38 -0
  185. package/dist/textarea.js +144 -0
  186. package/dist/textarea.js.map +1 -0
  187. package/dist/time-field.d.ts +28 -0
  188. package/dist/time-field.js +66 -0
  189. package/dist/time-field.js.map +1 -0
  190. package/dist/timing-7JA_zQHP.d.ts +27 -0
  191. package/dist/toast-store.d.ts +86 -0
  192. package/dist/toast-store.js +3 -0
  193. package/dist/toast-store.js.map +1 -0
  194. package/dist/toast.css +257 -0
  195. package/dist/toast.d.ts +12 -0
  196. package/dist/toast.js +310 -0
  197. package/dist/toast.js.map +1 -0
  198. package/dist/toggle-tag.d.ts +34 -0
  199. package/dist/toggle-tag.js +57 -0
  200. package/dist/toggle-tag.js.map +1 -0
  201. package/dist/toggle.css +152 -0
  202. package/dist/toggle.d.ts +31 -0
  203. package/dist/toggle.js +75 -0
  204. package/dist/toggle.js.map +1 -0
  205. package/dist/tooltip.css +86 -0
  206. package/dist/tooltip.d.ts +30 -0
  207. package/dist/tooltip.js +268 -0
  208. package/dist/tooltip.js.map +1 -0
  209. package/dist/types-SGMShK7R.d.ts +22 -0
  210. package/llms.txt +196 -0
  211. package/next.d.mts +7 -0
  212. package/next.mjs +59 -0
  213. package/package.json +257 -0
  214. package/src/components/composites/alert/Alert.tsx +141 -0
  215. package/src/components/composites/alert/alert.css +132 -0
  216. package/src/components/composites/date-picker/DateField.tsx +80 -0
  217. package/src/components/composites/date-picker/DateRangeField.tsx +110 -0
  218. package/src/components/composites/date-picker/DateTimeField.tsx +162 -0
  219. package/src/components/composites/date-picker/TimeField.tsx +78 -0
  220. package/src/components/composites/date-picker/calendar-panel.tsx +216 -0
  221. package/src/components/composites/date-picker/date-picker.css +595 -0
  222. package/src/components/composites/date-picker/date-utils.ts +56 -0
  223. package/src/components/composites/date-picker/field-shell.tsx +85 -0
  224. package/src/components/composites/date-picker/range-panel.tsx +363 -0
  225. package/src/components/composites/date-picker/time-core.tsx +290 -0
  226. package/src/components/composites/date-picker/use-day-focus.ts +35 -0
  227. package/src/components/composites/dialog/Dialog.tsx +198 -0
  228. package/src/components/composites/dialog/dialog.css +180 -0
  229. package/src/components/composites/pagination/Pagination.tsx +131 -0
  230. package/src/components/composites/pagination/pagination.css +43 -0
  231. package/src/components/composites/popover/Popover.tsx +141 -0
  232. package/src/components/composites/popover/popover.css +69 -0
  233. package/src/components/composites/select/MultiSelect.tsx +133 -0
  234. package/src/components/composites/select/Select.tsx +122 -0
  235. package/src/components/composites/select/core/index.ts +4 -0
  236. package/src/components/composites/select/core/menu.tsx +100 -0
  237. package/src/components/composites/select/core/panel.tsx +154 -0
  238. package/src/components/composites/select/core/trigger.tsx +74 -0
  239. package/src/components/composites/select/core/types.ts +39 -0
  240. package/src/components/composites/select/core/use-listbox.ts +182 -0
  241. package/src/components/composites/select/select.css +416 -0
  242. package/src/components/composites/sheet/Sheet.tsx +135 -0
  243. package/src/components/composites/sheet/sheet.css +26 -0
  244. package/src/components/composites/sheet/use-sheet-drag.ts +126 -0
  245. package/src/components/composites/table/Table.tsx +377 -0
  246. package/src/components/composites/table/table.css +348 -0
  247. package/src/components/composites/tabs/Tabs.tsx +278 -0
  248. package/src/components/composites/tabs/tabs.css +208 -0
  249. package/src/components/composites/toast/Toast.tsx +380 -0
  250. package/src/components/composites/toast/toast-store.ts +295 -0
  251. package/src/components/composites/toast/toast.css +257 -0
  252. package/src/components/composites/tooltip/Tooltip.tsx +110 -0
  253. package/src/components/composites/tooltip/tooltip-host.tsx +184 -0
  254. package/src/components/composites/tooltip/tooltip-store.ts +81 -0
  255. package/src/components/composites/tooltip/tooltip.css +86 -0
  256. package/src/components/dev/MotionDevtools.tsx +200 -0
  257. package/src/components/dev/motion-devtools.css +265 -0
  258. package/src/components/dev/slowmo-engine.ts +230 -0
  259. package/src/components/internal/glass/glass.css +100 -0
  260. package/src/components/internal/glass/glint.ts +9 -0
  261. package/src/components/internal/hooks/use-controllable.ts +21 -0
  262. package/src/components/internal/hooks/use-scroll-edges.ts +46 -0
  263. package/src/components/internal/icon/Icon.tsx +66 -0
  264. package/src/components/internal/icon/IconSlot.tsx +10 -0
  265. package/src/components/internal/icon/icon.css +33 -0
  266. package/src/components/internal/overlay/focus.ts +68 -0
  267. package/src/components/internal/overlay/layer.tsx +149 -0
  268. package/src/components/internal/overlay/modal.tsx +141 -0
  269. package/src/components/internal/overlay/overlay.css +23 -0
  270. package/src/components/internal/overlay/position.ts +103 -0
  271. package/src/components/internal/utils/cx.ts +3 -0
  272. package/src/components/internal/utils/token-px.ts +8 -0
  273. package/src/components/primitives/avatar/Avatar.tsx +120 -0
  274. package/src/components/primitives/avatar/AvatarGroup.tsx +56 -0
  275. package/src/components/primitives/avatar/avatar.css +200 -0
  276. package/src/components/primitives/badge/Badge.tsx +75 -0
  277. package/src/components/primitives/badge/CountBadge.tsx +20 -0
  278. package/src/components/primitives/badge/StatusBadge.tsx +111 -0
  279. package/src/components/primitives/badge/badge.css +182 -0
  280. package/src/components/primitives/badge/odometer.css +62 -0
  281. package/src/components/primitives/badge/odometer.tsx +36 -0
  282. package/src/components/primitives/button/Button.tsx +102 -0
  283. package/src/components/primitives/button/button.css +269 -0
  284. package/src/components/primitives/checkbox/Checkbox.tsx +76 -0
  285. package/src/components/primitives/checkbox/check-glyph.tsx +28 -0
  286. package/src/components/primitives/checkbox/checkbox.css +177 -0
  287. package/src/components/primitives/collapse/Collapse.tsx +86 -0
  288. package/src/components/primitives/collapse/collapse.css +88 -0
  289. package/src/components/primitives/collapse/collapse.typecheck.tsx +29 -0
  290. package/src/components/primitives/input/NumberField.tsx +139 -0
  291. package/src/components/primitives/input/OtpField.tsx +134 -0
  292. package/src/components/primitives/input/TextField.tsx +110 -0
  293. package/src/components/primitives/input/field-chrome.tsx +83 -0
  294. package/src/components/primitives/input/input.css +333 -0
  295. package/src/components/primitives/radio-group/RadioGroup.tsx +183 -0
  296. package/src/components/primitives/radio-group/radio-group.css +300 -0
  297. package/src/components/primitives/tag/Tag.tsx +143 -0
  298. package/src/components/primitives/tag/ToggleTag.tsx +97 -0
  299. package/src/components/primitives/tag/tag.css +248 -0
  300. package/src/components/primitives/textarea/Textarea.tsx +190 -0
  301. package/src/components/primitives/textarea/textarea.css +219 -0
  302. package/src/components/primitives/toggle/Toggle.tsx +105 -0
  303. package/src/components/primitives/toggle/toggle.css +152 -0
  304. package/src/css.d.ts +1 -0
  305. package/src/dom-props.ts +1 -0
  306. package/src/engine/animate.ts +141 -0
  307. package/src/engine/drag.ts +53 -0
  308. package/src/engine/ease.ts +155 -0
  309. package/src/engine/flip.ts +59 -0
  310. package/src/engine/index.ts +13 -0
  311. package/src/mcp/server.ts +420 -0
  312. package/src/motion/element.tsx +53 -0
  313. package/src/motion/flip.tsx +87 -0
  314. package/src/motion/glide.tsx +47 -0
  315. package/src/motion/motion-timing.ts +33 -0
  316. package/src/motion/presence-context.ts +18 -0
  317. package/src/motion/presence.tsx +91 -0
  318. package/src/motion/timing.ts +49 -0
  319. package/src/motion/use-motion.ts +64 -0
  320. package/src/styles.css +26 -0
  321. package/src/tokens/avatar.css +20 -0
  322. package/src/tokens/color.css +58 -0
  323. package/src/tokens/elevation.css +22 -0
  324. package/src/tokens/fonts.css +3 -0
  325. package/src/tokens/glass.css +20 -0
  326. package/src/tokens/icons.css +7 -0
  327. package/src/tokens/layers.css +6 -0
  328. package/src/tokens/motion-scale.ts +17 -0
  329. package/src/tokens/motion-tokens.ts +92 -0
  330. package/src/tokens/motion.css +49 -0
  331. package/src/tokens/radius.css +11 -0
  332. package/src/tokens/semantic.css +65 -0
  333. package/src/tokens/spacing.css +26 -0
  334. package/src/tokens/typography.css +52 -0
@@ -0,0 +1,131 @@
1
+ 'use client';
2
+
3
+ import './pagination.css';
4
+ import { useEffect, useRef, type HTMLAttributes } from 'react';
5
+ import { animate } from '../../../engine';
6
+ import { UIMotion } from '../../../tokens/motion-tokens';
7
+ import { tokenPx } from '../../internal/utils/token-px';
8
+ import { Icon } from '../../internal/icon/Icon';
9
+ import { Button } from '../../primitives/button/Button';
10
+ import type { DataAttributes } from '../../../dom-props';
11
+ import { cx } from '../../internal/utils/cx';
12
+
13
+ export interface PaginationProps {
14
+ /** Accessible name for the nav landmark - name the list ("Posts"), not "pagination". @default 'Pagination' */
15
+ ariaLabel?: string;
16
+ /** Items currently shown, 1-based inclusive: `[from, to]` (e.g. `[26, 50]`). */
17
+ range: [number, number];
18
+ /** Total item count - render `of N` only when the API reports one; omit for endless lists. */
19
+ total?: number | null;
20
+ /** A previous cursor exists. @default false */
21
+ hasPrev?: boolean;
22
+ /** A next cursor exists. @default false */
23
+ hasNext?: boolean;
24
+ /** Fired when the previous arrow is pressed. */
25
+ onPrev?: () => void;
26
+ /** Fired when the next arrow is pressed. */
27
+ onNext?: () => void;
28
+ /** A page fetch is in flight: both arrows go inert, the clicked arrow carries the spinner. @default false */
29
+ loading?: boolean;
30
+ /** Extra class(es) merged onto the root element. */
31
+ className?: string;
32
+ /** Standard <nav> attributes (style, data-*, aria-*, ...) forwarded to the root. */
33
+ htmlProps?: Omit<HTMLAttributes<HTMLElement>, 'className'> & DataAttributes;
34
+ }
35
+
36
+ let pgnTravelPx: number | null = null;
37
+ function pgnTravel() {
38
+ if (pgnTravelPx == null) pgnTravelPx = tokenPx('--space-2');
39
+ return pgnTravelPx;
40
+ }
41
+
42
+ function pgnFormat(n: number) {
43
+ return n.toLocaleString('en-US').replace(/,/g, '\u2009');
44
+ }
45
+
46
+ export function Pagination({
47
+ ariaLabel = 'Pagination',
48
+ range,
49
+ total = null,
50
+ hasPrev = false,
51
+ hasNext = false,
52
+ onPrev,
53
+ onNext,
54
+ loading = false,
55
+ className = '',
56
+ htmlProps,
57
+ }: PaginationProps) {
58
+ const rangeRef = useRef<HTMLSpanElement>(null);
59
+ const prevBtnRef = useRef<HTMLButtonElement>(null);
60
+ const nextBtnRef = useRef<HTMLButtonElement>(null);
61
+ const shownRef = useRef<[number, number]>(range);
62
+ const lastDirRef = useRef(0);
63
+
64
+ const from = range[0];
65
+ const to = range[1];
66
+
67
+ useEffect(() => {
68
+ const pf = shownRef.current[0],
69
+ pt = shownRef.current[1];
70
+ if (pf === from && pt === to) return;
71
+ const dir = from > pf ? 1 : -1;
72
+ shownRef.current = range;
73
+ if (rangeRef.current)
74
+ animate(rangeRef.current, { x: [dir * pgnTravel(), 0], opacity: [0, 1], timing: UIMotion.t.enter });
75
+ }, [from, to]);
76
+
77
+ useEffect(() => {
78
+ if (loading || document.activeElement !== document.body) return;
79
+ const d = lastDirRef.current;
80
+ if (d === 1 && !hasNext && hasPrev && prevBtnRef.current) prevBtnRef.current.focus();
81
+ if (d === -1 && !hasPrev && hasNext && nextBtnRef.current) nextBtnRef.current.focus();
82
+ }, [loading, hasPrev, hasNext]);
83
+
84
+ const prevBusy = loading && lastDirRef.current === -1;
85
+ const nextBusy = loading && lastDirRef.current === 1;
86
+
87
+ return (
88
+ <nav className={cx('pgn', className)} aria-label={ariaLabel} aria-busy={loading || undefined} {...htmlProps}>
89
+ <span className="pgn__readout" aria-live="polite">
90
+ <span className="pgn__range" ref={rangeRef}>
91
+ <b>
92
+ {pgnFormat(from)}-{pgnFormat(to)}
93
+ </b>
94
+ {total != null ? <span className="pgn__total"> of {pgnFormat(total)}</span> : null}
95
+ </span>
96
+ </span>
97
+ <div className="pgn__nav">
98
+ <Button
99
+ variant="ghost"
100
+ size="sm"
101
+ ref={prevBtnRef}
102
+ className="pgn__btn pgn__btn--prev"
103
+ loading={prevBusy}
104
+ disabled={!hasPrev || loading}
105
+ htmlProps={{ 'aria-label': 'Previous page' }}
106
+ onClick={() => {
107
+ lastDirRef.current = -1;
108
+ if (onPrev) onPrev();
109
+ }}
110
+ >
111
+ <Icon name="caret-left" size="sm" />
112
+ </Button>
113
+ <Button
114
+ variant="ghost"
115
+ size="sm"
116
+ ref={nextBtnRef}
117
+ className="pgn__btn pgn__btn--next"
118
+ loading={nextBusy}
119
+ disabled={!hasNext || loading}
120
+ htmlProps={{ 'aria-label': 'Next page' }}
121
+ onClick={() => {
122
+ lastDirRef.current = 1;
123
+ if (onNext) onNext();
124
+ }}
125
+ >
126
+ <Icon name="caret-right" size="sm" />
127
+ </Button>
128
+ </div>
129
+ </nav>
130
+ );
131
+ }
@@ -0,0 +1,43 @@
1
+ /* Pagination - cursor strip: layout, mono readout, square arrows + directional caret nudge (on Button vocabulary). */
2
+
3
+ .pgn {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ gap: var(--space-3);
7
+ }
8
+
9
+ /* mono tabular readout; .pgn__range is the node Motion slides - keep it transformable (inline-block) */
10
+ .pgn__readout {
11
+ font: var(--type-mono);
12
+ font-variant-numeric: tabular-nums;
13
+ color: var(--text-subtle);
14
+ white-space: nowrap;
15
+ }
16
+ .pgn__range {
17
+ display: inline-block;
18
+ }
19
+ .pgn__range b {
20
+ font-weight: var(--weight-medium);
21
+ color: var(--text-body);
22
+ }
23
+
24
+ /* arrows kept adjacent (not flanking the readout) so repeated paging needs no mouse travel and a growing readout never shifts them */
25
+ .pgn__nav {
26
+ display: flex;
27
+ gap: var(--space-1);
28
+ }
29
+ .pgn__btn {
30
+ width: var(--control-height-sm);
31
+ padding: 0;
32
+ }
33
+
34
+ /* Caret nudges toward its direction on hover - a 1px optical cue. */
35
+ .pgn__btn .icon {
36
+ transition: transform var(--duration-fast) var(--ease-standard);
37
+ }
38
+ .pgn__btn--prev:hover:not(:disabled) .icon {
39
+ transform: translateX(calc(var(--space-px) * -1));
40
+ }
41
+ .pgn__btn--next:hover:not(:disabled) .icon {
42
+ transform: translateX(var(--space-px));
43
+ }
@@ -0,0 +1,141 @@
1
+ 'use client';
2
+
3
+ import './popover.css';
4
+ import { Fragment, useId, useRef, type HTMLAttributes, type ReactElement, type ReactNode, type RefObject } from 'react';
5
+ import { Presence } from '../../../motion/presence';
6
+ import { useMotion, type MotionSpecs } from '../../../motion/use-motion';
7
+ import { resolveMotionTiming } from '../../../motion/motion-timing';
8
+ import type { DisableableAnimation } from '../../../motion/timing';
9
+ import { useControllable } from '../../internal/hooks/use-controllable';
10
+ import { ovCloneTrigger, useOverlayEntry, useOutsidePress, OverlayPortal } from '../../internal/overlay/layer';
11
+ import { useReturnFocus } from '../../internal/overlay/focus';
12
+ import { useAnchorPosition } from '../../internal/overlay/position';
13
+ import type { DataAttributes } from '../../../dom-props';
14
+ import { cx } from '../../internal/utils/cx';
15
+
16
+ const POPOVER_TIMING = {
17
+ open: { duration: 'base', ease: 'entrance' },
18
+ close: { duration: 'fast', ease: 'exit' },
19
+ } as const;
20
+
21
+ export interface PopoverProps {
22
+ /** Controlled open state. Omit to stay uncontrolled. */
23
+ open?: boolean;
24
+ /** Initial state when uncontrolled. Default false. */
25
+ defaultOpen?: boolean;
26
+ /** Fires whenever the open state changes. Pair with `open` for controlled use. */
27
+ onOpenChange?: (open: boolean) => void;
28
+
29
+ /** Cloned to toggle the panel; required as the anchor. */
30
+ trigger?: ReactElement | null;
31
+
32
+ /** Preferred side; flips to the opposite side when cramped. Default 'bottom'. */
33
+ side?: 'top' | 'bottom' | 'left' | 'right';
34
+ /** Cross-axis alignment against the trigger. Default 'start'. */
35
+ align?: 'start' | 'center' | 'end';
36
+ /** Caret tracking the trigger center. Default false. */
37
+ arrow?: boolean;
38
+
39
+ /** Esc + outside-press dismissal. Default true. */
40
+ dismissible?: boolean;
41
+
42
+ /** Base id for the panel; drives the trigger's `aria-controls`. Auto-generated when omitted. */
43
+ id?: string;
44
+ /** Standard attributes (className, style, data-*, ...) forwarded to the popover panel. */
45
+ htmlProps?: HTMLAttributes<HTMLDivElement> & DataAttributes;
46
+ /** Open/close timing - motion tokens only, or `null` to disable. @default open 'base'/'entrance', close 'fast'/'exit' */
47
+ animation?: DisableableAnimation;
48
+ /** The ENTIRE surface - paint AND semantics. Drive dismissal with `open`/`onOpenChange`. */
49
+ children: ReactNode;
50
+ }
51
+
52
+ function PopoverPanel({
53
+ panelId,
54
+ side,
55
+ align,
56
+ arrow,
57
+ dismissible,
58
+ requestClose,
59
+ triggerRef,
60
+ htmlProps,
61
+ animate,
62
+ exit,
63
+ children,
64
+ }: {
65
+ panelId: string;
66
+ side: 'top' | 'bottom' | 'left' | 'right';
67
+ align: 'start' | 'center' | 'end';
68
+ arrow: boolean;
69
+ dismissible: boolean;
70
+ requestClose: () => void;
71
+ triggerRef: RefObject<HTMLElement>;
72
+ htmlProps?: HTMLAttributes<HTMLDivElement> & DataAttributes;
73
+ children: ReactNode;
74
+ } & MotionSpecs) {
75
+ const panelRef = useRef<HTMLElement>(null);
76
+ const entry = useOverlayEntry({ nodeRef: panelRef, dismissible, requestClose });
77
+ useMotion(panelRef, { animate, exit });
78
+ useReturnFocus(panelRef);
79
+ useAnchorPosition({ side, align, arrow, triggerRef, panelRef });
80
+ useOutsidePress({ entry, refs: [triggerRef, panelRef], enabled: dismissible, onPress: requestClose });
81
+ return (
82
+ <div
83
+ {...htmlProps}
84
+ id={panelId}
85
+ ref={panelRef as RefObject<HTMLDivElement>}
86
+ className={cx('overlay-popover', htmlProps?.className)}
87
+ >
88
+ {arrow && <span className="overlay-popover__arrow" aria-hidden="true"></span>}
89
+ {children}
90
+ </div>
91
+ );
92
+ }
93
+
94
+ export function Popover({
95
+ open: controlledOpen,
96
+ defaultOpen = false,
97
+ onOpenChange,
98
+ trigger = null,
99
+ side = 'bottom',
100
+ align = 'start',
101
+ arrow = false,
102
+ dismissible = true,
103
+ id,
104
+ htmlProps,
105
+ animation,
106
+ children,
107
+ }: PopoverProps) {
108
+ const [open, setOpen] = useControllable(controlledOpen, defaultOpen, onOpenChange);
109
+ const triggerRef = useRef<HTMLElement>(null);
110
+ const autoId = useId();
111
+ const panelId = id || 'popover-' + autoId;
112
+ const close = () => setOpen(false);
113
+ const timings = resolveMotionTiming(animation, POPOVER_TIMING);
114
+
115
+ return (
116
+ <Fragment>
117
+ {ovCloneTrigger(trigger, { open, onPress: () => setOpen(!open), panelId, haspopup: 'true', triggerRef })}
118
+ <OverlayPortal>
119
+ <Presence>
120
+ {open && (
121
+ <PopoverPanel
122
+ key="panel"
123
+ animate={{ opacity: [0, 1], scale: [0.96, 1], timing: timings.open }}
124
+ exit={{ opacity: [0], scale: [0.96], timing: timings.close }}
125
+ panelId={panelId}
126
+ side={side}
127
+ align={align}
128
+ arrow={arrow}
129
+ dismissible={dismissible}
130
+ requestClose={close}
131
+ triggerRef={triggerRef}
132
+ htmlProps={htmlProps}
133
+ >
134
+ {children}
135
+ </PopoverPanel>
136
+ )}
137
+ </Presence>
138
+ </OverlayPortal>
139
+ </Fragment>
140
+ );
141
+ }
@@ -0,0 +1,69 @@
1
+ /* popover.css - anchored panel paint: fixed coords from useAnchorPosition; entrance origin = arrow tip, else the aligned edge (inset --space-4), else center. */
2
+ .overlay-popover {
3
+ position: fixed;
4
+ top: 0;
5
+ left: 0;
6
+ width: max-content;
7
+ outline: none;
8
+ }
9
+
10
+ .overlay-popover[data-align='start'] {
11
+ --ov-origin: var(--space-4);
12
+ }
13
+ .overlay-popover[data-align='center'] {
14
+ --ov-origin: 50%;
15
+ }
16
+ .overlay-popover[data-align='end'] {
17
+ --ov-origin: calc(100% - var(--space-4));
18
+ }
19
+
20
+ .overlay-popover[data-side='bottom'] {
21
+ transform-origin: var(--overlay-arrow-x, var(--ov-origin)) top;
22
+ }
23
+ .overlay-popover[data-side='top'] {
24
+ transform-origin: var(--overlay-arrow-x, var(--ov-origin)) bottom;
25
+ }
26
+ .overlay-popover[data-side='right'] {
27
+ transform-origin: left var(--overlay-arrow-y, var(--ov-origin));
28
+ }
29
+ .overlay-popover[data-side='left'] {
30
+ transform-origin: right var(--overlay-arrow-y, var(--ov-origin));
31
+ }
32
+
33
+ /* Arrow - a rotated square on the panel edge (--overlay-arrow-x/y); only its two outward edges keep the hairline, the panel covers the rest (must be opaque). */
34
+ .overlay-popover__arrow {
35
+ position: absolute;
36
+ width: 10px;
37
+ height: 10px;
38
+ background: var(--overlay-arrow-bg, var(--bg-surface-raised));
39
+ border: var(--border-hairline) solid var(--border-default);
40
+ pointer-events: none;
41
+ }
42
+ .overlay-popover[data-side='bottom'] .overlay-popover__arrow {
43
+ top: -5px;
44
+ left: var(--overlay-arrow-x, 50%);
45
+ transform: translateX(-50%) rotate(45deg);
46
+ border-right: none;
47
+ border-bottom: none;
48
+ }
49
+ .overlay-popover[data-side='top'] .overlay-popover__arrow {
50
+ bottom: -5px;
51
+ left: var(--overlay-arrow-x, 50%);
52
+ transform: translateX(-50%) rotate(45deg);
53
+ border-left: none;
54
+ border-top: none;
55
+ }
56
+ .overlay-popover[data-side='right'] .overlay-popover__arrow {
57
+ left: -5px;
58
+ top: var(--overlay-arrow-y, 50%);
59
+ transform: translateY(-50%) rotate(45deg);
60
+ border-right: none;
61
+ border-top: none;
62
+ }
63
+ .overlay-popover[data-side='left'] .overlay-popover__arrow {
64
+ right: -5px;
65
+ top: var(--overlay-arrow-y, 50%);
66
+ transform: translateY(-50%) rotate(45deg);
67
+ border-left: none;
68
+ border-bottom: none;
69
+ }
@@ -0,0 +1,133 @@
1
+ 'use client';
2
+
3
+ import './select.css';
4
+ import type { HTMLAttributes, ReactNode } from 'react';
5
+ import { useControllable } from '../../internal/hooks/use-controllable';
6
+ import { CheckGlyph } from '../../primitives/checkbox/check-glyph';
7
+ import { useListbox, ListboxPanel, SelectTrigger, type SelectOption, type SelectGroup } from './core';
8
+ import type { DisableableAnimation } from '../../../motion/timing';
9
+ import type { DataAttributes } from '../../../dom-props';
10
+ import { cx } from '../../internal/utils/cx';
11
+
12
+ export type { SelectOption, SelectGroup } from './core';
13
+
14
+ export interface MultiSelectProps {
15
+ /** The choices - a flat `SelectOption[]`, or `SelectGroup[]` to render labeled sections. @default [] */
16
+ options: SelectOption[] | SelectGroup[];
17
+ /** Controlled value - the array of selected option `value`s. Omit for uncontrolled (use `defaultValue`). */
18
+ value?: string[];
19
+ /** Initial selection when uncontrolled. @default [] */
20
+ defaultValue?: string[];
21
+ /** Fires with the NEXT array and the option that was toggled. Committing keeps the menu open. */
22
+ onChange?: (value: string[], toggled: SelectOption) => void;
23
+ /** Trigger text when nothing is selected. @default 'Select options' */
24
+ placeholder?: string;
25
+ /** Control height. @default 'md' */
26
+ size?: 'sm' | 'md' | 'lg';
27
+ /** Disabled - trigger is inert and the menu cannot open. @default false */
28
+ disabled?: boolean;
29
+ /** Danger ring + border. @default false */
30
+ invalid?: boolean;
31
+ /** Skeleton rows in the menu; trigger reads "Loading...". @default false */
32
+ loading?: boolean;
33
+ /** Type-to-filter field pinned above the list. @default false */
34
+ searchable?: boolean;
35
+ /** Placeholder for the `searchable` filter input. @default 'Filter options' */
36
+ searchPlaceholder?: string;
37
+ /** Your own icon node pinned before the trigger label; else the sole selected option's icon. */
38
+ leadingIcon?: ReactNode;
39
+ /** Base id for the trigger/menu/list ids and a11y wiring; auto-generated if omitted. */
40
+ id?: string;
41
+ /** Accessible name for the trigger and listbox - supply when there is no visible label. */
42
+ ariaLabel?: string;
43
+ /** Standard <div> attributes (className, style, data-*, ...) forwarded to the select root. */
44
+ htmlProps?: HTMLAttributes<HTMLDivElement> & DataAttributes;
45
+ /** Menu open/close timing - motion tokens only, or `null` to disable. @default duration 'base' + ease 'entrance'/'exit' */
46
+ animation?: DisableableAnimation;
47
+ }
48
+
49
+ function CheckboxTick({ checked }: { checked: boolean }) {
50
+ return (
51
+ <span className="cbx" aria-hidden="true">
52
+ <CheckGlyph checked={checked} readOnly tabIndex={-1} />
53
+ </span>
54
+ );
55
+ }
56
+
57
+ export function MultiSelect({
58
+ options = [],
59
+ value: controlledValue,
60
+ defaultValue = [],
61
+ onChange,
62
+ placeholder = 'Select options',
63
+ size = 'md',
64
+ disabled = false,
65
+ invalid = false,
66
+ loading = false,
67
+ searchable = false,
68
+ searchPlaceholder = 'Filter options',
69
+ leadingIcon = null,
70
+ id,
71
+ ariaLabel,
72
+ htmlProps,
73
+ animation,
74
+ }: MultiSelectProps) {
75
+ const [value, setValue] = useControllable<string[], SelectOption>(
76
+ controlledValue,
77
+ defaultValue,
78
+ onChange as ((next: string[], opt?: SelectOption) => void) | undefined,
79
+ );
80
+ const values = Array.isArray(value) ? value : [];
81
+ const isSelected = (v: string) => values.indexOf(v) !== -1;
82
+
83
+ const lb = useListbox({
84
+ options,
85
+ disabled,
86
+ loading,
87
+ searchable,
88
+ id,
89
+ idPrefix: 'mselect-',
90
+ isSelected,
91
+ onCommit: (opt) =>
92
+ setValue(isSelected(opt.value) ? values.filter((v) => v !== opt.value) : [...values, opt.value], opt),
93
+ closeOnCommit: false,
94
+ });
95
+
96
+ const selectedOptions = lb.flat.filter((o) => isSelected(o.value));
97
+ const isPlaceholder = !loading && selectedOptions.length === 0;
98
+
99
+ return (
100
+ <div
101
+ {...htmlProps}
102
+ className={cx('select', htmlProps?.className)}
103
+ data-multiple="true"
104
+ data-size={size}
105
+ data-open={lb.open ? 'true' : undefined}
106
+ data-disabled={disabled ? 'true' : undefined}
107
+ data-invalid={invalid ? 'true' : undefined}
108
+ data-loading={loading ? 'true' : undefined}
109
+ >
110
+ <SelectTrigger
111
+ lb={lb}
112
+ disabled={disabled}
113
+ invalid={invalid}
114
+ ariaLabel={ariaLabel}
115
+ leading={leadingIcon || (selectedOptions.length === 1 && selectedOptions[0].icon) || null}
116
+ text={loading ? 'Loading...' : isPlaceholder ? placeholder : selectedOptions[0].label}
117
+ isPlaceholder={isPlaceholder}
118
+ count={selectedOptions.length - 1}
119
+ />
120
+ <ListboxPanel
121
+ lb={lb}
122
+ loading={loading}
123
+ searchable={searchable}
124
+ searchPlaceholder={searchPlaceholder}
125
+ ariaLabel={ariaLabel}
126
+ multiple
127
+ animation={animation}
128
+ size={size}
129
+ check={(sel) => <CheckboxTick checked={sel} />}
130
+ />
131
+ </div>
132
+ );
133
+ }
@@ -0,0 +1,122 @@
1
+ 'use client';
2
+
3
+ import './select.css';
4
+ import type { HTMLAttributes, ReactNode } from 'react';
5
+ import { useControllable } from '../../internal/hooks/use-controllable';
6
+ import { useListbox, ListboxPanel, SelectTrigger, type SelectOption, type SelectGroup } from './core';
7
+ import type { DisableableAnimation } from '../../../motion/timing';
8
+ import type { DataAttributes } from '../../../dom-props';
9
+ import { ButtonProps } from '../../primitives/button/Button';
10
+ import { cx } from '../../internal/utils/cx';
11
+
12
+ export type { SelectOption, SelectGroup } from './core';
13
+
14
+ export interface SelectProps {
15
+ /** The choices - a flat `SelectOption[]`, or `SelectGroup[]` to render labeled sections. @default [] */
16
+ options: SelectOption[] | SelectGroup[];
17
+ /** Controlled value - matches an option's `value`, or `null` for none. Omit for uncontrolled (use `defaultValue`). */
18
+ value?: string | null;
19
+ /** Initial value when uncontrolled. @default null */
20
+ defaultValue?: string | null;
21
+ /** Fires on commit - gets the new `value` and its full `SelectOption`. Committing closes the menu. */
22
+ onChange?: (value: string, option: SelectOption) => void;
23
+ /** Trigger text when nothing is selected. @default 'Select an option' */
24
+ placeholder?: string;
25
+ /** Control height. @default 'md' */
26
+ size?: 'sm' | 'md' | 'lg';
27
+ /** Disabled - trigger is inert and the menu cannot open. @default false */
28
+ disabled?: boolean;
29
+ /** Danger ring + border. @default false */
30
+ invalid?: boolean;
31
+ /** Skeleton rows in the menu; trigger reads "Loading...". @default false */
32
+ loading?: boolean;
33
+ /** Type-to-filter field pinned above the list. @default false */
34
+ searchable?: boolean;
35
+ /** Placeholder for the `searchable` filter input. @default 'Filter options' */
36
+ searchPlaceholder?: string;
37
+ /** Your own icon node pinned before the trigger label; else the selected option's icon. */
38
+ leadingIcon?: ReactNode;
39
+ /** Base id for the trigger/menu/list ids and a11y wiring; auto-generated if omitted. */
40
+ id?: string;
41
+ /** Accessible name for the trigger and listbox - supply when there is no visible label. */
42
+ ariaLabel?: string;
43
+ /** Standard <div> attributes (className, style, data-*, ...) forwarded to the select root. */
44
+ htmlProps?: HTMLAttributes<HTMLDivElement> & DataAttributes;
45
+ /** Menu open/close timing - motion tokens only, or `null` to disable. @default duration 'base' + ease 'entrance'/'exit' */
46
+ animation?: DisableableAnimation;
47
+ /** Trigger props */
48
+ triggerProps?: ButtonProps;
49
+ /** Show check icon on selected value */
50
+ showCheck?: boolean;
51
+ }
52
+
53
+ export function Select({
54
+ options = [],
55
+ value: controlledValue,
56
+ defaultValue = null,
57
+ onChange,
58
+ placeholder = 'Select an option',
59
+ size = 'md',
60
+ disabled = false,
61
+ invalid = false,
62
+ loading = false,
63
+ searchable = false,
64
+ searchPlaceholder = 'Filter options',
65
+ leadingIcon = null,
66
+ id,
67
+ ariaLabel,
68
+ htmlProps,
69
+ animation,
70
+ triggerProps,
71
+ showCheck = true,
72
+ }: SelectProps) {
73
+ const [value, setValue] = useControllable<string | null, SelectOption>(controlledValue, defaultValue, onChange);
74
+
75
+ const lb = useListbox({
76
+ options,
77
+ disabled,
78
+ loading,
79
+ searchable,
80
+ id,
81
+ idPrefix: 'select-',
82
+ isSelected: (v) => v === value && value != null,
83
+ onCommit: (opt) => setValue(opt.value, opt),
84
+ closeOnCommit: true,
85
+ });
86
+
87
+ const selected = lb.flat.find((o) => o.value === value) || null;
88
+ const isPlaceholder = !loading && !selected;
89
+
90
+ return (
91
+ <div
92
+ {...htmlProps}
93
+ className={cx('select', htmlProps?.className)}
94
+ data-size={size}
95
+ data-open={lb.open ? 'true' : undefined}
96
+ data-disabled={disabled ? 'true' : undefined}
97
+ data-invalid={invalid ? 'true' : undefined}
98
+ data-loading={loading ? 'true' : undefined}
99
+ >
100
+ <SelectTrigger
101
+ lb={lb}
102
+ disabled={disabled}
103
+ invalid={invalid}
104
+ ariaLabel={ariaLabel}
105
+ leading={leadingIcon || (selected && selected.icon) || null}
106
+ text={loading ? 'Loading...' : isPlaceholder ? placeholder : selected.label}
107
+ isPlaceholder={isPlaceholder}
108
+ {...triggerProps}
109
+ />
110
+ <ListboxPanel
111
+ lb={lb}
112
+ loading={loading}
113
+ searchable={searchable}
114
+ searchPlaceholder={searchPlaceholder}
115
+ ariaLabel={ariaLabel}
116
+ animation={animation}
117
+ size={size}
118
+ {...(!showCheck && { check: () => null })}
119
+ />
120
+ </div>
121
+ );
122
+ }
@@ -0,0 +1,4 @@
1
+ export { useListbox, type ListboxState } from './use-listbox';
2
+ export { SelectTrigger } from './trigger';
3
+ export { ListboxPanel } from './panel';
4
+ export type { SelectOption, SelectGroup } from './types';