@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,183 @@
1
+ 'use client';
2
+
3
+ import './radio-group.css';
4
+ import { useId, useRef, useState, type CSSProperties, type FieldsetHTMLAttributes, type ReactNode } from 'react';
5
+ import { Icon } from '../../internal/icon/Icon';
6
+ import { IconSlot } from '../../internal/icon/IconSlot';
7
+ import { Collapse } from '../collapse/Collapse';
8
+ import { GlidePill, useGlide } from '../../../motion/glide';
9
+ import { useSharedFlip } from '../../../motion/flip';
10
+ import { UIMotion } from '../../../tokens/motion-tokens';
11
+ import type { DataAttributes } from '../../../dom-props';
12
+ import { cx } from '../../internal/utils/cx';
13
+ import type { FieldRequirementProps } from '../input/field-chrome';
14
+
15
+ const SM = UIMotion;
16
+
17
+ export interface RadioOption {
18
+ /** The stored value - what `onChange` returns and `value` matches. */
19
+ value: string;
20
+ /** Visible label (sentence case). */
21
+ label: ReactNode;
22
+ /** Optional secondary line (muted) explaining the choice. */
23
+ description?: ReactNode;
24
+ /** Leading icon - your own node. CARDS only; ignored for rows. */
25
+ icon?: ReactNode;
26
+ /** Disable just this option. */
27
+ disabled?: boolean;
28
+ }
29
+
30
+ interface RadioGroupOwnProps extends FieldRequirementProps {
31
+ /** Shared radio name - ties the options into one keyboard group. Required. */
32
+ name: string;
33
+ /** The selected value (controlled). */
34
+ value?: string;
35
+ /** Called with the chosen option's `value`. */
36
+ onChange?: (value: string) => void;
37
+ /** Group label / legend (sentence case). */
38
+ label?: ReactNode;
39
+ /** Persistent context shown under the legend, before the options. */
40
+ helper?: ReactNode;
41
+ /** Group-level error (e.g. required). Sets the error state + reveals the message. */
42
+ error?: ReactNode;
43
+ /** Skin: quiet rows (default) or selectable cards. */
44
+ variant?: 'rows' | 'cards';
45
+ /** Lay options out in a line instead of a stack. */
46
+ orientation?: 'vertical' | 'horizontal';
47
+ /** Control size: sm (dot 16) - md (default, dot 18). */
48
+ size?: 'sm' | 'md';
49
+ /** Disable the whole group. */
50
+ disabled?: boolean;
51
+ /** The options to choose between. */
52
+ options: RadioOption[];
53
+ /** Extra class(es) merged onto the root element. */
54
+ className?: string;
55
+ /** Inline styles merged onto the root element. */
56
+ style?: CSSProperties;
57
+ }
58
+
59
+ export interface RadioGroupProps extends RadioGroupOwnProps {
60
+ /** Standard <fieldset> attributes (aria-*, data-*, ...) forwarded to the root. */
61
+ htmlProps?: Omit<FieldsetHTMLAttributes<HTMLFieldSetElement>, keyof RadioGroupOwnProps> & DataAttributes;
62
+ }
63
+
64
+ export function RadioGroup({
65
+ name,
66
+ value,
67
+ onChange,
68
+ label,
69
+ helper,
70
+ error,
71
+ required,
72
+ optional,
73
+ variant = 'rows',
74
+ orientation = 'vertical',
75
+ size = 'md',
76
+ disabled,
77
+ options = [],
78
+ className = '',
79
+ style,
80
+ htmlProps,
81
+ }: RadioGroupProps) {
82
+ const groupId = useId();
83
+ const [hovered, setHovered] = useState<string | null>(null);
84
+ const optionsRef = useRef<HTMLDivElement>(null);
85
+ const glide = useGlide(optionsRef);
86
+ const markerRef = useSharedFlip<HTMLSpanElement>(groupId + ':marker', { timing: SM.t.layout });
87
+ const fillRef = useSharedFlip<HTMLSpanElement>(groupId + ':card-fill', { timing: SM.t.layout });
88
+ const cardHoverRef = useSharedFlip<HTMLSpanElement>(groupId + ':card-hover', { scale: false, timing: SM.t.layout });
89
+
90
+ const cls = cx(
91
+ 'rg',
92
+ variant === 'cards' && 'rg--cards',
93
+ orientation === 'horizontal' && 'rg--horizontal',
94
+ size === 'sm' && 'rg--sm',
95
+ error && 'is-error',
96
+ className,
97
+ );
98
+
99
+ return (
100
+ <fieldset className={cls} style={style} aria-invalid={error ? true : undefined} {...htmlProps}>
101
+ {label && (
102
+ <div className="rg__head">
103
+ <legend className="rg__label">
104
+ {label}
105
+ {required && (
106
+ <span className="rg__req" aria-hidden="true">
107
+ *
108
+ </span>
109
+ )}
110
+ {optional && <span className="rg__optional">(optional)</span>}
111
+ </legend>
112
+ {helper && <p className="rg__helper">{helper}</p>}
113
+ </div>
114
+ )}
115
+
116
+ <div
117
+ className="rg__options"
118
+ ref={optionsRef}
119
+ onPointerLeave={() => {
120
+ glide.leave();
121
+ setHovered(null);
122
+ }}
123
+ >
124
+ {variant === 'rows' && <GlidePill className="rg__hover" glide={glide} />}
125
+ {options.map((opt) => {
126
+ const selected = opt.value === value;
127
+ const isDisabled = disabled || opt.disabled;
128
+ return (
129
+ <label
130
+ key={opt.value}
131
+ className={cx('rg-opt', selected && 'is-selected', isDisabled && 'is-disabled')}
132
+ onPointerEnter={
133
+ isDisabled
134
+ ? undefined
135
+ : variant === 'rows'
136
+ ? (e) => glide.enter(e.currentTarget)
137
+ : () => setHovered(opt.value)
138
+ }
139
+ >
140
+ <input
141
+ className="rg-opt__input"
142
+ type="radio"
143
+ name={name}
144
+ value={opt.value}
145
+ checked={selected}
146
+ required={required}
147
+ disabled={isDisabled}
148
+ onChange={() => onChange && onChange(opt.value)}
149
+ />
150
+ {variant === 'cards' && hovered === opt.value && !selected && (
151
+ <span className="rg__card-hover" aria-hidden="true" ref={cardHoverRef}></span>
152
+ )}
153
+ {variant === 'cards' && selected && (
154
+ <span className="rg__card-fill" aria-hidden="true" ref={fillRef}></span>
155
+ )}
156
+ {variant === 'cards' && opt.icon && (
157
+ <span className="rg-opt__icon">
158
+ <IconSlot>{opt.icon}</IconSlot>
159
+ </span>
160
+ )}
161
+ <span className="rg-opt__control">
162
+ <span className="rg-opt__dot">
163
+ {selected && <span className="rg__marker" aria-hidden="true" ref={markerRef}></span>}
164
+ </span>
165
+ </span>
166
+ <span className="rg-opt__body">
167
+ <span className="rg-opt__label">{opt.label}</span>
168
+ {opt.description && <span className="rg-opt__desc">{opt.description}</span>}
169
+ </span>
170
+ </label>
171
+ );
172
+ })}
173
+ </div>
174
+
175
+ <Collapse open={!!error} fade className="rg__msg-wrap">
176
+ <div className="rg__msg">
177
+ <Icon name="warning-circle" size="sm" weight="fill" />
178
+ {error}
179
+ </div>
180
+ </Collapse>
181
+ </fieldset>
182
+ );
183
+ }
@@ -0,0 +1,300 @@
1
+ /* RadioGroup - single-select in two skins: rows (dot + label) and cards (selectable tiles). */
2
+
3
+ /* dot dims (18 / sm 16, inner 8 / sm 7) are bespoke, intentionally off the token ramps - don't "fix" to a token */
4
+ .rg {
5
+ --rg-dot: 18px;
6
+ --rg-dot-inner: 8px;
7
+
8
+ display: grid;
9
+ border: 0;
10
+ margin: 0;
11
+ padding: 0;
12
+ min-width: 0;
13
+ }
14
+ .rg--sm {
15
+ --rg-dot: 16px;
16
+ --rg-dot-inner: 7px;
17
+ }
18
+
19
+ .rg__head {
20
+ margin-bottom: var(--space-4);
21
+ }
22
+ .rg__label {
23
+ font: var(--type-caption);
24
+ font-weight: var(--weight-medium);
25
+ color: var(--text-body);
26
+ display: inline-flex;
27
+ align-items: center;
28
+ gap: var(--space-1);
29
+ line-height: 1;
30
+ padding: 0;
31
+ }
32
+ .rg__req {
33
+ color: var(--danger);
34
+ }
35
+ .rg__optional {
36
+ color: var(--text-subtle);
37
+ font-weight: var(--weight-regular);
38
+ }
39
+ .rg__helper {
40
+ font: var(--type-caption);
41
+ color: var(--text-muted);
42
+ margin-top: var(--space-1);
43
+ max-width: var(--measure-prose);
44
+ }
45
+
46
+ .rg__options {
47
+ position: relative; /* anchors the rows' shared glide pill */
48
+ display: grid;
49
+ gap: var(--space-1);
50
+ }
51
+ .rg--horizontal .rg__options {
52
+ display: flex;
53
+ flex-wrap: wrap;
54
+ gap: var(--space-2) var(--space-5);
55
+ }
56
+ .rg--cards .rg__options {
57
+ gap: var(--space-3);
58
+ }
59
+ .rg--cards.rg--horizontal .rg__options {
60
+ display: flex;
61
+ flex-wrap: wrap;
62
+ gap: var(--space-3);
63
+ }
64
+ .rg--cards.rg--horizontal .rg-opt {
65
+ flex: 1 1 0;
66
+ min-width: 12rem;
67
+ }
68
+
69
+ /* align-items:flex-start pins the dot to the first text line when a description wraps below */
70
+ .rg-opt {
71
+ position: relative;
72
+ display: flex;
73
+ align-items: flex-start;
74
+ gap: var(--space-3);
75
+ padding: var(--space-2) var(--space-3);
76
+ border-radius: var(--radius-md);
77
+ cursor: pointer;
78
+ transition: var(--transition-colors);
79
+ -webkit-tap-highlight-color: transparent;
80
+ }
81
+ /* Hover pill - one persistent fill (../motion/glide, first child of .rg__options) that
82
+ springs between rows; the positioned rows paint above it. Rows only; cards keep their
83
+ own layoutId hover (their opaque bg + press transform need the fill inside the card). */
84
+ .rg__hover {
85
+ opacity: 0;
86
+ position: absolute;
87
+ top: 0;
88
+ left: 0;
89
+ z-index: 0;
90
+ border-radius: var(--radius-md);
91
+ background: var(--neutral-wash);
92
+ pointer-events: none;
93
+ }
94
+ .rg-opt__control,
95
+ .rg-opt__body {
96
+ position: relative;
97
+ z-index: 1;
98
+ }
99
+
100
+ .rg-opt__input {
101
+ position: absolute;
102
+ width: 1px;
103
+ height: 1px;
104
+ margin: 0;
105
+ padding: 0;
106
+ opacity: 0;
107
+ pointer-events: none;
108
+ }
109
+
110
+ /* height = the label's line-box, so the dot optically centers on the first line */
111
+ .rg-opt__control {
112
+ flex: none;
113
+ height: var(--leading-body);
114
+ display: grid;
115
+ place-items: center;
116
+ }
117
+ .rg--sm .rg-opt__control {
118
+ height: var(--leading-caption);
119
+ }
120
+
121
+ .rg-opt__dot {
122
+ position: relative;
123
+ width: var(--rg-dot);
124
+ height: var(--rg-dot);
125
+ border-radius: var(--radius-full);
126
+ /* transparent so the ring-centre gap shows the surface behind (card tint), never a white halo */
127
+ background: transparent;
128
+ border: var(--border-hairline) solid var(--border-strong);
129
+ /* transform stays in the bundle so the press scale eases instead of snapping */
130
+ transition:
131
+ var(--transition-control),
132
+ transform var(--duration-fast) var(--ease-standard);
133
+ }
134
+ /* single marker glides between dots via Motion layoutId; centred by inset:0 + margin:auto, so no CSS transition here - Motion owns the move */
135
+ .rg__marker {
136
+ position: absolute;
137
+ inset: 0;
138
+ margin: auto;
139
+ width: var(--rg-dot-inner);
140
+ height: var(--rg-dot-inner);
141
+ border-radius: var(--radius-full);
142
+ background: var(--accent);
143
+ pointer-events: none;
144
+ will-change: transform;
145
+ }
146
+
147
+ .rg-opt__body {
148
+ display: grid;
149
+ gap: var(--space-px);
150
+ padding-top: 0;
151
+ }
152
+ .rg-opt__label {
153
+ font: var(--type-body);
154
+ color: var(--text-strong);
155
+ }
156
+ .rg--sm .rg-opt__label {
157
+ font: var(--type-caption);
158
+ font-weight: var(--weight-medium);
159
+ }
160
+ .rg-opt__desc {
161
+ font: var(--type-caption);
162
+ color: var(--text-muted);
163
+ text-wrap: pretty;
164
+ }
165
+
166
+ .rg-opt:hover:not(.is-disabled):not(.is-selected) .rg-opt__dot {
167
+ border-color: var(--text-muted);
168
+ }
169
+ .rg-opt:active:not(.is-disabled) .rg-opt__dot {
170
+ transform: scale(0.92);
171
+ }
172
+
173
+ /* .is-selected (React) drives selection, not :checked/:has - Chromium doesn't reliably invalidate those on a radio's implicit sibling-uncheck */
174
+ .rg-opt.is-selected .rg-opt__dot {
175
+ border-color: var(--accent);
176
+ }
177
+
178
+ .rg-opt__input:focus-visible ~ .rg-opt__control .rg-opt__dot {
179
+ border-color: var(--accent);
180
+ box-shadow: var(--ring-accent);
181
+ }
182
+
183
+ .rg-opt.is-disabled {
184
+ cursor: not-allowed;
185
+ }
186
+ .rg-opt.is-disabled .rg-opt__label {
187
+ color: var(--text-disabled);
188
+ }
189
+ .rg-opt.is-disabled .rg-opt__desc {
190
+ color: var(--text-disabled);
191
+ }
192
+ .rg-opt.is-disabled .rg-opt__dot {
193
+ background: var(--bg-muted);
194
+ border-color: var(--border-default);
195
+ }
196
+ .rg-opt.is-disabled.is-selected .rg-opt__dot {
197
+ border-color: var(--border-strong);
198
+ }
199
+ .rg-opt.is-disabled.is-selected .rg__marker {
200
+ background: var(--text-disabled);
201
+ }
202
+
203
+ .rg--cards .rg-opt {
204
+ position: relative;
205
+ align-items: center;
206
+ padding: var(--space-4);
207
+ background: var(--bg-surface);
208
+ border: var(--border-hairline) solid var(--border-default);
209
+ border-radius: var(--radius-lg);
210
+ box-shadow: var(--shadow-xs);
211
+ transition:
212
+ var(--transition-control),
213
+ transform var(--duration-fast) var(--ease-standard);
214
+ }
215
+ .rg--cards .rg-opt:active:not(.is-disabled) {
216
+ transform: translateY(var(--space-px));
217
+ box-shadow: var(--shadow-xs);
218
+ }
219
+
220
+ .rg--cards .rg-opt__control {
221
+ order: 3;
222
+ margin-left: auto;
223
+ }
224
+ .rg-opt__icon {
225
+ flex: none;
226
+ display: inline-flex;
227
+ color: var(--text-muted);
228
+ transition: color var(--duration-fast) var(--ease-standard);
229
+ }
230
+ .rg-opt__icon svg {
231
+ display: block;
232
+ }
233
+
234
+ /* selected fill is a shared layoutId layer that glides between cards - the card paints no selected bg/border of its own */
235
+ .rg__card-fill {
236
+ position: absolute;
237
+
238
+ /* negative inset covers the card's hairline so the accent edge replaces it */
239
+ inset: calc(-1 * var(--border-hairline));
240
+ z-index: 0;
241
+ border: var(--border-hairline) solid var(--accent);
242
+ border-radius: var(--radius-lg);
243
+ background: var(--accent-subtle);
244
+ pointer-events: none;
245
+ }
246
+
247
+ /* hover highlight - a neutral bordered fill that glides between cards (layoutId, non-selected
248
+ only), mirroring the selected fill muted. Replaces the old per-card border/shadow swap. */
249
+ .rg__card-hover {
250
+ position: absolute;
251
+ inset: calc(-1 * var(--border-hairline));
252
+ z-index: 0;
253
+ border: var(--border-hairline) solid var(--border-strong);
254
+ border-radius: var(--radius-lg);
255
+ background: var(--neutral-wash);
256
+ pointer-events: none;
257
+ }
258
+ .rg--cards .rg-opt > .rg-opt__icon,
259
+ .rg--cards .rg-opt > .rg-opt__control,
260
+ .rg--cards .rg-opt > .rg-opt__body {
261
+ position: relative;
262
+ z-index: 1;
263
+ }
264
+ .rg--cards .rg-opt.is-selected .rg-opt__icon {
265
+ color: var(--text-accent);
266
+ }
267
+ .rg--cards .rg-opt:has(.rg-opt__input:focus-visible) {
268
+ box-shadow: var(--ring-accent);
269
+ }
270
+
271
+ .rg--cards .rg-opt.is-disabled {
272
+ background: var(--bg-subtle);
273
+ border-color: var(--border-subtle);
274
+ box-shadow: none;
275
+ }
276
+ .rg--cards .rg-opt.is-disabled .rg-opt__icon {
277
+ color: var(--text-disabled);
278
+ }
279
+
280
+ /* in a <Collapse>: absent = zero reserved space; the padding-top is the options-message gap, inside the collapsible area */
281
+ .rg__msg {
282
+ font: var(--type-caption);
283
+ color: var(--danger-text);
284
+ display: flex;
285
+ align-items: center;
286
+ gap: var(--space-1);
287
+ min-height: 1em;
288
+ padding-top: var(--space-3);
289
+ }
290
+ .rg__msg i,
291
+ .rg__msg svg {
292
+ flex: none;
293
+ }
294
+
295
+ .rg.is-error .rg-opt:not(.is-selected) .rg-opt__dot {
296
+ border-color: var(--danger);
297
+ }
298
+ .rg.is-error.rg--cards .rg-opt:not(.is-selected) {
299
+ border-color: var(--danger);
300
+ }
@@ -0,0 +1,143 @@
1
+ 'use client';
2
+
3
+ import './tag.css';
4
+ import { createContext, useContext, type CSSProperties, type HTMLAttributes, type ReactNode, type Ref } from 'react';
5
+ import { Presence } from '../../../motion/presence';
6
+ import { Motion } from '../../../motion/element';
7
+ import { resolveMotionTiming } from '../../../motion/motion-timing';
8
+ import type { DisableableAnimation } from '../../../motion/timing';
9
+ import type { FlipTuning } from '../../../motion/flip';
10
+ import type { Layer } from '../../../engine';
11
+ import { IconSlot } from '../../internal/icon/IconSlot';
12
+ import type { DataAttributes } from '../../../dom-props';
13
+ import { cx } from '../../internal/utils/cx';
14
+
15
+ const TAG_TIMING = { open: { duration: 'base', ease: 'entrance' }, close: { duration: 'fast', ease: 'exit' } } as const;
16
+ const TAG_LAYOUT_TIMING = {
17
+ open: { duration: 'slow', ease: 'entrance' },
18
+ close: { duration: 'slow', ease: 'entrance' },
19
+ } as const;
20
+
21
+ interface TagGroupMotion {
22
+ animate: Layer;
23
+ exit: Layer;
24
+ layout: FlipTuning;
25
+ }
26
+
27
+ const TagGroupContext = createContext<TagGroupMotion | null>(null);
28
+
29
+ interface TagOwnProps {
30
+ /** Tag content; a string label also seeds the default remove-button `aria-label`. */
31
+ children: ReactNode;
32
+ /** Your own icon node, sized small and tinted to the tag's text. */
33
+ icon?: ReactNode | null;
34
+ /** Presence adds the remove button; called on click - remove the item yourself. */
35
+ onRemove?: (() => void) | null;
36
+ /** Accessible label for the remove button. Defaults to "Remove {label}" for string labels. */
37
+ removeLabel?: string;
38
+ /** 'md' = 28px (default) - 'sm' = 24px for dense rows. */
39
+ size?: 'md' | 'sm';
40
+ /** Set `data-disabled` on the tag and disable its remove button. @default false */
41
+ disabled?: boolean;
42
+ /** Extra class(es) merged onto the root element. */
43
+ className?: string;
44
+ /** Inline styles merged onto the root element. */
45
+ style?: CSSProperties;
46
+ }
47
+
48
+ /** Tag - the removable, editable label (stateless; the parent owns the list). */
49
+ export interface TagProps extends TagOwnProps {
50
+ /** Standard <span> attributes forwarded to the tag root. */
51
+ htmlProps?: Omit<HTMLAttributes<HTMLSpanElement>, keyof TagOwnProps> & DataAttributes;
52
+ /** Ref to the tag root span. */
53
+ ref?: Ref<HTMLSpanElement>;
54
+ }
55
+
56
+ interface TagGroupOwnProps {
57
+ /** Accessible name for the group (role="group"). */
58
+ ariaLabel?: string;
59
+ /** The `Tag`s; adds, removals and reflow are animated. */
60
+ children?: ReactNode;
61
+ /** Extra class(es) merged onto the group wrapper. */
62
+ className?: string;
63
+ /** Inline styles merged onto the group wrapper. */
64
+ style?: CSSProperties;
65
+ /** Add/remove/reflow timing for the member tags - motion tokens only, or `null` to disable. */
66
+ animation?: DisableableAnimation;
67
+ }
68
+
69
+ /** TagGroup - wrapping flex row + removal/insertion choreography. */
70
+ export interface TagGroupProps extends TagGroupOwnProps {
71
+ /** Standard <div> attributes forwarded to the group wrapper. */
72
+ htmlProps?: Omit<HTMLAttributes<HTMLDivElement>, keyof TagGroupOwnProps> & DataAttributes;
73
+ }
74
+
75
+ function TagRemoveGlyph() {
76
+ return (
77
+ <svg viewBox="0 0 12 12" fill="none" aria-hidden="true">
78
+ <path d="M3 3 L9 9 M9 3 L3 9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
79
+ </svg>
80
+ );
81
+ }
82
+
83
+ export function Tag({
84
+ children,
85
+ icon = null,
86
+ onRemove = null,
87
+ removeLabel,
88
+ size = 'md',
89
+ disabled = false,
90
+ className = '',
91
+ style,
92
+ htmlProps,
93
+ ref,
94
+ }: TagProps) {
95
+ const cls = cx('tag', size === 'sm' && 'tag--sm', className);
96
+ const xLabel = removeLabel || (typeof children === 'string' ? 'Remove ' + children : 'Remove');
97
+ const group = useContext(TagGroupContext);
98
+
99
+ return (
100
+ <Motion
101
+ as="span"
102
+ ref={ref}
103
+ animate={group?.animate}
104
+ exit={group?.exit}
105
+ layout={group?.layout}
106
+ className={cls}
107
+ style={style}
108
+ data-disabled={disabled ? 'true' : undefined}
109
+ {...htmlProps}
110
+ >
111
+ {icon && (
112
+ <span className="tag__icon">
113
+ <IconSlot size="sm">{icon}</IconSlot>
114
+ </span>
115
+ )}
116
+ <span className="tag__label">{children}</span>
117
+ {onRemove && (
118
+ <button type="button" className="tag__remove" aria-label={xLabel} disabled={disabled} onClick={onRemove}>
119
+ <TagRemoveGlyph />
120
+ </button>
121
+ )}
122
+ </Motion>
123
+ );
124
+ }
125
+
126
+ export function TagGroup({ ariaLabel, className = '', style, children, htmlProps, animation }: TagGroupProps) {
127
+ const step = resolveMotionTiming(animation, TAG_TIMING);
128
+ const layout = resolveMotionTiming(animation, TAG_LAYOUT_TIMING).open;
129
+
130
+ const motion = {
131
+ animate: { opacity: [0, 1], scale: [0.9, 1], timing: step.open },
132
+ exit: { opacity: [1, 0], scale: [1, 0.9], timing: step.close },
133
+ layout: { timing: layout },
134
+ };
135
+
136
+ return (
137
+ <div className={cx('tag-group', className)} style={style} role="group" aria-label={ariaLabel} {...htmlProps}>
138
+ <TagGroupContext.Provider value={motion}>
139
+ <Presence initial={false}>{children}</Presence>
140
+ </TagGroupContext.Provider>
141
+ </div>
142
+ );
143
+ }