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,46 @@
1
+ 'use client';
2
+
3
+ // CountBadge - count chip (counts, totals, deltas); roll mode is an odometer, digits roll on change.
4
+
5
+ import { Badge, type BadgeProps } from './Badge';
6
+
7
+ function Digit({ d }: { d: number }) {
8
+ return (
9
+ <span className="odo__col" aria-hidden="true">
10
+ <span className={'odo__strip odo__strip--' + d}>
11
+ {Array.from({ length: 10 }, (_, i) => (
12
+ <span key={i}>{i}</span>
13
+ ))}
14
+ </span>
15
+ </span>
16
+ );
17
+ }
18
+
19
+ export interface CountBadgeProps extends Omit<BadgeProps, 'children'> {
20
+ value: string | number;
21
+ /** Odometer: digits roll vertically on change. */
22
+ roll?: boolean;
23
+ }
24
+
25
+ export function CountBadge({ value, tone = 'neutral', roll = false, ...rest }: CountBadgeProps) {
26
+ const str = String(value);
27
+ return (
28
+ <Badge tone={tone} {...rest}>
29
+ {roll ? (
30
+ <span className="odo" role="text" aria-label={str}>
31
+ {str.split('').map((ch, i) =>
32
+ /\d/.test(ch) ? (
33
+ <Digit key={i} d={Number(ch)} />
34
+ ) : (
35
+ <span key={i} className="odo__fixed" aria-hidden="true">
36
+ {ch === ' ' ? ' ' : ch}
37
+ </span>
38
+ ),
39
+ )}
40
+ </span>
41
+ ) : (
42
+ str
43
+ )}
44
+ </Badge>
45
+ );
46
+ }
@@ -0,0 +1,132 @@
1
+ 'use client';
2
+
3
+ // StatusBadge - a status as a badge; canonical tone+label mapping; morph tweens width in place.
4
+
5
+ import * as React from 'react';
6
+ import { Badge, type BadgeProps } from './Badge';
7
+ import { UIMotion } from '../../tokens/motion-tokens';
8
+
9
+ const SM = UIMotion;
10
+
11
+ export type PostStatus = 'draft' | 'scheduled' | 'processing' | 'published' | 'failed';
12
+
13
+ export const POST_STATUS: Record<
14
+ PostStatus,
15
+ { tone: BadgeProps['tone']; label: string; live?: boolean }
16
+ > = {
17
+ draft: { tone: 'neutral', label: 'Draft' },
18
+ scheduled: { tone: 'info', label: 'Scheduled' },
19
+ processing: { tone: 'warning', label: 'Processing', live: true },
20
+ published: { tone: 'success', label: 'Published' },
21
+ failed: { tone: 'danger', label: 'Failed' },
22
+ };
23
+
24
+ const TERMINAL: Partial<Record<PostStatus, boolean>> = { published: true, failed: true };
25
+
26
+ export interface StatusBadgeProps extends Omit<BadgeProps, 'children' | 'tone'> {
27
+ status: PostStatus;
28
+ /** Morph in place as `status` changes instead of swapping. */
29
+ morph?: boolean;
30
+ }
31
+
32
+ export function StatusBadge({ status, morph = false, ...rest }: StatusBadgeProps) {
33
+ if (!morph) {
34
+ const s = POST_STATUS[status] || POST_STATUS.draft;
35
+ return (
36
+ <Badge tone={s.tone} dot live={!!s.live} {...rest}>
37
+ {s.label}
38
+ </Badge>
39
+ );
40
+ }
41
+ return <StatusMorph status={status} {...rest} />;
42
+ }
43
+
44
+ interface StatusMorphProps extends Omit<BadgeProps, 'children' | 'tone'> {
45
+ status: PostStatus;
46
+ }
47
+
48
+ interface Word {
49
+ key: number;
50
+ label: string;
51
+ cls: string;
52
+ }
53
+
54
+ function StatusMorph({ status, className = '', ...rest }: StatusMorphProps) {
55
+ const s = POST_STATUS[status] || POST_STATUS.draft;
56
+
57
+ const prev = React.useRef(status);
58
+ const keyRef = React.useRef(1);
59
+ const labelRef = React.useRef<HTMLSpanElement>(null);
60
+ const ghostRef = React.useRef<HTMLSpanElement>(null);
61
+ const [words, setWords] = React.useState<Word[]>([{ key: 0, label: s.label, cls: '' }]);
62
+ const [boxW, setBoxW] = React.useState<number>();
63
+
64
+ // initial width only; on change the width eases in the rAF below, synced to the word roll
65
+ React.useLayoutEffect(() => {
66
+ if (ghostRef.current) setBoxW(ghostRef.current.offsetWidth);
67
+ }, []);
68
+
69
+ React.useEffect(() => {
70
+ if (prev.current === status) return;
71
+ prev.current = status;
72
+ const next = POST_STATUS[status] || POST_STATUS.draft;
73
+ const nk = keyRef.current++;
74
+ // ghost already holds the new label
75
+ const nextW = ghostRef.current?.offsetWidth;
76
+
77
+ // add the new word primed below (invisible); the current word stays settled
78
+ setWords((ws) => ws.concat({ key: nk, label: next.label, cls: 'badge__word--in' }));
79
+ // next frame: roll the new word in, the old out, ease the box - one tween; clip hides the wider word
80
+ const raf = requestAnimationFrame(() =>
81
+ requestAnimationFrame(() => {
82
+ setWords((ws) =>
83
+ ws.map((w) => (w.key === nk ? { ...w, cls: '' } : { ...w, cls: 'badge__word--out' })),
84
+ );
85
+ if (nextW) setBoxW(nextW);
86
+ }),
87
+ );
88
+ // old word done rolling out - drop it; width already settled in the rAF, so this only prunes the DOM
89
+ const drop = setTimeout(
90
+ () => setWords((ws) => ws.filter((w) => !w.cls.includes('--out'))),
91
+ SM.dur.base * 1000 + 80,
92
+ );
93
+ // glint when a post lands on a terminal state
94
+ let glintT: ReturnType<typeof setTimeout> | undefined;
95
+ if (TERMINAL[status] && labelRef.current) {
96
+ const chip = labelRef.current.closest('.badge') as HTMLElement | null;
97
+ if (chip) {
98
+ chip.classList.remove('glass-glint');
99
+ // restart the one-shot
100
+ void chip.offsetWidth;
101
+ chip.classList.add('glass-glint');
102
+ glintT = setTimeout(() => chip.classList.remove('glass-glint'), SM.dur.slow * 1000 + 80);
103
+ }
104
+ }
105
+ return () => {
106
+ cancelAnimationFrame(raf);
107
+ clearTimeout(drop);
108
+ clearTimeout(glintT);
109
+ };
110
+ }, [status]);
111
+
112
+ return (
113
+ <Badge
114
+ tone={s.tone}
115
+ dot
116
+ live={!!s.live}
117
+ className={['badge--morph', className].filter(Boolean).join(' ')}
118
+ {...rest}
119
+ >
120
+ <span className="badge__morph" ref={labelRef} style={boxW ? { width: boxW } : undefined}>
121
+ <span className="badge__ghost" ref={ghostRef} aria-hidden="true">
122
+ {s.label}
123
+ </span>
124
+ {words.map((w) => (
125
+ <span key={w.key} className={['badge__word', w.cls].filter(Boolean).join(' ')}>
126
+ {w.label}
127
+ </span>
128
+ ))}
129
+ </span>
130
+ </Badge>
131
+ );
132
+ }
@@ -0,0 +1,243 @@
1
+ /* Badge / status pill - shape, tones, dot, status morph. */
2
+
3
+ .badge {
4
+ /* wire the tone vars into the glass surface hooks */
5
+ --glass-fg: var(--badge-fg);
6
+ --glass-tint: var(--badge-tint);
7
+
8
+ display: inline-flex;
9
+ align-items: center;
10
+ gap: var(--space-1);
11
+ padding-block: var(--space-1);
12
+ padding-inline: var(--space-2);
13
+ border-radius: var(--radius-sm);
14
+
15
+ font-family: var(--font-mono);
16
+ font-size: var(--size-micro);
17
+ line-height: var(--leading-micro);
18
+ font-weight: var(--weight-medium);
19
+ font-variant-numeric: tabular-nums;
20
+ letter-spacing: 0;
21
+ white-space: nowrap;
22
+ vertical-align: middle;
23
+ color: var(--badge-fg);
24
+ user-select: none;
25
+ -webkit-user-select: none;
26
+ }
27
+
28
+ .badge--sm {
29
+ padding-block: 0;
30
+ gap: var(--space-px);
31
+ }
32
+
33
+ .badge--pill {
34
+ border-radius: var(--radius-full);
35
+ }
36
+
37
+ .badge__dot {
38
+ position: relative;
39
+ width: 0.5em;
40
+ height: 0.5em;
41
+ flex: none;
42
+ border-radius: var(--radius-full);
43
+ background: var(--badge-dot, var(--badge-fg));
44
+ /* hue cross-fades as the status morphs */
45
+ transition: background-color var(--duration-base) var(--ease-standard);
46
+ }
47
+ .badge__dot--live::after {
48
+ content: '';
49
+ position: absolute;
50
+ inset: 0;
51
+ border-radius: inherit;
52
+ background: inherit;
53
+ animation: badge-pulse var(--duration-pulse) var(--ease-entrance) infinite;
54
+ }
55
+ @keyframes badge-pulse {
56
+ 0% {
57
+ transform: scale(1);
58
+ opacity: 0.5;
59
+ }
60
+ 70% {
61
+ transform: scale(2.6);
62
+ opacity: 0;
63
+ }
64
+ 100% {
65
+ transform: scale(2.6);
66
+ opacity: 0;
67
+ }
68
+ }
69
+
70
+ .badge__icon {
71
+ display: inline-flex;
72
+ flex: none;
73
+ width: 1em;
74
+ height: 1em;
75
+ margin-left: calc(var(--space-px) * -1);
76
+ }
77
+ .badge__icon svg {
78
+ display: block;
79
+ width: 100%;
80
+ height: 100%;
81
+ }
82
+
83
+ .badge--neutral {
84
+ --badge-fg: var(--text-muted);
85
+ --badge-tint: var(--glass-tint-neutral);
86
+ --badge-dot: var(--gray-500);
87
+ }
88
+ .badge--info {
89
+ --badge-fg: var(--info-text);
90
+ --badge-tint: var(--glass-tint-info);
91
+ --badge-dot: var(--info);
92
+ }
93
+ .badge--success {
94
+ --badge-fg: var(--success-text);
95
+ --badge-tint: var(--glass-tint-success);
96
+ --badge-dot: var(--success);
97
+ }
98
+ .badge--warning {
99
+ --badge-fg: var(--warning-text);
100
+ --badge-tint: var(--glass-tint-warning);
101
+ --badge-dot: var(--warning);
102
+ }
103
+ .badge--danger {
104
+ --badge-fg: var(--danger-text);
105
+ --badge-tint: var(--glass-tint-danger);
106
+ --badge-dot: var(--danger);
107
+ }
108
+
109
+ /* Outline variant - deliberately NOT glass: a flat, engraved hairline chip. */
110
+ .badge--outline {
111
+ position: relative;
112
+ isolation: isolate;
113
+ background: transparent;
114
+ box-shadow:
115
+ inset 0 0 0 var(--border-hairline) color-mix(in oklab, var(--badge-fg) 30%, transparent),
116
+ inset 0 1px 0 0 rgb(255 255 255 / 0.5),
117
+ var(--shadow-xs);
118
+ transition:
119
+ transform var(--duration-base) var(--ease-standard),
120
+ box-shadow var(--duration-base) var(--ease-standard),
121
+ background-color var(--duration-fast) var(--ease-standard);
122
+ }
123
+ .badge--outline .badge__dot {
124
+ box-shadow: 0 0 0 2.5px color-mix(in oklab, var(--badge-dot) 16%, transparent);
125
+ }
126
+ .badge--outline:hover {
127
+ transform: translateY(calc(var(--space-px) * -1));
128
+ background: color-mix(in oklab, var(--badge-fg) 7%, transparent);
129
+ box-shadow:
130
+ inset 0 0 0 var(--border-hairline) color-mix(in oklab, var(--badge-fg) 42%, transparent),
131
+ inset 0 1px 0 0 rgb(255 255 255 / 0.6),
132
+ var(--shadow-sm);
133
+ }
134
+
135
+ /* Status morph - a clipped one-cell grid stack: the ghost measures the next width, the box eases, words roll; shrink mirrors grow. */
136
+ .badge__morph {
137
+ position: relative;
138
+ display: inline-grid;
139
+ align-items: center;
140
+ justify-items: start;
141
+ overflow: hidden;
142
+ transition: width var(--duration-base) var(--ease-entrance);
143
+ }
144
+ .badge__morph > * {
145
+ grid-area: 1 / 1;
146
+ }
147
+ .badge__ghost {
148
+ visibility: hidden;
149
+ pointer-events: none;
150
+ }
151
+ .badge__word {
152
+ display: inline-block;
153
+ transition:
154
+ transform var(--duration-base) var(--ease-entrance),
155
+ opacity var(--duration-base) var(--ease-standard);
156
+ }
157
+ .badge__word--out {
158
+ transform: translateY(-52%);
159
+ opacity: 0;
160
+ }
161
+ .badge__word--in {
162
+ transform: translateY(52%);
163
+ opacity: 0;
164
+ transition: none;
165
+ }
166
+
167
+ /* Odometer - each digit is a column clipping a 0-9 strip; .odo__strip--N sets the translateY (no inline styles). */
168
+ .odo {
169
+ display: inline-flex;
170
+ align-items: center;
171
+ }
172
+ .odo__col {
173
+ display: inline-block;
174
+ height: 1em;
175
+ line-height: 1;
176
+ overflow: hidden;
177
+ }
178
+ .odo__strip {
179
+ display: flex;
180
+ flex-direction: column;
181
+ transition: transform var(--duration-slow) var(--ease-entrance);
182
+ will-change: transform;
183
+ }
184
+ .odo__strip > span {
185
+ height: 1em;
186
+ line-height: 1;
187
+ display: block;
188
+ text-align: center;
189
+ }
190
+ .odo__fixed {
191
+ display: inline-block;
192
+ }
193
+ .odo__strip--0 {
194
+ transform: translateY(0);
195
+ }
196
+ .odo__strip--1 {
197
+ transform: translateY(-1em);
198
+ }
199
+ .odo__strip--2 {
200
+ transform: translateY(-2em);
201
+ }
202
+ .odo__strip--3 {
203
+ transform: translateY(-3em);
204
+ }
205
+ .odo__strip--4 {
206
+ transform: translateY(-4em);
207
+ }
208
+ .odo__strip--5 {
209
+ transform: translateY(-5em);
210
+ }
211
+ .odo__strip--6 {
212
+ transform: translateY(-6em);
213
+ }
214
+ .odo__strip--7 {
215
+ transform: translateY(-7em);
216
+ }
217
+ .odo__strip--8 {
218
+ transform: translateY(-8em);
219
+ }
220
+ .odo__strip--9 {
221
+ transform: translateY(-9em);
222
+ }
223
+
224
+ @media (prefers-reduced-motion: reduce) {
225
+ .badge--outline {
226
+ transition: none;
227
+ }
228
+ .badge--outline:hover {
229
+ transform: none;
230
+ }
231
+ .badge__dot--live::after {
232
+ animation: none;
233
+ }
234
+ .badge__morph {
235
+ transition: none;
236
+ }
237
+ .badge__word {
238
+ transition: none;
239
+ }
240
+ .odo__strip {
241
+ transition: none;
242
+ }
243
+ }
@@ -0,0 +1,68 @@
1
+ 'use client';
2
+
3
+ // Button - primitive; composes button.css classes, renders icons, manages loading.
4
+
5
+ import type { ButtonHTMLAttributes, ReactNode } from 'react';
6
+
7
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
8
+ /** Visual weight / intent. @default 'primary' */
9
+ variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'link';
10
+ /** Control height. sm 28px - md 36px (default) - lg 40px. @default 'md' */
11
+ size?: 'sm' | 'md' | 'lg';
12
+ /** Loading - swaps content for a spinner and makes the button inert. */
13
+ loading?: boolean;
14
+ /** Leading icon node (e.g. a 16px <Icon>). Sized & aligned by the component. */
15
+ iconLeft?: ReactNode;
16
+ /** Trailing icon node. */
17
+ iconRight?: ReactNode;
18
+ /** Stretch to fill the container width. */
19
+ fullWidth?: boolean;
20
+ }
21
+
22
+ export function Button({
23
+ variant = 'primary',
24
+ size = 'md',
25
+ type = 'button',
26
+ disabled = false,
27
+ loading = false,
28
+ iconLeft = null,
29
+ iconRight = null,
30
+ fullWidth = false,
31
+ className = '',
32
+ children,
33
+ ...rest
34
+ }: ButtonProps) {
35
+ const classes = [
36
+ 'btn',
37
+ `btn--${variant}`,
38
+ size !== 'md' ? `btn--${size}` : '',
39
+ fullWidth ? 'btn--block' : '',
40
+ loading ? 'is-loading' : '',
41
+ className,
42
+ ]
43
+ .filter(Boolean)
44
+ .join(' ');
45
+
46
+ return (
47
+ <button
48
+ type={type}
49
+ className={classes}
50
+ disabled={disabled || loading}
51
+ aria-busy={loading || undefined}
52
+ {...rest}
53
+ >
54
+ {iconLeft ? (
55
+ <span className="btn__icon" aria-hidden="true">
56
+ {iconLeft}
57
+ </span>
58
+ ) : null}
59
+ <span className="btn__label">{children}</span>
60
+ {iconRight ? (
61
+ <span className="btn__icon" aria-hidden="true">
62
+ {iconRight}
63
+ </span>
64
+ ) : null}
65
+ {loading ? <span className="btn__spinner" aria-hidden="true" /> : null}
66
+ </button>
67
+ );
68
+ }
@@ -0,0 +1,222 @@
1
+ /* Button - variants, sizes, states; lit gradient, tactile press, composed focus ring. */
2
+
3
+ .btn {
4
+ --btn-shadow: none;
5
+ --btn-ring: var(--ring-accent);
6
+
7
+ position: relative;
8
+ isolation: isolate;
9
+ display: inline-flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ gap: var(--space-2);
13
+
14
+ height: var(--control-height);
15
+ padding: 0 var(--space-4);
16
+ border: var(--border-hairline) solid transparent;
17
+ border-radius: var(--radius-md);
18
+
19
+ font-family: var(--font-sans);
20
+ font-size: var(--size-body);
21
+ font-weight: var(--weight-semibold);
22
+ line-height: 1;
23
+ letter-spacing: var(--tracking-normal);
24
+ white-space: nowrap;
25
+
26
+ cursor: pointer;
27
+ user-select: none;
28
+ -webkit-user-select: none;
29
+ -webkit-tap-highlight-color: transparent;
30
+ appearance: none;
31
+ box-shadow: var(--btn-shadow);
32
+
33
+ transition:
34
+ background-color var(--duration-fast) var(--ease-standard),
35
+ border-color var(--duration-fast) var(--ease-standard),
36
+ color var(--duration-fast) var(--ease-standard),
37
+ box-shadow var(--duration-fast) var(--ease-standard),
38
+ transform var(--duration-fast) var(--ease-standard);
39
+ }
40
+
41
+ /* Composed focus ring: keep the resting shadow, add the ring on top. */
42
+ .btn:focus-visible {
43
+ outline: none;
44
+ box-shadow: var(--btn-shadow), var(--btn-ring);
45
+ }
46
+
47
+ .btn--sm {
48
+ height: var(--control-height-sm);
49
+ padding: 0 var(--space-3);
50
+ font-size: var(--size-caption);
51
+ border-radius: var(--radius-sm);
52
+ gap: var(--space-1);
53
+ }
54
+ .btn--lg {
55
+ height: var(--control-height-lg);
56
+ padding: 0 var(--space-5);
57
+ border-radius: var(--radius-md);
58
+ }
59
+ .btn--block {
60
+ width: 100%;
61
+ }
62
+
63
+ .btn__icon {
64
+ display: inline-flex;
65
+ flex: none;
66
+ width: 1em;
67
+ height: 1em;
68
+ }
69
+ .btn__icon svg {
70
+ display: block;
71
+ width: 100%;
72
+ height: 100%;
73
+ }
74
+ /* optical: pull icons slightly toward the label */
75
+ .btn__icon:first-child {
76
+ margin-left: calc(var(--space-1) * -1);
77
+ }
78
+ .btn__icon:last-child {
79
+ margin-right: calc(var(--space-1) * -1);
80
+ }
81
+
82
+ .btn--primary {
83
+ color: var(--text-on-accent);
84
+ background: linear-gradient(180deg, var(--accent), var(--accent-hover));
85
+ --btn-shadow: inset 0 1px 0 0 rgb(255 255 255 / 0.16), var(--shadow-sm);
86
+ }
87
+ .btn--primary:hover:not(:disabled):not(.is-loading) {
88
+ background: linear-gradient(180deg, var(--teal-400), var(--teal-500));
89
+ --btn-shadow: inset 0 1px 0 0 rgb(255 255 255 / 0.22), var(--shadow-md);
90
+ }
91
+ .btn--primary:active:not(:disabled):not(.is-loading) {
92
+ background: linear-gradient(180deg, var(--accent-hover), var(--accent-active));
93
+ transform: translateY(var(--space-px));
94
+ --btn-shadow:
95
+ inset 0 1px 1px 0 rgb(var(--shadow-rgb) / 0.4), 0 1px 1px 0 rgb(var(--shadow-rgb) / 0.1);
96
+ }
97
+
98
+ .btn--secondary {
99
+ color: var(--text-body);
100
+ background: var(--bg-surface);
101
+ border-color: var(--border-default);
102
+ --btn-shadow: inset 0 1px 0 0 rgb(255 255 255 / 0.6), 0 1px 2px 0 rgb(var(--shadow-rgb) / 0.05);
103
+ }
104
+ .btn--secondary:hover:not(:disabled):not(.is-loading) {
105
+ color: var(--text-strong);
106
+ background: var(--bg-subtle);
107
+ border-color: var(--border-strong);
108
+ }
109
+ .btn--secondary:active:not(:disabled):not(.is-loading) {
110
+ background: var(--bg-muted);
111
+ transform: translateY(var(--space-px));
112
+ --btn-shadow: inset 0 1px 2px 0 rgb(var(--shadow-rgb) / 0.07);
113
+ }
114
+
115
+ .btn--ghost {
116
+ color: var(--text-body);
117
+ background: transparent;
118
+ }
119
+ .btn--ghost:hover:not(:disabled):not(.is-loading) {
120
+ color: var(--text-strong);
121
+ background: var(--bg-subtle);
122
+ }
123
+ .btn--ghost:active:not(:disabled):not(.is-loading) {
124
+ background: var(--bg-muted);
125
+ transform: translateY(var(--space-px));
126
+ }
127
+
128
+ .btn--danger {
129
+ color: var(--text-on-accent);
130
+ background: linear-gradient(180deg, var(--red-500), var(--red-600));
131
+ --btn-ring: var(--ring-danger);
132
+ --btn-shadow: inset 0 1px 0 0 rgb(255 255 255 / 0.16), var(--shadow-sm);
133
+ }
134
+ .btn--danger:hover:not(:disabled):not(.is-loading) {
135
+ background: linear-gradient(180deg, var(--red-400), var(--red-500));
136
+ --btn-shadow: inset 0 1px 0 0 rgb(255 255 255 / 0.22), var(--shadow-md);
137
+ }
138
+ .btn--danger:active:not(:disabled):not(.is-loading) {
139
+ background: linear-gradient(180deg, var(--red-600), var(--red-700));
140
+ transform: translateY(var(--space-px));
141
+ --btn-shadow:
142
+ inset 0 1px 1px 0 rgb(var(--shadow-rgb) / 0.4), 0 1px 1px 0 rgb(var(--shadow-rgb) / 0.1);
143
+ }
144
+
145
+ .btn--link {
146
+ height: auto;
147
+ padding: 0;
148
+ border-radius: var(--radius-sm);
149
+ color: var(--text-accent);
150
+ background: none;
151
+ --btn-shadow: none;
152
+ font-weight: var(--weight-medium);
153
+ }
154
+ .btn--link:hover:not(:disabled):not(.is-loading) {
155
+ color: var(--accent-active);
156
+ text-decoration: underline;
157
+ text-underline-offset: var(--space-px);
158
+ }
159
+ .btn--link:active:not(:disabled):not(.is-loading) {
160
+ color: var(--accent-active);
161
+ }
162
+ .btn--link:focus-visible {
163
+ border-radius: var(--radius-sm);
164
+ box-shadow: var(--btn-ring);
165
+ }
166
+
167
+ /* Disabled - explicit per variant, never a blanket fade. */
168
+ .btn:disabled,
169
+ .btn.is-loading {
170
+ cursor: default;
171
+ }
172
+ .btn:disabled {
173
+ transform: none;
174
+ }
175
+ .btn--primary:disabled {
176
+ background: var(--accent-disabled);
177
+ color: var(--text-on-accent);
178
+ --btn-shadow: none;
179
+ }
180
+ .btn--danger:disabled {
181
+ background: var(--danger-disabled);
182
+ color: var(--text-on-accent);
183
+ --btn-shadow: none;
184
+ }
185
+ .btn--secondary:disabled {
186
+ color: var(--text-disabled);
187
+ background: var(--bg-surface);
188
+ border-color: var(--border-subtle);
189
+ --btn-shadow: none;
190
+ }
191
+ .btn--ghost:disabled {
192
+ color: var(--text-disabled);
193
+ background: transparent;
194
+ }
195
+ .btn--link:disabled {
196
+ color: var(--text-disabled);
197
+ text-decoration: none;
198
+ }
199
+
200
+ /* Loading - content holds its width, spinner overlays. */
201
+ .btn.is-loading .btn__label,
202
+ .btn.is-loading .btn__icon {
203
+ opacity: 0;
204
+ }
205
+ .btn__spinner {
206
+ position: absolute;
207
+ top: 50%;
208
+ left: 50%;
209
+ width: 1em;
210
+ height: 1em;
211
+ margin: -0.5em 0 0 -0.5em;
212
+ border-radius: var(--radius-full);
213
+ border: var(--border-emphasis) solid currentColor;
214
+ border-right-color: transparent;
215
+ opacity: 0.9;
216
+ animation: btn-spin var(--duration-spin) var(--ease-standard) infinite;
217
+ }
218
+ @keyframes btn-spin {
219
+ to {
220
+ transform: rotate(360deg);
221
+ }
222
+ }