premium-ds 0.1.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 (257) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +113 -0
  3. package/dist/alert.d.ts +31 -0
  4. package/dist/alert.js +6 -0
  5. package/dist/alert.js.map +1 -0
  6. package/dist/avatar-group.d.ts +13 -0
  7. package/dist/avatar-group.js +3 -0
  8. package/dist/avatar-group.js.map +1 -0
  9. package/dist/avatar.d.ts +25 -0
  10. package/dist/avatar.js +3 -0
  11. package/dist/avatar.js.map +1 -0
  12. package/dist/badge.d.ts +23 -0
  13. package/dist/badge.js +3 -0
  14. package/dist/badge.js.map +1 -0
  15. package/dist/button.d.ts +20 -0
  16. package/dist/button.js +3 -0
  17. package/dist/button.js.map +1 -0
  18. package/dist/checkbox.d.ts +25 -0
  19. package/dist/checkbox.js +3 -0
  20. package/dist/checkbox.js.map +1 -0
  21. package/dist/chunk-2OWHZ4JT.js +36 -0
  22. package/dist/chunk-2OWHZ4JT.js.map +1 -0
  23. package/dist/chunk-34SIXSYL.js +64 -0
  24. package/dist/chunk-34SIXSYL.js.map +1 -0
  25. package/dist/chunk-37O2ZXD6.js +55 -0
  26. package/dist/chunk-37O2ZXD6.js.map +1 -0
  27. package/dist/chunk-4AZL76UJ.js +89 -0
  28. package/dist/chunk-4AZL76UJ.js.map +1 -0
  29. package/dist/chunk-4HSCN5TZ.js +86 -0
  30. package/dist/chunk-4HSCN5TZ.js.map +1 -0
  31. package/dist/chunk-5DDOOT33.js +258 -0
  32. package/dist/chunk-5DDOOT33.js.map +1 -0
  33. package/dist/chunk-5FVHWIMY.js +117 -0
  34. package/dist/chunk-5FVHWIMY.js.map +1 -0
  35. package/dist/chunk-5K6KRJGX.js +147 -0
  36. package/dist/chunk-5K6KRJGX.js.map +1 -0
  37. package/dist/chunk-5PQMQBQC.js +74 -0
  38. package/dist/chunk-5PQMQBQC.js.map +1 -0
  39. package/dist/chunk-7OCTVQ7C.js +95 -0
  40. package/dist/chunk-7OCTVQ7C.js.map +1 -0
  41. package/dist/chunk-7OPMOET7.js +39 -0
  42. package/dist/chunk-7OPMOET7.js.map +1 -0
  43. package/dist/chunk-BXXS7YRC.js +270 -0
  44. package/dist/chunk-BXXS7YRC.js.map +1 -0
  45. package/dist/chunk-CV2Q4YXX.js +272 -0
  46. package/dist/chunk-CV2Q4YXX.js.map +1 -0
  47. package/dist/chunk-EIMMDWIW.js +282 -0
  48. package/dist/chunk-EIMMDWIW.js.map +1 -0
  49. package/dist/chunk-EZ2CWTBE.js +230 -0
  50. package/dist/chunk-EZ2CWTBE.js.map +1 -0
  51. package/dist/chunk-FGHDG3Y4.js +89 -0
  52. package/dist/chunk-FGHDG3Y4.js.map +1 -0
  53. package/dist/chunk-FPP2XLKX.js +127 -0
  54. package/dist/chunk-FPP2XLKX.js.map +1 -0
  55. package/dist/chunk-G6OY35DI.js +295 -0
  56. package/dist/chunk-G6OY35DI.js.map +1 -0
  57. package/dist/chunk-H6KWJNOE.js +65 -0
  58. package/dist/chunk-H6KWJNOE.js.map +1 -0
  59. package/dist/chunk-HGILYGY3.js +45 -0
  60. package/dist/chunk-HGILYGY3.js.map +1 -0
  61. package/dist/chunk-I3BCB4Z5.js +88 -0
  62. package/dist/chunk-I3BCB4Z5.js.map +1 -0
  63. package/dist/chunk-KBWNUUWM.js +582 -0
  64. package/dist/chunk-KBWNUUWM.js.map +1 -0
  65. package/dist/chunk-KN7JFAZ6.js +113 -0
  66. package/dist/chunk-KN7JFAZ6.js.map +1 -0
  67. package/dist/chunk-MEF7PI6U.js +16 -0
  68. package/dist/chunk-MEF7PI6U.js.map +1 -0
  69. package/dist/chunk-NKGMQL6I.js +310 -0
  70. package/dist/chunk-NKGMQL6I.js.map +1 -0
  71. package/dist/chunk-NMFQRGLL.js +127 -0
  72. package/dist/chunk-NMFQRGLL.js.map +1 -0
  73. package/dist/chunk-OUBWD6CX.js +433 -0
  74. package/dist/chunk-OUBWD6CX.js.map +1 -0
  75. package/dist/chunk-PFNXVBLU.js +96 -0
  76. package/dist/chunk-PFNXVBLU.js.map +1 -0
  77. package/dist/chunk-PUPZ4HME.js +165 -0
  78. package/dist/chunk-PUPZ4HME.js.map +1 -0
  79. package/dist/chunk-QFS52OK5.js +690 -0
  80. package/dist/chunk-QFS52OK5.js.map +1 -0
  81. package/dist/chunk-QNC6O3PG.js +45 -0
  82. package/dist/chunk-QNC6O3PG.js.map +1 -0
  83. package/dist/chunk-QUHOXWBK.js +82 -0
  84. package/dist/chunk-QUHOXWBK.js.map +1 -0
  85. package/dist/chunk-UIQGSTBJ.js +106 -0
  86. package/dist/chunk-UIQGSTBJ.js.map +1 -0
  87. package/dist/chunk-UJQKVP6V.js +193 -0
  88. package/dist/chunk-UJQKVP6V.js.map +1 -0
  89. package/dist/chunk-VVPGEAC6.js +11 -0
  90. package/dist/chunk-VVPGEAC6.js.map +1 -0
  91. package/dist/chunk-XA3T5KWA.js +58 -0
  92. package/dist/chunk-XA3T5KWA.js.map +1 -0
  93. package/dist/chunk-YSHJHSJM.js +19 -0
  94. package/dist/chunk-YSHJHSJM.js.map +1 -0
  95. package/dist/chunk-YVHOAVSM.js +182 -0
  96. package/dist/chunk-YVHOAVSM.js.map +1 -0
  97. package/dist/collapse.d.ts +16 -0
  98. package/dist/collapse.js +3 -0
  99. package/dist/collapse.js.map +1 -0
  100. package/dist/count-badge.d.ts +11 -0
  101. package/dist/count-badge.js +4 -0
  102. package/dist/count-badge.js.map +1 -0
  103. package/dist/date-field.d.ts +39 -0
  104. package/dist/date-field.js +8 -0
  105. package/dist/date-field.js.map +1 -0
  106. package/dist/date-range-field.d.ts +30 -0
  107. package/dist/date-range-field.js +8 -0
  108. package/dist/date-range-field.js.map +1 -0
  109. package/dist/datetime-field.d.ts +28 -0
  110. package/dist/datetime-field.js +10 -0
  111. package/dist/datetime-field.js.map +1 -0
  112. package/dist/dialog.d.ts +26 -0
  113. package/dist/dialog.js +7 -0
  114. package/dist/dialog.js.map +1 -0
  115. package/dist/index.d.ts +35 -0
  116. package/dist/index.js +40 -0
  117. package/dist/index.js.map +1 -0
  118. package/dist/motion-tokens.d.ts +29 -0
  119. package/dist/motion-tokens.js +3 -0
  120. package/dist/motion-tokens.js.map +1 -0
  121. package/dist/multi-select.d.ts +25 -0
  122. package/dist/multi-select.js +7 -0
  123. package/dist/multi-select.js.map +1 -0
  124. package/dist/number-field.d.ts +24 -0
  125. package/dist/number-field.js +4 -0
  126. package/dist/number-field.js.map +1 -0
  127. package/dist/otp-field.d.ts +20 -0
  128. package/dist/otp-field.js +3 -0
  129. package/dist/otp-field.js.map +1 -0
  130. package/dist/overlay.d.ts +31 -0
  131. package/dist/overlay.js +4 -0
  132. package/dist/overlay.js.map +1 -0
  133. package/dist/pagination.d.ts +24 -0
  134. package/dist/pagination.js +5 -0
  135. package/dist/pagination.js.map +1 -0
  136. package/dist/radio-group.d.ts +46 -0
  137. package/dist/radio-group.js +6 -0
  138. package/dist/radio-group.js.map +1 -0
  139. package/dist/select-core-SAyS-8w0.d.ts +16 -0
  140. package/dist/select.d.ts +27 -0
  141. package/dist/select.js +7 -0
  142. package/dist/select.js.map +1 -0
  143. package/dist/status-badge.d.ts +17 -0
  144. package/dist/status-badge.js +5 -0
  145. package/dist/status-badge.js.map +1 -0
  146. package/dist/table.d.ts +65 -0
  147. package/dist/table.js +5 -0
  148. package/dist/table.js.map +1 -0
  149. package/dist/tabs.d.ts +44 -0
  150. package/dist/tabs.js +5 -0
  151. package/dist/tabs.js.map +1 -0
  152. package/dist/tag.d.ts +28 -0
  153. package/dist/tag.js +5 -0
  154. package/dist/tag.js.map +1 -0
  155. package/dist/text-field.d.ts +30 -0
  156. package/dist/text-field.js +6 -0
  157. package/dist/text-field.js.map +1 -0
  158. package/dist/textarea.d.ts +33 -0
  159. package/dist/textarea.js +5 -0
  160. package/dist/textarea.js.map +1 -0
  161. package/dist/time-field.d.ts +27 -0
  162. package/dist/time-field.js +6 -0
  163. package/dist/time-field.js.map +1 -0
  164. package/dist/toast-store.d.ts +75 -0
  165. package/dist/toast-store.js +3 -0
  166. package/dist/toast-store.js.map +1 -0
  167. package/dist/toast.d.ts +3 -0
  168. package/dist/toast.js +6 -0
  169. package/dist/toast.js.map +1 -0
  170. package/dist/toggle-tag.d.ts +24 -0
  171. package/dist/toggle-tag.js +4 -0
  172. package/dist/toggle-tag.js.map +1 -0
  173. package/dist/toggle.d.ts +21 -0
  174. package/dist/toggle.js +3 -0
  175. package/dist/toggle.js.map +1 -0
  176. package/dist/tooltip.d.ts +27 -0
  177. package/dist/tooltip.js +4 -0
  178. package/dist/tooltip.js.map +1 -0
  179. package/llms.txt +165 -0
  180. package/package.json +205 -0
  181. package/src/components/alert/Alert.tsx +118 -0
  182. package/src/components/alert/alert.css +136 -0
  183. package/src/components/avatar/Avatar.tsx +128 -0
  184. package/src/components/avatar/AvatarGroup.tsx +50 -0
  185. package/src/components/avatar/avatar.css +200 -0
  186. package/src/components/badge/Badge.tsx +66 -0
  187. package/src/components/badge/CountBadge.tsx +46 -0
  188. package/src/components/badge/StatusBadge.tsx +132 -0
  189. package/src/components/badge/badge.css +243 -0
  190. package/src/components/button/Button.tsx +68 -0
  191. package/src/components/button/button.css +222 -0
  192. package/src/components/checkbox/Checkbox.tsx +90 -0
  193. package/src/components/checkbox/checkbox.css +179 -0
  194. package/src/components/date-picker/DateField.tsx +362 -0
  195. package/src/components/date-picker/DateRangeField.tsx +533 -0
  196. package/src/components/date-picker/DateTimeField.tsx +177 -0
  197. package/src/components/date-picker/TimeField.tsx +100 -0
  198. package/src/components/date-picker/date-picker.css +591 -0
  199. package/src/components/date-picker/date-utils.ts +55 -0
  200. package/src/components/date-picker/field-shell.tsx +78 -0
  201. package/src/components/date-picker/glide-pill.tsx +81 -0
  202. package/src/components/date-picker/time-core.tsx +305 -0
  203. package/src/components/dialog/Dialog.tsx +181 -0
  204. package/src/components/dialog/dialog.css +170 -0
  205. package/src/components/glass/glass.css +100 -0
  206. package/src/components/icon/Icon.tsx +76 -0
  207. package/src/components/icon/IconSlot.tsx +11 -0
  208. package/src/components/icon/icon.css +33 -0
  209. package/src/components/input/NumberField.tsx +117 -0
  210. package/src/components/input/OtpField.tsx +118 -0
  211. package/src/components/input/TextField.tsx +123 -0
  212. package/src/components/input/input.css +335 -0
  213. package/src/components/motion/Collapse.tsx +33 -0
  214. package/src/components/motion/collapse.css +41 -0
  215. package/src/components/overlay/Overlay.tsx +239 -0
  216. package/src/components/overlay/overlay-core.tsx +565 -0
  217. package/src/components/overlay/overlay.css +119 -0
  218. package/src/components/overlay/sheet-drag.tsx +146 -0
  219. package/src/components/pagination/Pagination.tsx +140 -0
  220. package/src/components/pagination/pagination.css +48 -0
  221. package/src/components/radio-group/RadioGroup.tsx +182 -0
  222. package/src/components/radio-group/radio-group.css +277 -0
  223. package/src/components/select/MultiSelect.tsx +251 -0
  224. package/src/components/select/Select.tsx +235 -0
  225. package/src/components/select/select-core.tsx +417 -0
  226. package/src/components/select/select.css +386 -0
  227. package/src/components/table/Table.tsx +433 -0
  228. package/src/components/table/table.css +348 -0
  229. package/src/components/tabs/Tabs.tsx +371 -0
  230. package/src/components/tabs/tabs.css +228 -0
  231. package/src/components/tag/Tag.tsx +145 -0
  232. package/src/components/tag/ToggleTag.tsx +125 -0
  233. package/src/components/tag/tag.css +248 -0
  234. package/src/components/textarea/Textarea.tsx +197 -0
  235. package/src/components/textarea/textarea.css +219 -0
  236. package/src/components/toast/Toast.tsx +349 -0
  237. package/src/components/toast/toast-store.ts +266 -0
  238. package/src/components/toast/toast.css +233 -0
  239. package/src/components/toggle/Toggle.tsx +94 -0
  240. package/src/components/toggle/toggle.css +152 -0
  241. package/src/components/tooltip/Tooltip.tsx +365 -0
  242. package/src/components/tooltip/tooltip.css +86 -0
  243. package/src/index.ts +42 -0
  244. package/src/styles.css +39 -0
  245. package/src/tokens/avatar.css +20 -0
  246. package/src/tokens/color.css +56 -0
  247. package/src/tokens/elevation.css +20 -0
  248. package/src/tokens/fonts.css +3 -0
  249. package/src/tokens/glass.css +21 -0
  250. package/src/tokens/icons.css +7 -0
  251. package/src/tokens/layers.css +6 -0
  252. package/src/tokens/motion-tokens.ts +72 -0
  253. package/src/tokens/motion.css +49 -0
  254. package/src/tokens/radius.css +11 -0
  255. package/src/tokens/semantic.css +75 -0
  256. package/src/tokens/spacing.css +26 -0
  257. package/src/tokens/typography.css +54 -0
@@ -0,0 +1,118 @@
1
+ 'use client';
2
+
3
+ /* Alert - the persistent, in-flow status message (banner is a paint modifier). */
4
+
5
+ import * as React from 'react';
6
+ import { motion, AnimatePresence } from 'motion/react';
7
+ import { UIMotion } from '../../tokens/motion-tokens';
8
+ import { Icon, type IconName } from '../icon/Icon';
9
+ import { IconSlot } from '../icon/IconSlot';
10
+
11
+ export type AlertTone = 'info' | 'success' | 'warning' | 'danger';
12
+
13
+ export interface AlertAction {
14
+ /** Sentence-case label, e.g. "Update billing". */
15
+ label: string;
16
+ onClick?: () => void;
17
+ }
18
+
19
+ export interface AlertProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
20
+ /** Status of the message; info/success are polite, warning/danger assertive. Default 'info'. */
21
+ tone?: AlertTone;
22
+ /** The message - sentence case, ideally one line. */
23
+ title: React.ReactNode;
24
+ /** Optional description; stays neutral - tone marks the message, not the prose. */
25
+ children?: React.ReactNode;
26
+ /** One action max, rendered as the system secondary small button. */
27
+ action?: AlertAction;
28
+ /** Renders the always-visible close button. Uncontrolled unless `open` is given. */
29
+ dismissible?: boolean;
30
+ /** Fires on dismiss. With `open` set, the parent owns hiding the alert. */
31
+ onDismiss?: () => void;
32
+ /** Controlled visibility; omit for uncontrolled. Exit eases shut (height collapse). */
33
+ open?: boolean;
34
+ /** App-level strip: square corners, hairline below only. Paint modifier. */
35
+ banner?: boolean;
36
+ /** Override the tone glyph; pass null to render no glyph. */
37
+ icon?: React.ReactNode | null;
38
+ }
39
+
40
+ const { t } = UIMotion;
41
+
42
+ const TONE_GLYPH: Record<AlertTone, IconName> = {
43
+ info: 'info',
44
+ success: 'check-circle',
45
+ warning: 'warning',
46
+ danger: 'warning-circle',
47
+ };
48
+ const TONE_ROLE: Record<AlertTone, string> = {
49
+ info: 'status',
50
+ success: 'status',
51
+ warning: 'alert',
52
+ danger: 'alert',
53
+ };
54
+
55
+ export function Alert({
56
+ tone = 'info',
57
+ title,
58
+ children = null,
59
+ action = null,
60
+ dismissible = false,
61
+ onDismiss,
62
+ open,
63
+ banner = false,
64
+ icon,
65
+ className = '',
66
+ ...rest
67
+ }: AlertProps) {
68
+ const [selfOpen, setSelfOpen] = React.useState(true);
69
+ const isOpen = open === undefined ? selfOpen : open;
70
+
71
+ const dismiss = () => {
72
+ if (open === undefined) setSelfOpen(false);
73
+ if (onDismiss) onDismiss();
74
+ };
75
+
76
+ const classes = ['alert', banner ? 'alert--banner' : '', className].filter(Boolean).join(' ');
77
+
78
+ return (
79
+ <AnimatePresence initial={false}>
80
+ {isOpen && (
81
+ <motion.div
82
+ key="alert-shell" /* explicit key - AnimatePresence strands un-keyed conditional children */
83
+ className="alert-shell"
84
+ initial={{ height: 0, opacity: 0 }}
85
+ animate={{ height: 'auto', opacity: 1 }}
86
+ exit={{ height: 0, opacity: 0, transition: { height: t.layout, opacity: t.exit } }}
87
+ transition={{ height: t.layout, opacity: t.enter }}
88
+ >
89
+ <div className={classes} data-tone={tone} role={TONE_ROLE[tone] || 'status'} {...rest}>
90
+ {icon === null ? null : (
91
+ <span className="alert__icon" aria-hidden="true">
92
+ {icon !== undefined ? (
93
+ <IconSlot size="md">{icon}</IconSlot>
94
+ ) : (
95
+ <Icon name={TONE_GLYPH[tone] || 'info'} size="md" />
96
+ )}
97
+ </span>
98
+ )}
99
+ <div className="alert__body">
100
+ <p className="alert__title">{title}</p>
101
+ {children != null && <p className="alert__desc">{children}</p>}
102
+ </div>
103
+ {action && (
104
+ <button type="button" className="btn btn--sm alert__action" onClick={action.onClick}>
105
+ {action.label}
106
+ </button>
107
+ )}
108
+ {dismissible && (
109
+ <button type="button" className="alert__close" aria-label="Dismiss" onClick={dismiss}>
110
+ <Icon name="close" size="sm" />
111
+ </button>
112
+ )}
113
+ </div>
114
+ </motion.div>
115
+ )}
116
+ </AnimatePresence>
117
+ );
118
+ }
@@ -0,0 +1,136 @@
1
+ /* alert.css - the persistent in-flow status message: a lit tinted pane per tone. */
2
+
3
+ .alert-shell {
4
+ overflow: hidden;
5
+ }
6
+
7
+ .alert {
8
+ /* tone hooks - repointed by [data-tone] below; info is the default */
9
+ --alert-hue: var(--info);
10
+ --alert-fg: var(--info-text);
11
+ --alert-bg: var(--info-subtle);
12
+ --alert-wash: var(--accent-wash); /* info's wash = accent (teal) */
13
+
14
+ box-sizing: border-box;
15
+ display: flex;
16
+ align-items: center;
17
+ gap: var(--space-3);
18
+ padding: var(--space-3) var(--space-4);
19
+ /* the lit pane: top-light gradient over the tint + 1px inner highlight (Button's lighting model) */
20
+ background: linear-gradient(
21
+ 180deg,
22
+ color-mix(in oklab, var(--bg-surface) 45%, var(--alert-bg)),
23
+ var(--alert-bg)
24
+ );
25
+ border: var(--border-hairline) solid color-mix(in oklab, var(--alert-hue) 30%, transparent);
26
+ border-radius: var(--radius-lg);
27
+ box-shadow: inset 0 1px 0 0 rgb(255 255 255 / 0.55);
28
+ }
29
+
30
+ .alert[data-tone='success'] {
31
+ --alert-hue: var(--success);
32
+ --alert-fg: var(--success-text);
33
+ --alert-bg: var(--success-subtle);
34
+ --alert-wash: var(--success-wash);
35
+ }
36
+ .alert[data-tone='warning'] {
37
+ --alert-hue: var(--warning);
38
+ --alert-fg: var(--warning-text);
39
+ --alert-bg: var(--warning-subtle);
40
+ --alert-wash: var(--warning-wash);
41
+ }
42
+ .alert[data-tone='danger'] {
43
+ --alert-hue: var(--danger);
44
+ --alert-fg: var(--danger-text);
45
+ --alert-bg: var(--danger-subtle);
46
+ --alert-wash: var(--danger-wash);
47
+ }
48
+
49
+ /* with a description: glyph rides the title line, controls stay centered */
50
+ .alert:has(.alert__desc) {
51
+ align-items: flex-start;
52
+ }
53
+ .alert:has(.alert__desc) .alert__action,
54
+ .alert:has(.alert__desc) .alert__close {
55
+ align-self: center;
56
+ }
57
+
58
+ .alert--banner {
59
+ border-radius: 0;
60
+ border-block-start: 0;
61
+ border-inline: 0;
62
+ padding-inline: var(--space-6);
63
+ }
64
+
65
+ /* Glyph - one --leading-body tall so it centers on the title line. */
66
+ .alert__icon {
67
+ flex: none;
68
+ display: grid;
69
+ place-items: center;
70
+ height: var(--leading-body);
71
+ color: var(--alert-hue);
72
+ }
73
+
74
+ .alert__body {
75
+ flex: 1 1 auto;
76
+ min-width: 0;
77
+ }
78
+ .alert__title {
79
+ margin: 0;
80
+ font: var(--type-label);
81
+ color: var(--alert-fg);
82
+ text-wrap: pretty;
83
+ }
84
+ .alert__desc {
85
+ margin: var(--space-1) 0 0;
86
+ font: var(--type-caption);
87
+ color: color-mix(in oklab, var(--alert-fg) 56%, var(--text-body));
88
+ text-wrap: pretty;
89
+ }
90
+
91
+ /* Action - the system .btn .btn--sm re-skinned to the pane's tone (alert-local chrome, not a global Button variant). */
92
+ .alert__action {
93
+ flex: none;
94
+ color: var(--alert-fg);
95
+ background: transparent;
96
+ }
97
+ .alert__action:hover:not(:disabled) {
98
+ color: var(--alert-fg);
99
+ background: var(--alert-wash);
100
+ }
101
+ .alert__action:active:not(:disabled) {
102
+ background: color-mix(in oklab, var(--alert-hue) 16%, transparent);
103
+ transform: translateY(var(--space-px));
104
+ }
105
+ .alert[data-tone='danger'] .alert__action {
106
+ --btn-ring: var(--ring-danger);
107
+ }
108
+
109
+ /* Dismiss - always visible (alerts persist; no hover-reveal like toast). */
110
+ .alert__close {
111
+ flex: none;
112
+ appearance: none;
113
+ display: grid;
114
+ place-items: center;
115
+ width: var(--space-6);
116
+ height: var(--space-6);
117
+ margin: 0;
118
+ padding: 0;
119
+ border: none;
120
+ border-radius: var(--radius-sm);
121
+ background: transparent;
122
+ color: color-mix(in oklab, var(--alert-fg) 64%, transparent);
123
+ cursor: pointer;
124
+ transition: var(--transition-control);
125
+ }
126
+ .alert__close:hover {
127
+ background: color-mix(in oklab, var(--alert-fg) 9%, transparent);
128
+ color: var(--alert-fg);
129
+ }
130
+ .alert__close:active {
131
+ background: color-mix(in oklab, var(--alert-fg) 14%, transparent);
132
+ }
133
+ .alert__close:focus-visible {
134
+ outline: none;
135
+ box-shadow: var(--focus-ring);
136
+ }
@@ -0,0 +1,128 @@
1
+ 'use client';
2
+
3
+ // Avatar - identity mark; content priority: image - icon - initials - silhouette.
4
+
5
+ import * as React from 'react';
6
+
7
+ export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
8
+ export type AvatarShape = 'circle' | 'square';
9
+ export type AvatarStatus = 'online' | 'away' | 'busy' | 'offline';
10
+ export type AvatarPaletteIndex = 1 | 2 | 3 | 4 | 5 | 6;
11
+
12
+ export interface AvatarProps extends React.HTMLAttributes<HTMLSpanElement> {
13
+ /** Image URL. Falls back to initials / icon / silhouette if absent or fails. */
14
+ src?: string | null;
15
+ /** Display name - drives initials generation, palette hash, and aria-label. */
16
+ name?: string | null;
17
+ /** Content override - any React node (e.g. <Icon name="globe" />). */
18
+ icon?: React.ReactNode | null;
19
+ /** 'circle' for people; 'square' for channels / brand pages. Default 'circle'. */
20
+ shape?: AvatarShape;
21
+ /** Size step. Default 'md' (32 px). */
22
+ size?: AvatarSize;
23
+ /** Presence indicator dot at bottom-right. Default null (hidden). */
24
+ status?: AvatarStatus | null;
25
+ /** Override identity slot 1-6 (blue - violet - plum - rose - clay - moss); auto from name hash when null, neutral when anonymous. */
26
+ paletteIndex?: AvatarPaletteIndex | null;
27
+ }
28
+
29
+ const _PALETTE_SLOTS = 6;
30
+
31
+ function _hash(str: string) {
32
+ let h = 0;
33
+ for (let i = 0; i < str.length; i++) h = ((h << 5) - h + str.charCodeAt(i)) | 0;
34
+ return (Math.abs(h) % _PALETTE_SLOTS) + 1;
35
+ }
36
+
37
+ function _initials(name: string | null, max: number) {
38
+ if (!name) return '';
39
+ const parts = name.trim().split(/\s+/).filter(Boolean);
40
+ if (parts.length === 0) return '';
41
+ if (parts.length === 1 || max === 1) return parts[0][0].toUpperCase();
42
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
43
+ }
44
+
45
+ function _Silhouette() {
46
+ return (
47
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
48
+ <circle cx="12" cy="8.5" r="3.8" fill="currentColor" />
49
+ <path d="M4 20.5C4 16.1 7.6 13 12 13s8 3.1 8 7.5" fill="currentColor" opacity="0.65" />
50
+ </svg>
51
+ );
52
+ }
53
+
54
+ export function Avatar({
55
+ src = null,
56
+ name = null,
57
+ icon = null,
58
+ shape = 'circle',
59
+ size = 'md',
60
+ status = null,
61
+ paletteIndex = null,
62
+ className = '',
63
+ style = {},
64
+ ...rest
65
+ }: AvatarProps) {
66
+ const [imgError, setImgError] = React.useState(false);
67
+ const showImg = Boolean(src) && !imgError;
68
+
69
+ const maxInits = size === 'xs' ? 1 : 2;
70
+ const inits = _initials(name, maxInits);
71
+ const slot = paletteIndex ?? (name ? _hash(name) : null);
72
+
73
+ const classes = [
74
+ 'avatar',
75
+ size !== 'md' ? `avatar--${size}` : '',
76
+ shape === 'square' ? 'avatar--square' : '',
77
+ className,
78
+ ]
79
+ .filter(Boolean)
80
+ .join(' ');
81
+
82
+ return (
83
+ <span
84
+ className={classes}
85
+ role={name ? 'img' : undefined}
86
+ aria-label={name || undefined}
87
+ data-palette={!showImg && slot != null ? slot : undefined}
88
+ style={style}
89
+ {...rest}
90
+ >
91
+ <span className="avatar__face">
92
+ {showImg && (
93
+ <img
94
+ className="avatar__img"
95
+ src={src}
96
+ alt={name || ''}
97
+ onError={() => setImgError(true)}
98
+ draggable={false}
99
+ />
100
+ )}
101
+ {!showImg && icon && (
102
+ <span className="avatar__icon" aria-hidden="true">
103
+ {icon}
104
+ </span>
105
+ )}
106
+ {!showImg && !icon && inits && (
107
+ <span className="avatar__initials" aria-hidden="true">
108
+ {inits}
109
+ </span>
110
+ )}
111
+ {!showImg && !icon && !inits && (
112
+ <span className="avatar__icon" aria-hidden="true">
113
+ <_Silhouette />
114
+ </span>
115
+ )}
116
+ </span>
117
+
118
+ {status && (
119
+ <span
120
+ className={`avatar__status avatar__status--${status}`}
121
+ role="img"
122
+ aria-label={status}
123
+ title={status}
124
+ />
125
+ )}
126
+ </span>
127
+ );
128
+ }
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+
3
+ // AvatarGroup - stacks Avatars with overlap + a "+N" overflow chip; CSS drives the hover-spread.
4
+
5
+ import * as React from 'react';
6
+ import type { AvatarSize } from './Avatar';
7
+
8
+ export interface AvatarGroupProps extends React.HTMLAttributes<HTMLSpanElement> {
9
+ children: React.ReactNode;
10
+ /** Max visible avatars before "+N" overflow chip. Default 5. */
11
+ max?: number;
12
+ /** Uniform size applied to all children. Default 'md'. */
13
+ size?: AvatarSize;
14
+ }
15
+
16
+ export function AvatarGroup({
17
+ children,
18
+ max = 5,
19
+ size = 'md',
20
+ className = '',
21
+ ...rest
22
+ }: AvatarGroupProps) {
23
+ const items = React.Children.toArray(children);
24
+ const visible = max > 0 ? items.slice(0, max) : items;
25
+ const overflow = items.length - visible.length;
26
+
27
+ return (
28
+ <span className={['avatar-group', className].filter(Boolean).join(' ')} {...rest}>
29
+ {visible.map((child, i) =>
30
+ React.isValidElement(child)
31
+ ? React.cloneElement(child as React.ReactElement<{ size?: AvatarSize }>, {
32
+ size,
33
+ key: child.key ?? i,
34
+ })
35
+ : child,
36
+ )}
37
+ {overflow > 0 && (
38
+ <span
39
+ className={`avatar avatar--${size} avatar--overflow`}
40
+ role="img"
41
+ aria-label={`${overflow} more`}
42
+ >
43
+ <span className="avatar__face">
44
+ <span className="avatar__initials">+{overflow}</span>
45
+ </span>
46
+ </span>
47
+ )}
48
+ </span>
49
+ );
50
+ }
@@ -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,66 @@
1
+ 'use client';
2
+
3
+ // Base badge chip - glass surface by default; the outline variant is flat (no glass classes).
4
+
5
+ import type { HTMLAttributes, ReactNode } from 'react';
6
+
7
+ export type BadgeTone = 'neutral' | 'info' | 'success' | 'warning' | 'danger';
8
+
9
+ export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
10
+ /** Status hue. @default 'neutral' */
11
+ tone?: BadgeTone;
12
+ /** Surface. @default 'glass' */
13
+ variant?: 'glass' | 'outline';
14
+ /** @default 'md' */
15
+ size?: 'sm' | 'md';
16
+ /** Leading status dot. */
17
+ dot?: boolean;
18
+ /** Dot pulses (implies dot) - for in-progress status. */
19
+ live?: boolean;
20
+ /** Fully-rounded shape. */
21
+ pill?: boolean;
22
+ /** Optional leading <Icon> (overrides dot if both set). */
23
+ icon?: ReactNode;
24
+ }
25
+
26
+ export function Badge({
27
+ tone = 'neutral',
28
+ variant = 'glass',
29
+ size = 'md',
30
+ dot = false,
31
+ live = false,
32
+ pill = false,
33
+ icon = null,
34
+ className = '',
35
+ children,
36
+ ...rest
37
+ }: BadgeProps) {
38
+ const isGlass = variant !== 'outline';
39
+ const classes = [
40
+ isGlass ? 'glass glass--interactive' : '',
41
+ 'badge',
42
+ `badge--${tone}`,
43
+ variant === 'outline' ? 'badge--outline' : '',
44
+ size !== 'md' ? `badge--${size}` : '',
45
+ pill ? 'badge--pill' : '',
46
+ className,
47
+ ]
48
+ .filter(Boolean)
49
+ .join(' ');
50
+
51
+ const showDot = (dot || live) && !icon;
52
+
53
+ return (
54
+ <span className={classes} {...rest}>
55
+ {showDot ? (
56
+ <span className={`badge__dot${live ? ' badge__dot--live' : ''}`} aria-hidden="true" />
57
+ ) : null}
58
+ {icon ? (
59
+ <span className="badge__icon" aria-hidden="true">
60
+ {icon}
61
+ </span>
62
+ ) : null}
63
+ <span className="badge__label">{children}</span>
64
+ </span>
65
+ );
66
+ }