@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,66 @@
1
+ 'use client';
2
+
3
+ import './icon.css';
4
+ import {
5
+ ArrowUp,
6
+ Calendar,
7
+ CalendarDot,
8
+ CaretDown,
9
+ CaretLeft,
10
+ CaretRight,
11
+ CaretUp,
12
+ Check,
13
+ CheckCircle,
14
+ Clock,
15
+ Globe,
16
+ Info,
17
+ MagnifyingGlass,
18
+ Warning,
19
+ WarningCircle,
20
+ X,
21
+ } from '@phosphor-icons/react';
22
+ import type { IconWeight } from '@phosphor-icons/react';
23
+ import { cx } from '../utils/cx';
24
+
25
+ type Glyph = typeof X;
26
+
27
+ const REGISTRY = {
28
+ x: X,
29
+ close: X,
30
+ check: Check,
31
+ 'arrow-up': ArrowUp,
32
+ 'caret-up': CaretUp,
33
+ 'caret-down': CaretDown,
34
+ 'caret-left': CaretLeft,
35
+ 'caret-right': CaretRight,
36
+ 'magnifying-glass': MagnifyingGlass,
37
+ calendar: Calendar,
38
+ 'calendar-dot': CalendarDot,
39
+ clock: Clock,
40
+ globe: Globe,
41
+ info: Info,
42
+ warning: Warning,
43
+ 'warning-circle': WarningCircle,
44
+ 'check-circle': CheckCircle,
45
+ } satisfies Record<string, Glyph>;
46
+
47
+ export type IconName = keyof typeof REGISTRY;
48
+ export type IconSize = 'sm' | 'md' | 'lg';
49
+ export type { IconWeight };
50
+
51
+ export interface IconProps {
52
+ name: IconName;
53
+ size?: IconSize;
54
+ weight?: IconWeight;
55
+ label?: string;
56
+ /** Extra class(es) merged onto the root element. */
57
+ className?: string;
58
+ }
59
+
60
+ const SIZE_PX: Record<IconSize, number> = { sm: 16, md: 20, lg: 24 };
61
+
62
+ export function Icon({ name, size = 'md', weight = 'regular', label, className = '' }: IconProps) {
63
+ const Glyph = REGISTRY[name];
64
+ const a11y = label ? { role: 'img', 'aria-label': label } : { 'aria-hidden': true };
65
+ return <Glyph size={SIZE_PX[size]} weight={weight} className={cx('icon', className)} {...a11y} />;
66
+ }
@@ -0,0 +1,10 @@
1
+ 'use client';
2
+
3
+ import './icon.css';
4
+ import type { ReactNode } from 'react';
5
+ import type { IconSize } from './Icon';
6
+
7
+ export function IconSlot({ size = 'md', children }: { size?: IconSize; children?: ReactNode }) {
8
+ if (!children) return null;
9
+ return <span className={`icon-slot icon-slot--${size}`}>{children}</span>;
10
+ }
@@ -0,0 +1,33 @@
1
+ /* Icon - base layout (inline/flex seating); size and color come from props/tokens. */
2
+
3
+ .icon {
4
+ flex: none;
5
+ vertical-align: middle;
6
+ }
7
+
8
+ /* icon-slot - locks a consumer icon node to the token size and currentColor. */
9
+ .icon-slot {
10
+ display: inline-flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ flex: none;
14
+ color: currentColor;
15
+ }
16
+ .icon-slot--sm {
17
+ width: var(--icon-sm);
18
+ height: var(--icon-sm);
19
+ }
20
+ .icon-slot--md {
21
+ width: var(--icon-md);
22
+ height: var(--icon-md);
23
+ }
24
+ .icon-slot--lg {
25
+ width: var(--icon-lg);
26
+ height: var(--icon-lg);
27
+ }
28
+ .icon-slot > svg,
29
+ .icon-slot > img {
30
+ width: 100%;
31
+ height: 100%;
32
+ display: block;
33
+ }
@@ -0,0 +1,68 @@
1
+ 'use client';
2
+
3
+ import { useEffect, type RefObject } from 'react';
4
+ import { type OverlayEntry, ovIsTop, ovInOverlayAbove } from './layer';
5
+
6
+ export function useReturnFocus(nodeRef: RefObject<HTMLElement>) {
7
+ useEffect(() => {
8
+ const prev = document.activeElement as HTMLElement | null;
9
+ return () => {
10
+ const a = document.activeElement;
11
+ const orphaned = !a || a === document.body || (nodeRef.current && nodeRef.current.contains(a));
12
+ if (orphaned && prev && prev.isConnected && typeof prev.focus === 'function') {
13
+ queueMicrotask(() => {
14
+ if (prev.isConnected) prev.focus({ preventScroll: true });
15
+ });
16
+ }
17
+ };
18
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
19
+ }
20
+
21
+ const OV_FOCUSABLE =
22
+ 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), ' +
23
+ 'select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
24
+
25
+ export function useFocusTrap({ panelRef, entry }: { panelRef: RefObject<HTMLElement>; entry: OverlayEntry }) {
26
+ useEffect(() => {
27
+ const panel = panelRef.current;
28
+ if (!panel) return undefined;
29
+ const focusables = () =>
30
+ Array.from(panel.querySelectorAll<HTMLElement>(OV_FOCUSABLE)).filter(
31
+ (el) => el.offsetParent !== null || el === document.activeElement,
32
+ );
33
+
34
+ const seed = panel.querySelector<HTMLElement>('[autofocus], [data-autofocus]') || focusables()[0] || panel;
35
+ seed.focus({ preventScroll: true });
36
+
37
+ const onKeyDown = (e: KeyboardEvent) => {
38
+ if (e.key !== 'Tab') return;
39
+ const f = focusables();
40
+ if (f.length === 0) {
41
+ e.preventDefault();
42
+ return;
43
+ }
44
+ const cur = document.activeElement;
45
+ if (e.shiftKey) {
46
+ if (cur === f[0] || cur === panel) {
47
+ e.preventDefault();
48
+ f[f.length - 1].focus();
49
+ }
50
+ } else if (cur === f[f.length - 1]) {
51
+ e.preventDefault();
52
+ f[0].focus();
53
+ }
54
+ };
55
+ const onFocusIn = (e: FocusEvent) => {
56
+ if (panel.contains(e.target as Node)) return;
57
+ if (!ovIsTop(entry) || ovInOverlayAbove(entry, e.target)) return;
58
+ (focusables()[0] || panel).focus({ preventScroll: true });
59
+ };
60
+
61
+ panel.addEventListener('keydown', onKeyDown);
62
+ document.addEventListener('focusin', onFocusIn);
63
+ return () => {
64
+ panel.removeEventListener('keydown', onKeyDown);
65
+ document.removeEventListener('focusin', onFocusIn);
66
+ };
67
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
68
+ }
@@ -0,0 +1,149 @@
1
+ 'use client';
2
+
3
+ import {
4
+ cloneElement,
5
+ useEffect,
6
+ useLayoutEffect,
7
+ useRef,
8
+ type ReactElement,
9
+ type ReactNode,
10
+ type Ref,
11
+ type RefObject,
12
+ } from 'react';
13
+ import { createPortal } from 'react-dom';
14
+
15
+ interface OverlayEntry {
16
+ contains: (t: EventTarget | null) => boolean;
17
+ isDismissible: () => boolean;
18
+ requestClose: () => void;
19
+ _dismissible?: boolean;
20
+ _close?: () => void;
21
+ }
22
+
23
+ const ovStack: OverlayEntry[] = [];
24
+
25
+ function ovOnDocKeyDown(e: KeyboardEvent) {
26
+ if (e.key !== 'Escape' || e.defaultPrevented || ovStack.length === 0) return;
27
+ const top = ovStack[ovStack.length - 1];
28
+ if (!top.isDismissible()) return;
29
+ e.preventDefault();
30
+ top.requestClose();
31
+ }
32
+
33
+ function ovIsTop(entry: OverlayEntry) {
34
+ return ovStack[ovStack.length - 1] === entry;
35
+ }
36
+
37
+ function ovInOverlayAbove(entry: OverlayEntry, target: EventTarget | null) {
38
+ const i = ovStack.indexOf(entry);
39
+ return i >= 0 && ovStack.slice(i + 1).some((e) => e.contains(target));
40
+ }
41
+
42
+ function useOverlayEntry({
43
+ nodeRef,
44
+ dismissible,
45
+ requestClose,
46
+ }: {
47
+ nodeRef: RefObject<HTMLElement>;
48
+ dismissible: boolean;
49
+ requestClose: () => void;
50
+ }): OverlayEntry {
51
+ const ref = useRef<OverlayEntry>(null);
52
+ if (!ref.current) {
53
+ ref.current = {
54
+ contains: (t) => Boolean(nodeRef.current && nodeRef.current.contains(t as Node)),
55
+ isDismissible: () => ref.current._dismissible,
56
+ requestClose: () => ref.current._close(),
57
+ };
58
+ }
59
+ ref.current._dismissible = dismissible;
60
+ ref.current._close = requestClose;
61
+
62
+ useLayoutEffect(() => {
63
+ const entry = ref.current;
64
+ if (ovStack.length === 0) document.addEventListener('keydown', ovOnDocKeyDown);
65
+ ovStack.push(entry);
66
+ if (nodeRef.current) nodeRef.current.style.zIndex = 'calc(var(--layer-overlay) + ' + (ovStack.length - 1) + ')';
67
+ return () => {
68
+ const i = ovStack.indexOf(entry);
69
+ if (i >= 0) ovStack.splice(i, 1);
70
+ if (ovStack.length === 0) document.removeEventListener('keydown', ovOnDocKeyDown);
71
+ };
72
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
73
+
74
+ return ref.current;
75
+ }
76
+
77
+ function useOutsidePress({
78
+ entry,
79
+ refs,
80
+ enabled,
81
+ onPress,
82
+ }: {
83
+ entry: OverlayEntry;
84
+ refs: RefObject<HTMLElement>[];
85
+ enabled: boolean;
86
+ onPress: () => void;
87
+ }) {
88
+ const latest = useRef<{ enabled: boolean; onPress: () => void }>(null);
89
+ latest.current = { enabled, onPress };
90
+ useEffect(() => {
91
+ const onDown = (e: PointerEvent) => {
92
+ if (!latest.current.enabled) return;
93
+ const t = e.target;
94
+ if (refs.some((r) => r.current && r.current.contains(t as Node))) return;
95
+ if (ovInOverlayAbove(entry, t)) return;
96
+ latest.current.onPress();
97
+ };
98
+ document.addEventListener('pointerdown', onDown, true);
99
+ return () => document.removeEventListener('pointerdown', onDown, true);
100
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
101
+ }
102
+
103
+ function ovCloneTrigger(
104
+ trigger: ReactElement | null,
105
+ {
106
+ open,
107
+ onPress,
108
+ panelId,
109
+ haspopup,
110
+ triggerRef,
111
+ }: { open: boolean; onPress: () => void; panelId: string; haspopup: string; triggerRef: RefObject<HTMLElement> },
112
+ ): ReactElement | null {
113
+ if (!trigger) return null;
114
+ const own = trigger.props as { onClick?: (...args: unknown[]) => void };
115
+ const ownRef = (trigger as ReactElement & { ref?: Ref<HTMLElement> }).ref;
116
+ return cloneElement(trigger, {
117
+ 'aria-haspopup': haspopup,
118
+ 'aria-expanded': open,
119
+ 'aria-controls': open ? panelId : undefined,
120
+ onClick: (...args: unknown[]) => {
121
+ own.onClick?.(...args);
122
+ onPress();
123
+ },
124
+ ref: (node: HTMLElement | null) => {
125
+ triggerRef.current = node;
126
+ if (typeof ownRef === 'function') ownRef(node);
127
+ else if (ownRef) ownRef.current = node;
128
+ },
129
+ } as Partial<typeof trigger.props>);
130
+ }
131
+
132
+ function OverlayPortal({ children }: { children: ReactNode }) {
133
+ const hostRef = useRef<HTMLDivElement | null>(null);
134
+ if (typeof document !== 'undefined' && !hostRef.current) {
135
+ hostRef.current = document.createElement('div');
136
+ hostRef.current.setAttribute('data-overlay-root', '');
137
+ }
138
+ useLayoutEffect(() => {
139
+ const el = hostRef.current;
140
+ if (!el) return;
141
+ document.body.appendChild(el);
142
+ return () => el.remove();
143
+ }, []);
144
+ if (!hostRef.current) return null;
145
+ return createPortal(children, hostRef.current);
146
+ }
147
+
148
+ export { ovIsTop, ovInOverlayAbove, useOverlayEntry, useOutsidePress, ovCloneTrigger, OverlayPortal };
149
+ export type { OverlayEntry };
@@ -0,0 +1,141 @@
1
+ 'use client';
2
+
3
+ import './overlay.css';
4
+ import { useEffect, useRef, type ReactNode, type RefObject } from 'react';
5
+ import { animate, type Layer, type Playback } from '../../../engine';
6
+ import { useMotion, run, type MotionSpec, type MotionSpecs } from '../../../motion/use-motion';
7
+ import { UIMotion } from '../../../tokens/motion-tokens';
8
+ import { useOverlayEntry } from './layer';
9
+ import { useReturnFocus, useFocusTrap } from './focus';
10
+ import { cx } from '../utils/cx';
11
+
12
+ const SM = UIMotion;
13
+
14
+ let ovLocks = 0;
15
+ let ovSavedOverflow = '';
16
+ let ovSavedPad = '';
17
+ function useScrollLock() {
18
+ useEffect(() => {
19
+ if (++ovLocks === 1) {
20
+ const body = document.body;
21
+ const gutter = window.innerWidth - document.documentElement.clientWidth;
22
+ ovSavedOverflow = body.style.overflow;
23
+ ovSavedPad = body.style.paddingRight;
24
+ body.style.overflow = 'hidden';
25
+ if (gutter > 0) body.style.paddingRight = (parseFloat(getComputedStyle(body).paddingRight) || 0) + gutter + 'px';
26
+ }
27
+ return () => {
28
+ if (--ovLocks === 0) {
29
+ document.body.style.overflow = ovSavedOverflow;
30
+ document.body.style.paddingRight = ovSavedPad;
31
+ }
32
+ };
33
+ }, []);
34
+ }
35
+
36
+ let ovInertCount = 0;
37
+ const ovInerted = new Set<HTMLElement>();
38
+ function useInertOutside() {
39
+ useEffect(() => {
40
+ if (++ovInertCount === 1) {
41
+ for (const el of Array.from(document.body.children) as HTMLElement[]) {
42
+ if (el.hasAttribute('data-overlay-root') || el.tagName === 'SCRIPT' || el.tagName === 'STYLE' || el.inert)
43
+ continue;
44
+ el.inert = true;
45
+ ovInerted.add(el);
46
+ }
47
+ }
48
+ return () => {
49
+ if (--ovInertCount === 0) {
50
+ ovInerted.forEach((el) => {
51
+ el.inert = false;
52
+ });
53
+ ovInerted.clear();
54
+ }
55
+ };
56
+ }, []);
57
+ }
58
+
59
+ const ovScrimTiming = {
60
+ open: { duration: SM.dur.slow, ease: SM.ease.entrance },
61
+ close: { duration: SM.dur.base, ease: SM.ease.standard },
62
+ };
63
+
64
+ function OverlayScrim({
65
+ dismissible,
66
+ onPress,
67
+ scrimRef,
68
+ }: {
69
+ dismissible: boolean;
70
+ onPress: () => void;
71
+ scrimRef: RefObject<HTMLDivElement | null>;
72
+ }) {
73
+ const down = useRef(false);
74
+ return (
75
+ <div
76
+ ref={scrimRef}
77
+ className="overlay-scrim"
78
+ aria-hidden="true"
79
+ onPointerDown={(e) => {
80
+ down.current = e.target === e.currentTarget;
81
+ }}
82
+ onClick={(e) => {
83
+ if (dismissible && down.current && e.target === e.currentTarget) onPress();
84
+ }}
85
+ ></div>
86
+ );
87
+ }
88
+
89
+ export function ModalShell({
90
+ layerClass,
91
+ panelClass,
92
+ panelId,
93
+ dismissible,
94
+ requestClose,
95
+ panelRef: externalPanelRef = null,
96
+ panelProps = {},
97
+ animate: animateSpec,
98
+ exit,
99
+ children,
100
+ }: {
101
+ layerClass: string;
102
+ panelClass: string;
103
+ panelId: string;
104
+ dismissible: boolean;
105
+ requestClose: () => void;
106
+ panelRef?: RefObject<HTMLElement> | null;
107
+ panelProps?: Record<string, any>;
108
+ children: ReactNode;
109
+ } & MotionSpecs) {
110
+ const layerRef = useRef<HTMLDivElement>(null);
111
+ const scrimRef = useRef<HTMLDivElement>(null);
112
+ const internalPanelRef = useRef<HTMLElement>(null);
113
+ const panelRef = externalPanelRef || internalPanelRef;
114
+ const entry = useOverlayEntry({ nodeRef: layerRef, dismissible, requestClose });
115
+
116
+ const withScrim = (dir: 'open' | 'close', panelSpec: MotionSpec | undefined) => (): Playback[] => {
117
+ const fade: Layer = { opacity: dir === 'open' ? [0, 1] : [0], timing: ovScrimTiming[dir] };
118
+ const plays = scrimRef.current ? [animate(scrimRef.current, fade)] : [];
119
+ return panelRef.current ? plays.concat(run(panelSpec, panelRef.current)) : plays;
120
+ };
121
+
122
+ useMotion(layerRef, { animate: withScrim('open', animateSpec), exit: withScrim('close', exit) });
123
+ useScrollLock();
124
+ useInertOutside();
125
+ useReturnFocus(layerRef);
126
+ useFocusTrap({ panelRef, entry });
127
+ return (
128
+ <div ref={layerRef} className={'overlay-layer ' + layerClass}>
129
+ <OverlayScrim dismissible={dismissible} onPress={requestClose} scrimRef={scrimRef} />
130
+ <div
131
+ id={panelId}
132
+ tabIndex={-1}
133
+ {...panelProps}
134
+ ref={panelRef as RefObject<HTMLDivElement>}
135
+ className={cx('overlay-panel', panelClass, panelProps.className)}
136
+ >
137
+ {children}
138
+ </div>
139
+ </div>
140
+ );
141
+ }
@@ -0,0 +1,23 @@
1
+ /* overlay.css - the modal shell geometry shared by dialog and sheet: a fixed
2
+ full-viewport layer, the scrim filling it, and the grid cell the panel
3
+ mounts in (min 0 + max 100% give a scroll constraint chain). Per-component
4
+ placement lives with each consumer (dialog.css, sheet.css); the popover
5
+ paints itself entirely from popover.css. */
6
+ .overlay-layer {
7
+ position: fixed;
8
+ inset: 0;
9
+ display: grid;
10
+ }
11
+ .overlay-scrim {
12
+ position: absolute;
13
+ inset: 0;
14
+ background: var(--bg-overlay);
15
+ }
16
+
17
+ .overlay-panel {
18
+ position: relative;
19
+ display: grid;
20
+ outline: none;
21
+ min-width: 0;
22
+ min-height: 0;
23
+ }
@@ -0,0 +1,103 @@
1
+ 'use client';
2
+
3
+ import { useLayoutEffect, type RefObject } from 'react';
4
+ import { tokenPx } from '../utils/token-px';
5
+
6
+ interface UseAnchorPositionProps {
7
+ side: 'top' | 'bottom' | 'left' | 'right';
8
+ align: 'start' | 'center' | 'end';
9
+ arrow: boolean;
10
+ triggerRef: RefObject<HTMLElement>;
11
+ panelRef: RefObject<HTMLElement>;
12
+ }
13
+
14
+ export function useAnchorPosition({ side, align, arrow, triggerRef, panelRef }: UseAnchorPositionProps) {
15
+ useLayoutEffect(() => {
16
+ const place = () => {
17
+ const t = triggerRef.current;
18
+ const p = panelRef.current;
19
+ if (!t || !p) return;
20
+
21
+ const r = t.getBoundingClientRect();
22
+ const pw = p.offsetWidth;
23
+ const ph = p.offsetHeight;
24
+
25
+ const edge = tokenPx('--space-2');
26
+
27
+ const gap = arrow ? tokenPx('--space-2') + 3 : 0;
28
+
29
+ const vw = window.innerWidth;
30
+ const vh = window.innerHeight;
31
+
32
+ const room: Record<string, number> = { top: r.top, bottom: vh - r.bottom, left: r.left, right: vw - r.right };
33
+
34
+ const opposite: Record<string, 'top' | 'bottom' | 'left' | 'right'> = {
35
+ top: 'bottom',
36
+ bottom: 'top',
37
+ left: 'right',
38
+ right: 'left',
39
+ };
40
+
41
+ const vertical = side === 'top' || side === 'bottom';
42
+
43
+ let s = side;
44
+ if (room[s] < (vertical ? ph : pw) + gap && room[opposite[s]] > room[s]) {
45
+ s = opposite[s];
46
+ }
47
+
48
+ let x: number;
49
+ let y: number;
50
+
51
+ if (vertical) {
52
+ y = s === 'top' ? r.top - ph - gap : r.bottom + gap;
53
+
54
+ x = align === 'start' ? r.left : align === 'end' ? r.right - pw : r.left + (r.width - pw) / 2;
55
+
56
+ x = Math.min(Math.max(x, edge), vw - pw - edge);
57
+
58
+ y = Math.max(Math.min(y, vh - ph - edge), edge);
59
+ } else {
60
+ x = s === 'left' ? r.left - pw - gap : r.right + gap;
61
+
62
+ y = align === 'start' ? r.top : align === 'end' ? r.bottom - ph : r.top + (r.height - ph) / 2;
63
+
64
+ y = Math.min(Math.max(y, edge), vh - ph - edge);
65
+ x = Math.max(Math.min(x, vw - pw - edge), edge);
66
+ }
67
+
68
+ p.style.left = Math.round(x) + 'px';
69
+ p.style.top = Math.round(y) + 'px';
70
+
71
+ p.setAttribute('data-side', s);
72
+ p.setAttribute('data-align', align);
73
+
74
+ if (arrow) {
75
+ if (vertical) {
76
+ p.style.setProperty(
77
+ '--overlay-arrow-x',
78
+ Math.round(Math.min(Math.max(r.left + r.width / 2 - x, 12), pw - 12)) + 'px',
79
+ );
80
+ } else {
81
+ p.style.setProperty(
82
+ '--overlay-arrow-y',
83
+ Math.round(Math.min(Math.max(r.top + r.height / 2 - y, 12), ph - 12)) + 'px',
84
+ );
85
+ }
86
+ }
87
+ };
88
+
89
+ place();
90
+
91
+ window.addEventListener('scroll', place, true);
92
+ window.addEventListener('resize', place);
93
+
94
+ const ro = new ResizeObserver(place);
95
+ ro.observe(panelRef.current!);
96
+
97
+ return () => {
98
+ window.removeEventListener('scroll', place, true);
99
+ window.removeEventListener('resize', place);
100
+ ro.disconnect();
101
+ };
102
+ }, [side, align, arrow]); // eslint-disable-line react-hooks/exhaustive-deps
103
+ }
@@ -0,0 +1,3 @@
1
+ export function cx(...parts: Array<string | false | null | undefined>): string {
2
+ return parts.filter(Boolean).join(' ');
3
+ }
@@ -0,0 +1,8 @@
1
+ export function tokenPx(token: string, fallback = 0): number {
2
+ if (typeof document === 'undefined') return fallback;
3
+ const root = document.documentElement;
4
+ const v = getComputedStyle(root).getPropertyValue(token).trim();
5
+ const n = parseFloat(v);
6
+ if (Number.isNaN(n)) return fallback;
7
+ return v.endsWith('rem') ? n * parseFloat(getComputedStyle(root).fontSize) : n;
8
+ }