@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,169 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as SelectPrimitive from "@radix-ui/react-select";
5
+ import { Check, ChevronDown, ChevronUp } from "lucide-react";
6
+ import { cn } from "../lib/utils";
7
+
8
+ /**
9
+ * Select — keyboard-accessible dropdown built on Radix.
10
+ *
11
+ * <Select value={v} onValueChange={setV}>
12
+ * <SelectTrigger><SelectValue placeholder="Pick a venue" /></SelectTrigger>
13
+ * <SelectContent>
14
+ * <SelectItem value="astoria">Astoria Turf</SelectItem>
15
+ * …
16
+ * </SelectContent>
17
+ * </Select>
18
+ *
19
+ * Native <select> is fine for tiny lists in dense forms; reach for this
20
+ * when you want typeahead, item groups, or styled portals.
21
+ */
22
+ export const Select = SelectPrimitive.Root;
23
+ export const SelectGroup = SelectPrimitive.Group;
24
+ export const SelectValue = SelectPrimitive.Value;
25
+
26
+ export const SelectTrigger = React.forwardRef<
27
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
28
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
29
+ >(({ className, children, ...props }, ref) => (
30
+ <SelectPrimitive.Trigger
31
+ ref={ref}
32
+ className={cn(
33
+ "flex h-8 pointer-coarse:h-11 w-full items-center justify-between gap-2 rounded-sm border border-border-strong bg-background px-2.5 text-sm",
34
+ "placeholder:text-text-subtle",
35
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
36
+ "disabled:cursor-not-allowed disabled:opacity-50",
37
+ "[&>span]:line-clamp-1",
38
+ className
39
+ )}
40
+ {...props}
41
+ >
42
+ {children}
43
+ <SelectPrimitive.Icon asChild>
44
+ <ChevronDown
45
+ className="h-3.5 w-3.5 opacity-60 shrink-0"
46
+ strokeWidth={1.75}
47
+ />
48
+ </SelectPrimitive.Icon>
49
+ </SelectPrimitive.Trigger>
50
+ ));
51
+ SelectTrigger.displayName = "SelectTrigger";
52
+
53
+ export const SelectScrollUpButton = React.forwardRef<
54
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
55
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
56
+ >(({ className, ...props }, ref) => (
57
+ <SelectPrimitive.ScrollUpButton
58
+ ref={ref}
59
+ className={cn(
60
+ "flex cursor-default items-center justify-center py-1",
61
+ className
62
+ )}
63
+ {...props}
64
+ >
65
+ <ChevronUp className="h-3 w-3" strokeWidth={1.75} />
66
+ </SelectPrimitive.ScrollUpButton>
67
+ ));
68
+ SelectScrollUpButton.displayName = "SelectScrollUpButton";
69
+
70
+ export const SelectScrollDownButton = React.forwardRef<
71
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
72
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
73
+ >(({ className, ...props }, ref) => (
74
+ <SelectPrimitive.ScrollDownButton
75
+ ref={ref}
76
+ className={cn(
77
+ "flex cursor-default items-center justify-center py-1",
78
+ className
79
+ )}
80
+ {...props}
81
+ >
82
+ <ChevronDown className="h-3 w-3" strokeWidth={1.75} />
83
+ </SelectPrimitive.ScrollDownButton>
84
+ ));
85
+ SelectScrollDownButton.displayName = "SelectScrollDownButton";
86
+
87
+ export const SelectContent = React.forwardRef<
88
+ React.ElementRef<typeof SelectPrimitive.Content>,
89
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
90
+ >(({ className, children, position = "popper", ...props }, ref) => (
91
+ <SelectPrimitive.Portal>
92
+ <SelectPrimitive.Content
93
+ ref={ref}
94
+ className={cn(
95
+ "relative z-50 max-h-80 min-w-[8rem] overflow-hidden rounded-md border border-border-strong bg-background text-foreground shadow-md",
96
+ "data-[state=open]:animate-fade-in",
97
+ position === "popper" &&
98
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
99
+ className
100
+ )}
101
+ position={position}
102
+ {...props}
103
+ >
104
+ <SelectScrollUpButton />
105
+ <SelectPrimitive.Viewport
106
+ className={cn(
107
+ "p-1",
108
+ position === "popper" &&
109
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
110
+ )}
111
+ >
112
+ {children}
113
+ </SelectPrimitive.Viewport>
114
+ <SelectScrollDownButton />
115
+ </SelectPrimitive.Content>
116
+ </SelectPrimitive.Portal>
117
+ ));
118
+ SelectContent.displayName = "SelectContent";
119
+
120
+ export const SelectLabel = React.forwardRef<
121
+ React.ElementRef<typeof SelectPrimitive.Label>,
122
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
123
+ >(({ className, ...props }, ref) => (
124
+ <SelectPrimitive.Label
125
+ ref={ref}
126
+ className={cn(
127
+ "py-1.5 pl-7 pr-2 text-[10px] font-semibold uppercase tracking-[0.06em] text-text-muted",
128
+ className
129
+ )}
130
+ {...props}
131
+ />
132
+ ));
133
+ SelectLabel.displayName = "SelectLabel";
134
+
135
+ export const SelectItem = React.forwardRef<
136
+ React.ElementRef<typeof SelectPrimitive.Item>,
137
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
138
+ >(({ className, children, ...props }, ref) => (
139
+ <SelectPrimitive.Item
140
+ ref={ref}
141
+ className={cn(
142
+ "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pointer-coarse:min-h-11 pl-7 pr-2 text-sm outline-none transition-colors",
143
+ "focus:bg-bg-subtle focus:text-foreground",
144
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
145
+ className
146
+ )}
147
+ {...props}
148
+ >
149
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
150
+ <SelectPrimitive.ItemIndicator>
151
+ <Check className="h-3 w-3" strokeWidth={2.5} />
152
+ </SelectPrimitive.ItemIndicator>
153
+ </span>
154
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
155
+ </SelectPrimitive.Item>
156
+ ));
157
+ SelectItem.displayName = "SelectItem";
158
+
159
+ export const SelectSeparator = React.forwardRef<
160
+ React.ElementRef<typeof SelectPrimitive.Separator>,
161
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
162
+ >(({ className, ...props }, ref) => (
163
+ <SelectPrimitive.Separator
164
+ ref={ref}
165
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
166
+ {...props}
167
+ />
168
+ ));
169
+ SelectSeparator.displayName = "SelectSeparator";
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as SeparatorPrimitive from "@radix-ui/react-separator";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * Separator — a thin divider (`<hr>` for horizontal, vertical bar for
9
+ * vertical). Decorative by default. Set `decorative={false}` when the
10
+ * separator semantically separates content groups (screen readers will
11
+ * announce it).
12
+ */
13
+ export const Separator = React.forwardRef<
14
+ React.ElementRef<typeof SeparatorPrimitive.Root>,
15
+ React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
16
+ >(
17
+ (
18
+ { className, orientation = "horizontal", decorative = true, ...props },
19
+ ref
20
+ ) => (
21
+ <SeparatorPrimitive.Root
22
+ ref={ref}
23
+ decorative={decorative}
24
+ orientation={orientation}
25
+ className={cn(
26
+ "shrink-0 bg-border",
27
+ orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
28
+ className
29
+ )}
30
+ {...props}
31
+ />
32
+ )
33
+ );
34
+ Separator.displayName = "Separator";
@@ -0,0 +1,168 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Slot } from "@radix-ui/react-slot";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * Sidebar primitives — composable nav sidebar.
9
+ *
10
+ * <Sidebar>
11
+ * <SidebarHeader><BrandMark /></SidebarHeader>
12
+ * <SidebarBody>
13
+ * <SidebarSection label="Today">
14
+ * <SidebarItem asChild active>
15
+ * <Link href="/admin">
16
+ * <LayoutDashboard className="h-4 w-4" strokeWidth={1.75} />
17
+ * <span className="flex-1 truncate">Today</span>
18
+ * </Link>
19
+ * </SidebarItem>
20
+ * </SidebarSection>
21
+ * </SidebarBody>
22
+ * <SidebarFooter>v0.1</SidebarFooter>
23
+ * </Sidebar>
24
+ *
25
+ * For non-Next contexts, pass `href` prop directly; SidebarItem renders
26
+ * an <a> by default.
27
+ */
28
+ export const Sidebar = React.forwardRef<
29
+ HTMLElement,
30
+ React.HTMLAttributes<HTMLElement>
31
+ >(({ className, ...props }, ref) => (
32
+ <aside
33
+ ref={ref}
34
+ aria-label="Sidebar navigation"
35
+ className={cn(
36
+ "hidden lg:flex flex-col w-56 shrink-0 border-r border-border bg-bg-subtle/40 sticky top-0 h-screen",
37
+ className
38
+ )}
39
+ {...props}
40
+ />
41
+ ));
42
+ Sidebar.displayName = "Sidebar";
43
+
44
+ export const SidebarHeader = React.forwardRef<
45
+ HTMLDivElement,
46
+ React.HTMLAttributes<HTMLDivElement>
47
+ >(({ className, ...props }, ref) => (
48
+ <div
49
+ ref={ref}
50
+ className={cn("px-5 py-4 border-b border-border", className)}
51
+ {...props}
52
+ />
53
+ ));
54
+ SidebarHeader.displayName = "SidebarHeader";
55
+
56
+ export const SidebarBody = React.forwardRef<
57
+ HTMLElement,
58
+ React.HTMLAttributes<HTMLElement>
59
+ >(({ className, ...props }, ref) => (
60
+ <nav
61
+ ref={ref}
62
+ className={cn("flex-1 overflow-y-auto scrollbar-thin py-3 px-2", className)}
63
+ {...props}
64
+ />
65
+ ));
66
+ SidebarBody.displayName = "SidebarBody";
67
+
68
+ export const SidebarFooter = React.forwardRef<
69
+ HTMLDivElement,
70
+ React.HTMLAttributes<HTMLDivElement>
71
+ >(({ className, ...props }, ref) => (
72
+ <div
73
+ ref={ref}
74
+ className={cn("px-3 py-3 border-t border-border", className)}
75
+ {...props}
76
+ />
77
+ ));
78
+ SidebarFooter.displayName = "SidebarFooter";
79
+
80
+ export interface SidebarSectionProps extends React.HTMLAttributes<HTMLDivElement> {
81
+ label?: React.ReactNode;
82
+ }
83
+
84
+ export const SidebarSection = React.forwardRef<
85
+ HTMLDivElement,
86
+ SidebarSectionProps
87
+ >(({ className, label, children, ...props }, ref) => (
88
+ <div ref={ref} className={cn("mt-4 first:mt-0", className)} {...props}>
89
+ {label && (
90
+ <div className="px-2.5 pb-1 text-[10px] font-semibold uppercase tracking-[0.06em] text-text-subtle">
91
+ {label}
92
+ </div>
93
+ )}
94
+ <ul className="space-y-0.5">
95
+ {React.Children.map(children, (child) =>
96
+ child == null || child === false ? null : <li>{child}</li>
97
+ )}
98
+ </ul>
99
+ </div>
100
+ ));
101
+ SidebarSection.displayName = "SidebarSection";
102
+
103
+ export interface SidebarItemProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
104
+ active?: boolean;
105
+ /** Lucide icon (or any node). Rendered before the label. */
106
+ icon?: React.ReactNode;
107
+ /** Right-aligned badge (e.g., warning count). */
108
+ badge?: React.ReactNode;
109
+ /** Use Radix Slot to render a custom element (e.g., Next.js Link). */
110
+ asChild?: boolean;
111
+ }
112
+
113
+ export const SidebarItem = React.forwardRef<
114
+ HTMLAnchorElement,
115
+ SidebarItemProps
116
+ >(
117
+ (
118
+ { asChild, active, icon, badge, className, children, href, ...rest },
119
+ ref
120
+ ) => {
121
+ const baseClasses = cn(
122
+ "group flex items-center gap-2.5 rounded-sm px-2.5 py-1.5 text-sm transition-colors",
123
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
124
+ active
125
+ ? "bg-bg-subtle text-foreground font-medium border-l-2 border-l-brand-forest -ml-px pl-[10px]"
126
+ : "text-text-muted hover:bg-bg-muted hover:text-foreground",
127
+ className
128
+ );
129
+
130
+ if (asChild) {
131
+ const SlotComp = Slot as React.ElementType;
132
+ return (
133
+ <SlotComp
134
+ ref={ref}
135
+ className={baseClasses}
136
+ aria-current={active ? "page" : undefined}
137
+ {...rest}
138
+ >
139
+ {children}
140
+ </SlotComp>
141
+ );
142
+ }
143
+
144
+ return (
145
+ <a
146
+ ref={ref}
147
+ href={href}
148
+ className={baseClasses}
149
+ aria-current={active ? "page" : undefined}
150
+ {...rest}
151
+ >
152
+ {icon && (
153
+ <span
154
+ className={cn(
155
+ "shrink-0 flex items-center",
156
+ active ? "text-brand-forest" : "text-text-muted"
157
+ )}
158
+ >
159
+ {icon}
160
+ </span>
161
+ )}
162
+ <span className="flex-1 truncate">{children}</span>
163
+ {badge && <span className="shrink-0">{badge}</span>}
164
+ </a>
165
+ );
166
+ }
167
+ );
168
+ SidebarItem.displayName = "SidebarItem";
@@ -0,0 +1,85 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ /**
5
+ * Skeleton — per design.md §12
6
+ * Use over spinners. Pulse opacity 0.5 → 1, 1.5s. Respects prefers-reduced-motion.
7
+ */
8
+ export function Skeleton({
9
+ className,
10
+ ...props
11
+ }: React.HTMLAttributes<HTMLDivElement>) {
12
+ return (
13
+ <div
14
+ className={cn("animate-pulse rounded bg-bg-muted", className)}
15
+ aria-hidden="true"
16
+ {...props}
17
+ />
18
+ );
19
+ }
20
+
21
+ /**
22
+ * SkeletonList — loading placeholder echoing a row list (§3.14:
23
+ * skeletons echo the final shape). Use as the `loading` slot of
24
+ * ThreeState for list views.
25
+ */
26
+ export function SkeletonList({
27
+ rows = 3,
28
+ className,
29
+ ...props
30
+ }: React.HTMLAttributes<HTMLDivElement> & { rows?: number }) {
31
+ return (
32
+ <div
33
+ className={cn("space-y-2", className)}
34
+ role="status"
35
+ aria-label="Loading"
36
+ {...props}
37
+ >
38
+ {Array.from({ length: rows }, (_, i) => (
39
+ <div key={i} className="flex items-center gap-3">
40
+ <Skeleton className="h-8 w-8 rounded-full shrink-0" />
41
+ <div className="flex-1 space-y-1.5">
42
+ <Skeleton className="h-3 w-2/5" />
43
+ <Skeleton className="h-3 w-3/5" />
44
+ </div>
45
+ </div>
46
+ ))}
47
+ </div>
48
+ );
49
+ }
50
+
51
+ /**
52
+ * SkeletonTable — loading placeholder echoing a data table. Match
53
+ * `columns` to the real table so the layout doesn't jump on load.
54
+ */
55
+ export function SkeletonTable({
56
+ rows = 5,
57
+ columns = 4,
58
+ className,
59
+ ...props
60
+ }: React.HTMLAttributes<HTMLDivElement> & {
61
+ rows?: number;
62
+ columns?: number;
63
+ }) {
64
+ return (
65
+ <div
66
+ className={cn("space-y-2", className)}
67
+ role="status"
68
+ aria-label="Loading"
69
+ {...props}
70
+ >
71
+ <div className="flex gap-4 border-b border-border pb-2">
72
+ {Array.from({ length: columns }, (_, i) => (
73
+ <Skeleton key={i} className="h-3 flex-1" />
74
+ ))}
75
+ </div>
76
+ {Array.from({ length: rows }, (_, r) => (
77
+ <div key={r} className="flex gap-4 py-1">
78
+ {Array.from({ length: columns }, (_, c) => (
79
+ <Skeleton key={c} className="h-4 flex-1" />
80
+ ))}
81
+ </div>
82
+ ))}
83
+ </div>
84
+ );
85
+ }
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as SliderPrimitive from "@radix-ui/react-slider";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * Slider — Radix-backed range input. Keyboard: arrows step, PageUp/Down
9
+ * big-step, Home/End min/max. Pass `aria-label` (or wire a Label to the
10
+ * thumb via `aria-labelledby`) — the a11y suite enforces a name.
11
+ *
12
+ * <Slider aria-label="Max price" min={0} max={50} step={5}
13
+ * defaultValue={[20]} />
14
+ *
15
+ * Supports multiple thumbs by passing more values: defaultValue={[10, 30]}.
16
+ */
17
+ export const Slider = React.forwardRef<
18
+ React.ElementRef<typeof SliderPrimitive.Root>,
19
+ React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
20
+ >(({ className, "aria-label": ariaLabel, ...props }, ref) => {
21
+ const values = props.value ?? props.defaultValue ?? [0];
22
+ // role="slider" lives on each Radix THUMB, not the root — a label on
23
+ // the root is invisible to screen readers, so name the thumbs.
24
+ const thumbLabel = (i: number) =>
25
+ values.length > 1
26
+ ? `${ariaLabel} (${i + 1} of ${values.length})`
27
+ : ariaLabel;
28
+ return (
29
+ <SliderPrimitive.Root
30
+ ref={ref}
31
+ className={cn(
32
+ "relative flex w-full touch-none select-none items-center",
33
+ // Comfortable grab zone without visual bulk.
34
+ "h-5 pointer-coarse:h-11",
35
+ "data-[disabled]:opacity-50 data-[disabled]:pointer-events-none",
36
+ className
37
+ )}
38
+ {...props}
39
+ >
40
+ <SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-bg-muted">
41
+ <SliderPrimitive.Range className="absolute h-full bg-brand-forest" />
42
+ </SliderPrimitive.Track>
43
+ {values.map((_, i) => (
44
+ <SliderPrimitive.Thumb
45
+ key={i}
46
+ aria-label={thumbLabel(i)}
47
+ className={cn(
48
+ "block h-4 w-4 rounded-full border-2 border-brand-forest bg-background shadow-sm transition-colors",
49
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
50
+ // Invisible hit-area extension: 24px desktop / 44px touch.
51
+ "relative after:absolute after:-inset-1 pointer-coarse:after:-inset-3.5"
52
+ )}
53
+ />
54
+ ))}
55
+ </SliderPrimitive.Root>
56
+ );
57
+ });
58
+ Slider.displayName = "Slider";
@@ -0,0 +1,59 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ /**
5
+ * SpecList + Spec — semantic key-value display (`<dl>` / `<dt>` / `<dd>`).
6
+ * Use for game detail "About this game" panels, settings summaries,
7
+ * receipts.
8
+ *
9
+ * <SpecList columns={2}>
10
+ * <Spec label="Format">5v5</Spec>
11
+ * <Spec label="Capacity">10 players</Spec>
12
+ * <Spec label="Start"><Timestamp iso={…} /></Spec>
13
+ * </SpecList>
14
+ */
15
+ const COLUMNS_MAP = {
16
+ 1: "grid-cols-1",
17
+ 2: "grid-cols-2",
18
+ 3: "grid-cols-3",
19
+ 4: "grid-cols-4",
20
+ } as const;
21
+
22
+ const GAP_MAP = {
23
+ 2: "gap-2",
24
+ 3: "gap-3",
25
+ 4: "gap-4",
26
+ 6: "gap-6",
27
+ } as const;
28
+
29
+ export interface SpecListProps extends React.HTMLAttributes<HTMLDListElement> {
30
+ columns?: keyof typeof COLUMNS_MAP;
31
+ gap?: keyof typeof GAP_MAP;
32
+ }
33
+
34
+ export const SpecList = React.forwardRef<HTMLDListElement, SpecListProps>(
35
+ ({ className, columns = 2, gap = 4, ...props }, ref) => (
36
+ <dl
37
+ ref={ref}
38
+ className={cn("grid", COLUMNS_MAP[columns], GAP_MAP[gap], className)}
39
+ {...props}
40
+ />
41
+ )
42
+ );
43
+ SpecList.displayName = "SpecList";
44
+
45
+ export interface SpecProps extends React.HTMLAttributes<HTMLDivElement> {
46
+ label: React.ReactNode;
47
+ }
48
+
49
+ export const Spec = React.forwardRef<HTMLDivElement, SpecProps>(
50
+ ({ className, label, children, ...props }, ref) => (
51
+ <div ref={ref} className={cn("", className)} {...props}>
52
+ <dt className="text-[10px] font-medium uppercase tracking-[0.04em] text-text-muted">
53
+ {label}
54
+ </dt>
55
+ <dd className="text-sm mt-0.5 text-foreground">{children}</dd>
56
+ </div>
57
+ )
58
+ );
59
+ Spec.displayName = "Spec";
@@ -0,0 +1,36 @@
1
+ import { Loader2 } from "lucide-react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ /**
5
+ * Spinner — tiny inline loader for buttons + per-row "working" state.
6
+ * Prefer Skeleton for full-region loading (per design.md §3.14).
7
+ *
8
+ * Honors prefers-reduced-motion via globals.css.
9
+ */
10
+ const SIZE_MAP = {
11
+ sm: "h-3 w-3",
12
+ default: "h-4 w-4",
13
+ lg: "h-5 w-5",
14
+ } as const;
15
+
16
+ export interface SpinnerProps {
17
+ size?: keyof typeof SIZE_MAP;
18
+ className?: string;
19
+ /** Accessible label (default "Loading") */
20
+ label?: string;
21
+ }
22
+
23
+ export function Spinner({
24
+ size = "default",
25
+ className,
26
+ label = "Loading",
27
+ }: SpinnerProps) {
28
+ return (
29
+ <Loader2
30
+ role="status"
31
+ aria-label={label}
32
+ className={cn("animate-spin", SIZE_MAP[size], className)}
33
+ strokeWidth={2}
34
+ />
35
+ );
36
+ }
@@ -0,0 +1,80 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ /**
5
+ * Stack — flexbox with consistent gap. Use Stack when you'd otherwise
6
+ * reach for `flex flex-col gap-3` (or `space-y-3`); it documents intent
7
+ * and keeps gap tokens consistent across the app.
8
+ *
9
+ * <Stack gap={2}> // vertical, gap-2
10
+ * <Stack direction="row" gap={4} align="center" justify="between">
11
+ */
12
+ const GAP_MAP = {
13
+ 0: "gap-0",
14
+ 0.5: "gap-0.5",
15
+ 1: "gap-1",
16
+ 1.5: "gap-1.5",
17
+ 2: "gap-2",
18
+ 2.5: "gap-2.5",
19
+ 3: "gap-3",
20
+ 4: "gap-4",
21
+ 5: "gap-5",
22
+ 6: "gap-6",
23
+ 8: "gap-8",
24
+ 10: "gap-10",
25
+ } as const;
26
+
27
+ const ALIGN_MAP = {
28
+ start: "items-start",
29
+ center: "items-center",
30
+ end: "items-end",
31
+ baseline: "items-baseline",
32
+ stretch: "items-stretch",
33
+ } as const;
34
+
35
+ const JUSTIFY_MAP = {
36
+ start: "justify-start",
37
+ center: "justify-center",
38
+ end: "justify-end",
39
+ between: "justify-between",
40
+ around: "justify-around",
41
+ evenly: "justify-evenly",
42
+ } as const;
43
+
44
+ export interface StackProps extends React.HTMLAttributes<HTMLDivElement> {
45
+ direction?: "column" | "row";
46
+ gap?: keyof typeof GAP_MAP;
47
+ align?: keyof typeof ALIGN_MAP;
48
+ justify?: keyof typeof JUSTIFY_MAP;
49
+ wrap?: boolean;
50
+ }
51
+
52
+ export const Stack = React.forwardRef<HTMLDivElement, StackProps>(
53
+ (
54
+ {
55
+ className,
56
+ direction = "column",
57
+ gap = 3,
58
+ align,
59
+ justify,
60
+ wrap,
61
+ ...props
62
+ },
63
+ ref
64
+ ) => (
65
+ <div
66
+ ref={ref}
67
+ className={cn(
68
+ "flex",
69
+ direction === "column" ? "flex-col" : "flex-row",
70
+ GAP_MAP[gap],
71
+ align && ALIGN_MAP[align],
72
+ justify && JUSTIFY_MAP[justify],
73
+ wrap && "flex-wrap",
74
+ className
75
+ )}
76
+ {...props}
77
+ />
78
+ )
79
+ );
80
+ Stack.displayName = "Stack";