@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,120 @@
1
+ 'use client';
2
+
3
+ import './avatar.css';
4
+ import { useState, type CSSProperties, type HTMLAttributes, type ReactNode } from 'react';
5
+ import type { DataAttributes } from '../../../dom-props';
6
+ import { cx } from '../../internal/utils/cx';
7
+
8
+ export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
9
+ export type AvatarShape = 'circle' | 'square';
10
+ export type AvatarStatus = 'online' | 'away' | 'busy' | 'offline';
11
+ export type AvatarPaletteIndex = 1 | 2 | 3 | 4 | 5 | 6;
12
+
13
+ interface AvatarOwnProps {
14
+ /** Image URL. Falls back to initials / icon / silhouette if absent or fails. */
15
+ src?: string | null;
16
+ /** Display name - drives initials generation, palette hash, and aria-label. */
17
+ name?: string | null;
18
+ /** Content override - any React node (e.g. <Icon name="globe" />). */
19
+ icon?: ReactNode | null;
20
+ /** 'circle' for people; 'square' for channels / brand pages. Default 'circle'. */
21
+ shape?: AvatarShape;
22
+ /** Size step. Default 'md' (32 px). */
23
+ size?: AvatarSize;
24
+ /** Presence indicator dot at bottom-right. Default null (hidden). */
25
+ status?: AvatarStatus | null;
26
+ /** Override identity slot 1-6 (blue - violet - plum - rose - clay - moss); auto from name hash when null, neutral when anonymous. */
27
+ paletteIndex?: AvatarPaletteIndex | null;
28
+ /** Extra class(es) merged onto the root. */
29
+ className?: string;
30
+ /** Inline styles merged onto the root. */
31
+ style?: CSSProperties;
32
+ }
33
+
34
+ export interface AvatarProps extends AvatarOwnProps {
35
+ /** Standard <span> attributes (onClick, title, data-*, ...) forwarded to the root. */
36
+ htmlProps?: Omit<HTMLAttributes<HTMLSpanElement>, keyof AvatarOwnProps> & DataAttributes;
37
+ }
38
+
39
+ const PALETTE_SLOTS = 6;
40
+
41
+ function hashName(str: string) {
42
+ let h = 0;
43
+ for (let i = 0; i < str.length; i++) h = ((h << 5) - h + str.charCodeAt(i)) | 0;
44
+ return (Math.abs(h) % PALETTE_SLOTS) + 1;
45
+ }
46
+
47
+ function initialsOf(name: string | null, max: number) {
48
+ if (!name) return '';
49
+ const parts = name.trim().split(/\s+/).filter(Boolean);
50
+ if (parts.length === 0) return '';
51
+ if (parts.length === 1 || max === 1) return parts[0][0].toUpperCase();
52
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
53
+ }
54
+
55
+ function Silhouette() {
56
+ return (
57
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
58
+ <circle cx="12" cy="8.5" r="3.8" fill="currentColor" />
59
+ <path d="M4 20.5C4 16.1 7.6 13 12 13s8 3.1 8 7.5" fill="currentColor" opacity="0.65" />
60
+ </svg>
61
+ );
62
+ }
63
+
64
+ export function Avatar({
65
+ src = null,
66
+ name = null,
67
+ icon = null,
68
+ shape = 'circle',
69
+ size = 'md',
70
+ status = null,
71
+ paletteIndex = null,
72
+ className = '',
73
+ style = {},
74
+ htmlProps,
75
+ }: AvatarProps) {
76
+ const [imgError, setImgError] = useState(false);
77
+ const showImg = Boolean(src) && !imgError;
78
+
79
+ const maxInits = size === 'xs' ? 1 : 2;
80
+ const inits = initialsOf(name, maxInits);
81
+ const slot = paletteIndex ?? (name ? hashName(name) : null);
82
+
83
+ const cls = cx('avatar', size !== 'md' && `avatar--${size}`, shape === 'square' && 'avatar--square', className);
84
+
85
+ return (
86
+ <span
87
+ className={cls}
88
+ role={name ? 'img' : undefined}
89
+ aria-label={name || undefined}
90
+ data-palette={!showImg && slot != null ? slot : undefined}
91
+ style={style}
92
+ {...htmlProps}
93
+ >
94
+ <span className="avatar__face">
95
+ {showImg && (
96
+ <img className="avatar__img" src={src} alt={name || ''} onError={() => setImgError(true)} draggable={false} />
97
+ )}
98
+ {!showImg && icon && (
99
+ <span className="avatar__icon" aria-hidden="true">
100
+ {icon}
101
+ </span>
102
+ )}
103
+ {!showImg && !icon && inits && (
104
+ <span className="avatar_initialsOf" aria-hidden="true">
105
+ {inits}
106
+ </span>
107
+ )}
108
+ {!showImg && !icon && !inits && (
109
+ <span className="avatar__icon" aria-hidden="true">
110
+ <Silhouette />
111
+ </span>
112
+ )}
113
+ </span>
114
+
115
+ {status && (
116
+ <span className={`avatar__status avatar__status--${status}`} role="img" aria-label={status} title={status} />
117
+ )}
118
+ </span>
119
+ );
120
+ }
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+
3
+ import './avatar.css';
4
+ import {
5
+ Children,
6
+ cloneElement,
7
+ isValidElement,
8
+ type CSSProperties,
9
+ type HTMLAttributes,
10
+ type ReactElement,
11
+ type ReactNode,
12
+ } from 'react';
13
+ import type { AvatarSize } from './Avatar';
14
+ import type { DataAttributes } from '../../../dom-props';
15
+ import { cx } from '../../internal/utils/cx';
16
+
17
+ interface AvatarGroupOwnProps {
18
+ /** The `Avatar` elements to stack; each is cloned to force `size`, then sliced to `max`. */
19
+ children: ReactNode;
20
+ /** Max visible avatars before "+N" overflow chip. Default 5. */
21
+ max?: number;
22
+ /** Uniform size applied to all children. Default 'md'. */
23
+ size?: AvatarSize;
24
+ /** Extra class(es) merged onto the group wrapper. */
25
+ className?: string;
26
+ /** Inline styles merged onto the group wrapper. */
27
+ style?: CSSProperties;
28
+ }
29
+
30
+ export interface AvatarGroupProps extends AvatarGroupOwnProps {
31
+ /** Standard <span> attributes forwarded to the group wrapper. */
32
+ htmlProps?: Omit<HTMLAttributes<HTMLSpanElement>, keyof AvatarGroupOwnProps> & DataAttributes;
33
+ }
34
+
35
+ export function AvatarGroup({ children, max = 5, size = 'md', className = '', style, htmlProps }: AvatarGroupProps) {
36
+ const items = Children.toArray(children);
37
+ const visible = max > 0 ? items.slice(0, max) : items;
38
+ const overflow = items.length - visible.length;
39
+
40
+ return (
41
+ <span className={cx('avatar-group', className)} style={style} {...htmlProps}>
42
+ {visible.map((child, i) =>
43
+ isValidElement(child)
44
+ ? cloneElement(child as ReactElement<{ size?: AvatarSize }>, { size, key: child.key ?? i })
45
+ : child,
46
+ )}
47
+ {overflow > 0 && (
48
+ <span className={`avatar avatar--${size} avatar--overflow`} role="img" aria-label={`${overflow} more`}>
49
+ <span className="avatar__face">
50
+ <span className="avatar__initials">+{overflow}</span>
51
+ </span>
52
+ </span>
53
+ )}
54
+ </span>
55
+ );
56
+ }
@@ -0,0 +1,200 @@
1
+ /* Avatar (image - initials - icon) + AvatarGroup - face/status structure, token sizes, 6 identity slots. */
2
+
3
+ .avatar {
4
+ --avatar-size: 2rem;
5
+ --avatar-bg: var(--bg-muted);
6
+ --avatar-fg: var(--text-muted);
7
+ /* --avatar-ring: injected by .avatar-group, cascades to .avatar__face */
8
+
9
+ position: relative;
10
+ display: inline-flex;
11
+ flex: none;
12
+ vertical-align: middle;
13
+ width: var(--avatar-size);
14
+ height: var(--avatar-size);
15
+ }
16
+
17
+ .avatar__face {
18
+ width: 100%;
19
+ height: 100%;
20
+ border-radius: var(--radius-full);
21
+ overflow: hidden;
22
+
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+
27
+ background: var(--avatar-bg);
28
+ color: var(--avatar-fg);
29
+
30
+ /* sheen from top, shade from bottom over the slot tint; an image child paints over both (section B3 gradient exception) */
31
+ background:
32
+ linear-gradient(to bottom, var(--avatar-sheen), transparent 55%),
33
+ linear-gradient(to top, var(--avatar-shade), transparent 45%), var(--avatar-bg);
34
+
35
+ /* Crisp 1px edge-catch + optional group separator ring via cascade */
36
+ box-shadow:
37
+ inset 0 1px 0 0 oklch(1 0 0 / 0.5),
38
+ var(--avatar-ring, none);
39
+
40
+ /* keyline keyed to the slot's own hue (fg at low alpha), not uniform black; outline paints above children so it rings photos too */
41
+ outline: var(--border-hairline) solid color-mix(in oklch, var(--avatar-fg) 18%, transparent);
42
+ outline-offset: -1px;
43
+
44
+ transition: box-shadow var(--duration-fast) var(--ease-standard);
45
+ }
46
+
47
+ .avatar__img {
48
+ display: block;
49
+ width: 100%;
50
+ height: 100%;
51
+ object-fit: cover;
52
+ }
53
+
54
+ /* lettermark, ratio-sized (section A7 exception); text-box trim = true optical centering; negative margin returns the trailing --tracking-caps so the pair stays centered */
55
+ .avatar__initials {
56
+ font-family: var(--font-sans);
57
+ font-weight: var(--weight-semibold);
58
+ font-size: calc(var(--avatar-size) * 0.42);
59
+ line-height: 1;
60
+ text-box: trim-both cap alphabetic;
61
+ letter-spacing: var(--tracking-caps);
62
+ margin-right: calc(-1 * var(--tracking-caps));
63
+ text-transform: uppercase;
64
+ user-select: none;
65
+ -webkit-user-select: none;
66
+ }
67
+
68
+ .avatar__icon {
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ width: 56%;
73
+ height: 56%;
74
+ opacity: 0.5;
75
+ }
76
+ .avatar__icon svg {
77
+ display: block;
78
+ width: 100%;
79
+ height: 100%;
80
+ }
81
+
82
+ .avatar--xs {
83
+ --avatar-size: 1.25rem;
84
+ }
85
+ .avatar--sm {
86
+ --avatar-size: 1.5rem;
87
+ }
88
+ .avatar--lg {
89
+ --avatar-size: 2.5rem;
90
+ }
91
+ .avatar--xl {
92
+ --avatar-size: 3rem;
93
+ }
94
+
95
+ /* square - radius scales with size (concentric nesting) */
96
+ .avatar--square .avatar__face {
97
+ border-radius: var(--radius-md);
98
+ }
99
+
100
+ .avatar--square.avatar--lg .avatar__face,
101
+ .avatar--square.avatar--xl .avatar__face {
102
+ border-radius: var(--radius-lg);
103
+ }
104
+
105
+ /* data-palette 1-6: identity slots (tokens/avatar.css); no attr = neutral; identity colouring, never status */
106
+
107
+ .avatar[data-palette='1'] {
108
+ --avatar-bg: var(--avatar-1-bg);
109
+ --avatar-fg: var(--avatar-1-fg);
110
+ }
111
+ .avatar[data-palette='2'] {
112
+ --avatar-bg: var(--avatar-2-bg);
113
+ --avatar-fg: var(--avatar-2-fg);
114
+ }
115
+ .avatar[data-palette='3'] {
116
+ --avatar-bg: var(--avatar-3-bg);
117
+ --avatar-fg: var(--avatar-3-fg);
118
+ }
119
+ .avatar[data-palette='4'] {
120
+ --avatar-bg: var(--avatar-4-bg);
121
+ --avatar-fg: var(--avatar-4-fg);
122
+ }
123
+ .avatar[data-palette='5'] {
124
+ --avatar-bg: var(--avatar-5-bg);
125
+ --avatar-fg: var(--avatar-5-fg);
126
+ }
127
+ .avatar[data-palette='6'] {
128
+ --avatar-bg: var(--avatar-6-bg);
129
+ --avatar-fg: var(--avatar-6-fg);
130
+ }
131
+
132
+ /* status dot lives outside .avatar__face (which clips) so it's never cut; ~27% of size, min 6px */
133
+
134
+ .avatar__status {
135
+ position: absolute;
136
+ bottom: 0;
137
+ right: 0;
138
+
139
+ width: calc(var(--avatar-size) * 0.275);
140
+ height: calc(var(--avatar-size) * 0.275);
141
+ min-width: 6px;
142
+ min-height: 6px;
143
+
144
+ border-radius: var(--radius-full);
145
+ box-shadow: 0 0 0 2px var(--bg-surface);
146
+ transition: background-color var(--duration-fast) var(--ease-standard);
147
+ }
148
+
149
+ .avatar__status--online {
150
+ background: var(--success);
151
+ }
152
+ .avatar__status--away {
153
+ background: var(--warning);
154
+ }
155
+ .avatar__status--busy {
156
+ background: var(--danger);
157
+ }
158
+ .avatar__status--offline {
159
+ background: var(--border-strong);
160
+ }
161
+
162
+ .avatar-group {
163
+ display: inline-flex;
164
+ align-items: center;
165
+ }
166
+
167
+ /* inject --avatar-ring on group children - cascades to .avatar__face */
168
+ .avatar-group > .avatar {
169
+ --avatar-ring: 0 0 0 2px var(--bg-surface);
170
+ }
171
+
172
+ .avatar-group > .avatar + .avatar {
173
+ margin-left: calc(var(--avatar-size) * -0.3);
174
+ transition: margin-left var(--duration-base) var(--ease-entrance);
175
+ }
176
+
177
+ .avatar-group:hover > .avatar + .avatar {
178
+ margin-left: var(--space-1);
179
+ }
180
+
181
+ /* "+N" chip - mono numerals, tracking off (mono is already spaced), smaller ratio to fit "+12" */
182
+ .avatar--overflow .avatar__initials {
183
+ font-family: var(--font-mono);
184
+ font-variant-numeric: tabular-nums;
185
+ font-size: calc(var(--avatar-size) * 0.36);
186
+ letter-spacing: var(--tracking-normal);
187
+ margin-right: 0;
188
+ }
189
+
190
+ @media (prefers-reduced-motion: reduce) {
191
+ .avatar__face {
192
+ transition: none;
193
+ }
194
+ .avatar__status {
195
+ transition: none;
196
+ }
197
+ .avatar-group > .avatar + .avatar {
198
+ transition: none;
199
+ }
200
+ }
@@ -0,0 +1,75 @@
1
+ 'use client';
2
+
3
+ import './badge.css';
4
+ import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
5
+ import type { DataAttributes } from '../../../dom-props';
6
+ import { cx } from '../../internal/utils/cx';
7
+
8
+ export type BadgeTone = 'neutral' | 'info' | 'success' | 'warning' | 'danger';
9
+
10
+ interface BadgeOwnProps {
11
+ /** Status hue. @default 'neutral' */
12
+ tone?: BadgeTone;
13
+ /** Surface. @default 'glass' */
14
+ variant?: 'glass' | 'outline';
15
+ /** @default 'md' */
16
+ size?: 'sm' | 'md';
17
+ /** Leading status dot. */
18
+ dot?: boolean;
19
+ /** Dot pulses (implies dot) - for in-progress status. */
20
+ live?: boolean;
21
+ /** Fully-rounded shape. */
22
+ pill?: boolean;
23
+ /** Optional leading <Icon> (overrides dot if both set). */
24
+ icon?: ReactNode;
25
+ /** Extra class(es) merged onto the chip. */
26
+ className?: string;
27
+ /** Inline styles merged onto the chip. */
28
+ style?: CSSProperties;
29
+ /** Chip label. */
30
+ children?: ReactNode;
31
+ }
32
+
33
+ export interface BadgeProps extends BadgeOwnProps {
34
+ /** Standard <span> attributes (aria-*, data-*, title, ...) forwarded to the chip. */
35
+ htmlProps?: Omit<HTMLAttributes<HTMLSpanElement>, keyof BadgeOwnProps> & DataAttributes;
36
+ }
37
+
38
+ export function Badge({
39
+ tone = 'neutral',
40
+ variant = 'glass',
41
+ size = 'md',
42
+ dot = false,
43
+ live = false,
44
+ pill = false,
45
+ icon = null,
46
+ className = '',
47
+ style,
48
+ children,
49
+ htmlProps,
50
+ }: BadgeProps) {
51
+ const isGlass = variant !== 'outline';
52
+ const cls = cx(
53
+ isGlass && 'glass glass--interactive',
54
+ 'badge',
55
+ `badge--${tone}`,
56
+ variant === 'outline' && 'badge--outline',
57
+ size !== 'md' && `badge--${size}`,
58
+ pill && 'badge--pill',
59
+ className,
60
+ );
61
+
62
+ const showDot = (dot || live) && !icon;
63
+
64
+ return (
65
+ <span className={cls} style={style} {...htmlProps}>
66
+ {showDot ? <span className={`badge__dot${live ? ' badge__dot--live' : ''}`} aria-hidden="true" /> : null}
67
+ {icon ? (
68
+ <span className="badge__icon" aria-hidden="true">
69
+ {icon}
70
+ </span>
71
+ ) : null}
72
+ <span className="badge__label">{children}</span>
73
+ </span>
74
+ );
75
+ }
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+
3
+ import { Badge, type BadgeProps } from './Badge';
4
+ import { Odometer } from './odometer';
5
+
6
+ export interface CountBadgeProps extends Omit<BadgeProps, 'children'> {
7
+ /** The chip content - stringified; in `roll` mode each digit becomes an odometer column. */
8
+ value: string | number;
9
+ /** Odometer: digits roll vertically on change. */
10
+ roll?: boolean;
11
+ }
12
+
13
+ export function CountBadge({ value, tone = 'neutral', roll = false, ...rest }: CountBadgeProps) {
14
+ const str = String(value);
15
+ return (
16
+ <Badge tone={tone} {...rest}>
17
+ {roll ? <Odometer value={str} /> : str}
18
+ </Badge>
19
+ );
20
+ }
@@ -0,0 +1,111 @@
1
+ 'use client';
2
+
3
+ import './badge.css';
4
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
5
+ import { Badge, type BadgeProps } from './Badge';
6
+ import { UIMotion } from '../../../tokens/motion-tokens';
7
+ import { fireGlint } from '../../internal/glass/glint';
8
+ import { cx } from '../../internal/utils/cx';
9
+
10
+ const SM = UIMotion;
11
+
12
+ export type PostStatus = 'draft' | 'scheduled' | 'processing' | 'published' | 'failed';
13
+
14
+ export const POST_STATUS: Record<PostStatus, { tone: BadgeProps['tone']; label: string; live?: boolean }> = {
15
+ draft: { tone: 'neutral', label: 'Draft' },
16
+ scheduled: { tone: 'info', label: 'Scheduled' },
17
+ processing: { tone: 'warning', label: 'Processing', live: true },
18
+ published: { tone: 'success', label: 'Published' },
19
+ failed: { tone: 'danger', label: 'Failed' },
20
+ };
21
+
22
+ const TERMINAL: Partial<Record<PostStatus, boolean>> = { published: true, failed: true };
23
+
24
+ export interface StatusBadgeProps extends Omit<BadgeProps, 'children' | 'tone'> {
25
+ /** Lifecycle state - maps to a canonical `tone` + label (`processing` also pulses live). Unknown falls back to `draft`. */
26
+ status: PostStatus;
27
+ /** Morph in place as `status` changes instead of swapping. */
28
+ morph?: boolean;
29
+ }
30
+
31
+ export function StatusBadge({ status, morph = false, ...rest }: StatusBadgeProps) {
32
+ if (!morph) {
33
+ const s = POST_STATUS[status] || POST_STATUS.draft;
34
+ return (
35
+ <Badge tone={s.tone} dot live={!!s.live} {...rest}>
36
+ {s.label}
37
+ </Badge>
38
+ );
39
+ }
40
+ return <StatusMorph status={status} {...rest} />;
41
+ }
42
+
43
+ interface StatusMorphProps extends Omit<BadgeProps, 'children' | 'tone'> {
44
+ status: PostStatus;
45
+ }
46
+
47
+ interface Word {
48
+ key: number;
49
+ label: string;
50
+ cls: string;
51
+ }
52
+
53
+ function StatusMorph({ status, className = '', ...rest }: StatusMorphProps) {
54
+ const s = POST_STATUS[status] || POST_STATUS.draft;
55
+
56
+ const prev = useRef(status);
57
+ const keyRef = useRef(1);
58
+ const labelRef = useRef<HTMLSpanElement>(null);
59
+ const ghostRef = useRef<HTMLSpanElement>(null);
60
+ const [words, setWords] = useState<Word[]>([{ key: 0, label: s.label, cls: '' }]);
61
+ const [boxW, setBoxW] = useState<number>();
62
+
63
+ useLayoutEffect(() => {
64
+ if (ghostRef.current) setBoxW(ghostRef.current.offsetWidth);
65
+ }, []);
66
+
67
+ useEffect(() => {
68
+ if (prev.current === status) return;
69
+ prev.current = status;
70
+ const next = POST_STATUS[status] || POST_STATUS.draft;
71
+ const nk = keyRef.current++;
72
+ const nextW = ghostRef.current?.offsetWidth;
73
+
74
+ setWords((ws) => ws.concat({ key: nk, label: next.label, cls: 'badge__word--in' }));
75
+ const raf = requestAnimationFrame(() =>
76
+ requestAnimationFrame(() => {
77
+ setWords((ws) => ws.map((w) => (w.key === nk ? { ...w, cls: '' } : { ...w, cls: 'badge__word--out' })));
78
+ if (nextW) setBoxW(nextW);
79
+ }),
80
+ );
81
+ const drop = setTimeout(
82
+ () => setWords((ws) => ws.filter((w) => !w.cls.includes('--out'))),
83
+ SM.dur.base * 1000 + 80,
84
+ );
85
+ let stopGlint: (() => void) | undefined;
86
+ if (TERMINAL[status] && labelRef.current) {
87
+ const chip = labelRef.current.closest('.badge') as HTMLElement | null;
88
+ if (chip) stopGlint = fireGlint(chip);
89
+ }
90
+ return () => {
91
+ cancelAnimationFrame(raf);
92
+ clearTimeout(drop);
93
+ stopGlint?.();
94
+ };
95
+ }, [status]);
96
+
97
+ return (
98
+ <Badge tone={s.tone} dot live={!!s.live} className={cx('badge--morph', className)} {...rest}>
99
+ <span className="badge__morph" ref={labelRef} style={boxW ? { width: boxW } : undefined}>
100
+ <span className="badge__ghost" ref={ghostRef} aria-hidden="true">
101
+ {s.label}
102
+ </span>
103
+ {words.map((w) => (
104
+ <span key={w.key} className={cx('badge__word', w.cls)}>
105
+ {w.label}
106
+ </span>
107
+ ))}
108
+ </span>
109
+ </Badge>
110
+ );
111
+ }