@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,97 @@
1
+ 'use client';
2
+
3
+ import './tag.css';
4
+ import '../collapse/collapse.css';
5
+ import type { ButtonHTMLAttributes, CSSProperties, ReactNode } from 'react';
6
+ import { IconSlot } from '../../internal/icon/IconSlot';
7
+ import { useControllable } from '../../internal/hooks/use-controllable';
8
+ import type { DataAttributes } from '../../../dom-props';
9
+ import { cx } from '../../internal/utils/cx';
10
+
11
+ interface ToggleTagOwnProps {
12
+ /** Chip content; the label shown beside the tick or `icon`. */
13
+ children: ReactNode;
14
+ /** Controlled selected value. Omit for uncontrolled. */
15
+ selected?: boolean;
16
+ /** Uncontrolled initial value. */
17
+ defaultSelected?: boolean;
18
+ /** Fires with the next value on every toggle. */
19
+ onChange?: (selected: boolean) => void;
20
+ /** Your own icon node; replaces the tick. Selected state reads from the wash. */
21
+ icon?: ReactNode | null;
22
+ /** Optional result count - rendered mono/tabular. */
23
+ count?: ReactNode;
24
+ /** 'md' = 28px (default) - 'sm' = 24px for dense rows. */
25
+ size?: 'md' | 'sm';
26
+ /** Disable the button, blocking toggles. @default false */
27
+ disabled?: boolean;
28
+ /** Extra class(es) merged onto the root element. */
29
+ className?: string;
30
+ /** Inline styles merged onto the root element. */
31
+ style?: CSSProperties;
32
+ }
33
+
34
+ /** ToggleTag - the on/off filter chip (<button aria-pressed>). */
35
+ export interface ToggleTagProps extends ToggleTagOwnProps {
36
+ /** Standard <button> attributes forwarded to the chip. */
37
+ htmlProps?: Omit<ButtonHTMLAttributes<HTMLButtonElement>, keyof ToggleTagOwnProps> & DataAttributes;
38
+ }
39
+
40
+ function ToggleTagTick({ selected }: { selected: boolean }) {
41
+ return (
42
+ <span className="tag__check collapse" data-axis="width" data-open={selected ? 'true' : 'false'} aria-hidden="true">
43
+ <span className="collapse__inner">
44
+ <svg viewBox="0 0 12 12" fill="none">
45
+ <path
46
+ d="M2.5 6.5 L5 9 L9.5 3.5"
47
+ pathLength="1"
48
+ strokeWidth="1.5"
49
+ strokeLinecap="round"
50
+ strokeLinejoin="round"
51
+ />
52
+ </svg>
53
+ </span>
54
+ </span>
55
+ );
56
+ }
57
+
58
+ export function ToggleTag({
59
+ children,
60
+ selected: selectedProp,
61
+ defaultSelected = false,
62
+ onChange,
63
+ icon = null,
64
+ count = null,
65
+ size = 'md',
66
+ disabled = false,
67
+ className = '',
68
+ style,
69
+ htmlProps,
70
+ }: ToggleTagProps) {
71
+ const [selected, setSelected] = useControllable(selectedProp, defaultSelected, onChange);
72
+ const toggle = () => setSelected(!selected);
73
+
74
+ const cls = cx('tag', 'tag--toggle', size === 'sm' && 'tag--sm', className);
75
+
76
+ return (
77
+ <button
78
+ type="button"
79
+ className={cls}
80
+ style={style}
81
+ aria-pressed={selected}
82
+ disabled={disabled}
83
+ onClick={toggle}
84
+ {...htmlProps}
85
+ >
86
+ {icon ? (
87
+ <span className="tag__icon">
88
+ <IconSlot size="sm">{icon}</IconSlot>
89
+ </span>
90
+ ) : (
91
+ <ToggleTagTick selected={selected} />
92
+ )}
93
+ <span className="tag__label">{children}</span>
94
+ {count != null && <span className="tag__count">{count}</span>}
95
+ </button>
96
+ );
97
+ }
@@ -0,0 +1,248 @@
1
+ /* Tag / chip - interactive label (button-family chrome): base .tag + .tag--toggle sibling. */
2
+
3
+ .tag {
4
+ --tag-h: var(--control-height-sm);
5
+ /* button-family DNA: lit from above + a contact shadow (.btn--secondary) */
6
+ --tag-shadow: inset 0 1px 0 0 rgb(255 255 255 / 0.6), 0 1px 2px 0 rgb(var(--shadow-rgb) / 0.05);
7
+
8
+ display: inline-flex;
9
+ align-items: center;
10
+ gap: var(--space-1);
11
+ height: var(--tag-h);
12
+ max-width: 100%;
13
+ padding-inline: var(--space-2);
14
+ border: var(--border-hairline) solid var(--border-default);
15
+ border-radius: var(--radius-sm);
16
+ background: var(--bg-surface);
17
+ color: var(--text-body);
18
+
19
+ font-family: var(--font-sans);
20
+ font-size: var(--size-caption);
21
+ line-height: 1;
22
+ font-weight: var(--weight-medium);
23
+ letter-spacing: var(--tracking-normal);
24
+ white-space: nowrap;
25
+ vertical-align: middle;
26
+ user-select: none;
27
+ -webkit-user-select: none;
28
+ box-shadow: var(--tag-shadow);
29
+
30
+ /* no transform transition here - Motion writes transforms on grouped tags (section C) */
31
+ transition: var(--transition-control);
32
+ }
33
+
34
+ .tag__label {
35
+ min-width: 0;
36
+ overflow: hidden;
37
+ text-overflow: ellipsis;
38
+ }
39
+
40
+ .tag--sm {
41
+ --tag-h: var(--space-5);
42
+ font-size: var(--size-micro);
43
+ }
44
+
45
+ .tag__icon {
46
+ display: inline-flex;
47
+ flex: none;
48
+ }
49
+ .tag__icon svg,
50
+ .tag__icon i {
51
+ display: block;
52
+ }
53
+
54
+ /* Dual-weight icon crossfade - transitions, not animations, so nothing plays on first paint. */
55
+ .tag__icon--dual {
56
+ display: inline-grid;
57
+ }
58
+ .tag__icon--dual > :is(i, svg) {
59
+ grid-area: 1 / 1;
60
+ transition:
61
+ opacity var(--duration-base) var(--ease-standard),
62
+ transform var(--duration-base) var(--ease-standard);
63
+ }
64
+ .tag__icon--dual > :is(i, svg):last-child {
65
+ opacity: 0;
66
+ transform: scale(0.6);
67
+ }
68
+ .tag--toggle[aria-pressed='true'] .tag__icon--dual > :is(i, svg):first-child {
69
+ opacity: 0;
70
+ }
71
+ .tag--toggle[aria-pressed='true'] .tag__icon--dual > :is(i, svg):last-child {
72
+ opacity: 1;
73
+ transform: scale(1);
74
+ transition:
75
+ opacity var(--duration-fast) var(--ease-standard),
76
+ transform var(--duration-slow) var(--ease-spring);
77
+ }
78
+
79
+ .tag__count {
80
+ font-family: var(--font-mono);
81
+ font-size: var(--size-micro);
82
+ font-variant-numeric: tabular-nums;
83
+ color: var(--text-subtle);
84
+ transition: color var(--duration-fast) var(--ease-standard);
85
+ }
86
+
87
+ .tag__remove {
88
+ display: inline-flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ flex: none;
92
+ width: calc(var(--tag-h) - var(--space-2));
93
+ height: calc(var(--tag-h) - var(--space-2));
94
+ margin-right: calc(var(--space-1) * -1);
95
+ padding: 0;
96
+ border: 0;
97
+ border-radius: var(--radius-full);
98
+ background: transparent;
99
+ color: var(--text-subtle);
100
+ cursor: pointer;
101
+ appearance: none;
102
+ -webkit-tap-highlight-color: transparent;
103
+ transition:
104
+ color var(--duration-fast) var(--ease-standard),
105
+ background-color var(--duration-fast) var(--ease-standard),
106
+ transform var(--duration-fast) var(--ease-standard);
107
+ }
108
+ .tag__remove svg {
109
+ display: block;
110
+ width: var(--space-3);
111
+ height: var(--space-3);
112
+ transition: transform var(--duration-base) var(--ease-standard);
113
+ }
114
+ .tag__remove:hover {
115
+ color: var(--text-strong);
116
+ background: var(--bg-muted);
117
+ }
118
+ .tag__remove:hover svg {
119
+ transform: rotate(30deg);
120
+ }
121
+ .tag__remove:active {
122
+ transform: scale(0.9);
123
+ }
124
+ .tag__remove:focus-visible {
125
+ outline: none;
126
+ box-shadow: var(--focus-ring);
127
+ }
128
+
129
+ .tag:has(.tag__remove:hover):not([data-disabled]),
130
+ .tag:has(.tag__remove:focus-visible) {
131
+ color: var(--text-muted);
132
+ border-color: var(--border-strong);
133
+ }
134
+
135
+ .tag--toggle {
136
+ cursor: pointer;
137
+ appearance: none;
138
+ -webkit-tap-highlight-color: transparent;
139
+ transition:
140
+ var(--transition-control),
141
+ transform var(--duration-fast) var(--ease-standard);
142
+ }
143
+ .tag--toggle:hover:not(:disabled) {
144
+ color: var(--text-strong);
145
+ border-color: var(--border-strong);
146
+ background: var(--neutral-wash);
147
+ }
148
+ .tag--toggle:active:not(:disabled) {
149
+ transform: translateY(var(--space-px));
150
+ background: var(--bg-muted);
151
+ --tag-shadow: inset 0 1px 2px 0 rgb(var(--shadow-rgb) / 0.07);
152
+ }
153
+ /* composed ring - never erases the resting shadow (.btn pattern) */
154
+ .tag--toggle:focus-visible {
155
+ outline: none;
156
+ box-shadow: var(--tag-shadow), var(--focus-ring);
157
+ }
158
+
159
+ /* Selected = accent wash, never a solid fill; the top sheen comes off (matte), the contact shadow stays. */
160
+ .tag--toggle[aria-pressed='true'] {
161
+ color: var(--text-accent);
162
+ border-color: var(--accent-border);
163
+ background: var(--accent-wash);
164
+ --tag-shadow: 0 1px 2px 0 rgb(var(--shadow-rgb) / 0.05);
165
+ }
166
+ .tag--toggle[aria-pressed='true']:hover:not(:disabled) {
167
+ color: var(--accent-active);
168
+ border-color: var(--accent-border);
169
+ background: var(--accent-wash);
170
+ }
171
+ .tag--toggle[aria-pressed='true']:active:not(:disabled) {
172
+ background: var(--accent-subtle);
173
+ }
174
+ .tag--toggle[aria-pressed='true'] .tag__count {
175
+ color: inherit;
176
+ }
177
+
178
+ /* Tick slot (Collapse width mechanism); the negative margin cancels the flex gap while collapsed, the inner margin restores it when open. */
179
+ .tag__check {
180
+ margin-right: calc(var(--space-1) * -1);
181
+ }
182
+ .tag__check svg {
183
+ display: block;
184
+ width: var(--space-3);
185
+ height: var(--space-3);
186
+ margin-right: var(--space-1);
187
+ }
188
+ .tag__check path {
189
+ stroke: currentColor;
190
+ stroke-dasharray: 1;
191
+ stroke-dashoffset: 1;
192
+ transition: stroke-dashoffset var(--duration-base) var(--ease-standard);
193
+ }
194
+ .tag--toggle[aria-pressed='true'] .tag__check path {
195
+ stroke-dashoffset: 0;
196
+ transition: stroke-dashoffset var(--duration-slow) var(--ease-entrance) 60ms;
197
+ }
198
+
199
+ .tag[data-disabled],
200
+ .tag--toggle:disabled {
201
+ color: var(--text-disabled);
202
+ border-color: var(--border-subtle);
203
+ background: var(--bg-surface);
204
+ cursor: default;
205
+ --tag-shadow: none;
206
+ }
207
+ .tag[data-disabled] .tag__count,
208
+ .tag--toggle:disabled .tag__count {
209
+ color: var(--text-disabled);
210
+ }
211
+ .tag[data-disabled] .tag__remove {
212
+ color: var(--text-disabled);
213
+ background: transparent;
214
+ cursor: default;
215
+ pointer-events: none;
216
+ }
217
+ .tag--toggle:disabled {
218
+ transform: none;
219
+ }
220
+ .tag--toggle:disabled[aria-pressed='true'] {
221
+ background: var(--bg-muted);
222
+ border-color: var(--border-subtle);
223
+ }
224
+
225
+ .tag-group {
226
+ display: flex;
227
+ flex-wrap: wrap;
228
+ align-items: center;
229
+ gap: var(--space-2);
230
+ }
231
+
232
+ @media (prefers-reduced-motion: reduce) {
233
+ .tag__remove svg {
234
+ transition: none;
235
+ }
236
+ .tag__remove:hover svg {
237
+ transform: none;
238
+ }
239
+ .tag__remove:active {
240
+ transform: none;
241
+ }
242
+ .tag--toggle:active:not(:disabled) {
243
+ transform: none;
244
+ }
245
+ .tag__icon--dual > :is(i, svg) {
246
+ transition: none;
247
+ }
248
+ }
@@ -0,0 +1,190 @@
1
+ 'use client';
2
+
3
+ import './textarea.css';
4
+ import {
5
+ useEffect,
6
+ useLayoutEffect,
7
+ useRef,
8
+ type ChangeEventHandler,
9
+ type CSSProperties,
10
+ type KeyboardEvent,
11
+ type ReactNode,
12
+ type TextareaHTMLAttributes,
13
+ } from 'react';
14
+ import { FieldLabel, FieldMessage, resolveFieldMessage, type FieldMessagingProps } from '../input/field-chrome';
15
+ import type { DataAttributes } from '../../../dom-props';
16
+ import { cx } from '../../internal/utils/cx';
17
+
18
+ const RING_C = (2 * Math.PI * 7).toFixed(2);
19
+
20
+ /** The native <textarea> props Textarea surfaces at the top level (the rest live in `htmlProps`). */
21
+ type TextareaNative = Pick<
22
+ TextareaHTMLAttributes<HTMLTextAreaElement>,
23
+ 'placeholder' | 'disabled' | 'readOnly' | 'onKeyDown'
24
+ >;
25
+
26
+ interface TextareaOwnProps extends FieldMessagingProps, TextareaNative {
27
+ /** Controlled text value. @default '' */
28
+ value?: string;
29
+ /** Change handler - fired on each edit with the textarea change event. */
30
+ onChange?: ChangeEventHandler<HTMLTextAreaElement>;
31
+ /** Fired on ⌘/Ctrl+Enter with the current text. */
32
+ onSubmit?: (value: string) => void;
33
+ /** Soft char limit: shows the meter + over-limit highlight. Does NOT truncate (use native maxLength for a hard stop). */
34
+ max?: number;
35
+ /** Rows shown before auto-grow kicks in. @default 3 */
36
+ minRows?: number;
37
+ /** Row cap - grows up to this, then the box scrolls. @default 10 */
38
+ maxRows?: number;
39
+ /** Remaining-chars threshold that flips the meter amber. Default 20. */
40
+ warnAt?: number;
41
+ /** Footer hint, left of the meter - e.g. a ⌘↵ affordance. */
42
+ hint?: ReactNode;
43
+ /** md (default) - lg (prominent composer). */
44
+ size?: 'md' | 'lg';
45
+ /** Extra class(es) merged onto the field root. */
46
+ className?: string;
47
+ /** Inline styles merged onto the field root. */
48
+ style?: CSSProperties;
49
+ }
50
+
51
+ export interface TextareaProps extends TextareaOwnProps {
52
+ /** Standard <textarea> attributes (name, maxLength, aria-*, onKeyDown, ...) forwarded to the textarea. */
53
+ htmlProps?: Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, keyof TextareaOwnProps | 'rows'> & DataAttributes;
54
+ }
55
+
56
+ export function Textarea({
57
+ id,
58
+ label,
59
+ required,
60
+ optional,
61
+ placeholder,
62
+ helper,
63
+ error,
64
+ warning,
65
+ success,
66
+ value = '',
67
+ onChange,
68
+ onKeyDown,
69
+ onSubmit,
70
+ max,
71
+ minRows = 3,
72
+ maxRows = 10,
73
+ warnAt = 20,
74
+ hint,
75
+ size,
76
+ disabled,
77
+ readOnly,
78
+ className = '',
79
+ style,
80
+ htmlProps,
81
+ }: TextareaProps) {
82
+ const taRef = useRef<HTMLTextAreaElement>(null);
83
+ const stackRef = useRef<HTMLDivElement>(null);
84
+ const mirrorRef = useRef<HTMLDivElement>(null);
85
+ const text = value ?? '';
86
+ const count = text.length;
87
+ const over = max ? Math.max(count - max, 0) : 0;
88
+ const remaining = max ? max - count : 0;
89
+ const meterState = over ? 'is-over' : max && remaining <= warnAt ? 'is-near' : '';
90
+
91
+ const { state, msg, msgIcon } = resolveFieldMessage(error, warning, success, helper);
92
+
93
+ const resize = () => {
94
+ const el = taRef.current,
95
+ stack = stackRef.current;
96
+ if (!el || !stack) return;
97
+ const start = stack.offsetHeight;
98
+ el.style.height = 'auto';
99
+ el.style.height = el.scrollHeight + 'px';
100
+ const content = el.offsetHeight;
101
+ const maxPx = parseFloat(getComputedStyle(stack).maxHeight) || Infinity;
102
+ const target = Math.min(content, maxPx);
103
+ if (target === start) {
104
+ stack.style.overflowY = content > maxPx ? 'auto' : 'clip';
105
+ return;
106
+ }
107
+ stack.style.overflowY = 'clip';
108
+ stack.style.height = start + 'px';
109
+ void stack.offsetHeight;
110
+ stack.style.height = target + 'px';
111
+ };
112
+ useLayoutEffect(resize, [text, size]);
113
+ useEffect(() => {
114
+ const stack = stackRef.current;
115
+ if (!stack) return undefined;
116
+ let w = stack.offsetWidth;
117
+ const ro = new ResizeObserver(() => {
118
+ if (stack.offsetWidth !== w) {
119
+ w = stack.offsetWidth;
120
+ resize();
121
+ }
122
+ });
123
+ ro.observe(stack);
124
+ return () => ro.disconnect();
125
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
126
+
127
+ const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
128
+ if (onSubmit && (e.metaKey || e.ctrlKey) && e.key === 'Enter') {
129
+ e.preventDefault();
130
+ onSubmit(text);
131
+ }
132
+ onKeyDown?.(e);
133
+ };
134
+
135
+ const cls = cx('fld', 'fld--txa', size === 'lg' && 'fld--lg', state, className);
136
+ const boxCls = cx('txa', disabled && 'is-disabled', readOnly && 'is-readonly');
137
+
138
+ return (
139
+ <div className={cls} style={style}>
140
+ <FieldLabel id={id} label={label} required={required} optional={optional} />
141
+ <div className={boxCls} style={{ '--txa-min-rows': minRows, '--txa-max-rows': maxRows } as CSSProperties}>
142
+ <div className="txa__stack" ref={stackRef}>
143
+ <div className="txa__mirror" ref={mirrorRef} aria-hidden="true">
144
+ {over ? text.slice(0, max) : text}
145
+ {over ? <mark>{text.slice(max)}</mark> : null}
146
+ {'\n'}
147
+ </div>
148
+ <textarea
149
+ id={id}
150
+ className="txa__input"
151
+ ref={taRef}
152
+ rows={minRows}
153
+ value={text}
154
+ placeholder={placeholder}
155
+ required={required}
156
+ disabled={disabled}
157
+ readOnly={readOnly}
158
+ onChange={onChange}
159
+ onKeyDown={handleKeyDown}
160
+ onScroll={() => {
161
+ if (mirrorRef.current) mirrorRef.current.scrollTop = taRef.current.scrollTop;
162
+ }}
163
+ aria-invalid={error ? true : undefined}
164
+ {...htmlProps}
165
+ />
166
+ </div>
167
+ {(max || hint) && (
168
+ <div className="txa__bar">
169
+ {hint && <span className="txa__hint">{hint}</span>}
170
+ {max && (
171
+ <span className={cx('txa__meter', meterState)}>
172
+ <span className="txa__count">{over || remaining <= warnAt ? remaining : `${count} / ${max}`}</span>
173
+ <svg
174
+ className="txa__ring"
175
+ viewBox="0 0 16 16"
176
+ aria-hidden="true"
177
+ style={{ '--txa-ring-c': RING_C, '--txa-ring-p': Math.min(count / max, 1) } as CSSProperties}
178
+ >
179
+ <circle className="txa__ring-trk" cx="8" cy="8" r="7" />
180
+ <circle className="txa__ring-prg" cx="8" cy="8" r="7" />
181
+ </svg>
182
+ </span>
183
+ )}
184
+ </div>
185
+ )}
186
+ </div>
187
+ <FieldMessage message={msg} icon={msgIcon} />
188
+ </div>
189
+ );
190
+ }