@dtsports/design-system 0.2.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 (340) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/dist/components/accordion.d.ts +19 -0
  4. package/dist/components/accordion.d.ts.map +1 -0
  5. package/dist/components/accordion.js +26 -0
  6. package/dist/components/accordion.js.map +1 -0
  7. package/dist/components/alert.d.ts +26 -0
  8. package/dist/components/alert.d.ts.map +1 -0
  9. package/dist/components/alert.js.map +1 -0
  10. package/dist/components/app-shell.d.ts +21 -0
  11. package/dist/components/app-shell.d.ts.map +1 -0
  12. package/dist/components/app-shell.js +25 -0
  13. package/dist/components/app-shell.js.map +1 -0
  14. package/dist/components/aspect-ratio.d.ts +16 -0
  15. package/dist/components/aspect-ratio.d.ts.map +1 -0
  16. package/dist/components/aspect-ratio.js +17 -0
  17. package/dist/components/aspect-ratio.js.map +1 -0
  18. package/dist/components/avatar-group.d.ts +31 -0
  19. package/dist/components/avatar-group.d.ts.map +1 -0
  20. package/dist/components/avatar.d.ts +15 -0
  21. package/dist/components/avatar.d.ts.map +1 -0
  22. package/dist/components/avatar.js +20 -0
  23. package/dist/components/avatar.js.map +1 -0
  24. package/dist/components/badge.d.ts +27 -0
  25. package/dist/components/badge.d.ts.map +1 -0
  26. package/dist/components/badge.js +51 -0
  27. package/dist/components/badge.js.map +1 -0
  28. package/dist/components/breadcrumb.js.map +1 -0
  29. package/dist/components/button.d.ts +33 -0
  30. package/dist/components/button.d.ts.map +1 -0
  31. package/dist/components/button.js +58 -0
  32. package/dist/components/button.js.map +1 -0
  33. package/dist/components/calendar.d.ts +15 -0
  34. package/dist/components/calendar.d.ts.map +1 -0
  35. package/dist/components/calendar.js +33 -0
  36. package/dist/components/calendar.js.map +1 -0
  37. package/dist/components/capacity-bar.d.ts +32 -0
  38. package/dist/components/capacity-bar.d.ts.map +1 -0
  39. package/dist/components/capacity-bar.js +29 -0
  40. package/dist/components/capacity-bar.js.map +1 -0
  41. package/dist/components/card.d.ts +26 -0
  42. package/dist/components/card.d.ts.map +1 -0
  43. package/dist/components/card.js +20 -0
  44. package/dist/components/checkbox.d.ts +11 -0
  45. package/dist/components/checkbox.d.ts.map +1 -0
  46. package/dist/components/checkbox.js +19 -0
  47. package/dist/components/chip.d.ts +23 -0
  48. package/dist/components/chip.d.ts.map +1 -0
  49. package/dist/components/chip.js +32 -0
  50. package/dist/components/chip.js.map +1 -0
  51. package/dist/components/collapsible.d.ts +16 -0
  52. package/dist/components/collapsible.js +22 -0
  53. package/dist/components/collapsible.js.map +1 -0
  54. package/dist/components/combobox.d.ts +38 -0
  55. package/dist/components/combobox.d.ts.map +1 -0
  56. package/dist/components/combobox.js +17 -0
  57. package/dist/components/combobox.js.map +1 -0
  58. package/dist/components/confirm-action.d.ts +36 -0
  59. package/dist/components/confirm-action.d.ts.map +1 -0
  60. package/dist/components/confirm-action.js +21 -0
  61. package/dist/components/confirm-action.js.map +1 -0
  62. package/dist/components/container.js.map +1 -0
  63. package/dist/components/date-picker.d.ts.map +1 -0
  64. package/dist/components/date-picker.js +22 -0
  65. package/dist/components/date-picker.js.map +1 -0
  66. package/dist/components/dialog.d.ts +28 -0
  67. package/dist/components/dialog.d.ts.map +1 -0
  68. package/dist/components/dialog.js.map +1 -0
  69. package/dist/components/drawer.js +55 -0
  70. package/dist/components/drawer.js.map +1 -0
  71. package/dist/components/dropdown-menu.d.ts +39 -0
  72. package/dist/components/dropdown-menu.d.ts.map +1 -0
  73. package/dist/components/dropdown-menu.js +47 -0
  74. package/dist/components/empty-state.d.ts +15 -0
  75. package/dist/components/empty-state.d.ts.map +1 -0
  76. package/dist/components/empty-state.js +6 -0
  77. package/dist/components/form-field.d.ts +32 -0
  78. package/dist/components/form-field.d.ts.map +1 -0
  79. package/dist/components/form-field.js +31 -0
  80. package/dist/components/form-field.js.map +1 -0
  81. package/dist/components/input.d.ts +10 -0
  82. package/dist/components/input.d.ts.map +1 -0
  83. package/dist/components/input.js +6 -0
  84. package/dist/components/input.js.map +1 -0
  85. package/dist/components/label.d.ts +11 -0
  86. package/dist/components/label.d.ts.map +1 -0
  87. package/dist/components/label.js +13 -0
  88. package/dist/components/label.js.map +1 -0
  89. package/dist/components/live-pill.d.ts +14 -0
  90. package/dist/components/live-pill.js +15 -0
  91. package/dist/components/live-pill.js.map +1 -0
  92. package/dist/components/metric-tile.d.ts +23 -0
  93. package/dist/components/metric-tile.d.ts.map +1 -0
  94. package/dist/components/money-breakdown.d.ts +36 -0
  95. package/dist/components/money-breakdown.d.ts.map +1 -0
  96. package/dist/components/money-breakdown.js +12 -0
  97. package/dist/components/money-breakdown.js.map +1 -0
  98. package/dist/components/number-input.d.ts +16 -0
  99. package/dist/components/number-input.d.ts.map +1 -0
  100. package/dist/components/number-input.js +30 -0
  101. package/dist/components/number-input.js.map +1 -0
  102. package/dist/components/pagination.d.ts +27 -0
  103. package/dist/components/pagination.d.ts.map +1 -0
  104. package/dist/components/pagination.js +52 -0
  105. package/dist/components/pagination.js.map +1 -0
  106. package/dist/components/popover.d.ts +24 -0
  107. package/dist/components/popover.d.ts.map +1 -0
  108. package/dist/components/popover.js +28 -0
  109. package/dist/components/popover.js.map +1 -0
  110. package/dist/components/progress.d.ts +13 -0
  111. package/dist/components/progress.d.ts.map +1 -0
  112. package/dist/components/progress.js +17 -0
  113. package/dist/components/radio-group.d.ts +10 -0
  114. package/dist/components/radio-group.d.ts.map +1 -0
  115. package/dist/components/radio-group.js +17 -0
  116. package/dist/components/radio-group.js.map +1 -0
  117. package/dist/components/scroll-area.d.ts.map +1 -0
  118. package/dist/components/scroll-area.js +22 -0
  119. package/dist/components/scroll-area.js.map +1 -0
  120. package/dist/components/select.d.ts +27 -0
  121. package/dist/components/select.d.ts.map +1 -0
  122. package/dist/components/select.js +40 -0
  123. package/dist/components/select.js.map +1 -0
  124. package/dist/components/separator.d.ts +10 -0
  125. package/dist/components/separator.d.ts.map +1 -0
  126. package/dist/components/separator.js +14 -0
  127. package/dist/components/separator.js.map +1 -0
  128. package/dist/components/sidebar.d.ts.map +1 -0
  129. package/dist/components/sidebar.js +48 -0
  130. package/dist/components/sidebar.js.map +1 -0
  131. package/dist/components/skeleton.d.ts +23 -0
  132. package/dist/components/skeleton.js +25 -0
  133. package/dist/components/skeleton.js.map +1 -0
  134. package/dist/components/slider.d.ts +14 -0
  135. package/dist/components/slider.d.ts.map +1 -0
  136. package/dist/components/slider.js +30 -0
  137. package/dist/components/slider.js.map +1 -0
  138. package/dist/components/spec.d.ts +35 -0
  139. package/dist/components/spec.js +31 -0
  140. package/dist/components/spec.js.map +1 -0
  141. package/dist/components/spinner.d.ts +20 -0
  142. package/dist/components/spinner.d.ts.map +1 -0
  143. package/dist/components/spinner.js +18 -0
  144. package/dist/components/spinner.js.map +1 -0
  145. package/dist/components/stack.d.ts +48 -0
  146. package/dist/components/stack.d.ts.map +1 -0
  147. package/dist/components/stack.js +43 -0
  148. package/dist/components/stack.js.map +1 -0
  149. package/dist/components/status-pill.js +34 -0
  150. package/dist/components/status-pill.js.map +1 -0
  151. package/dist/components/switch.d.ts +9 -0
  152. package/dist/components/switch.d.ts.map +1 -0
  153. package/dist/components/switch.js +15 -0
  154. package/dist/components/switch.js.map +1 -0
  155. package/dist/components/table.d.ts +31 -0
  156. package/dist/components/table.d.ts.map +1 -0
  157. package/dist/components/tabs.d.ts +22 -0
  158. package/dist/components/tabs.d.ts.map +1 -0
  159. package/dist/components/tabs.js +28 -0
  160. package/dist/components/tabs.js.map +1 -0
  161. package/dist/components/textarea.d.ts +8 -0
  162. package/dist/components/textarea.d.ts.map +1 -0
  163. package/dist/components/textarea.js +6 -0
  164. package/dist/components/textarea.js.map +1 -0
  165. package/dist/components/theme-provider.d.ts +35 -0
  166. package/dist/components/theme-provider.d.ts.map +1 -0
  167. package/dist/components/theme-provider.js.map +1 -0
  168. package/dist/components/theme-toggle.d.ts +10 -0
  169. package/dist/components/theme-toggle.d.ts.map +1 -0
  170. package/dist/components/theme-toggle.js.map +1 -0
  171. package/dist/components/three-state.d.ts +39 -0
  172. package/dist/components/three-state.d.ts.map +1 -0
  173. package/dist/components/three-state.js +24 -0
  174. package/dist/components/three-state.js.map +1 -0
  175. package/dist/components/timestamp.d.ts +24 -0
  176. package/dist/components/timestamp.js +64 -0
  177. package/dist/components/timestamp.js.map +1 -0
  178. package/dist/components/toaster.d.ts +27 -0
  179. package/dist/components/toaster.d.ts.map +1 -0
  180. package/dist/components/toaster.js +21 -0
  181. package/dist/components/toaster.js.map +1 -0
  182. package/dist/components/tooltip.d.ts +28 -0
  183. package/dist/components/tooltip.d.ts.map +1 -0
  184. package/dist/components/tooltip.js.map +1 -0
  185. package/dist/components/top-bar.d.ts +13 -0
  186. package/dist/components/top-bar.d.ts.map +1 -0
  187. package/dist/components/top-bar.js +16 -0
  188. package/dist/index.d.ts +76 -0
  189. package/dist/index.d.ts.map +1 -0
  190. package/dist/index.js +86 -0
  191. package/dist/index.js.map +1 -0
  192. package/dist/lib/utils.d.ts +7 -0
  193. package/dist/lib/utils.d.ts.map +1 -0
  194. package/dist/lib/utils.js +10 -0
  195. package/dist/lib/utils.js.map +1 -0
  196. package/dist/marketing/footer.d.ts +31 -0
  197. package/dist/marketing/footer.d.ts.map +1 -0
  198. package/dist/marketing/footer.js +9 -0
  199. package/dist/marketing/footer.js.map +1 -0
  200. package/dist/marketing/hero.d.ts +29 -0
  201. package/dist/marketing/hero.d.ts.map +1 -0
  202. package/dist/marketing/hero.js +7 -0
  203. package/dist/marketing/hero.js.map +1 -0
  204. package/dist/marketing/index.d.ts +21 -0
  205. package/dist/marketing/index.d.ts.map +1 -0
  206. package/dist/marketing/index.js +21 -0
  207. package/dist/marketing/index.js.map +1 -0
  208. package/dist/marketing/logo-cloud.d.ts +15 -0
  209. package/dist/marketing/logo-cloud.d.ts.map +1 -0
  210. package/dist/marketing/logo-cloud.js +6 -0
  211. package/dist/marketing/logo-cloud.js.map +1 -0
  212. package/dist/marketing/nav.d.ts +33 -0
  213. package/dist/marketing/nav.d.ts.map +1 -0
  214. package/dist/marketing/nav.js +14 -0
  215. package/dist/marketing/nav.js.map +1 -0
  216. package/dist/marketing/prose.d.ts +12 -0
  217. package/dist/marketing/prose.d.ts.map +1 -0
  218. package/dist/marketing/prose.js +18 -0
  219. package/dist/marketing/prose.js.map +1 -0
  220. package/dist/marketing/section.d.ts +22 -0
  221. package/dist/marketing/section.d.ts.map +1 -0
  222. package/dist/marketing/section.js +7 -0
  223. package/dist/marketing/section.js.map +1 -0
  224. package/dist/showcase/components/code-block.d.ts +11 -0
  225. package/dist/showcase/components/code-block.d.ts.map +1 -0
  226. package/dist/showcase/components/code-block.js +20 -0
  227. package/dist/showcase/components/code-block.js.map +1 -0
  228. package/dist/showcase/components/example.d.ts +16 -0
  229. package/dist/showcase/components/example.d.ts.map +1 -0
  230. package/dist/showcase/components/example.js +14 -0
  231. package/dist/showcase/components/example.js.map +1 -0
  232. package/dist/showcase/components/section.d.ts +21 -0
  233. package/dist/showcase/components/section.d.ts.map +1 -0
  234. package/dist/showcase/components/section.js +10 -0
  235. package/dist/showcase/components/section.js.map +1 -0
  236. package/dist/showcase/components/swatch.d.ts +17 -0
  237. package/dist/showcase/components/swatch.d.ts.map +1 -0
  238. package/dist/showcase/components/swatch.js +6 -0
  239. package/dist/showcase/components/swatch.js.map +1 -0
  240. package/dist/showcase/index.d.ts +17 -0
  241. package/dist/showcase/index.d.ts.map +1 -0
  242. package/dist/showcase/index.js +17 -0
  243. package/dist/showcase/index.js.map +1 -0
  244. package/dist/showcase/page.d.ts +2 -0
  245. package/dist/showcase/page.d.ts.map +1 -0
  246. package/dist/showcase/page.js +1085 -0
  247. package/dist/showcase/page.js.map +1 -0
  248. package/dist/styles/generated/tokens.d.ts +131 -0
  249. package/dist/styles/generated/tokens.d.ts.map +1 -0
  250. package/dist/styles/generated/tokens.js +107 -0
  251. package/dist/styles/generated/tokens.js.map +1 -0
  252. package/dist/styles/globals.css +131 -0
  253. package/dist/styles/tailwind.preset.d.ts +181 -0
  254. package/dist/styles/tailwind.preset.d.ts.map +1 -0
  255. package/dist/styles/tailwind.preset.js +114 -0
  256. package/dist/styles/tailwind.preset.js.map +1 -0
  257. package/dist/tokens/index.d.ts +176 -0
  258. package/dist/tokens/index.d.ts.map +1 -0
  259. package/dist/tokens/index.js +139 -0
  260. package/dist/tokens/index.js.map +1 -0
  261. package/dist/tokens/tokens.json +154 -0
  262. package/package.json +124 -0
  263. package/src/components/accordion.tsx +76 -0
  264. package/src/components/alert.tsx +113 -0
  265. package/src/components/app-shell.tsx +43 -0
  266. package/src/components/aspect-ratio.tsx +18 -0
  267. package/src/components/avatar-group.tsx +73 -0
  268. package/src/components/avatar.tsx +81 -0
  269. package/src/components/badge.tsx +77 -0
  270. package/src/components/breadcrumb.tsx +121 -0
  271. package/src/components/button.tsx +83 -0
  272. package/src/components/calendar.tsx +85 -0
  273. package/src/components/capacity-bar.tsx +80 -0
  274. package/src/components/card.tsx +104 -0
  275. package/src/components/checkbox.tsx +46 -0
  276. package/src/components/chip.tsx +74 -0
  277. package/src/components/collapsible.tsx +54 -0
  278. package/src/components/combobox.tsx +140 -0
  279. package/src/components/confirm-action.tsx +111 -0
  280. package/src/components/container.tsx +39 -0
  281. package/src/components/date-picker.tsx +98 -0
  282. package/src/components/dialog.tsx +174 -0
  283. package/src/components/drawer.tsx +157 -0
  284. package/src/components/dropdown-menu.tsx +200 -0
  285. package/src/components/empty-state.tsx +46 -0
  286. package/src/components/form-field.tsx +118 -0
  287. package/src/components/input.tsx +29 -0
  288. package/src/components/label.tsx +35 -0
  289. package/src/components/live-pill.tsx +35 -0
  290. package/src/components/metric-tile.tsx +83 -0
  291. package/src/components/money-breakdown.tsx +92 -0
  292. package/src/components/number-input.tsx +113 -0
  293. package/src/components/pagination.tsx +157 -0
  294. package/src/components/popover.tsx +46 -0
  295. package/src/components/progress.tsx +34 -0
  296. package/src/components/radio-group.tsx +46 -0
  297. package/src/components/scroll-area.tsx +55 -0
  298. package/src/components/select.tsx +169 -0
  299. package/src/components/separator.tsx +34 -0
  300. package/src/components/sidebar.tsx +168 -0
  301. package/src/components/skeleton.tsx +85 -0
  302. package/src/components/slider.tsx +58 -0
  303. package/src/components/spec.tsx +59 -0
  304. package/src/components/spinner.tsx +36 -0
  305. package/src/components/stack.tsx +80 -0
  306. package/src/components/status-pill.tsx +100 -0
  307. package/src/components/switch.tsx +37 -0
  308. package/src/components/table.tsx +129 -0
  309. package/src/components/tabs.tsx +72 -0
  310. package/src/components/textarea.tsx +27 -0
  311. package/src/components/theme-provider.tsx +131 -0
  312. package/src/components/theme-toggle.tsx +34 -0
  313. package/src/components/three-state.tsx +90 -0
  314. package/src/components/timestamp.tsx +107 -0
  315. package/src/components/toaster.tsx +55 -0
  316. package/src/components/tooltip.tsx +49 -0
  317. package/src/components/top-bar.tsx +29 -0
  318. package/src/index.ts +221 -0
  319. package/src/lib/utils.ts +10 -0
  320. package/src/marketing/footer.tsx +93 -0
  321. package/src/marketing/hero.tsx +92 -0
  322. package/src/marketing/index.ts +24 -0
  323. package/src/marketing/logo-cloud.tsx +35 -0
  324. package/src/marketing/nav.tsx +122 -0
  325. package/src/marketing/prose.tsx +45 -0
  326. package/src/marketing/section.tsx +77 -0
  327. package/src/showcase/components/code-block.tsx +66 -0
  328. package/src/showcase/components/example.tsx +47 -0
  329. package/src/showcase/components/section.tsx +70 -0
  330. package/src/showcase/components/swatch.tsx +52 -0
  331. package/src/showcase/index.tsx +16 -0
  332. package/src/showcase/page.tsx +4216 -0
  333. package/src/styles/generated/tokens.ts +119 -0
  334. package/src/styles/globals.css +131 -0
  335. package/src/styles/tailwind.preset.ts +134 -0
  336. package/src/test/a11y.test.tsx +638 -0
  337. package/src/test/marketing-a11y.test.tsx +138 -0
  338. package/src/test/setup.ts +47 -0
  339. package/src/tokens/index.ts +154 -0
  340. package/src/tokens/tokens.json +154 -0
@@ -0,0 +1,157 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
5
+ import { X } from "lucide-react";
6
+ import { cva, type VariantProps } from "class-variance-authority";
7
+ import { cn } from "../lib/utils";
8
+
9
+ /**
10
+ * Drawer / Sheet — side-pushed panel for secondary tasks.
11
+ *
12
+ * Use a Drawer (not a Dialog) for:
13
+ * - Editing a row inline
14
+ * - Viewing details without losing place in the list
15
+ * - Mobile filter sheets
16
+ * - Audit timelines, side-by-side comparisons
17
+ *
18
+ * Per design.md §3.17, drawers replace modal stacks — never nest a
19
+ * dialog inside a drawer or vice versa.
20
+ *
21
+ * Backed by Radix Dialog under the hood, so focus trap, escape close,
22
+ * and aria semantics work out of the box.
23
+ */
24
+ export const Drawer = DialogPrimitive.Root;
25
+ export const DrawerTrigger = DialogPrimitive.Trigger;
26
+ export const DrawerClose = DialogPrimitive.Close;
27
+ export const DrawerPortal = DialogPrimitive.Portal;
28
+
29
+ export const DrawerOverlay = React.forwardRef<
30
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
31
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
32
+ >(({ className, ...props }, ref) => (
33
+ <DialogPrimitive.Overlay
34
+ ref={ref}
35
+ className={cn(
36
+ "fixed inset-0 z-50 bg-black/40 backdrop-blur-[4px]",
37
+ "data-[state=open]:animate-fade-in",
38
+ className
39
+ )}
40
+ {...props}
41
+ />
42
+ ));
43
+ DrawerOverlay.displayName = "DrawerOverlay";
44
+
45
+ export const drawerVariants = cva(
46
+ "fixed z-50 flex flex-col bg-background border-border-strong shadow-md outline-none transition-transform",
47
+ {
48
+ variants: {
49
+ side: {
50
+ top: "inset-x-0 top-0 border-b max-h-[80vh] data-[state=open]:animate-fade-in",
51
+ bottom:
52
+ "inset-x-0 bottom-0 border-t max-h-[80vh] data-[state=open]:animate-fade-in",
53
+ left: "inset-y-0 left-0 h-full w-80 border-r data-[state=open]:animate-fade-in",
54
+ right:
55
+ "inset-y-0 right-0 h-full w-96 border-l data-[state=open]:animate-fade-in",
56
+ },
57
+ },
58
+ defaultVariants: { side: "right" },
59
+ }
60
+ );
61
+
62
+ export interface DrawerContentProps
63
+ extends
64
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
65
+ VariantProps<typeof drawerVariants> {}
66
+
67
+ export const DrawerContent = React.forwardRef<
68
+ React.ElementRef<typeof DialogPrimitive.Content>,
69
+ DrawerContentProps
70
+ >(({ side = "right", className, children, ...props }, ref) => (
71
+ <DrawerPortal>
72
+ <DrawerOverlay />
73
+ <DialogPrimitive.Content
74
+ ref={ref}
75
+ className={cn(drawerVariants({ side }), className)}
76
+ {...props}
77
+ >
78
+ {children}
79
+ <DialogPrimitive.Close
80
+ aria-label="Close drawer"
81
+ className="absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center rounded-sm text-text-muted transition-colors hover:bg-bg-subtle hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring pointer-coarse:after:absolute pointer-coarse:after:-inset-2.5"
82
+ >
83
+ <X className="h-4 w-4" strokeWidth={1.75} />
84
+ </DialogPrimitive.Close>
85
+ </DialogPrimitive.Content>
86
+ </DrawerPortal>
87
+ ));
88
+ DrawerContent.displayName = "DrawerContent";
89
+
90
+ export function DrawerHeader({
91
+ className,
92
+ ...props
93
+ }: React.HTMLAttributes<HTMLDivElement>) {
94
+ return (
95
+ <div
96
+ className={cn("px-5 pt-4 pb-3 border-b border-border", className)}
97
+ {...props}
98
+ />
99
+ );
100
+ }
101
+
102
+ export const DrawerTitle = React.forwardRef<
103
+ React.ElementRef<typeof DialogPrimitive.Title>,
104
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
105
+ >(({ className, ...props }, ref) => (
106
+ <DialogPrimitive.Title
107
+ ref={ref}
108
+ className={cn(
109
+ "text-md font-semibold tracking-[-0.01em] text-foreground",
110
+ className
111
+ )}
112
+ {...props}
113
+ />
114
+ ));
115
+ DrawerTitle.displayName = "DrawerTitle";
116
+
117
+ export const DrawerDescription = React.forwardRef<
118
+ React.ElementRef<typeof DialogPrimitive.Description>,
119
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
120
+ >(({ className, ...props }, ref) => (
121
+ <DialogPrimitive.Description
122
+ ref={ref}
123
+ className={cn("text-xs text-text-muted mt-0.5", className)}
124
+ {...props}
125
+ />
126
+ ));
127
+ DrawerDescription.displayName = "DrawerDescription";
128
+
129
+ export function DrawerBody({
130
+ className,
131
+ ...props
132
+ }: React.HTMLAttributes<HTMLDivElement>) {
133
+ return (
134
+ <div
135
+ className={cn(
136
+ "px-5 py-4 text-sm text-foreground overflow-y-auto flex-1",
137
+ className
138
+ )}
139
+ {...props}
140
+ />
141
+ );
142
+ }
143
+
144
+ export function DrawerFooter({
145
+ className,
146
+ ...props
147
+ }: React.HTMLAttributes<HTMLDivElement>) {
148
+ return (
149
+ <div
150
+ className={cn(
151
+ "flex items-center justify-end gap-2 px-5 py-3 border-t border-border bg-bg-subtle/50",
152
+ className
153
+ )}
154
+ {...props}
155
+ />
156
+ );
157
+ }
@@ -0,0 +1,200 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
5
+ import { Check, ChevronRight, Circle } from "lucide-react";
6
+ import { cn } from "../lib/utils";
7
+
8
+ /**
9
+ * DropdownMenu — for actions menus (kebab on table rows, user avatar
10
+ * menu in topbar). NOT for selecting a value — use Select for that.
11
+ *
12
+ * <DropdownMenu>
13
+ * <DropdownMenuTrigger asChild><Button>Actions</Button></DropdownMenuTrigger>
14
+ * <DropdownMenuContent>
15
+ * <DropdownMenuLabel>Game</DropdownMenuLabel>
16
+ * <DropdownMenuItem>View roster</DropdownMenuItem>
17
+ * <DropdownMenuItem>Broadcast…</DropdownMenuItem>
18
+ * <DropdownMenuSeparator />
19
+ * <DropdownMenuItem className="text-danger">Cancel game…</DropdownMenuItem>
20
+ * </DropdownMenuContent>
21
+ * </DropdownMenu>
22
+ */
23
+ export const DropdownMenu = DropdownMenuPrimitive.Root;
24
+ export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
25
+ export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
26
+ export const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
27
+ export const DropdownMenuSub = DropdownMenuPrimitive.Sub;
28
+ export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
29
+
30
+ export const DropdownMenuContent = React.forwardRef<
31
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
32
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
33
+ >(({ className, sideOffset = 4, ...props }, ref) => (
34
+ <DropdownMenuPrimitive.Portal>
35
+ <DropdownMenuPrimitive.Content
36
+ ref={ref}
37
+ sideOffset={sideOffset}
38
+ className={cn(
39
+ "z-50 min-w-[8rem] overflow-hidden rounded border border-border-strong bg-background text-foreground shadow-md p-1",
40
+ "data-[state=open]:animate-fade-in",
41
+ className
42
+ )}
43
+ {...props}
44
+ />
45
+ </DropdownMenuPrimitive.Portal>
46
+ ));
47
+ DropdownMenuContent.displayName = "DropdownMenuContent";
48
+
49
+ export const DropdownMenuItem = React.forwardRef<
50
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
51
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
52
+ inset?: boolean;
53
+ }
54
+ >(({ className, inset, ...props }, ref) => (
55
+ <DropdownMenuPrimitive.Item
56
+ ref={ref}
57
+ className={cn(
58
+ "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 pointer-coarse:min-h-11 text-sm outline-none transition-colors",
59
+ "focus:bg-bg-subtle focus:text-foreground",
60
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
61
+ inset && "pl-8",
62
+ className
63
+ )}
64
+ {...props}
65
+ />
66
+ ));
67
+ DropdownMenuItem.displayName = "DropdownMenuItem";
68
+
69
+ export const DropdownMenuCheckboxItem = React.forwardRef<
70
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
71
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
72
+ >(({ className, children, checked, ...props }, ref) => (
73
+ <DropdownMenuPrimitive.CheckboxItem
74
+ ref={ref}
75
+ className={cn(
76
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pointer-coarse:min-h-11 pl-8 pr-2 text-sm outline-none transition-colors",
77
+ "focus:bg-bg-subtle focus:text-foreground",
78
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
79
+ className
80
+ )}
81
+ checked={checked}
82
+ {...props}
83
+ >
84
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
85
+ <DropdownMenuPrimitive.ItemIndicator>
86
+ <Check className="h-3 w-3" strokeWidth={2.5} />
87
+ </DropdownMenuPrimitive.ItemIndicator>
88
+ </span>
89
+ {children}
90
+ </DropdownMenuPrimitive.CheckboxItem>
91
+ ));
92
+ DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem";
93
+
94
+ export const DropdownMenuRadioItem = React.forwardRef<
95
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
96
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
97
+ >(({ className, children, ...props }, ref) => (
98
+ <DropdownMenuPrimitive.RadioItem
99
+ ref={ref}
100
+ className={cn(
101
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pointer-coarse:min-h-11 pl-8 pr-2 text-sm outline-none transition-colors",
102
+ "focus:bg-bg-subtle focus:text-foreground",
103
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
104
+ className
105
+ )}
106
+ {...props}
107
+ >
108
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
109
+ <DropdownMenuPrimitive.ItemIndicator>
110
+ <Circle className="h-1.5 w-1.5 fill-current" strokeWidth={0} />
111
+ </DropdownMenuPrimitive.ItemIndicator>
112
+ </span>
113
+ {children}
114
+ </DropdownMenuPrimitive.RadioItem>
115
+ ));
116
+ DropdownMenuRadioItem.displayName = "DropdownMenuRadioItem";
117
+
118
+ export const DropdownMenuLabel = React.forwardRef<
119
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
120
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
121
+ inset?: boolean;
122
+ }
123
+ >(({ className, inset, ...props }, ref) => (
124
+ <DropdownMenuPrimitive.Label
125
+ ref={ref}
126
+ className={cn(
127
+ "px-2 py-1.5 text-[10px] font-semibold uppercase tracking-[0.06em] text-text-muted",
128
+ inset && "pl-8",
129
+ className
130
+ )}
131
+ {...props}
132
+ />
133
+ ));
134
+ DropdownMenuLabel.displayName = "DropdownMenuLabel";
135
+
136
+ export const DropdownMenuSeparator = React.forwardRef<
137
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
138
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
139
+ >(({ className, ...props }, ref) => (
140
+ <DropdownMenuPrimitive.Separator
141
+ ref={ref}
142
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
143
+ {...props}
144
+ />
145
+ ));
146
+ DropdownMenuSeparator.displayName = "DropdownMenuSeparator";
147
+
148
+ export function DropdownMenuShortcut({
149
+ className,
150
+ ...props
151
+ }: React.HTMLAttributes<HTMLSpanElement>) {
152
+ return (
153
+ <span
154
+ className={cn(
155
+ "ml-auto text-[10px] font-mono tracking-widest text-text-subtle tabular",
156
+ className
157
+ )}
158
+ {...props}
159
+ />
160
+ );
161
+ }
162
+
163
+ export const DropdownMenuSubTrigger = React.forwardRef<
164
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
165
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
166
+ inset?: boolean;
167
+ }
168
+ >(({ className, inset, children, ...props }, ref) => (
169
+ <DropdownMenuPrimitive.SubTrigger
170
+ ref={ref}
171
+ className={cn(
172
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 pointer-coarse:min-h-11 text-sm outline-none transition-colors",
173
+ "focus:bg-bg-subtle focus:text-foreground",
174
+ "data-[state=open]:bg-bg-subtle",
175
+ inset && "pl-8",
176
+ className
177
+ )}
178
+ {...props}
179
+ >
180
+ {children}
181
+ <ChevronRight className="ml-auto h-3 w-3" strokeWidth={1.75} />
182
+ </DropdownMenuPrimitive.SubTrigger>
183
+ ));
184
+ DropdownMenuSubTrigger.displayName = "DropdownMenuSubTrigger";
185
+
186
+ export const DropdownMenuSubContent = React.forwardRef<
187
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
188
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
189
+ >(({ className, ...props }, ref) => (
190
+ <DropdownMenuPrimitive.SubContent
191
+ ref={ref}
192
+ className={cn(
193
+ "z-50 min-w-[8rem] overflow-hidden rounded border border-border-strong bg-background text-foreground shadow-md p-1",
194
+ "data-[state=open]:animate-fade-in",
195
+ className
196
+ )}
197
+ {...props}
198
+ />
199
+ ));
200
+ DropdownMenuSubContent.displayName = "DropdownMenuSubContent";
@@ -0,0 +1,46 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ /**
5
+ * EmptyState — per design.md §10
6
+ * - Centered, single line text-sm muted
7
+ * - Optional small CTA
8
+ * - NO illustrations. A subtle Lucide icon at most.
9
+ */
10
+ export interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
11
+ icon?: React.ReactNode;
12
+ title: string;
13
+ description?: string;
14
+ action?: React.ReactNode;
15
+ }
16
+
17
+ export function EmptyState({
18
+ icon,
19
+ title,
20
+ description,
21
+ action,
22
+ className,
23
+ ...props
24
+ }: EmptyStateProps) {
25
+ return (
26
+ <div
27
+ role="status"
28
+ className={cn(
29
+ "flex flex-col items-center justify-center gap-2 px-6 py-10 text-center",
30
+ className
31
+ )}
32
+ {...props}
33
+ >
34
+ {icon && (
35
+ <div className="text-text-subtle mb-1" aria-hidden="true">
36
+ {icon}
37
+ </div>
38
+ )}
39
+ <div className="text-sm font-medium text-foreground">{title}</div>
40
+ {description && (
41
+ <div className="text-xs text-text-muted max-w-xs">{description}</div>
42
+ )}
43
+ {action && <div className="mt-2">{action}</div>}
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,118 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "../lib/utils";
5
+
6
+ /**
7
+ * FormField — composition wrapper that auto-wires Label + control +
8
+ * helper/error text + the right aria attributes.
9
+ *
10
+ * <FormField label="Email" required helper="We'll send confirmations here.">
11
+ * <Input type="email" autoComplete="email" />
12
+ * </FormField>
13
+ *
14
+ * <FormField label="Card" required error="Card number is too short.">
15
+ * <Input />
16
+ * </FormField>
17
+ *
18
+ * The child input is cloned with: `id`, `aria-describedby`, `aria-invalid`,
19
+ * so consumers don't have to remember the a11y plumbing.
20
+ */
21
+ export interface FormFieldProps {
22
+ /** Optional explicit id — autogenerated if omitted. */
23
+ id?: string;
24
+ label?: React.ReactNode;
25
+ required?: boolean;
26
+ helper?: React.ReactNode;
27
+ error?: React.ReactNode;
28
+ /** Hide label visually but keep for screen readers */
29
+ hideLabel?: boolean;
30
+ /** Inline layout — label sits on the left of the control */
31
+ inline?: boolean;
32
+ className?: string;
33
+ children: React.ReactNode;
34
+ }
35
+
36
+ export function FormField({
37
+ id,
38
+ label,
39
+ required,
40
+ helper,
41
+ error,
42
+ hideLabel,
43
+ inline,
44
+ className,
45
+ children,
46
+ }: FormFieldProps) {
47
+ const reactId = React.useId();
48
+ const fieldId = id ?? reactId;
49
+ const helperId = `${fieldId}-helper`;
50
+ const errorId = `${fieldId}-error`;
51
+
52
+ const describedBy =
53
+ [error ? errorId : null, helper && !error ? helperId : null]
54
+ .filter(Boolean)
55
+ .join(" ") || undefined;
56
+
57
+ // Clone first valid child so we can inject id + aria props.
58
+ const child = React.Children.map(children, (c) => {
59
+ if (!React.isValidElement(c)) return c;
60
+ const el = c as React.ReactElement<{
61
+ id?: string;
62
+ "aria-describedby"?: string;
63
+ "aria-invalid"?: boolean;
64
+ }>;
65
+ return React.cloneElement(el, {
66
+ id: el.props.id ?? fieldId,
67
+ "aria-describedby": el.props["aria-describedby"] ?? describedBy,
68
+ "aria-invalid": el.props["aria-invalid"] ?? (error ? true : undefined),
69
+ });
70
+ });
71
+
72
+ const labelEl = label && (
73
+ <label
74
+ htmlFor={fieldId}
75
+ className={cn(
76
+ "block text-xs font-medium uppercase tracking-[0.04em] text-text-muted",
77
+ hideLabel && "sr-only"
78
+ )}
79
+ >
80
+ {label}
81
+ {required && (
82
+ <span aria-hidden="true" className="ml-0.5 text-danger">
83
+ *
84
+ </span>
85
+ )}
86
+ </label>
87
+ );
88
+
89
+ const messages =
90
+ error || helper ? (
91
+ <p
92
+ id={error ? errorId : helperId}
93
+ className={cn("text-xs", error ? "text-danger" : "text-text-muted")}
94
+ >
95
+ {error ?? helper}
96
+ </p>
97
+ ) : null;
98
+
99
+ if (inline) {
100
+ return (
101
+ <div className={cn("flex items-start gap-3", className)}>
102
+ {labelEl && <div className="pt-1.5 w-32 shrink-0">{labelEl}</div>}
103
+ <div className="flex-1 min-w-0 space-y-1">
104
+ {child}
105
+ {messages}
106
+ </div>
107
+ </div>
108
+ );
109
+ }
110
+
111
+ return (
112
+ <div className={cn("space-y-1.5", className)}>
113
+ {labelEl}
114
+ {child}
115
+ {messages}
116
+ </div>
117
+ );
118
+ }
@@ -0,0 +1,29 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ /**
5
+ * Input — per design.md §10
6
+ * Height 32px, padding 0 10px, 1px border-strong, 2px radius.
7
+ * Focus: 2px ring in the theme ring color (no border color change).
8
+ * Error: pass `aria-invalid` true → border + ring switch to danger.
9
+ */
10
+ export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
11
+
12
+ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
13
+ ({ className, type = "text", ...props }, ref) => (
14
+ <input
15
+ ref={ref}
16
+ type={type}
17
+ className={cn(
18
+ "flex h-8 pointer-coarse:h-11 w-full rounded-sm border border-border-strong bg-background px-2.5 text-sm text-foreground transition-colors duration-150 ease-out",
19
+ "placeholder:text-text-subtle",
20
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
21
+ "disabled:cursor-not-allowed disabled:bg-bg-muted disabled:opacity-60",
22
+ "aria-[invalid=true]:border-danger aria-[invalid=true]:focus-visible:ring-danger",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ )
28
+ );
29
+ Input.displayName = "Input";
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as LabelPrimitive from "@radix-ui/react-label";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * Label — per design.md §10
9
+ * Uppercase, tracking-wide, text-xs, muted color.
10
+ * Always pair with its input via `htmlFor`.
11
+ */
12
+ export const Label = React.forwardRef<
13
+ React.ElementRef<typeof LabelPrimitive.Root>,
14
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & {
15
+ required?: boolean;
16
+ }
17
+ >(({ className, children, required, ...props }, ref) => (
18
+ <LabelPrimitive.Root
19
+ ref={ref}
20
+ className={cn(
21
+ "block text-xs font-medium uppercase tracking-[0.04em] text-text-muted",
22
+ "peer-disabled:cursor-not-allowed peer-disabled:opacity-60",
23
+ className
24
+ )}
25
+ {...props}
26
+ >
27
+ {children}
28
+ {required && (
29
+ <span aria-hidden="true" className="ml-0.5 text-danger">
30
+ *
31
+ </span>
32
+ )}
33
+ </LabelPrimitive.Root>
34
+ ));
35
+ Label.displayName = "Label";
@@ -0,0 +1,35 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ /**
5
+ * LivePill — per design.md §10 & §12
6
+ * The ONE continuously animated element in the system.
7
+ * Renders a lime pill with a pulsing dot and "LIVE" label.
8
+ *
9
+ * Accessibility:
10
+ * - aria-live="polite" so SR users hear "Live" without interrupting
11
+ * - The pulse animation honors prefers-reduced-motion via globals.css
12
+ */
13
+ export function LivePill({
14
+ className,
15
+ label = "Live",
16
+ ...props
17
+ }: React.HTMLAttributes<HTMLSpanElement> & { label?: string }) {
18
+ return (
19
+ <span
20
+ role="status"
21
+ aria-live="polite"
22
+ className={cn(
23
+ "inline-flex items-center gap-1.5 rounded-full bg-brand-lime px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.06em] text-brand-lime-foreground",
24
+ className
25
+ )}
26
+ {...props}
27
+ >
28
+ <span
29
+ className="block h-1.5 w-1.5 rounded-full bg-brand-lime-foreground animate-live-pulse"
30
+ aria-hidden="true"
31
+ />
32
+ {label}
33
+ </span>
34
+ );
35
+ }