@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,4216 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import {
5
+ AlertTriangle,
6
+ CalendarDays,
7
+ CalendarPlus,
8
+ CheckCircle2,
9
+ ChevronDown,
10
+ CircleHelp,
11
+ Edit,
12
+ Inbox,
13
+ Info,
14
+ LayoutDashboard,
15
+ MapPin,
16
+ MoreHorizontal,
17
+ Repeat,
18
+ Settings,
19
+ Trash2,
20
+ Trophy,
21
+ Users,
22
+ XCircle,
23
+ Zap,
24
+ } from "lucide-react";
25
+ import {
26
+ Accordion,
27
+ AccordionContent,
28
+ AccordionItem,
29
+ AccordionTrigger,
30
+ Alert,
31
+ AspectRatio,
32
+ Calendar,
33
+ Collapsible,
34
+ CollapsibleContent,
35
+ CollapsibleTrigger,
36
+ Combobox,
37
+ DatePicker,
38
+ SkeletonList,
39
+ SkeletonTable,
40
+ Slider,
41
+ Toaster,
42
+ toast,
43
+ Avatar,
44
+ AvatarGroup,
45
+ Badge,
46
+ Breadcrumb,
47
+ BreadcrumbItem,
48
+ BreadcrumbLink,
49
+ BreadcrumbList,
50
+ BreadcrumbPage,
51
+ BreadcrumbSeparator,
52
+ Button,
53
+ CapacityBar,
54
+ Chip,
55
+ Container,
56
+ Card,
57
+ CardContent,
58
+ CardDescription,
59
+ CardFooter,
60
+ CardHeader,
61
+ CardTitle,
62
+ Checkbox,
63
+ ConfirmAction,
64
+ Dialog,
65
+ DialogBody,
66
+ DialogContent,
67
+ DialogDescription,
68
+ DialogFooter,
69
+ DialogHeader,
70
+ DialogTitle,
71
+ DialogTrigger,
72
+ Drawer,
73
+ DrawerBody,
74
+ DrawerContent,
75
+ DrawerDescription,
76
+ DrawerFooter,
77
+ DrawerHeader,
78
+ DrawerTitle,
79
+ DrawerTrigger,
80
+ DropdownMenu,
81
+ DropdownMenuCheckboxItem,
82
+ DropdownMenuContent,
83
+ DropdownMenuItem,
84
+ DropdownMenuLabel,
85
+ DropdownMenuSeparator,
86
+ DropdownMenuShortcut,
87
+ DropdownMenuTrigger,
88
+ EmptyState,
89
+ FormField,
90
+ Input,
91
+ Label,
92
+ LivePill,
93
+ MetricTile,
94
+ MoneyBreakdown,
95
+ NumberInput,
96
+ Pagination,
97
+ Popover,
98
+ PopoverContent,
99
+ PopoverTrigger,
100
+ Progress,
101
+ RadioGroup,
102
+ RadioGroupItem,
103
+ ScrollArea,
104
+ Select,
105
+ SelectContent,
106
+ SelectGroup,
107
+ SelectItem,
108
+ SelectLabel,
109
+ SelectSeparator,
110
+ SelectTrigger,
111
+ SelectValue,
112
+ Separator,
113
+ SidebarBody,
114
+ SidebarFooter,
115
+ SidebarHeader,
116
+ SidebarItem,
117
+ SidebarSection,
118
+ Skeleton,
119
+ Spec,
120
+ SpecList,
121
+ Spinner,
122
+ Stack,
123
+ StatusPill,
124
+ Switch,
125
+ Table,
126
+ TableBody,
127
+ TableCaption,
128
+ TableCell,
129
+ TableHead,
130
+ TableHeader,
131
+ TableRow,
132
+ Tabs,
133
+ TabsContent,
134
+ TabsList,
135
+ TabsTrigger,
136
+ Textarea,
137
+ ThemeToggle,
138
+ ThreeState,
139
+ Timestamp,
140
+ Tooltip,
141
+ TooltipContent,
142
+ TooltipTrigger,
143
+ TopBar,
144
+ type GameStatus,
145
+ type ListState,
146
+ } from "..";
147
+
148
+ // Showcase-only helpers (live in the app, not the design system).
149
+ import { Footer, Hero, LogoCloud, MarketingNav, Prose } from "../marketing";
150
+ import { Swatch } from "./components/swatch";
151
+ import { DocSection, DocSubsection } from "./components/section";
152
+ import { CodeBlock } from "./components/code-block";
153
+ import { Example, PreviewCode } from "./components/example";
154
+
155
+ type NavItem = { id: string; label: string };
156
+ type NavGroup = { id: string; label: string; items: NavItem[] };
157
+
158
+ const NAV_GROUPS: NavGroup[] = [
159
+ {
160
+ id: "group-principles",
161
+ label: "Principles",
162
+ items: [
163
+ { id: "principles-visual", label: "Visual principles" },
164
+ { id: "principles-ux", label: "UX principles" },
165
+ ],
166
+ },
167
+ {
168
+ id: "group-foundations",
169
+ label: "Foundations",
170
+ items: [
171
+ { id: "colors", label: "Colors" },
172
+ { id: "typography", label: "Typography" },
173
+ { id: "spacing", label: "Spacing" },
174
+ { id: "radius", label: "Radius" },
175
+ { id: "elevation", label: "Elevation" },
176
+ { id: "icons", label: "Iconography" },
177
+ { id: "motion", label: "Motion" },
178
+ ],
179
+ },
180
+ {
181
+ id: "group-components",
182
+ label: "Components",
183
+ items: [
184
+ { id: "buttons", label: "Buttons" },
185
+ { id: "inputs", label: "Inputs" },
186
+ { id: "checkbox", label: "Checkbox" },
187
+ { id: "radio", label: "Radio group" },
188
+ { id: "switch", label: "Switch" },
189
+ { id: "select", label: "Select" },
190
+ { id: "combobox", label: "Combobox" },
191
+ { id: "textarea", label: "Textarea" },
192
+ { id: "number-input", label: "Number input" },
193
+ { id: "slider", label: "Slider" },
194
+ { id: "date-picker", label: "Date picker" },
195
+ { id: "form-field", label: "Form field" },
196
+ { id: "alert", label: "Alert" },
197
+ { id: "badges", label: "Badges" },
198
+ { id: "chip", label: "Chip" },
199
+ { id: "status", label: "Status pill" },
200
+ { id: "cards", label: "Cards" },
201
+ { id: "metrics", label: "Metric tiles" },
202
+ { id: "spec", label: "Spec list" },
203
+ { id: "capacity", label: "Capacity bar" },
204
+ { id: "tables", label: "Tables" },
205
+ { id: "pagination", label: "Pagination" },
206
+ { id: "tabs", label: "Tabs" },
207
+ { id: "accordion", label: "Accordion" },
208
+ { id: "collapsible", label: "Collapsible" },
209
+ { id: "dialog", label: "Dialog" },
210
+ { id: "drawer", label: "Drawer" },
211
+ { id: "popover", label: "Popover" },
212
+ { id: "dropdown", label: "Dropdown menu" },
213
+ { id: "tooltip", label: "Tooltip" },
214
+ { id: "separator", label: "Separator" },
215
+ { id: "toast", label: "Toasts" },
216
+ { id: "empty", label: "Empty states" },
217
+ { id: "avatars", label: "Avatars" },
218
+ { id: "avatar-group", label: "Avatar group" },
219
+ { id: "skeleton", label: "Skeleton" },
220
+ { id: "spinner", label: "Spinner" },
221
+ { id: "progress", label: "Progress" },
222
+ ],
223
+ },
224
+ {
225
+ id: "group-layout",
226
+ label: "Layout",
227
+ items: [
228
+ { id: "app-shell", label: "App shell" },
229
+ { id: "sidebar-comp", label: "Sidebar" },
230
+ { id: "top-bar", label: "Top bar" },
231
+ { id: "breadcrumb", label: "Breadcrumb" },
232
+ { id: "container", label: "Container" },
233
+ { id: "stack", label: "Stack" },
234
+ { id: "aspect-ratio", label: "Aspect ratio" },
235
+ { id: "scroll-area", label: "Scroll area" },
236
+ ],
237
+ },
238
+ {
239
+ id: "group-patterns",
240
+ label: "Patterns",
241
+ items: [{ id: "patterns", label: "UX patterns" }],
242
+ },
243
+ {
244
+ id: "group-marketing",
245
+ label: "Marketing",
246
+ items: [{ id: "marketing", label: "Marketing tier" }],
247
+ },
248
+ {
249
+ id: "group-reference",
250
+ label: "Reference",
251
+ items: [
252
+ { id: "a11y", label: "Accessibility" },
253
+ { id: "usage", label: "Using the system" },
254
+ ],
255
+ },
256
+ ];
257
+
258
+ export default function DesignSystemPage() {
259
+ return (
260
+ <div className="flex min-h-screen">
261
+ {/* Sidebar — sticky TOC */}
262
+ <aside
263
+ aria-label="Design system contents"
264
+ className="hidden lg:flex flex-col w-56 shrink-0 border-r border-border bg-bg-subtle/40 sticky top-0 h-screen"
265
+ >
266
+ <div className="px-5 py-4 border-b border-border">
267
+ <div className="text-xs font-medium uppercase tracking-[0.04em] text-text-muted">
268
+ DT Sports
269
+ </div>
270
+ <div className="text-md font-semibold tracking-[-0.01em] mt-0.5">
271
+ Design System
272
+ </div>
273
+ </div>
274
+ <nav className="flex-1 overflow-y-auto scrollbar-thin py-3 px-2">
275
+ {NAV_GROUPS.map((group, gi) => (
276
+ <div key={group.id} className={gi === 0 ? "" : "mt-4"}>
277
+ <div className="px-2.5 pb-1 text-[10px] font-semibold uppercase tracking-[0.06em] text-text-subtle">
278
+ {group.label}
279
+ </div>
280
+ <ul className="space-y-0.5">
281
+ {group.items.map((item) => (
282
+ <li key={item.id}>
283
+ <a
284
+ href={`#${item.id}`}
285
+ className="block rounded-sm px-2.5 py-1.5 text-sm text-text-muted hover:bg-bg-muted hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
286
+ >
287
+ {item.label}
288
+ </a>
289
+ </li>
290
+ ))}
291
+ </ul>
292
+ </div>
293
+ ))}
294
+ </nav>
295
+ </aside>
296
+
297
+ {/* Main */}
298
+ <main id="main" className="flex-1 min-w-0">
299
+ {/* Topbar */}
300
+ <header className="sticky top-0 z-20 bg-background/95 backdrop-blur border-b border-border">
301
+ <div className="flex h-12 items-center justify-between px-6 lg:px-8">
302
+ <div className="flex items-center gap-2 text-sm text-text-muted">
303
+ <span>Design System</span>
304
+ <span className="text-text-subtle">/</span>
305
+ <span className="text-foreground font-medium">v0.1</span>
306
+ </div>
307
+ <div className="flex items-center gap-2">
308
+ <Button variant="secondary" size="sm" asChild>
309
+ <a href="/admin">View admin demo →</a>
310
+ </Button>
311
+ <Badge variant="forest" size="sm">
312
+ Exo 2 · 14px base
313
+ </Badge>
314
+ <ThemeToggle />
315
+ </div>
316
+ </div>
317
+ </header>
318
+
319
+ {/* Page header */}
320
+ <div className="px-6 lg:px-8 pt-10 pb-6 max-w-5xl">
321
+ <h1 className="text-3xl font-bold tracking-[-0.01em]">
322
+ DT Sports — Design System
323
+ </h1>
324
+ <p className="mt-2 text-base text-text-muted max-w-2xl">
325
+ A dense, data-first UI for hosts and players. Sharp corners, small
326
+ spacing, sports-turf color. Built for tables and dashboards. This
327
+ page is the living source of truth — every section below is the real
328
+ component, rendered live.
329
+ </p>
330
+
331
+ <div className="mt-4 flex flex-wrap gap-2">
332
+ <Badge variant="lime" size="sm" className="rounded">
333
+ #B4D334
334
+ </Badge>
335
+ <Badge variant="forest" size="sm" className="rounded">
336
+ #176E3E
337
+ </Badge>
338
+ <Badge variant="outline" size="sm" className="rounded">
339
+ Exo 2
340
+ </Badge>
341
+ <Badge variant="outline" size="sm" className="rounded">
342
+ 4px base spacing
343
+ </Badge>
344
+ <Badge variant="outline" size="sm" className="rounded">
345
+ 2–6px corners
346
+ </Badge>
347
+ <Badge variant="outline" size="sm" className="rounded">
348
+ WCAG AA
349
+ </Badge>
350
+ </div>
351
+ </div>
352
+
353
+ <div className="px-6 lg:px-8 max-w-5xl pb-24">
354
+ {/* Principles */}
355
+ <GroupHeader
356
+ id="group-principles"
357
+ label="Principles"
358
+ description="The rules that decide every design call. Visual = what to draw. UX = what to do."
359
+ first
360
+ />
361
+ <VisualPrinciplesSection />
362
+ <UXPrinciplesSection />
363
+
364
+ {/* Foundations */}
365
+ <GroupHeader
366
+ id="group-foundations"
367
+ label="Foundations"
368
+ description="Design tokens — colors, type, spacing, radius, elevation, icons, motion. Everything else composes from these."
369
+ />
370
+ <ColorsSection />
371
+ <TypographySection />
372
+ <SpacingSection />
373
+ <RadiusSection />
374
+ <ElevationSection />
375
+ <IconsSection />
376
+ <MotionSection />
377
+
378
+ {/* Components */}
379
+ <GroupHeader
380
+ id="group-components"
381
+ label="Components"
382
+ description="The building blocks. Each is variant-driven via cva(), accepts className for composition, and ships with a11y baked in."
383
+ />
384
+ <ButtonsSection />
385
+ <InputsSection />
386
+ <CheckboxSection />
387
+ <RadioSection />
388
+ <SwitchSection />
389
+ <SelectSection />
390
+ <ComboboxSection />
391
+ <TextareaSection />
392
+ <NumberInputSection />
393
+ <SliderSection />
394
+ <DatePickerSection />
395
+ <FormFieldSection />
396
+ <AlertSection />
397
+ <BadgesSection />
398
+ <ChipSection />
399
+ <StatusSection />
400
+ <CardsSection />
401
+ <MetricsSection />
402
+ <SpecSection />
403
+ <CapacitySection />
404
+ <TablesSection />
405
+ <PaginationSection />
406
+ <TabsSection />
407
+ <AccordionSection />
408
+ <CollapsibleSection />
409
+ <DialogSection />
410
+ <DrawerSection />
411
+ <PopoverSection />
412
+ <DropdownSection />
413
+ <TooltipSection />
414
+ <SeparatorSection />
415
+ <ToastSection />
416
+ <EmptyStateSection />
417
+ <AvatarsSection />
418
+ <AvatarGroupSection />
419
+ <SkeletonSection />
420
+ <SpinnerSection />
421
+ <ProgressSection />
422
+
423
+ {/* Layout */}
424
+ <GroupHeader
425
+ id="group-layout"
426
+ label="Layout"
427
+ description="Page-level building blocks. AppShell + Sidebar + TopBar compose admin chrome; Container + Stack handle content padding and gaps; AspectRatio + ScrollArea handle bounded regions; Breadcrumb is the standard nav crumb trail."
428
+ />
429
+ <AppShellSection />
430
+ <SidebarComponentSection />
431
+ <TopBarSection />
432
+ <BreadcrumbSection />
433
+ <ContainerSection />
434
+ <StackSection />
435
+ <AspectRatioSection />
436
+ <ScrollAreaSection />
437
+
438
+ {/* Patterns */}
439
+ <GroupHeader
440
+ id="group-patterns"
441
+ label="Patterns"
442
+ description="Composed UX behaviors. Reach for these to honor the §3 UX principles without thinking about it."
443
+ />
444
+ <UXPatternsSection />
445
+
446
+ {/* Marketing */}
447
+ <GroupHeader
448
+ id="group-marketing"
449
+ label="Marketing"
450
+ description="The website tier — fluid display type, generous spacing, page-level primitives. Import from @dtsports/design-system/marketing."
451
+ />
452
+ <MarketingTierSection />
453
+
454
+ {/* Reference */}
455
+ <GroupHeader
456
+ id="group-reference"
457
+ label="Reference"
458
+ description="How the system is wired up + how to consume it."
459
+ />
460
+ <AccessibilitySection />
461
+ <UsageSection />
462
+ </div>
463
+ </main>
464
+ <Toaster />
465
+ </div>
466
+ );
467
+ }
468
+
469
+ /* ─────────────────────────────────────────────────────────────────────── */
470
+ /* Group headers */
471
+ /* ─────────────────────────────────────────────────────────────────────── */
472
+
473
+ function GroupHeader({
474
+ id,
475
+ label,
476
+ description,
477
+ first = false,
478
+ }: {
479
+ id: string;
480
+ label: string;
481
+ description?: string;
482
+ first?: boolean;
483
+ }) {
484
+ return (
485
+ <section
486
+ id={id}
487
+ aria-label={label}
488
+ className={first ? "scroll-mt-16" : "scroll-mt-16 pt-12"}
489
+ >
490
+ <div className="flex items-center gap-3">
491
+ <div className="text-xs font-semibold uppercase tracking-[0.08em] text-brand-forest">
492
+ {label}
493
+ </div>
494
+ <div className="flex-1 h-px bg-brand-forest/30" aria-hidden="true" />
495
+ </div>
496
+ {description && (
497
+ <p className="mt-2 text-sm text-text-muted max-w-2xl">{description}</p>
498
+ )}
499
+ </section>
500
+ );
501
+ }
502
+
503
+ /* ─────────────────────────────────────────────────────────────────────── */
504
+ /* Sections */
505
+ /* ─────────────────────────────────────────────────────────────────────── */
506
+
507
+ function VisualPrinciplesSection() {
508
+ const principles = [
509
+ [
510
+ "Density over decoration",
511
+ "13–14px body. 4px base. Rows over cards. Show more, click less.",
512
+ ],
513
+ [
514
+ "Borders not shadows",
515
+ "1px borders separate. Shadows reserved for floating layers.",
516
+ ],
517
+ ["Brand as accent", "Lime + forest punctuate. Most chrome stays neutral."],
518
+ [
519
+ "One CTA per view",
520
+ "Lime is the loudest. Use it once for the action that matters.",
521
+ ],
522
+ ["Numbers first-class", "Tabular figures, right-aligned, mono for IDs."],
523
+ [
524
+ "Status is iconic + colored",
525
+ "Color never alone. Always pair with text or icon.",
526
+ ],
527
+ ["Live > polished", "Skeletons over spinners. Real data fast."],
528
+ [
529
+ "Predictable motion",
530
+ "150ms ease-out, opacity + small translate. No springs.",
531
+ ],
532
+ ];
533
+
534
+ return (
535
+ <DocSection
536
+ id="principles-visual"
537
+ title="Visual principles"
538
+ description="What to draw. The eight rules behind the look + feel."
539
+ >
540
+ <ul className="grid gap-3 md:grid-cols-2">
541
+ {principles.map(([title, desc]) => (
542
+ <li
543
+ key={title}
544
+ className="rounded border border-border bg-background p-4"
545
+ >
546
+ <div className="text-sm font-semibold">{title}</div>
547
+ <div className="text-xs text-text-muted mt-1">{desc}</div>
548
+ </li>
549
+ ))}
550
+ </ul>
551
+ </DocSection>
552
+ );
553
+ }
554
+
555
+ function UXPrinciplesSection() {
556
+ const principles: Array<{
557
+ n: string;
558
+ title: string;
559
+ body: string;
560
+ example?: string;
561
+ }> = [
562
+ {
563
+ n: "3.1",
564
+ title: "Two audiences, two mental models",
565
+ body: "Player optimizes for speed-to-confidence; operator optimizes for scan + act. Every page can name which audience it serves.",
566
+ },
567
+ {
568
+ n: "3.2",
569
+ title: "Information hierarchy per surface",
570
+ body: "Pick 3–4 signals that drive the decision. Demote the rest.",
571
+ example: "Game card: time · venue · price · spots. Period.",
572
+ },
573
+ {
574
+ n: "3.3",
575
+ title: "Progressive disclosure",
576
+ body: "Show the minimum to decide. Reveal details on demand — except at the moment of commitment, where consequences are inline.",
577
+ },
578
+ {
579
+ n: "3.4",
580
+ title: "Latency budgets",
581
+ body: "Browse < 300ms. Detail < 500ms. Skeletons over 200ms. Refunds are async with explicit timeline.",
582
+ },
583
+ {
584
+ n: "3.5",
585
+ title: "Optimistic vs pessimistic UI",
586
+ body: "Reversible no-money actions go optimistic. Money / emails / audits go pessimistic.",
587
+ },
588
+ {
589
+ n: "3.6",
590
+ title: "Error recovery",
591
+ body: "What happened · why in plain English · what to do next. Never 'Something went wrong.'",
592
+ },
593
+ {
594
+ n: "3.7",
595
+ title: "Forgiveness + undo",
596
+ body: "5s undo toast for cheap reversals. Confirm dialog with concrete consequences for destructive actions.",
597
+ },
598
+ {
599
+ n: "3.8",
600
+ title: "Smart defaults",
601
+ body: "Don't ask for things they almost always want. Skill = Mixed. Confirmation email = on. Credit = auto-apply.",
602
+ },
603
+ {
604
+ n: "3.9",
605
+ title: "State persistence",
606
+ body: "Filters in URL params. Drafts auto-save. Tables remember sort and column visibility per user.",
607
+ },
608
+ {
609
+ n: "3.10",
610
+ title: "Affordance + feedback",
611
+ body: "Visible hover on every interactive surface. Outcome ≤ 150ms via toast / dialog / inline. No silent success or failure.",
612
+ },
613
+ {
614
+ n: "3.11",
615
+ title: "Confirmation discipline",
616
+ body: "Confirm only destructive / irreversible. Show concrete consequences: '8 players will be refunded $14 each.'",
617
+ },
618
+ {
619
+ n: "3.12",
620
+ title: "Live data discipline",
621
+ body: "Spots + price poll every 30s. Quote locked for 5 min at checkout. Never yank state mid-action.",
622
+ },
623
+ {
624
+ n: "3.13",
625
+ title: "Mobile-first interactions",
626
+ body: "32px+ tap targets. No hover-only reveals. Sticky bottom CTA on narrow screens.",
627
+ },
628
+ {
629
+ n: "3.14",
630
+ title: "Empty / loading / full",
631
+ body: "Every list designs all three states (plus error). Skeletons echo the final shape.",
632
+ },
633
+ {
634
+ n: "3.15",
635
+ title: "Money UX",
636
+ body: "Mandatory pre-charge breakdown. Receipt verbatim matches. No surprise debits.",
637
+ },
638
+ {
639
+ n: "3.16",
640
+ title: "Time UX",
641
+ body: "Future events in venue tz; tooltip reveals user tz. Within 24h, show ticker.",
642
+ },
643
+ {
644
+ n: "3.17",
645
+ title: "One screen, one job",
646
+ body: "If a page feels like an admin AND a profile, split it. Drawers for secondary tasks.",
647
+ },
648
+ {
649
+ n: "3.18",
650
+ title: "Onboarding in the flow",
651
+ body: "First booking IS the onboarding. No tour modal. Inline tooltips at first touch only.",
652
+ },
653
+ {
654
+ n: "3.19",
655
+ title: "Voice consistency",
656
+ body: "UI, toasts, emails, SMS, support all sound like one person. Direct, operational, no hype.",
657
+ },
658
+ ];
659
+
660
+ return (
661
+ <DocSection
662
+ id="principles-ux"
663
+ title="UX principles"
664
+ description="How the surface behaves. These shape every interaction — flows, defaults, errors, feedback, money, time."
665
+ >
666
+ <ul className="grid gap-3 md:grid-cols-2">
667
+ {principles.map(({ n, title, body, example }) => (
668
+ <li
669
+ key={n}
670
+ className="rounded border border-border bg-background p-4 flex flex-col"
671
+ >
672
+ <div className="flex items-baseline gap-2">
673
+ <span className="text-[10px] font-mono text-text-subtle tabular">
674
+ §{n}
675
+ </span>
676
+ <h4 className="text-sm font-semibold">{title}</h4>
677
+ </div>
678
+ <p className="mt-1.5 text-xs text-text-muted">{body}</p>
679
+ {example && (
680
+ <p className="mt-2 text-xs text-text-subtle border-t border-border pt-2">
681
+ <span className="font-medium text-foreground">Example. </span>
682
+ {example}
683
+ </p>
684
+ )}
685
+ </li>
686
+ ))}
687
+ </ul>
688
+
689
+ <p className="text-xs text-text-muted">
690
+ See <code className="font-mono">design.md §3</code> for the full
691
+ treatment with do/don&apos;t tables.
692
+ </p>
693
+ </DocSection>
694
+ );
695
+ }
696
+
697
+ function ColorsSection() {
698
+ return (
699
+ <DocSection
700
+ id="colors"
701
+ title="Colors"
702
+ description="Brand + neutrals + semantic. Verified for WCAG AA contrast. White-on-lime fails AA — never do it."
703
+ >
704
+ <DocSubsection title="Brand">
705
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
706
+ <Swatch
707
+ token="brand-lime"
708
+ bg="bg-brand-lime"
709
+ value="#B4D334"
710
+ textSample="black"
711
+ contrast="black 11.2:1 ✓ AAA"
712
+ />
713
+ <Swatch
714
+ token="brand-forest"
715
+ bg="bg-brand-forest"
716
+ value="#176E3E"
717
+ textSample="white"
718
+ contrast="white 6.9:1 ✓ AA"
719
+ />
720
+ <Swatch
721
+ token="foreground"
722
+ bg="bg-foreground"
723
+ value="#0A0A0A"
724
+ textSample="white"
725
+ contrast="white 19:1 ✓ AAA"
726
+ />
727
+ <Swatch
728
+ token="background"
729
+ bg="bg-background border-border-strong"
730
+ value="#FFFFFF"
731
+ textSample="black"
732
+ contrast="black 19:1 ✓ AAA"
733
+ />
734
+ </div>
735
+ </DocSubsection>
736
+
737
+ <DocSubsection title="Neutrals">
738
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
739
+ <Swatch token="bg-subtle" bg="bg-bg-subtle" value="hsl(0 0% 98%)" />
740
+ <Swatch token="bg-muted" bg="bg-bg-muted" value="hsl(240 5% 96%)" />
741
+ <Swatch token="border" bg="bg-border" value="hsl(0 0% 90%)" />
742
+ <Swatch
743
+ token="border-strong"
744
+ bg="bg-border-strong"
745
+ value="hsl(0 0% 83%)"
746
+ />
747
+ <Swatch
748
+ token="text-muted"
749
+ bg="bg-text-muted"
750
+ value="hsl(0 0% 32%)"
751
+ textSample="white"
752
+ />
753
+ <Swatch
754
+ token="text-subtle"
755
+ bg="bg-text-subtle"
756
+ value="hsl(0 0% 45%)"
757
+ textSample="white"
758
+ />
759
+ </div>
760
+ </DocSubsection>
761
+
762
+ <DocSubsection
763
+ title="Semantic"
764
+ description="Use sparingly — these signal state, not decoration."
765
+ >
766
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
767
+ <Swatch
768
+ token="success"
769
+ bg="bg-success"
770
+ value="#16A34A"
771
+ textSample="white"
772
+ />
773
+ <Swatch
774
+ token="warning"
775
+ bg="bg-warning"
776
+ value="#F59E0B"
777
+ textSample="black"
778
+ />
779
+ <Swatch
780
+ token="danger"
781
+ bg="bg-danger"
782
+ value="#DC2626"
783
+ textSample="white"
784
+ />
785
+ <Swatch
786
+ token="info"
787
+ bg="bg-info"
788
+ value="#2563EB"
789
+ textSample="white"
790
+ />
791
+ </div>
792
+ </DocSubsection>
793
+
794
+ <DocSubsection
795
+ title="Critical contrast rules"
796
+ description="Use this checklist whenever you place text on a colored background."
797
+ >
798
+ <div className="rounded border border-border bg-background overflow-hidden">
799
+ <table className="w-full text-sm">
800
+ <thead className="bg-bg-subtle border-b border-border">
801
+ <tr>
802
+ <th className="text-left px-3 py-2 text-xs font-medium uppercase tracking-[0.04em] text-text-muted">
803
+ Pair
804
+ </th>
805
+ <th className="text-left px-3 py-2 text-xs font-medium uppercase tracking-[0.04em] text-text-muted">
806
+ Ratio
807
+ </th>
808
+ <th className="text-left px-3 py-2 text-xs font-medium uppercase tracking-[0.04em] text-text-muted">
809
+ Verdict
810
+ </th>
811
+ </tr>
812
+ </thead>
813
+ <tbody>
814
+ <tr className="border-b border-border">
815
+ <td className="px-3 py-2">Foreground on background</td>
816
+ <td className="px-3 py-2 tabular">19:1</td>
817
+ <td className="px-3 py-2 text-brand-forest font-medium">
818
+ AAA ✓
819
+ </td>
820
+ </tr>
821
+ <tr className="border-b border-border">
822
+ <td className="px-3 py-2">Brand-forest on white</td>
823
+ <td className="px-3 py-2 tabular">6.1:1</td>
824
+ <td className="px-3 py-2 text-brand-forest font-medium">
825
+ AA ✓
826
+ </td>
827
+ </tr>
828
+ <tr className="border-b border-border">
829
+ <td className="px-3 py-2">White on brand-forest</td>
830
+ <td className="px-3 py-2 tabular">6.9:1</td>
831
+ <td className="px-3 py-2 text-brand-forest font-medium">
832
+ AA ✓
833
+ </td>
834
+ </tr>
835
+ <tr className="border-b border-border">
836
+ <td className="px-3 py-2">Black on brand-lime</td>
837
+ <td className="px-3 py-2 tabular">11.2:1</td>
838
+ <td className="px-3 py-2 text-brand-forest font-medium">
839
+ AAA ✓
840
+ </td>
841
+ </tr>
842
+ <tr>
843
+ <td className="px-3 py-2 font-medium">White on brand-lime</td>
844
+ <td className="px-3 py-2 tabular">1.5:1</td>
845
+ <td className="px-3 py-2 text-danger font-medium">
846
+ FAIL — never use
847
+ </td>
848
+ </tr>
849
+ </tbody>
850
+ </table>
851
+ </div>
852
+ </DocSubsection>
853
+ </DocSection>
854
+ );
855
+ }
856
+
857
+ function TypographySection() {
858
+ const scale: Array<[string, string, string]> = [
859
+ ["text-xs", "11 / 16", "Table cells (compact), badges, microcopy"],
860
+ ["text-sm", "13 / 18", "DEFAULT body text, inputs, buttons"],
861
+ ["text-base", "14 / 20", "Paragraphs, card body"],
862
+ ["text-md", "15 / 22", "Section headings in cards"],
863
+ ["text-lg", "17 / 24", "Subsection titles"],
864
+ ["text-xl", "20 / 28", "Page headings"],
865
+ ["text-2xl", "24 / 32", "Metric values"],
866
+ ["text-3xl", "30 / 36", "Hero — rare"],
867
+ ];
868
+
869
+ return (
870
+ <DocSection
871
+ id="typography"
872
+ title="Typography"
873
+ description="Exo 2. Geometric, slightly sporty, dense at small sizes. Mono fallback for IDs and code."
874
+ >
875
+ <DocSubsection title="Scale">
876
+ <div className="rounded border border-border bg-background divide-y divide-border">
877
+ {scale.map(([size, lh, use]) => (
878
+ <div key={size} className="flex items-baseline gap-4 px-4 py-3">
879
+ <div className="w-28 shrink-0 text-xs font-mono text-text-muted">
880
+ {size}
881
+ </div>
882
+ <div className={`${size} font-medium flex-1`}>
883
+ The quick brown fox 0123
884
+ </div>
885
+ <div className="hidden md:block text-xs text-text-subtle tabular w-16 text-right">
886
+ {lh}
887
+ </div>
888
+ <div className="hidden lg:block text-xs text-text-muted w-72">
889
+ {use}
890
+ </div>
891
+ </div>
892
+ ))}
893
+ </div>
894
+ </DocSubsection>
895
+
896
+ <DocSubsection title="Weights">
897
+ <div className="space-y-1">
898
+ <p className="text-base font-normal">Regular 400 — body text</p>
899
+ <p className="text-base font-medium">Medium 500 — emphasis</p>
900
+ <p className="text-base font-semibold">Semibold 600 — headings</p>
901
+ <p className="text-base font-bold">Bold 700 — rare, page titles</p>
902
+ </div>
903
+ </DocSubsection>
904
+
905
+ <DocSubsection
906
+ title="Numerals (tabular)"
907
+ description="Add the .tabular utility to every cell, price, count, or aligned number."
908
+ >
909
+ <div className="grid grid-cols-2 gap-4">
910
+ <div className="rounded border border-border bg-background p-4">
911
+ <div className="text-xs font-medium uppercase tracking-[0.04em] text-text-muted mb-2">
912
+ Without (default)
913
+ </div>
914
+ <pre className="text-base">{"100\n111\n222\n330"}</pre>
915
+ </div>
916
+ <div className="rounded border border-border bg-background p-4">
917
+ <div className="text-xs font-medium uppercase tracking-[0.04em] text-text-muted mb-2">
918
+ With .tabular
919
+ </div>
920
+ <pre className="text-base tabular">{"100\n111\n222\n330"}</pre>
921
+ </div>
922
+ </div>
923
+ </DocSubsection>
924
+ </DocSection>
925
+ );
926
+ }
927
+
928
+ function SpacingSection() {
929
+ const tokens: Array<[string, string]> = [
930
+ ["0.5", "2px"],
931
+ ["1", "4px"],
932
+ ["1.5", "6px"],
933
+ ["2", "8px"],
934
+ ["2.5", "10px"],
935
+ ["3", "12px"],
936
+ ["4", "16px"],
937
+ ["5", "20px"],
938
+ ["6", "24px"],
939
+ ["8", "32px"],
940
+ ["10", "40px"],
941
+ ["12", "48px"],
942
+ ];
943
+
944
+ return (
945
+ <DocSection
946
+ id="spacing"
947
+ title="Spacing"
948
+ description="4px base. Compact. Default gutters 12–16px, card padding 16–20px, page padding 32px."
949
+ >
950
+ <div className="rounded border border-border bg-background p-4 space-y-1.5">
951
+ {tokens.map(([token, px]) => (
952
+ <div key={token} className="flex items-center gap-3">
953
+ <div className="w-14 text-xs font-mono text-text-muted">
954
+ {token}
955
+ </div>
956
+ <div className="w-14 text-xs font-mono text-text-subtle tabular">
957
+ {px}
958
+ </div>
959
+ <div
960
+ className="h-3 bg-brand-forest rounded-sm"
961
+ style={{ width: px }}
962
+ />
963
+ </div>
964
+ ))}
965
+ </div>
966
+ </DocSection>
967
+ );
968
+ }
969
+
970
+ function RadiusSection() {
971
+ const tokens: Array<[string, string, string]> = [
972
+ ["none", "0", "rounded-none"],
973
+ ["sm", "2px", "rounded-sm"],
974
+ ["DEFAULT", "4px", "rounded"],
975
+ ["md", "6px", "rounded-md"],
976
+ ["full", "9999px", "rounded-full"],
977
+ ];
978
+
979
+ return (
980
+ <DocSection
981
+ id="radius"
982
+ title="Corner radius"
983
+ description="Minimal — sharp, modern. No rounded-xl or rounded-2xl anywhere."
984
+ >
985
+ <div className="grid grid-cols-2 md:grid-cols-5 gap-4">
986
+ {tokens.map(([name, px, cls]) => (
987
+ <div key={name} className="flex flex-col items-start gap-2">
988
+ <div
989
+ className={`h-16 w-full bg-brand-forest ${cls}`}
990
+ aria-hidden="true"
991
+ />
992
+ <div className="text-xs">
993
+ <div className="font-medium">{name}</div>
994
+ <div className="text-text-muted font-mono tabular">{px}</div>
995
+ <div className="text-text-subtle font-mono">{cls}</div>
996
+ </div>
997
+ </div>
998
+ ))}
999
+ </div>
1000
+ </DocSection>
1001
+ );
1002
+ }
1003
+
1004
+ function ElevationSection() {
1005
+ return (
1006
+ <DocSection
1007
+ id="elevation"
1008
+ title="Elevation"
1009
+ description="Borders first. Shadows only for floating layers."
1010
+ >
1011
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
1012
+ <div className="bg-bg-subtle p-8 rounded">
1013
+ <div className="h-20 rounded border border-border bg-background flex items-center justify-center text-xs text-text-muted">
1014
+ shadow-none (default)
1015
+ </div>
1016
+ </div>
1017
+ <div className="bg-bg-subtle p-8 rounded">
1018
+ <div className="h-20 rounded border border-border bg-background shadow-sm flex items-center justify-center text-xs text-text-muted">
1019
+ shadow-sm (popovers)
1020
+ </div>
1021
+ </div>
1022
+ <div className="bg-bg-subtle p-8 rounded">
1023
+ <div className="h-20 rounded border border-border-strong bg-background shadow-md flex items-center justify-center text-xs text-text-muted">
1024
+ shadow-md (modals)
1025
+ </div>
1026
+ </div>
1027
+ <div className="bg-bg-subtle p-8 rounded">
1028
+ <div className="h-20 rounded border border-border-strong bg-background shadow-lg flex items-center justify-center text-xs text-text-muted">
1029
+ shadow-lg (marketing)
1030
+ </div>
1031
+ </div>
1032
+ </div>
1033
+ </DocSection>
1034
+ );
1035
+ }
1036
+
1037
+ function IconsSection() {
1038
+ const icons = [
1039
+ { Icon: CalendarPlus, name: "CalendarPlus" },
1040
+ { Icon: MapPin, name: "MapPin" },
1041
+ { Icon: Zap, name: "Zap" },
1042
+ { Icon: CheckCircle2, name: "CheckCircle2" },
1043
+ { Icon: AlertTriangle, name: "AlertTriangle" },
1044
+ { Icon: XCircle, name: "XCircle" },
1045
+ { Icon: Info, name: "Info" },
1046
+ { Icon: CircleHelp, name: "CircleHelp" },
1047
+ ];
1048
+ return (
1049
+ <DocSection
1050
+ id="icons"
1051
+ title="Iconography"
1052
+ description="Lucide icons, stroke 1.75, currentColor. 14–16px inline, 18–20px standalone."
1053
+ >
1054
+ <div className="grid grid-cols-4 md:grid-cols-8 gap-3">
1055
+ {icons.map(({ Icon, name }) => (
1056
+ <div
1057
+ key={name}
1058
+ className="flex flex-col items-center gap-1.5 rounded border border-border bg-background py-3"
1059
+ >
1060
+ <Icon className="h-4 w-4" strokeWidth={1.75} />
1061
+ <div className="text-[10px] font-mono text-text-muted">{name}</div>
1062
+ </div>
1063
+ ))}
1064
+ </div>
1065
+ <CodeBlock
1066
+ language="tsx"
1067
+ code={`import { CalendarPlus } from "lucide-react";\n\n<CalendarPlus className="h-4 w-4" strokeWidth={1.75} />`}
1068
+ />
1069
+ </DocSection>
1070
+ );
1071
+ }
1072
+
1073
+ function ButtonsSection() {
1074
+ return (
1075
+ <DocSection
1076
+ id="buttons"
1077
+ title="Buttons"
1078
+ description="One accent (lime) per view. Forest is the default primary. 32px tap target on default size."
1079
+ >
1080
+ <DocSubsection title="Variants">
1081
+ <PreviewCode
1082
+ preview={
1083
+ <div className="flex flex-wrap gap-2">
1084
+ <Button variant="accent">Book now</Button>
1085
+ <Button variant="primary">Confirm</Button>
1086
+ <Button variant="secondary">Cancel</Button>
1087
+ <Button variant="ghost">More</Button>
1088
+ <Button variant="destructive">Cancel game</Button>
1089
+ <Button variant="link">View receipt</Button>
1090
+ </div>
1091
+ }
1092
+ code={
1093
+ <CodeBlock
1094
+ language="tsx"
1095
+ code={`<Button variant="accent">Book now</Button>
1096
+ <Button variant="primary">Confirm</Button>
1097
+ <Button variant="secondary">Cancel</Button>
1098
+ <Button variant="ghost">More</Button>
1099
+ <Button variant="destructive">Cancel game</Button>
1100
+ <Button variant="link">View receipt</Button>`}
1101
+ />
1102
+ }
1103
+ />
1104
+ </DocSubsection>
1105
+
1106
+ <DocSubsection title="Sizes">
1107
+ <PreviewCode
1108
+ preview={
1109
+ <div className="flex flex-wrap items-end gap-2">
1110
+ <Button size="xs">XS</Button>
1111
+ <Button size="sm">SM</Button>
1112
+ <Button size="default">Default</Button>
1113
+ <Button size="lg">LG</Button>
1114
+ <Button size="icon" aria-label="Delete">
1115
+ <Trash2 className="h-4 w-4" strokeWidth={1.75} />
1116
+ </Button>
1117
+ </div>
1118
+ }
1119
+ code={
1120
+ <CodeBlock
1121
+ language="tsx"
1122
+ code={`<Button size="xs">XS</Button>
1123
+ <Button size="sm">SM</Button>
1124
+ <Button size="default">Default</Button>
1125
+ <Button size="lg">LG</Button>
1126
+
1127
+ {/* Icon-only buttons MUST have aria-label */}
1128
+ <Button size="icon" aria-label="Delete">
1129
+ <Trash2 className="h-4 w-4" />
1130
+ </Button>`}
1131
+ />
1132
+ }
1133
+ />
1134
+ </DocSubsection>
1135
+
1136
+ <DocSubsection title="With icon + state">
1137
+ <PreviewCode
1138
+ preview={
1139
+ <div className="flex flex-wrap gap-2">
1140
+ <Button variant="accent">
1141
+ <Zap className="h-4 w-4" strokeWidth={1.75} />
1142
+ Go Live
1143
+ </Button>
1144
+ <Button variant="primary" disabled>
1145
+ Disabled
1146
+ </Button>
1147
+ </div>
1148
+ }
1149
+ code={
1150
+ <CodeBlock
1151
+ language="tsx"
1152
+ code={`<Button variant="accent">
1153
+ <Zap className="h-4 w-4" /> Go Live
1154
+ </Button>
1155
+ <Button disabled>Disabled</Button>`}
1156
+ />
1157
+ }
1158
+ />
1159
+ </DocSubsection>
1160
+ </DocSection>
1161
+ );
1162
+ }
1163
+
1164
+ function InputsSection() {
1165
+ return (
1166
+ <DocSection
1167
+ id="inputs"
1168
+ title="Inputs & Forms"
1169
+ description="Labels above inputs. Required marker is a danger asterisk. Helper text below input; replaced by error text on validation failure."
1170
+ >
1171
+ <Example>
1172
+ <form
1173
+ className="grid gap-4 max-w-md"
1174
+ onSubmit={(e) => {
1175
+ e.preventDefault();
1176
+ toast.success("Form submitted (demo).");
1177
+ }}
1178
+ noValidate
1179
+ >
1180
+ <div className="space-y-1.5">
1181
+ <Label htmlFor="email" required>
1182
+ Email
1183
+ </Label>
1184
+ <Input
1185
+ id="email"
1186
+ type="email"
1187
+ placeholder="you@dtsports.tv"
1188
+ autoComplete="email"
1189
+ required
1190
+ aria-describedby="email-helper"
1191
+ />
1192
+ <p id="email-helper" className="text-xs text-text-muted">
1193
+ We&apos;ll send booking confirmations here.
1194
+ </p>
1195
+ </div>
1196
+
1197
+ <div className="space-y-1.5">
1198
+ <Label htmlFor="phone">Mobile</Label>
1199
+ <Input
1200
+ id="phone"
1201
+ type="tel"
1202
+ placeholder="+1 555 123 4567"
1203
+ autoComplete="tel"
1204
+ />
1205
+ </div>
1206
+
1207
+ <div className="space-y-1.5">
1208
+ <Label htmlFor="bad" required>
1209
+ Card number
1210
+ </Label>
1211
+ <Input
1212
+ id="bad"
1213
+ type="text"
1214
+ defaultValue="4242 4242 0000"
1215
+ aria-invalid="true"
1216
+ aria-describedby="bad-error"
1217
+ />
1218
+ <p id="bad-error" className="text-xs text-danger">
1219
+ Card number is invalid.
1220
+ </p>
1221
+ </div>
1222
+
1223
+ <div className="flex justify-end gap-2 pt-2">
1224
+ <Button variant="ghost" type="button">
1225
+ Cancel
1226
+ </Button>
1227
+ <Button variant="accent" type="submit">
1228
+ Pay $14.00
1229
+ </Button>
1230
+ </div>
1231
+ </form>
1232
+ </Example>
1233
+ </DocSection>
1234
+ );
1235
+ }
1236
+
1237
+ function BadgesSection() {
1238
+ return (
1239
+ <DocSection
1240
+ id="badges"
1241
+ title="Badges"
1242
+ description="Uppercase pills used as status markers. Always paired with text. Square shape by default; pill shape reserved for game status."
1243
+ >
1244
+ <PreviewCode
1245
+ preview={
1246
+ <div className="flex flex-wrap gap-2">
1247
+ <Badge>Neutral</Badge>
1248
+ <Badge variant="outline">Outline</Badge>
1249
+ <Badge variant="forest">Forest</Badge>
1250
+ <Badge variant="lime">Lime</Badge>
1251
+ <Badge variant="success">Success</Badge>
1252
+ <Badge variant="warning">Warning</Badge>
1253
+ <Badge variant="danger">Danger</Badge>
1254
+ <Badge variant="info">Info</Badge>
1255
+ </div>
1256
+ }
1257
+ code={
1258
+ <CodeBlock
1259
+ language="tsx"
1260
+ code={`<Badge>Neutral</Badge>
1261
+ <Badge variant="outline">Outline</Badge>
1262
+ <Badge variant="forest">Forest</Badge>
1263
+ <Badge variant="lime">Lime</Badge>
1264
+ <Badge variant="success">Success</Badge>
1265
+ <Badge variant="warning">Warning</Badge>
1266
+ <Badge variant="danger">Danger</Badge>
1267
+ <Badge variant="info">Info</Badge>`}
1268
+ />
1269
+ }
1270
+ />
1271
+ </DocSection>
1272
+ );
1273
+ }
1274
+
1275
+ function StatusSection() {
1276
+ const statuses: GameStatus[] = [
1277
+ "draft",
1278
+ "scheduled",
1279
+ "filling",
1280
+ "sold_out",
1281
+ "live",
1282
+ "completed",
1283
+ "cancelled",
1284
+ ];
1285
+ return (
1286
+ <DocSection
1287
+ id="status"
1288
+ title="Status pill"
1289
+ description="One canonical visualization for the game-status vocabulary. The LIVE state is the only continuously animated element."
1290
+ >
1291
+ <PreviewCode
1292
+ preview={
1293
+ <div className="flex flex-wrap gap-2">
1294
+ {statuses.map((s) => (
1295
+ <StatusPill key={s} status={s} />
1296
+ ))}
1297
+ </div>
1298
+ }
1299
+ code={
1300
+ <CodeBlock
1301
+ language="tsx"
1302
+ code={`<StatusPill status="draft" />
1303
+ <StatusPill status="scheduled" />
1304
+ <StatusPill status="filling" />
1305
+ <StatusPill status="sold_out" />
1306
+ <StatusPill status="live" />
1307
+ <StatusPill status="completed" />
1308
+ <StatusPill status="cancelled" />`}
1309
+ />
1310
+ }
1311
+ />
1312
+ </DocSection>
1313
+ );
1314
+ }
1315
+
1316
+ function CardsSection() {
1317
+ return (
1318
+ <DocSection
1319
+ id="cards"
1320
+ title="Cards"
1321
+ description="1px border, 4px radius, no shadow at rest. Compose Header / Content / Footer."
1322
+ >
1323
+ <Card className="max-w-md">
1324
+ <CardHeader>
1325
+ <div>
1326
+ <CardTitle>Astoria Turf · Field A</CardTitle>
1327
+ <CardDescription>5v5 · Saturday, 6:00 pm</CardDescription>
1328
+ </div>
1329
+ <StatusPill status="filling" />
1330
+ </CardHeader>
1331
+ <CardContent>
1332
+ <div className="grid grid-cols-3 gap-3 text-sm">
1333
+ <div>
1334
+ <div className="text-xs uppercase tracking-[0.04em] text-text-muted">
1335
+ Price
1336
+ </div>
1337
+ <div className="tabular font-semibold mt-0.5">$14.00</div>
1338
+ </div>
1339
+ <div>
1340
+ <div className="text-xs uppercase tracking-[0.04em] text-text-muted">
1341
+ Spots
1342
+ </div>
1343
+ <div className="tabular font-semibold mt-0.5">8 / 10</div>
1344
+ </div>
1345
+ <div>
1346
+ <div className="text-xs uppercase tracking-[0.04em] text-text-muted">
1347
+ Skill
1348
+ </div>
1349
+ <div className="font-semibold mt-0.5">Mixed</div>
1350
+ </div>
1351
+ </div>
1352
+ </CardContent>
1353
+ <CardFooter>
1354
+ <Button variant="ghost" size="sm">
1355
+ Details
1356
+ </Button>
1357
+ <Button variant="accent" size="sm">
1358
+ Book now
1359
+ </Button>
1360
+ </CardFooter>
1361
+ </Card>
1362
+ </DocSection>
1363
+ );
1364
+ }
1365
+
1366
+ function MetricsSection() {
1367
+ return (
1368
+ <DocSection
1369
+ id="metrics"
1370
+ title="Metric tiles"
1371
+ description="Three lines exactly: label / value / delta. Use 3–4 across in dashboard rows."
1372
+ >
1373
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
1374
+ <MetricTile label="This week" value="$1,840" delta={12} />
1375
+ <MetricTile label="Bookings" value="124" delta={-3} />
1376
+ <MetricTile label="Fill rate" value="73%" delta={0} />
1377
+ <MetricTile label="Refunds" value="4" delta={50} invertDelta />
1378
+ </div>
1379
+ <CodeBlock
1380
+ language="tsx"
1381
+ code={`<MetricTile label="This week" value="$1,840" delta={12} />
1382
+ <MetricTile label="Bookings" value="124" delta={-3} />
1383
+ <MetricTile label="Fill rate" value="73%" delta={0} />
1384
+ <MetricTile label="Refunds" value="4" delta={50} invertDelta />`}
1385
+ />
1386
+ </DocSection>
1387
+ );
1388
+ }
1389
+
1390
+ function TablesSection() {
1391
+ const rows = [
1392
+ {
1393
+ date: "Sat May 22",
1394
+ time: "6:00 pm",
1395
+ venue: "Astoria Turf · A",
1396
+ format: "5v5",
1397
+ price: 14,
1398
+ filled: 8,
1399
+ capacity: 10,
1400
+ status: "filling" as GameStatus,
1401
+ },
1402
+ {
1403
+ date: "Sat May 22",
1404
+ time: "7:00 pm",
1405
+ venue: "McCarren · Court 1",
1406
+ format: "3v3",
1407
+ price: 18,
1408
+ filled: 6,
1409
+ capacity: 6,
1410
+ status: "sold_out" as GameStatus,
1411
+ },
1412
+ {
1413
+ date: "Sat May 22",
1414
+ time: "8:00 pm",
1415
+ venue: "Astoria Turf · B",
1416
+ format: "5v5",
1417
+ price: 12,
1418
+ filled: 3,
1419
+ capacity: 10,
1420
+ status: "scheduled" as GameStatus,
1421
+ },
1422
+ {
1423
+ date: "Sat May 22",
1424
+ time: "9:00 pm",
1425
+ venue: "Astoria Turf · A",
1426
+ format: "5v5",
1427
+ price: 16,
1428
+ filled: 10,
1429
+ capacity: 10,
1430
+ status: "live" as GameStatus,
1431
+ },
1432
+ ];
1433
+
1434
+ return (
1435
+ <DocSection
1436
+ id="tables"
1437
+ title="Tables"
1438
+ description="The centerpiece. 36px row height, numeric columns right-aligned with tabular figures. Use this shape for every list of structured data."
1439
+ >
1440
+ <div className="rounded border border-border bg-background overflow-hidden">
1441
+ <Table>
1442
+ <TableCaption className="sr-only">
1443
+ Upcoming pickup games for Saturday, May 22.
1444
+ </TableCaption>
1445
+ <TableHeader>
1446
+ <TableRow>
1447
+ <TableHead>Date</TableHead>
1448
+ <TableHead>Time</TableHead>
1449
+ <TableHead>Venue</TableHead>
1450
+ <TableHead>Format</TableHead>
1451
+ <TableHead className="text-right">Price</TableHead>
1452
+ <TableHead className="text-right">Spots</TableHead>
1453
+ <TableHead>Status</TableHead>
1454
+ </TableRow>
1455
+ </TableHeader>
1456
+ <TableBody>
1457
+ {rows.map((r, i) => (
1458
+ <TableRow key={i} interactive selected={i === 1}>
1459
+ <TableCell>{r.date}</TableCell>
1460
+ <TableCell>{r.time}</TableCell>
1461
+ <TableCell className="font-medium">{r.venue}</TableCell>
1462
+ <TableCell>{r.format}</TableCell>
1463
+ <TableCell className="text-right tabular">
1464
+ ${r.price.toFixed(2)}
1465
+ </TableCell>
1466
+ <TableCell className="text-right tabular">
1467
+ {r.filled} / {r.capacity}
1468
+ </TableCell>
1469
+ <TableCell>
1470
+ <StatusPill status={r.status} />
1471
+ </TableCell>
1472
+ </TableRow>
1473
+ ))}
1474
+ </TableBody>
1475
+ </Table>
1476
+ </div>
1477
+ </DocSection>
1478
+ );
1479
+ }
1480
+
1481
+ function DialogSection() {
1482
+ return (
1483
+ <DocSection
1484
+ id="dialog"
1485
+ title="Dialog"
1486
+ description="Focus-trapped, escape-to-close, returns focus to trigger. Accent CTA on the right; secondary on the left."
1487
+ >
1488
+ <Dialog>
1489
+ <DialogTrigger asChild>
1490
+ <Button variant="destructive">Cancel game…</Button>
1491
+ </DialogTrigger>
1492
+ <DialogContent>
1493
+ <DialogHeader>
1494
+ <DialogTitle>Cancel this game?</DialogTitle>
1495
+ <DialogDescription>
1496
+ 8 players will be refunded as credit and notified via email.
1497
+ </DialogDescription>
1498
+ </DialogHeader>
1499
+ <DialogBody>
1500
+ <p className="text-sm text-text-muted">
1501
+ This action is logged and cannot be undone. Players will receive a
1502
+ credit of{" "}
1503
+ <span className="tabular font-medium text-foreground">
1504
+ $14.00
1505
+ </span>{" "}
1506
+ each, applied automatically to their next booking.
1507
+ </p>
1508
+ </DialogBody>
1509
+ <DialogFooter>
1510
+ <Button variant="ghost">Keep game</Button>
1511
+ <Button variant="destructive">Cancel + refund all</Button>
1512
+ </DialogFooter>
1513
+ </DialogContent>
1514
+ </Dialog>
1515
+ </DocSection>
1516
+ );
1517
+ }
1518
+
1519
+ function ToastSection() {
1520
+ return (
1521
+ <DocSection
1522
+ id="toast"
1523
+ title="Toasts"
1524
+ description="Top-right, 4s auto-dismiss (persistent for danger). Status communicated via icon + color, never color alone."
1525
+ >
1526
+ <div className="flex flex-wrap gap-2">
1527
+ <Button
1528
+ variant="secondary"
1529
+ onClick={() =>
1530
+ toast.success("Booking confirmed.", {
1531
+ description: "Saturday, May 22 · 6:00 pm at Astoria Turf",
1532
+ icon: (
1533
+ <CheckCircle2
1534
+ className="h-4 w-4 text-success"
1535
+ strokeWidth={1.75}
1536
+ />
1537
+ ),
1538
+ })
1539
+ }
1540
+ >
1541
+ Show success
1542
+ </Button>
1543
+ <Button
1544
+ variant="secondary"
1545
+ onClick={() =>
1546
+ toast.warning("3 spots left.", {
1547
+ description: "Saturday's 7 pm at McCarren is filling fast.",
1548
+ icon: (
1549
+ <AlertTriangle
1550
+ className="h-4 w-4 text-warning"
1551
+ strokeWidth={1.75}
1552
+ />
1553
+ ),
1554
+ })
1555
+ }
1556
+ >
1557
+ Show warning
1558
+ </Button>
1559
+ <Button
1560
+ variant="secondary"
1561
+ onClick={() =>
1562
+ toast.error("Payment declined.", {
1563
+ description: "Try a different card or contact your bank.",
1564
+ icon: (
1565
+ <XCircle className="h-4 w-4 text-danger" strokeWidth={1.75} />
1566
+ ),
1567
+ duration: 10000,
1568
+ })
1569
+ }
1570
+ >
1571
+ Show danger
1572
+ </Button>
1573
+ <Button
1574
+ variant="secondary"
1575
+ onClick={() =>
1576
+ toast("Stream goes live at 6:00 pm.", {
1577
+ description: "Watch on DT Sports TV from any game page.",
1578
+ icon: <Info className="h-4 w-4 text-info" strokeWidth={1.75} />,
1579
+ })
1580
+ }
1581
+ >
1582
+ Show info
1583
+ </Button>
1584
+ </div>
1585
+ </DocSection>
1586
+ );
1587
+ }
1588
+
1589
+ function EmptyStateSection() {
1590
+ return (
1591
+ <DocSection
1592
+ id="empty"
1593
+ title="Empty states"
1594
+ description="Center-aligned text-sm. No illustrations. Subtle Lucide icon at most."
1595
+ >
1596
+ <div className="rounded border border-border bg-background">
1597
+ <EmptyState
1598
+ icon={<Inbox className="h-6 w-6" strokeWidth={1.5} />}
1599
+ title="No upcoming games"
1600
+ description="Once you book or host a game, it will show up here."
1601
+ action={
1602
+ <Button size="sm" variant="accent">
1603
+ Browse games
1604
+ </Button>
1605
+ }
1606
+ />
1607
+ </div>
1608
+ </DocSection>
1609
+ );
1610
+ }
1611
+
1612
+ function AvatarsSection() {
1613
+ return (
1614
+ <DocSection
1615
+ id="avatars"
1616
+ title="Avatars"
1617
+ description="Fallback uses forest background with initials. Status dot indicates check-in state."
1618
+ >
1619
+ <PreviewCode
1620
+ preview={
1621
+ <div className="flex items-end gap-3">
1622
+ <Avatar name="Alex Chen" size="sm" />
1623
+ <Avatar name="Jordan Lee" />
1624
+ <Avatar name="Sam Patel" status="checked-in" />
1625
+ <Avatar name="Maria Garcia" status="booked" />
1626
+ <Avatar name="Danny Tran" size="lg" />
1627
+ </div>
1628
+ }
1629
+ code={
1630
+ <CodeBlock
1631
+ language="tsx"
1632
+ code={`<Avatar name="Alex Chen" size="sm" />
1633
+ <Avatar name="Jordan Lee" />
1634
+ <Avatar name="Sam Patel" status="checked-in" />
1635
+ <Avatar name="Maria Garcia" status="booked" />
1636
+ <Avatar name="Danny Tran" size="lg" />`}
1637
+ />
1638
+ }
1639
+ />
1640
+ </DocSection>
1641
+ );
1642
+ }
1643
+
1644
+ function SkeletonSection() {
1645
+ return (
1646
+ <DocSection
1647
+ id="skeleton"
1648
+ title="Skeleton"
1649
+ description="Use over spinners. The placeholder echoes the final layout so the transition feels immediate."
1650
+ >
1651
+ <div className="rounded border border-border bg-background p-5 max-w-md space-y-3">
1652
+ <Skeleton className="h-4 w-1/3" />
1653
+ <Skeleton className="h-3 w-2/3" />
1654
+ <Skeleton className="h-3 w-1/2" />
1655
+ <div className="flex gap-2 pt-2">
1656
+ <Skeleton className="h-8 w-20" />
1657
+ <Skeleton className="h-8 w-20" />
1658
+ </div>
1659
+ </div>
1660
+ <DocSubsection
1661
+ title="Composites"
1662
+ description="Pre-built placeholders for the two most common list shapes — match rows/columns to the real data so nothing jumps on load."
1663
+ >
1664
+ <div className="grid gap-4 md:grid-cols-2">
1665
+ <div className="rounded border border-border bg-background p-4">
1666
+ <div className="text-xs font-medium text-text-muted mb-3">
1667
+ SkeletonList rows={"{3}"}
1668
+ </div>
1669
+ <SkeletonList rows={3} />
1670
+ </div>
1671
+ <div className="rounded border border-border bg-background p-4">
1672
+ <div className="text-xs font-medium text-text-muted mb-3">
1673
+ SkeletonTable rows={"{3}"} columns={"{4}"}
1674
+ </div>
1675
+ <SkeletonTable rows={3} columns={4} />
1676
+ </div>
1677
+ </div>
1678
+ </DocSubsection>
1679
+ </DocSection>
1680
+ );
1681
+ }
1682
+
1683
+ function UXPatternsSection() {
1684
+ return (
1685
+ <DocSection
1686
+ id="patterns"
1687
+ title="UX patterns"
1688
+ description="The principles above made concrete. These are the components you reach for to honor §3 without thinking about it."
1689
+ >
1690
+ <ThreeStatePatternDemo />
1691
+ <MoneyBreakdownDemo />
1692
+ <TimestampDemo />
1693
+ <ConfirmActionDemo />
1694
+ <UndoToastDemo />
1695
+ </DocSection>
1696
+ );
1697
+ }
1698
+
1699
+ function ThreeStatePatternDemo() {
1700
+ const [state, setState] = React.useState<ListState<string[]>>({
1701
+ kind: "data",
1702
+ data: ["Game A", "Game B"],
1703
+ });
1704
+
1705
+ const states: Array<{ label: string; value: ListState<string[]> }> = [
1706
+ {
1707
+ label: "Loading",
1708
+ value: { kind: "loading" },
1709
+ },
1710
+ { label: "Empty", value: { kind: "empty" } },
1711
+ {
1712
+ label: "Error",
1713
+ value: {
1714
+ kind: "error",
1715
+ error: new Error("Network unreachable. Check your connection."),
1716
+ retry: () => setState({ kind: "loading" }),
1717
+ },
1718
+ },
1719
+ {
1720
+ label: "Full",
1721
+ value: {
1722
+ kind: "data",
1723
+ data: ["Sat 6pm · Astoria", "Sat 7pm · McCarren"],
1724
+ },
1725
+ },
1726
+ ];
1727
+
1728
+ return (
1729
+ <DocSubsection
1730
+ title="Empty / loading / error / full"
1731
+ description="Every list view uses this wrapper. Forces all four states to exist before merge. (§3.14)"
1732
+ >
1733
+ <div className="flex flex-wrap gap-2 mb-3">
1734
+ {states.map(({ label, value }) => (
1735
+ <Button
1736
+ key={label}
1737
+ size="sm"
1738
+ variant={state.kind === value.kind ? "primary" : "secondary"}
1739
+ onClick={() => setState(value)}
1740
+ aria-pressed={state.kind === value.kind}
1741
+ >
1742
+ {label}
1743
+ </Button>
1744
+ ))}
1745
+ </div>
1746
+ <div className="rounded border border-border bg-background min-h-[160px]">
1747
+ <ThreeState
1748
+ state={state}
1749
+ empty={{
1750
+ icon: <Inbox className="h-6 w-6" strokeWidth={1.5} />,
1751
+ title: "No games this week",
1752
+ description: "Once you book or host one, it'll show up here.",
1753
+ action: (
1754
+ <Button size="sm" variant="accent">
1755
+ Browse games
1756
+ </Button>
1757
+ ),
1758
+ }}
1759
+ >
1760
+ {state.kind === "data" && (
1761
+ <ul className="divide-y divide-border">
1762
+ {state.data.map((row) => (
1763
+ <li
1764
+ key={row}
1765
+ className="flex items-center justify-between px-4 py-2.5 text-sm"
1766
+ >
1767
+ <span>{row}</span>
1768
+ <StatusPill status="scheduled" />
1769
+ </li>
1770
+ ))}
1771
+ </ul>
1772
+ )}
1773
+ </ThreeState>
1774
+ </div>
1775
+ <CodeBlock
1776
+ language="tsx"
1777
+ code={`<ThreeState
1778
+ state={state}
1779
+ empty={{ title: "No games this week", action: <Button>Browse</Button> }}
1780
+ >
1781
+ <RealList items={state.kind === "data" ? state.data : []} />
1782
+ </ThreeState>`}
1783
+ />
1784
+ </DocSubsection>
1785
+ );
1786
+ }
1787
+
1788
+ function MoneyBreakdownDemo() {
1789
+ return (
1790
+ <DocSubsection
1791
+ title="Money breakdown"
1792
+ description="Mandatory before any charge. Every line is named, signed, tabular-aligned. Receipt must match this verbatim. (§3.15)"
1793
+ >
1794
+ <PreviewCode
1795
+ preview={
1796
+ <div className="w-full">
1797
+ <MoneyBreakdown
1798
+ lines={[
1799
+ { label: "Game fee", cents: 1400 },
1800
+ {
1801
+ label: "Credit applied",
1802
+ cents: -600,
1803
+ muted: true,
1804
+ hint: "From cancelled game on May 18",
1805
+ },
1806
+ { label: "Sales tax (8.875%)", cents: 71, muted: true },
1807
+ ]}
1808
+ totalLabel="Charged to card"
1809
+ totalCents={871}
1810
+ />
1811
+ </div>
1812
+ }
1813
+ code={
1814
+ <CodeBlock
1815
+ language="tsx"
1816
+ code={`<MoneyBreakdown
1817
+ lines={[
1818
+ { label: "Game fee", cents: 1400 },
1819
+ { label: "Credit applied", cents: -600, muted: true },
1820
+ { label: "Sales tax", cents: 71, muted: true },
1821
+ ]}
1822
+ totalLabel="Charged to card"
1823
+ totalCents={871}
1824
+ />`}
1825
+ />
1826
+ }
1827
+ />
1828
+ </DocSubsection>
1829
+ );
1830
+ }
1831
+
1832
+ function TimestampDemo() {
1833
+ // Demo times are frozen at first render on purpose — recomputing "now"
1834
+ // on every render would make the labels drift mid-session.
1835
+ const [{ inThreeHours, fiveMinAgo, nextSaturday }] = React.useState(() => {
1836
+ const now = Date.now();
1837
+ const d = new Date(now);
1838
+ d.setDate(d.getDate() + ((6 - d.getDay() + 7) % 7 || 7));
1839
+ d.setHours(18, 0, 0, 0);
1840
+ return {
1841
+ inThreeHours: new Date(now + 3 * 3600 * 1000).toISOString(),
1842
+ fiveMinAgo: new Date(now - 5 * 60 * 1000).toISOString(),
1843
+ nextSaturday: d.toISOString(),
1844
+ };
1845
+ });
1846
+
1847
+ return (
1848
+ <DocSubsection
1849
+ title="Timestamp"
1850
+ description="Future events in venue tz with user-tz tooltip; within 24h show a ticker; past events relative within 7d. (§3.16)"
1851
+ >
1852
+ <PreviewCode
1853
+ preview={
1854
+ <div className="space-y-2 text-sm">
1855
+ <div className="flex items-center gap-3">
1856
+ <span className="text-text-muted text-xs w-32">In 3 hours</span>
1857
+ <Timestamp
1858
+ iso={inThreeHours}
1859
+ venueTz="America/New_York"
1860
+ relative
1861
+ />
1862
+ </div>
1863
+ <div className="flex items-center gap-3">
1864
+ <span className="text-text-muted text-xs w-32">5 min ago</span>
1865
+ <Timestamp iso={fiveMinAgo} relative />
1866
+ </div>
1867
+ <div className="flex items-center gap-3">
1868
+ <span className="text-text-muted text-xs w-32">
1869
+ Next Saturday
1870
+ </span>
1871
+ <Timestamp iso={nextSaturday} venueTz="America/New_York" />
1872
+ </div>
1873
+ <p className="text-xs text-text-subtle pt-2">
1874
+ Hover the timestamps with a different user TZ → tooltip reveals
1875
+ &quot;Your time:&quot; alongside venue time.
1876
+ </p>
1877
+ </div>
1878
+ }
1879
+ code={
1880
+ <CodeBlock
1881
+ language="tsx"
1882
+ code={`<Timestamp iso="2026-05-22T22:00:00Z" venueTz="America/New_York" relative />
1883
+ <Timestamp iso="2026-05-22T17:55:00Z" relative />
1884
+ <Timestamp iso="2026-05-29T22:00:00Z" venueTz="America/New_York" />`}
1885
+ />
1886
+ }
1887
+ />
1888
+ </DocSubsection>
1889
+ );
1890
+ }
1891
+
1892
+ function ConfirmActionDemo() {
1893
+ return (
1894
+ <DocSubsection
1895
+ title="ConfirmAction"
1896
+ description="One helper for every destructive / irreversible action. Requires concrete consequence text — abstract 'Are you sure?' is rejected by API. (§3.11)"
1897
+ >
1898
+ <div className="flex flex-wrap gap-2">
1899
+ <ConfirmAction
1900
+ title="Cancel this game?"
1901
+ description="This action is logged and cannot be undone."
1902
+ consequence={
1903
+ <>
1904
+ 8 players will be refunded{" "}
1905
+ <span className="tabular font-medium text-foreground">
1906
+ $14.00
1907
+ </span>{" "}
1908
+ each as credit and notified via email.
1909
+ </>
1910
+ }
1911
+ confirmLabel="Cancel + refund all"
1912
+ confirmVariant="destructive"
1913
+ onConfirm={async () => {
1914
+ await new Promise((r) => setTimeout(r, 800));
1915
+ toast.success("Game cancelled. 8 refunds issued as credit.");
1916
+ }}
1917
+ >
1918
+ <Button variant="destructive" size="sm">
1919
+ Cancel game…
1920
+ </Button>
1921
+ </ConfirmAction>
1922
+
1923
+ <ConfirmAction
1924
+ title="Refund this booking?"
1925
+ consequence={
1926
+ <>
1927
+ Alex Chen will receive{" "}
1928
+ <span className="tabular font-medium text-foreground">
1929
+ $14.00
1930
+ </span>{" "}
1931
+ back to their card in 3–5 business days.
1932
+ </>
1933
+ }
1934
+ confirmLabel="Refund $14.00"
1935
+ confirmVariant="destructive"
1936
+ onConfirm={async () => {
1937
+ await new Promise((r) => setTimeout(r, 600));
1938
+ toast.success("Refund initiated.");
1939
+ }}
1940
+ >
1941
+ <Button variant="secondary" size="sm">
1942
+ Refund booking…
1943
+ </Button>
1944
+ </ConfirmAction>
1945
+ </div>
1946
+ <CodeBlock
1947
+ language="tsx"
1948
+ code={`<ConfirmAction
1949
+ title="Cancel this game?"
1950
+ consequence="8 players will be refunded $14.00 each as credit and emailed."
1951
+ confirmLabel="Cancel + refund all"
1952
+ confirmVariant="destructive"
1953
+ onConfirm={async () => await cancelGame(id)}
1954
+ >
1955
+ <Button variant="destructive">Cancel game…</Button>
1956
+ </ConfirmAction>`}
1957
+ />
1958
+ </DocSubsection>
1959
+ );
1960
+ }
1961
+
1962
+ function UndoToastDemo() {
1963
+ return (
1964
+ <DocSubsection
1965
+ title="Undo toast"
1966
+ description="The right pattern for cheap reversals (delete draft, dismiss notification). 5s window. Money actions don't get undo — they get ConfirmAction. (§3.7)"
1967
+ >
1968
+ <div className="flex flex-wrap gap-2">
1969
+ <Button
1970
+ variant="secondary"
1971
+ size="sm"
1972
+ onClick={() => {
1973
+ const tid = toast("Draft deleted.", {
1974
+ description: "Sat May 22 · 6:00 pm",
1975
+ duration: 5000,
1976
+ action: {
1977
+ label: "Undo",
1978
+ onClick: () => {
1979
+ toast.success("Draft restored.");
1980
+ },
1981
+ },
1982
+ });
1983
+ return tid;
1984
+ }}
1985
+ >
1986
+ Delete draft
1987
+ </Button>
1988
+ <Button
1989
+ variant="secondary"
1990
+ size="sm"
1991
+ onClick={() =>
1992
+ toast("Notification dismissed.", {
1993
+ duration: 5000,
1994
+ action: {
1995
+ label: "Undo",
1996
+ onClick: () => toast.success("Notification restored."),
1997
+ },
1998
+ })
1999
+ }
2000
+ >
2001
+ Dismiss notification
2002
+ </Button>
2003
+ </div>
2004
+ <CodeBlock
2005
+ language="tsx"
2006
+ code={`toast("Draft deleted.", {
2007
+ description: "Sat May 22 · 6:00 pm",
2008
+ duration: 5000,
2009
+ action: { label: "Undo", onClick: () => restoreDraft(id) },
2010
+ });`}
2011
+ />
2012
+ </DocSubsection>
2013
+ );
2014
+ }
2015
+
2016
+ function MotionSection() {
2017
+ return (
2018
+ <DocSection
2019
+ id="motion"
2020
+ title="Motion"
2021
+ description="150ms ease-out. No springs, no scale-on-hover, no parallax. The LIVE dot is the only continuously animated element."
2022
+ >
2023
+ <div className="flex flex-wrap items-center gap-6">
2024
+ <LivePill />
2025
+ <span className="text-sm text-text-muted">
2026
+ ← Animation respects{" "}
2027
+ <code className="font-mono">prefers-reduced-motion</code>
2028
+ </span>
2029
+ </div>
2030
+ <CodeBlock
2031
+ language="ts"
2032
+ code={`// tailwind.config.ts (excerpt)
2033
+ keyframes: {
2034
+ "live-pulse": {
2035
+ "0%, 100%": { opacity: "1" },
2036
+ "50%": { opacity: "0.4" },
2037
+ },
2038
+ },
2039
+ animation: {
2040
+ "live-pulse": "live-pulse 1.5s ease-in-out infinite",
2041
+ },`}
2042
+ />
2043
+ </DocSection>
2044
+ );
2045
+ }
2046
+
2047
+ function AccessibilitySection() {
2048
+ const checks: Array<[string, string]> = [
2049
+ [
2050
+ "Color contrast",
2051
+ "All text ≥ WCAG AA; AAA where possible. White-on-lime is forbidden.",
2052
+ ],
2053
+ [
2054
+ "Focus rings",
2055
+ ":focus-visible across all interactive elements. 2px brand-forest @ 50%, 2px offset.",
2056
+ ],
2057
+ [
2058
+ "Tap targets",
2059
+ "32px+ on default buttons / inputs. Smaller sizes only in dense toolbars.",
2060
+ ],
2061
+ [
2062
+ "Status semantics",
2063
+ "Color never alone — every status badge has text and/or icon.",
2064
+ ],
2065
+ [
2066
+ "Reduced motion",
2067
+ "globals.css disables animations + transitions when prefers-reduced-motion is set.",
2068
+ ],
2069
+ [
2070
+ "Form a11y",
2071
+ "Every input has a Label with htmlFor. Errors use aria-invalid + aria-describedby.",
2072
+ ],
2073
+ [
2074
+ "Dialogs",
2075
+ "Radix handles focus-trap, escape-to-close, focus return. Title + Description wired to aria.",
2076
+ ],
2077
+ [
2078
+ "Tables",
2079
+ "<th scope='col'>. Sortable columns announce sort state via aria-sort.",
2080
+ ],
2081
+ ["Skip link", "First focusable element jumps to <main id='main'>."],
2082
+ [
2083
+ "Icons",
2084
+ "Decorative icons get aria-hidden; meaningful icons get aria-label or accompanying text.",
2085
+ ],
2086
+ ];
2087
+ return (
2088
+ <DocSection
2089
+ id="a11y"
2090
+ title="Accessibility"
2091
+ description="Baked in — every primitive ships with the rules below. Verify with axe DevTools before merging UI changes."
2092
+ >
2093
+ <ul className="rounded border border-border bg-background divide-y divide-border">
2094
+ {checks.map(([label, body]) => (
2095
+ <li key={label} className="flex gap-3 px-4 py-3">
2096
+ <CheckCircle2
2097
+ className="h-4 w-4 mt-0.5 shrink-0 text-brand-forest"
2098
+ strokeWidth={2}
2099
+ />
2100
+ <div>
2101
+ <div className="text-sm font-medium">{label}</div>
2102
+ <div className="text-xs text-text-muted mt-0.5">{body}</div>
2103
+ </div>
2104
+ </li>
2105
+ ))}
2106
+ </ul>
2107
+ </DocSection>
2108
+ );
2109
+ }
2110
+
2111
+ function UsageSection() {
2112
+ return (
2113
+ <DocSection
2114
+ id="usage"
2115
+ title="Using the system"
2116
+ description="How to consume the components, extend variants, and respect the rules."
2117
+ >
2118
+ <DocSubsection
2119
+ title="Import paths"
2120
+ description="Everything ships from one entry. The package is a workspace dep — no build step, source TS is consumed directly via Next's transpilePackages."
2121
+ >
2122
+ <CodeBlock
2123
+ language="tsx"
2124
+ code={`import {
2125
+ Button, Input, Label, Badge,
2126
+ Card, CardHeader, CardTitle, CardContent, CardFooter,
2127
+ Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
2128
+ Dialog, DialogContent, DialogHeader, DialogTitle, DialogBody, DialogFooter,
2129
+ LivePill, StatusPill, MetricTile, EmptyState, Avatar, Skeleton,
2130
+ Timestamp, MoneyBreakdown, ThreeState, ConfirmAction,
2131
+ ThemeProvider, useTheme, ThemeToggle,
2132
+ cn,
2133
+ type GameStatus, type ListState,
2134
+ } from "@dtsports/design-system";
2135
+
2136
+ import { toast } from "@dtsports/design-system";`}
2137
+ />
2138
+ </DocSubsection>
2139
+
2140
+ <DocSubsection
2141
+ title="Composing classes with cn()"
2142
+ description="Use cn() to merge Tailwind classes so callers can override variants without duplication."
2143
+ >
2144
+ <CodeBlock
2145
+ language="tsx"
2146
+ code={`import { cn } from "@dtsports/design-system";
2147
+
2148
+ export function MyCard({ className, ...props }) {
2149
+ return (
2150
+ <div
2151
+ className={cn("rounded border border-border bg-background p-4", className)}
2152
+ {...props}
2153
+ />
2154
+ );
2155
+ }`}
2156
+ />
2157
+ </DocSubsection>
2158
+
2159
+ <DocSubsection
2160
+ title="Dark mode"
2161
+ description="The ThemeProvider toggles a `dark` class on <html>. All design tokens have a dark counterpart in globals.css. Components don't need any dark: classes — they inherit the variables."
2162
+ >
2163
+ <div className="flex items-center gap-3 text-sm text-text-muted">
2164
+ Try the toggle in the topbar (top right). <ThemeToggle />
2165
+ </div>
2166
+ </DocSubsection>
2167
+
2168
+ <DocSubsection
2169
+ title="Adding a new variant"
2170
+ description="Edit the variant's cva() call in components/ui/<name>.tsx. Keep new variants minimal — if a use case fits an existing one, prefer that."
2171
+ >
2172
+ <CodeBlock
2173
+ language="tsx"
2174
+ code={`// components/ui/button.tsx
2175
+ const buttonVariants = cva(/* base classes */, {
2176
+ variants: {
2177
+ variant: {
2178
+ // existing variants...
2179
+ success: "bg-success text-white hover:bg-success/90", // new
2180
+ },
2181
+ },
2182
+ });`}
2183
+ />
2184
+ </DocSubsection>
2185
+
2186
+ <DocSubsection
2187
+ title="Do / don't"
2188
+ description="Quick reference. If in doubt, default to the left column."
2189
+ >
2190
+ <div className="grid md:grid-cols-2 gap-3">
2191
+ <div className="rounded border border-border bg-background p-4">
2192
+ <div className="text-xs font-medium uppercase tracking-[0.04em] text-brand-forest mb-2">
2193
+ Do
2194
+ </div>
2195
+ <ul className="text-sm text-foreground space-y-1.5">
2196
+ <li>· Default to text-sm (13px)</li>
2197
+ <li>· Tables for any list of 3+ structured rows</li>
2198
+ <li>· 1px borders for separation</li>
2199
+ <li>· Forest first, lime for the loudest CTA only</li>
2200
+ <li>· Right-align numbers + add .tabular</li>
2201
+ <li>· Skeletons over spinners</li>
2202
+ <li>· Pair status colors with icons or labels</li>
2203
+ </ul>
2204
+ </div>
2205
+ <div className="rounded border border-border bg-background p-4">
2206
+ <div className="text-xs font-medium uppercase tracking-[0.04em] text-danger mb-2">
2207
+ Don&apos;t
2208
+ </div>
2209
+ <ul className="text-sm text-foreground space-y-1.5">
2210
+ <li>· rounded-xl / rounded-2xl anywhere</li>
2211
+ <li>· White text on lime</li>
2212
+ <li>· Multiple accent (lime) buttons on one screen</li>
2213
+ <li>· Emojis in product UI</li>
2214
+ <li>· Scale-on-hover or bouncy animations</li>
2215
+ <li>· Tinted card backgrounds to indicate stacking</li>
2216
+ <li>· Title Case in UI labels</li>
2217
+ </ul>
2218
+ </div>
2219
+ </div>
2220
+ </DocSubsection>
2221
+ </DocSection>
2222
+ );
2223
+ }
2224
+
2225
+ /* ─────────────────────────────────────────────────────────────────────── */
2226
+ /* Batch A — Forms */
2227
+ /* ─────────────────────────────────────────────────────────────────────── */
2228
+
2229
+ function CheckboxSection() {
2230
+ const [marketing, setMarketing] = React.useState<boolean | "indeterminate">(
2231
+ false
2232
+ );
2233
+ return (
2234
+ <DocSection
2235
+ id="checkbox"
2236
+ title="Checkbox"
2237
+ description="Square 16px control. Brand-forest fill when checked. Supports `indeterminate` (e.g., partial selection in a tree). Always pair with a Label or use FormField."
2238
+ >
2239
+ <PreviewCode
2240
+ preview={
2241
+ <div className="space-y-3">
2242
+ <div className="flex items-center gap-2">
2243
+ <Checkbox id="ck-waiver" defaultChecked />
2244
+ <Label
2245
+ htmlFor="ck-waiver"
2246
+ className="normal-case tracking-normal text-sm font-normal cursor-pointer"
2247
+ >
2248
+ I&apos;ve read and accept the waiver
2249
+ </Label>
2250
+ </div>
2251
+ <div className="flex items-center gap-2">
2252
+ <Checkbox id="ck-credit" />
2253
+ <Label
2254
+ htmlFor="ck-credit"
2255
+ className="normal-case tracking-normal text-sm font-normal cursor-pointer"
2256
+ >
2257
+ Apply $6.00 credit at checkout
2258
+ </Label>
2259
+ </div>
2260
+ <div className="flex items-center gap-2">
2261
+ <Checkbox
2262
+ id="ck-ind"
2263
+ checked={marketing}
2264
+ onCheckedChange={setMarketing}
2265
+ />
2266
+ <Label
2267
+ htmlFor="ck-ind"
2268
+ className="normal-case tracking-normal text-sm font-normal cursor-pointer"
2269
+ >
2270
+ Marketing emails (indeterminate when toggled)
2271
+ </Label>
2272
+ <Button
2273
+ size="xs"
2274
+ variant="ghost"
2275
+ onClick={() =>
2276
+ setMarketing(
2277
+ marketing === "indeterminate" ? false : "indeterminate"
2278
+ )
2279
+ }
2280
+ >
2281
+ toggle
2282
+ </Button>
2283
+ </div>
2284
+ <div className="flex items-center gap-2">
2285
+ <Checkbox id="ck-disabled" disabled defaultChecked />
2286
+ <Label
2287
+ htmlFor="ck-disabled"
2288
+ className="normal-case tracking-normal text-sm font-normal opacity-60"
2289
+ >
2290
+ Disabled (locked in)
2291
+ </Label>
2292
+ </div>
2293
+ </div>
2294
+ }
2295
+ code={
2296
+ <CodeBlock
2297
+ language="tsx"
2298
+ code={`<div className="flex items-center gap-2">
2299
+ <Checkbox id="ck" defaultChecked />
2300
+ <Label htmlFor="ck">I've read and accept the waiver</Label>
2301
+ </div>
2302
+
2303
+ {/* Indeterminate state */}
2304
+ <Checkbox checked="indeterminate" onCheckedChange={…} />`}
2305
+ />
2306
+ }
2307
+ />
2308
+ </DocSection>
2309
+ );
2310
+ }
2311
+
2312
+ function RadioSection() {
2313
+ const [skill, setSkill] = React.useState("mixed");
2314
+ return (
2315
+ <DocSection
2316
+ id="radio"
2317
+ title="Radio group"
2318
+ description="Pick one from a small set. Keyboard nav (Arrow / Home / End) handled by Radix. For >5 options, prefer Select."
2319
+ >
2320
+ <PreviewCode
2321
+ preview={
2322
+ <RadioGroup value={skill} onValueChange={setSkill}>
2323
+ {[
2324
+ ["beginner", "Beginner", "Learning the basics"],
2325
+ ["intermediate", "Intermediate", "Comfortable with the rules"],
2326
+ ["mixed", "Mixed skill", "Open to anyone"],
2327
+ ["advanced", "Advanced", "Competitive only"],
2328
+ ].map(([value, title, desc]) => (
2329
+ <div key={value} className="flex items-start gap-2.5">
2330
+ <RadioGroupItem
2331
+ value={value}
2332
+ id={`rg-${value}`}
2333
+ className="mt-0.5"
2334
+ />
2335
+ <Label
2336
+ htmlFor={`rg-${value}`}
2337
+ className="normal-case tracking-normal cursor-pointer"
2338
+ >
2339
+ <span className="block text-sm font-medium text-foreground">
2340
+ {title}
2341
+ </span>
2342
+ <span className="block text-xs text-text-muted mt-0.5">
2343
+ {desc}
2344
+ </span>
2345
+ </Label>
2346
+ </div>
2347
+ ))}
2348
+ </RadioGroup>
2349
+ }
2350
+ code={
2351
+ <CodeBlock
2352
+ language="tsx"
2353
+ code={`<RadioGroup value={skill} onValueChange={setSkill}>
2354
+ <div className="flex items-center gap-2">
2355
+ <RadioGroupItem value="beginner" id="b" />
2356
+ <Label htmlFor="b">Beginner</Label>
2357
+ </div>
2358
+ <div className="flex items-center gap-2">
2359
+ <RadioGroupItem value="advanced" id="a" />
2360
+ <Label htmlFor="a">Advanced</Label>
2361
+ </div>
2362
+ </RadioGroup>`}
2363
+ />
2364
+ }
2365
+ />
2366
+ </DocSection>
2367
+ );
2368
+ }
2369
+
2370
+ function SwitchSection() {
2371
+ return (
2372
+ <DocSection
2373
+ id="switch"
2374
+ title="Switch"
2375
+ description="For instantly-applied toggles (notifications, quiet hours, theme). Use Checkbox for form fields that need an explicit Save."
2376
+ >
2377
+ <PreviewCode
2378
+ preview={
2379
+ <div className="space-y-3 max-w-md">
2380
+ <div className="flex items-center justify-between gap-3">
2381
+ <Label
2382
+ htmlFor="sw-credits"
2383
+ className="normal-case tracking-normal text-sm font-normal flex-1 cursor-pointer"
2384
+ >
2385
+ Auto-apply credits at checkout
2386
+ </Label>
2387
+ <Switch id="sw-credits" defaultChecked />
2388
+ </div>
2389
+ <div className="flex items-center justify-between gap-3">
2390
+ <div className="flex-1">
2391
+ <Label
2392
+ htmlFor="sw-quiet"
2393
+ className="normal-case tracking-normal text-sm font-normal cursor-pointer"
2394
+ >
2395
+ SMS quiet hours
2396
+ </Label>
2397
+ <p className="text-xs text-text-muted mt-0.5">
2398
+ No SMS 9 pm – 8 am local
2399
+ </p>
2400
+ </div>
2401
+ <Switch id="sw-quiet" defaultChecked />
2402
+ </div>
2403
+ <div className="flex items-center justify-between gap-3">
2404
+ <Label
2405
+ htmlFor="sw-disabled"
2406
+ className="normal-case tracking-normal text-sm font-normal flex-1 opacity-60"
2407
+ >
2408
+ Push notifications (not yet enabled)
2409
+ </Label>
2410
+ <Switch id="sw-disabled" disabled />
2411
+ </div>
2412
+ </div>
2413
+ }
2414
+ code={
2415
+ <CodeBlock
2416
+ language="tsx"
2417
+ code={`<div className="flex items-center justify-between">
2418
+ <Label htmlFor="sw-credits">Auto-apply credits</Label>
2419
+ <Switch id="sw-credits" defaultChecked />
2420
+ </div>`}
2421
+ />
2422
+ }
2423
+ />
2424
+ </DocSection>
2425
+ );
2426
+ }
2427
+
2428
+ function SelectSection() {
2429
+ const [venue, setVenue] = React.useState("");
2430
+ return (
2431
+ <DocSection
2432
+ id="select"
2433
+ title="Select"
2434
+ description="Keyboard-accessible dropdown. Use for venue/field/sport pickers and other curated lists. Supports groups, labels, separators."
2435
+ >
2436
+ <PreviewCode
2437
+ preview={
2438
+ <div className="max-w-xs">
2439
+ <Select value={venue} onValueChange={setVenue}>
2440
+ <SelectTrigger>
2441
+ <SelectValue placeholder="Pick a venue" />
2442
+ </SelectTrigger>
2443
+ <SelectContent>
2444
+ <SelectGroup>
2445
+ <SelectLabel>NYC</SelectLabel>
2446
+ <SelectItem value="astoria">Astoria Turf</SelectItem>
2447
+ <SelectItem value="mccarren">McCarren Park</SelectItem>
2448
+ </SelectGroup>
2449
+ <SelectSeparator />
2450
+ <SelectGroup>
2451
+ <SelectLabel>Coming soon</SelectLabel>
2452
+ <SelectItem value="williamsburg" disabled>
2453
+ Williamsburg Field
2454
+ </SelectItem>
2455
+ <SelectItem value="bushwick" disabled>
2456
+ Bushwick Park
2457
+ </SelectItem>
2458
+ </SelectGroup>
2459
+ </SelectContent>
2460
+ </Select>
2461
+ {venue && (
2462
+ <p className="mt-2 text-xs text-text-muted tabular">
2463
+ Selected: <span className="font-mono">{venue}</span>
2464
+ </p>
2465
+ )}
2466
+ </div>
2467
+ }
2468
+ code={
2469
+ <CodeBlock
2470
+ language="tsx"
2471
+ code={`<Select value={venue} onValueChange={setVenue}>
2472
+ <SelectTrigger>
2473
+ <SelectValue placeholder="Pick a venue" />
2474
+ </SelectTrigger>
2475
+ <SelectContent>
2476
+ <SelectGroup>
2477
+ <SelectLabel>NYC</SelectLabel>
2478
+ <SelectItem value="astoria">Astoria Turf</SelectItem>
2479
+ <SelectItem value="mccarren">McCarren Park</SelectItem>
2480
+ </SelectGroup>
2481
+ </SelectContent>
2482
+ </Select>`}
2483
+ />
2484
+ }
2485
+ />
2486
+ </DocSection>
2487
+ );
2488
+ }
2489
+
2490
+ function TextareaSection() {
2491
+ return (
2492
+ <DocSection
2493
+ id="textarea"
2494
+ title="Textarea"
2495
+ description="Multi-line input. Same focus / aria-invalid pattern as Input. Defaults to vertical resize only."
2496
+ >
2497
+ <PreviewCode
2498
+ preview={
2499
+ <Textarea
2500
+ defaultValue={`- Turf shoes or flats only — no metal cleats.
2501
+ - Bring two shirt colors (dark / light).
2502
+ - Arrive 10 minutes before start.`}
2503
+ placeholder="House rules…"
2504
+ className="max-w-md"
2505
+ rows={5}
2506
+ />
2507
+ }
2508
+ code={
2509
+ <CodeBlock
2510
+ language="tsx"
2511
+ code={`<Textarea
2512
+ placeholder="House rules…"
2513
+ defaultValue="- Turf shoes only…"
2514
+ rows={5}
2515
+ />
2516
+
2517
+ {/* Error state */}
2518
+ <Textarea aria-invalid="true" />`}
2519
+ />
2520
+ }
2521
+ />
2522
+ </DocSection>
2523
+ );
2524
+ }
2525
+
2526
+ function NumberInputSection() {
2527
+ const [capacity, setCapacity] = React.useState(10);
2528
+ const [price, setPrice] = React.useState(14);
2529
+ return (
2530
+ <DocSection
2531
+ id="number-input"
2532
+ title="Number input"
2533
+ description="Stepper-style numeric input with +/− buttons. Use for capacity, min-to-run, price, quantity. Always set min/max for safety."
2534
+ >
2535
+ <PreviewCode
2536
+ preview={
2537
+ <div className="flex items-end gap-4">
2538
+ <div>
2539
+ <Label htmlFor="ni-cap">Capacity</Label>
2540
+ <NumberInput
2541
+ id="ni-cap"
2542
+ value={capacity}
2543
+ onChange={setCapacity}
2544
+ min={2}
2545
+ max={22}
2546
+ step={1}
2547
+ className="mt-1.5"
2548
+ />
2549
+ </div>
2550
+ <div>
2551
+ <Label htmlFor="ni-price">Base price ($)</Label>
2552
+ <NumberInput
2553
+ id="ni-price"
2554
+ value={price}
2555
+ onChange={setPrice}
2556
+ min={0}
2557
+ max={100}
2558
+ step={1}
2559
+ className="mt-1.5"
2560
+ />
2561
+ </div>
2562
+ <Button
2563
+ variant="ghost"
2564
+ size="sm"
2565
+ onClick={() => {
2566
+ setCapacity(10);
2567
+ setPrice(14);
2568
+ }}
2569
+ >
2570
+ Reset
2571
+ </Button>
2572
+ </div>
2573
+ }
2574
+ code={
2575
+ <CodeBlock
2576
+ language="tsx"
2577
+ code={`<NumberInput
2578
+ value={capacity}
2579
+ onChange={setCapacity}
2580
+ min={2}
2581
+ max={22}
2582
+ step={1}
2583
+ />`}
2584
+ />
2585
+ }
2586
+ />
2587
+ </DocSection>
2588
+ );
2589
+ }
2590
+
2591
+ function FormFieldSection() {
2592
+ return (
2593
+ <DocSection
2594
+ id="form-field"
2595
+ title="Form field"
2596
+ description="Composition wrapper. Auto-generates an id, links the Label, and injects aria-describedby / aria-invalid into the control. Switch between helper and error text via props."
2597
+ >
2598
+ <Example>
2599
+ <form
2600
+ className="grid gap-4 max-w-md"
2601
+ onSubmit={(e) => {
2602
+ e.preventDefault();
2603
+ toast.success("Form submitted (demo).");
2604
+ }}
2605
+ noValidate
2606
+ >
2607
+ <FormField
2608
+ label="Email"
2609
+ required
2610
+ helper="We'll send booking confirmations here."
2611
+ >
2612
+ <Input
2613
+ type="email"
2614
+ placeholder="you@dtsports.tv"
2615
+ autoComplete="email"
2616
+ required
2617
+ />
2618
+ </FormField>
2619
+
2620
+ <FormField
2621
+ label="Skill level"
2622
+ helper="Helps us match you with the right games."
2623
+ >
2624
+ <Select defaultValue="mixed">
2625
+ <SelectTrigger>
2626
+ <SelectValue />
2627
+ </SelectTrigger>
2628
+ <SelectContent>
2629
+ <SelectItem value="beginner">Beginner</SelectItem>
2630
+ <SelectItem value="intermediate">Intermediate</SelectItem>
2631
+ <SelectItem value="mixed">Mixed</SelectItem>
2632
+ <SelectItem value="advanced">Advanced</SelectItem>
2633
+ </SelectContent>
2634
+ </Select>
2635
+ </FormField>
2636
+
2637
+ <FormField
2638
+ label="House rules"
2639
+ helper="Markdown, one bullet per rule."
2640
+ >
2641
+ <Textarea
2642
+ rows={4}
2643
+ defaultValue="- Turf shoes only.\n- Bring two shirt colors."
2644
+ />
2645
+ </FormField>
2646
+
2647
+ <FormField
2648
+ label="Card number"
2649
+ required
2650
+ error="Card number is too short."
2651
+ >
2652
+ <Input defaultValue="4242 4242" />
2653
+ </FormField>
2654
+
2655
+ <div className="flex justify-end gap-2 pt-2">
2656
+ <Button variant="ghost" type="button">
2657
+ Cancel
2658
+ </Button>
2659
+ <Button variant="accent" type="submit">
2660
+ Submit
2661
+ </Button>
2662
+ </div>
2663
+ </form>
2664
+ </Example>
2665
+ <CodeBlock
2666
+ language="tsx"
2667
+ code={`<FormField
2668
+ label="Email"
2669
+ required
2670
+ helper="We'll send booking confirmations here."
2671
+ >
2672
+ <Input type="email" autoComplete="email" />
2673
+ </FormField>
2674
+
2675
+ {/* Error replaces helper, sets aria-invalid + aria-describedby */}
2676
+ <FormField label="Card" required error="Card number is too short.">
2677
+ <Input />
2678
+ </FormField>`}
2679
+ />
2680
+ </DocSection>
2681
+ );
2682
+ }
2683
+
2684
+ function AlertSection() {
2685
+ const [dismissed, setDismissed] = React.useState(false);
2686
+ return (
2687
+ <DocSection
2688
+ id="alert"
2689
+ title="Alert"
2690
+ description="Inline status messages that stay put. Use Toast for transient feedback; use Alert when the message belongs inline. Danger variant uses role='alert' for SR interruption."
2691
+ >
2692
+ <div className="space-y-3 max-w-2xl">
2693
+ <Alert variant="info" title="DT Sports TV streams on YouTube.">
2694
+ Paste any YouTube URL when creating a game — we embed it
2695
+ automatically.
2696
+ </Alert>
2697
+ <Alert variant="warning" title="1 game below minimum to run">
2698
+ Astoria · Field B · 8 pm has 2/6. Promote from waitlist or cancel +
2699
+ refund before 6 pm.
2700
+ </Alert>
2701
+ <Alert variant="danger" title="Payment failed">
2702
+ Your bank declined the charge. Try a different card or contact your
2703
+ bank.
2704
+ </Alert>
2705
+ <Alert variant="success" title="Booking confirmed.">
2706
+ Saturday, May 25 at 6:00 pm at Astoria Turf · Field A.
2707
+ </Alert>
2708
+ {!dismissed && (
2709
+ <Alert
2710
+ variant="info"
2711
+ title="Dismissible"
2712
+ dismissible
2713
+ onDismiss={() => setDismissed(true)}
2714
+ >
2715
+ Set `dismissible` and provide `onDismiss` for a close button.
2716
+ </Alert>
2717
+ )}
2718
+ {dismissed && (
2719
+ <Button size="sm" variant="ghost" onClick={() => setDismissed(false)}>
2720
+ Restore dismissed alert
2721
+ </Button>
2722
+ )}
2723
+ </div>
2724
+ <CodeBlock
2725
+ language="tsx"
2726
+ code={`<Alert variant="warning" title="1 game below minimum">
2727
+ Astoria Field B at 8 pm needs 4 more.
2728
+ </Alert>
2729
+
2730
+ <Alert variant="info" dismissible onDismiss={() => …}>
2731
+
2732
+ </Alert>`}
2733
+ />
2734
+ </DocSection>
2735
+ );
2736
+ }
2737
+
2738
+ /* ─────────────────────────────────────────────────────────────────────── */
2739
+ /* Mid-page additions */
2740
+ /* ─────────────────────────────────────────────────────────────────────── */
2741
+
2742
+ function CapacitySection() {
2743
+ return (
2744
+ <DocSection
2745
+ id="capacity"
2746
+ title="Capacity bar"
2747
+ description="The spots-filled visualization used everywhere games appear. Three colors: forest (normal), warning (below minToRun), foreground (full). Includes role='progressbar' and a default aria-label."
2748
+ >
2749
+ <PreviewCode
2750
+ preview={
2751
+ <div className="space-y-3 max-w-md">
2752
+ <CapacityBar filled={3} capacity={10} minToRun={6} />
2753
+ <CapacityBar filled={5} capacity={10} minToRun={6} />
2754
+ <CapacityBar filled={8} capacity={10} minToRun={6} />
2755
+ <CapacityBar filled={10} capacity={10} minToRun={6} />
2756
+ <div className="pt-2">
2757
+ <CapacityBar filled={4} capacity={10} minToRun={6} size="lg" />
2758
+ </div>
2759
+ </div>
2760
+ }
2761
+ code={
2762
+ <CodeBlock
2763
+ language="tsx"
2764
+ code={`<CapacityBar filled={8} capacity={10} minToRun={6} />
2765
+
2766
+ {/* Hide count */}
2767
+ <CapacityBar filled={3} capacity={10} showCount={false} />
2768
+
2769
+ {/* Larger */}
2770
+ <CapacityBar filled={4} capacity={10} size="lg" />`}
2771
+ />
2772
+ }
2773
+ />
2774
+ </DocSection>
2775
+ );
2776
+ }
2777
+
2778
+ function TabsSection() {
2779
+ return (
2780
+ <DocSection
2781
+ id="tabs"
2782
+ title="Tabs"
2783
+ description="Underline-style horizontal tabs. Keyboard nav (←/→/Home/End) handled by Radix. Use for time-window filters or status filters in dashboards."
2784
+ >
2785
+ <Tabs defaultValue="today" className="w-full">
2786
+ <TabsList>
2787
+ <TabsTrigger value="today">Today</TabsTrigger>
2788
+ <TabsTrigger value="week">
2789
+ This week
2790
+ <Badge variant="neutral" size="xs">
2791
+ 12
2792
+ </Badge>
2793
+ </TabsTrigger>
2794
+ <TabsTrigger value="past">Past</TabsTrigger>
2795
+ <TabsTrigger value="cancelled" disabled>
2796
+ Cancelled
2797
+ </TabsTrigger>
2798
+ </TabsList>
2799
+ <TabsContent value="today">
2800
+ <div className="rounded border border-border bg-background p-4 text-sm text-text-muted">
2801
+ 6 games scheduled for today.
2802
+ </div>
2803
+ </TabsContent>
2804
+ <TabsContent value="week">
2805
+ <div className="rounded border border-border bg-background p-4 text-sm text-text-muted">
2806
+ 12 games this week across 2 venues.
2807
+ </div>
2808
+ </TabsContent>
2809
+ <TabsContent value="past">
2810
+ <div className="rounded border border-border bg-background p-4 text-sm text-text-muted">
2811
+ Past games would render here.
2812
+ </div>
2813
+ </TabsContent>
2814
+ </Tabs>
2815
+ <CodeBlock
2816
+ language="tsx"
2817
+ code={`<Tabs defaultValue="today">
2818
+ <TabsList>
2819
+ <TabsTrigger value="today">Today</TabsTrigger>
2820
+ <TabsTrigger value="week">This week</TabsTrigger>
2821
+ </TabsList>
2822
+ <TabsContent value="today">…</TabsContent>
2823
+ <TabsContent value="week">…</TabsContent>
2824
+ </Tabs>`}
2825
+ />
2826
+ </DocSection>
2827
+ );
2828
+ }
2829
+
2830
+ function AccordionSection() {
2831
+ return (
2832
+ <DocSection
2833
+ id="accordion"
2834
+ title="Accordion"
2835
+ description="Stacked expandable sections. `type='single' collapsible` for FAQ-style, `type='multiple'` for settings panels. The chevron rotates on open."
2836
+ >
2837
+ <Accordion
2838
+ type="single"
2839
+ collapsible
2840
+ className="max-w-2xl rounded border border-border bg-background px-4"
2841
+ >
2842
+ <AccordionItem value="rules">
2843
+ <AccordionTrigger>House rules — Astoria Turf</AccordionTrigger>
2844
+ <AccordionContent>
2845
+ <ul className="text-sm text-text-muted space-y-1.5">
2846
+ <li>· Turf shoes or flats only — no metal cleats.</li>
2847
+ <li>· Bring two shirt colors (dark / light).</li>
2848
+ <li>· Arrive 10 minutes before start for check-in.</li>
2849
+ <li>· No spectators on the turf — sideline benches only.</li>
2850
+ </ul>
2851
+ </AccordionContent>
2852
+ </AccordionItem>
2853
+ <AccordionItem value="policy">
2854
+ <AccordionTrigger>Cancellation policy</AccordionTrigger>
2855
+ <AccordionContent>
2856
+ <p className="text-sm text-text-muted">
2857
+ Refunds are issued as DT Sports credit (instant). Cancel 24h+ for
2858
+ full credit, less than 2h for none. DT Sports cancellations are
2859
+ always 100%.
2860
+ </p>
2861
+ </AccordionContent>
2862
+ </AccordionItem>
2863
+ <AccordionItem value="about">
2864
+ <AccordionTrigger>About this format</AccordionTrigger>
2865
+ <AccordionContent>
2866
+ <p className="text-sm text-text-muted">
2867
+ 5v5 turf soccer, 90 minutes, two halves. We balance teams on-site
2868
+ after warm-ups.
2869
+ </p>
2870
+ </AccordionContent>
2871
+ </AccordionItem>
2872
+ </Accordion>
2873
+ <CodeBlock
2874
+ language="tsx"
2875
+ code={`<Accordion type="single" collapsible>
2876
+ <AccordionItem value="rules">
2877
+ <AccordionTrigger>House rules</AccordionTrigger>
2878
+ <AccordionContent>…</AccordionContent>
2879
+ </AccordionItem>
2880
+ </Accordion>`}
2881
+ />
2882
+ </DocSection>
2883
+ );
2884
+ }
2885
+
2886
+ function DrawerSection() {
2887
+ return (
2888
+ <DocSection
2889
+ id="drawer"
2890
+ title="Drawer"
2891
+ description="Side-pushed panel for secondary tasks. Use instead of nested dialogs (per §3.17). Default side is right; `side='bottom'` for mobile filter sheets."
2892
+ >
2893
+ <div className="flex flex-wrap gap-2">
2894
+ <Drawer>
2895
+ <DrawerTrigger asChild>
2896
+ <Button variant="secondary">
2897
+ <Edit className="h-4 w-4" strokeWidth={1.75} />
2898
+ Edit game (right)
2899
+ </Button>
2900
+ </DrawerTrigger>
2901
+ <DrawerContent side="right">
2902
+ <DrawerHeader>
2903
+ <DrawerTitle>Edit game</DrawerTitle>
2904
+ <DrawerDescription>
2905
+ Astoria Turf · Field A · Sat May 25, 6:00 pm
2906
+ </DrawerDescription>
2907
+ </DrawerHeader>
2908
+ <DrawerBody>
2909
+ <div className="space-y-4">
2910
+ <FormField label="Capacity">
2911
+ <NumberInput defaultValue={10} min={2} max={22} />
2912
+ </FormField>
2913
+ <FormField label="Base price">
2914
+ <NumberInput defaultValue={14} min={0} max={100} />
2915
+ </FormField>
2916
+ <FormField
2917
+ label="Notes"
2918
+ helper="Visible to players in confirmation email."
2919
+ >
2920
+ <Textarea rows={3} placeholder="Optional…" />
2921
+ </FormField>
2922
+ </div>
2923
+ </DrawerBody>
2924
+ <DrawerFooter>
2925
+ <Button variant="ghost" size="sm">
2926
+ Cancel
2927
+ </Button>
2928
+ <Button variant="primary" size="sm">
2929
+ Save changes
2930
+ </Button>
2931
+ </DrawerFooter>
2932
+ </DrawerContent>
2933
+ </Drawer>
2934
+
2935
+ <Drawer>
2936
+ <DrawerTrigger asChild>
2937
+ <Button variant="secondary">Filters (bottom)</Button>
2938
+ </DrawerTrigger>
2939
+ <DrawerContent side="bottom">
2940
+ <DrawerHeader>
2941
+ <DrawerTitle>Filters</DrawerTitle>
2942
+ </DrawerHeader>
2943
+ <DrawerBody>
2944
+ <p className="text-sm text-text-muted">
2945
+ A bottom drawer is the right pattern for mobile filter sheets.
2946
+ </p>
2947
+ </DrawerBody>
2948
+ </DrawerContent>
2949
+ </Drawer>
2950
+ </div>
2951
+ <CodeBlock
2952
+ language="tsx"
2953
+ code={`<Drawer>
2954
+ <DrawerTrigger asChild><Button>Edit…</Button></DrawerTrigger>
2955
+ <DrawerContent side="right">
2956
+ <DrawerHeader>
2957
+ <DrawerTitle>Edit game</DrawerTitle>
2958
+ <DrawerDescription>Astoria Turf · Field A</DrawerDescription>
2959
+ </DrawerHeader>
2960
+ <DrawerBody>…</DrawerBody>
2961
+ <DrawerFooter>
2962
+ <Button variant="ghost">Cancel</Button>
2963
+ <Button>Save</Button>
2964
+ </DrawerFooter>
2965
+ </DrawerContent>
2966
+ </Drawer>`}
2967
+ />
2968
+ </DocSection>
2969
+ );
2970
+ }
2971
+
2972
+ function PopoverSection() {
2973
+ return (
2974
+ <DocSection
2975
+ id="popover"
2976
+ title="Popover"
2977
+ description="Generic click-to-open floating panel. Use for date pickers, mini filter menus, inline 'quick edit' forms. For action menus, use DropdownMenu."
2978
+ >
2979
+ <div className="flex flex-wrap gap-2">
2980
+ <Popover>
2981
+ <PopoverTrigger asChild>
2982
+ <Button variant="secondary" size="sm">
2983
+ <Info className="h-4 w-4" strokeWidth={1.75} />
2984
+ Quick add player
2985
+ </Button>
2986
+ </PopoverTrigger>
2987
+ <PopoverContent>
2988
+ <div className="space-y-3">
2989
+ <div>
2990
+ <h4 className="text-sm font-semibold">Walk-in player</h4>
2991
+ <p className="text-xs text-text-muted mt-0.5">
2992
+ Add a player who showed up without booking.
2993
+ </p>
2994
+ </div>
2995
+ <FormField label="Name">
2996
+ <Input placeholder="Alex Chen" />
2997
+ </FormField>
2998
+ <FormField label="Charge">
2999
+ <NumberInput defaultValue={14} min={0} max={50} />
3000
+ </FormField>
3001
+ <Button size="sm" variant="accent" className="w-full">
3002
+ Add & charge
3003
+ </Button>
3004
+ </div>
3005
+ </PopoverContent>
3006
+ </Popover>
3007
+ </div>
3008
+ <CodeBlock
3009
+ language="tsx"
3010
+ code={`<Popover>
3011
+ <PopoverTrigger asChild><Button>Quick add</Button></PopoverTrigger>
3012
+ <PopoverContent>
3013
+ <h4>Walk-in player</h4>
3014
+
3015
+ </PopoverContent>
3016
+ </Popover>`}
3017
+ />
3018
+ </DocSection>
3019
+ );
3020
+ }
3021
+
3022
+ function DropdownSection() {
3023
+ const [autoApply, setAutoApply] = React.useState(true);
3024
+ return (
3025
+ <DocSection
3026
+ id="dropdown"
3027
+ title="Dropdown menu"
3028
+ description="Actions menu (kebab on table rows, user avatar menu). Supports labels, separators, checkbox items, sub-menus, keyboard shortcuts. NOT for selecting a value — use Select for that."
3029
+ >
3030
+ <div className="flex flex-wrap gap-3">
3031
+ <DropdownMenu>
3032
+ <DropdownMenuTrigger asChild>
3033
+ <Button variant="ghost" size="icon" aria-label="More actions">
3034
+ <MoreHorizontal className="h-4 w-4" strokeWidth={1.75} />
3035
+ </Button>
3036
+ </DropdownMenuTrigger>
3037
+ <DropdownMenuContent align="start">
3038
+ <DropdownMenuLabel>Game · Astoria · 6 pm</DropdownMenuLabel>
3039
+ <DropdownMenuItem onSelect={() => toast("View roster.")}>
3040
+ <Users className="h-3.5 w-3.5" strokeWidth={1.75} />
3041
+ View roster
3042
+ <DropdownMenuShortcut>⌘R</DropdownMenuShortcut>
3043
+ </DropdownMenuItem>
3044
+ <DropdownMenuItem onSelect={() => toast("Edit game.")}>
3045
+ <Edit className="h-3.5 w-3.5" strokeWidth={1.75} />
3046
+ Edit
3047
+ <DropdownMenuShortcut>⌘E</DropdownMenuShortcut>
3048
+ </DropdownMenuItem>
3049
+ <DropdownMenuSeparator />
3050
+ <DropdownMenuCheckboxItem
3051
+ checked={autoApply}
3052
+ onCheckedChange={(c) => setAutoApply(!!c)}
3053
+ >
3054
+ Auto-promote waitlist
3055
+ </DropdownMenuCheckboxItem>
3056
+ <DropdownMenuSeparator />
3057
+ <DropdownMenuItem
3058
+ className="text-danger focus:text-danger focus:bg-danger/10"
3059
+ onSelect={() => toast.error("Cancel-game flow not built yet.")}
3060
+ >
3061
+ <Trash2 className="h-3.5 w-3.5" strokeWidth={1.75} />
3062
+ Cancel game…
3063
+ </DropdownMenuItem>
3064
+ </DropdownMenuContent>
3065
+ </DropdownMenu>
3066
+
3067
+ <DropdownMenu>
3068
+ <DropdownMenuTrigger asChild>
3069
+ <Button variant="secondary" size="sm">
3070
+ <Settings className="h-3.5 w-3.5" strokeWidth={1.75} />
3071
+ Settings
3072
+ <ChevronDown className="h-3 w-3 opacity-60" strokeWidth={1.75} />
3073
+ </Button>
3074
+ </DropdownMenuTrigger>
3075
+ <DropdownMenuContent align="end">
3076
+ <DropdownMenuLabel>Profile</DropdownMenuLabel>
3077
+ <DropdownMenuItem>Edit profile</DropdownMenuItem>
3078
+ <DropdownMenuItem>Notification preferences</DropdownMenuItem>
3079
+ <DropdownMenuSeparator />
3080
+ <DropdownMenuItem className="text-danger focus:text-danger focus:bg-danger/10">
3081
+ Sign out
3082
+ <DropdownMenuShortcut>⌘Q</DropdownMenuShortcut>
3083
+ </DropdownMenuItem>
3084
+ </DropdownMenuContent>
3085
+ </DropdownMenu>
3086
+ </div>
3087
+ <CodeBlock
3088
+ language="tsx"
3089
+ code={`<DropdownMenu>
3090
+ <DropdownMenuTrigger asChild>
3091
+ <Button size="icon"><MoreHorizontal /></Button>
3092
+ </DropdownMenuTrigger>
3093
+ <DropdownMenuContent>
3094
+ <DropdownMenuLabel>Game</DropdownMenuLabel>
3095
+ <DropdownMenuItem>View roster</DropdownMenuItem>
3096
+ <DropdownMenuItem>Edit</DropdownMenuItem>
3097
+ <DropdownMenuSeparator />
3098
+ <DropdownMenuCheckboxItem checked={auto} onCheckedChange={setAuto}>
3099
+ Auto-promote waitlist
3100
+ </DropdownMenuCheckboxItem>
3101
+ <DropdownMenuSeparator />
3102
+ <DropdownMenuItem className="text-danger">Cancel…</DropdownMenuItem>
3103
+ </DropdownMenuContent>
3104
+ </DropdownMenu>`}
3105
+ />
3106
+ </DocSection>
3107
+ );
3108
+ }
3109
+
3110
+ function TooltipSection() {
3111
+ return (
3112
+ <DocSection
3113
+ id="tooltip"
3114
+ title="Tooltip"
3115
+ description="Keyboard-accessible hover hints. Preferred over native title= (mouse-only). The TooltipProvider is already wired into ThemeProvider — no extra setup needed in your app."
3116
+ >
3117
+ <div className="flex items-center gap-3">
3118
+ <Tooltip>
3119
+ <TooltipTrigger asChild>
3120
+ <Button variant="ghost" size="icon" aria-label="View roster">
3121
+ <Users className="h-4 w-4" strokeWidth={1.75} />
3122
+ </Button>
3123
+ </TooltipTrigger>
3124
+ <TooltipContent>View roster</TooltipContent>
3125
+ </Tooltip>
3126
+ <Tooltip>
3127
+ <TooltipTrigger asChild>
3128
+ <Button variant="ghost" size="icon" aria-label="Edit game">
3129
+ <Edit className="h-4 w-4" strokeWidth={1.75} />
3130
+ </Button>
3131
+ </TooltipTrigger>
3132
+ <TooltipContent>Edit game · ⌘E</TooltipContent>
3133
+ </Tooltip>
3134
+ <Tooltip>
3135
+ <TooltipTrigger asChild>
3136
+ <Button variant="ghost" size="icon" aria-label="Delete">
3137
+ <Trash2 className="h-4 w-4 text-danger" strokeWidth={1.75} />
3138
+ </Button>
3139
+ </TooltipTrigger>
3140
+ <TooltipContent>Delete (no undo)</TooltipContent>
3141
+ </Tooltip>
3142
+ <span className="text-xs text-text-muted ml-3">
3143
+ Tab through them — tooltips appear on keyboard focus, not just mouse.
3144
+ </span>
3145
+ </div>
3146
+ <CodeBlock
3147
+ language="tsx"
3148
+ code={`<Tooltip>
3149
+ <TooltipTrigger asChild>
3150
+ <Button size="icon" aria-label="View roster">
3151
+ <Users />
3152
+ </Button>
3153
+ </TooltipTrigger>
3154
+ <TooltipContent>View roster</TooltipContent>
3155
+ </Tooltip>`}
3156
+ />
3157
+ </DocSection>
3158
+ );
3159
+ }
3160
+
3161
+ function SeparatorSection() {
3162
+ return (
3163
+ <DocSection
3164
+ id="separator"
3165
+ title="Separator"
3166
+ description="Thin divider (<hr> horizontal, vertical bar). Decorative by default; set decorative={false} when it semantically separates groups."
3167
+ >
3168
+ <div className="rounded border border-border bg-background p-4 max-w-md">
3169
+ <div className="space-y-1">
3170
+ <div className="text-sm font-semibold">Saturday&apos;s games</div>
3171
+ <p className="text-xs text-text-muted">3 venues · 6 games</p>
3172
+ </div>
3173
+ <Separator className="my-3" />
3174
+ <div className="flex h-5 items-center gap-3 text-sm">
3175
+ <div className="tabular">6:00 pm</div>
3176
+ <Separator orientation="vertical" />
3177
+ <div>Astoria · Field A</div>
3178
+ <Separator orientation="vertical" />
3179
+ <div className="tabular text-text-muted">8/10</div>
3180
+ </div>
3181
+ </div>
3182
+ <CodeBlock
3183
+ language="tsx"
3184
+ code={`<Separator /> {/* horizontal */}
3185
+ <Separator orientation="vertical" />`}
3186
+ />
3187
+ </DocSection>
3188
+ );
3189
+ }
3190
+
3191
+ function SpinnerSection() {
3192
+ return (
3193
+ <DocSection
3194
+ id="spinner"
3195
+ title="Spinner"
3196
+ description="Tiny inline loader for buttons and per-row 'working' state. Prefer Skeleton for full-region loading (per §3.14)."
3197
+ >
3198
+ <PreviewCode
3199
+ preview={
3200
+ <div className="flex items-center gap-3">
3201
+ <Spinner size="sm" />
3202
+ <Spinner />
3203
+ <Spinner size="lg" />
3204
+ <Button variant="primary" disabled>
3205
+ <Spinner size="sm" /> Processing payment…
3206
+ </Button>
3207
+ </div>
3208
+ }
3209
+ code={
3210
+ <CodeBlock
3211
+ language="tsx"
3212
+ code={`<Spinner size="sm" />
3213
+ <Spinner />
3214
+ <Spinner size="lg" />
3215
+
3216
+ {/* In a button */}
3217
+ <Button disabled>
3218
+ <Spinner size="sm" /> Processing…
3219
+ </Button>`}
3220
+ />
3221
+ }
3222
+ />
3223
+ </DocSection>
3224
+ );
3225
+ }
3226
+
3227
+ function ProgressSection() {
3228
+ const [value, setValue] = React.useState(33);
3229
+ return (
3230
+ <DocSection
3231
+ id="progress"
3232
+ title="Progress"
3233
+ description="For multi-step flows with a known percent (waiver → emergency contact → done; file uploads). For spots filled, use CapacityBar. For unknown duration, use Spinner or Skeleton."
3234
+ >
3235
+ <PreviewCode
3236
+ preview={
3237
+ <div className="space-y-4 max-w-md">
3238
+ <div>
3239
+ <div className="flex items-center justify-between mb-1.5">
3240
+ <span className="text-xs text-text-muted">
3241
+ Step 1 of 3 — Profile
3242
+ </span>
3243
+ <span className="text-xs tabular font-medium">{value}%</span>
3244
+ </div>
3245
+ <Progress value={value} />
3246
+ </div>
3247
+ <div className="flex gap-2">
3248
+ <Button
3249
+ size="sm"
3250
+ variant="ghost"
3251
+ onClick={() => setValue(Math.max(0, value - 33))}
3252
+ >
3253
+ Back
3254
+ </Button>
3255
+ <Button
3256
+ size="sm"
3257
+ variant="primary"
3258
+ onClick={() => setValue(Math.min(100, value + 33))}
3259
+ >
3260
+ Next
3261
+ </Button>
3262
+ </div>
3263
+ </div>
3264
+ }
3265
+ code={
3266
+ <CodeBlock
3267
+ language="tsx"
3268
+ code={`<Progress value={66} />
3269
+
3270
+ {/* With label + percent */}
3271
+ <div className="flex justify-between text-xs">
3272
+ <span>Step 2 of 3</span>
3273
+ <span>66%</span>
3274
+ </div>
3275
+ <Progress value={66} />`}
3276
+ />
3277
+ }
3278
+ />
3279
+ </DocSection>
3280
+ );
3281
+ }
3282
+
3283
+ /* ─────────────────────────────────────────────────────────────────────── */
3284
+ /* Batch C — Data + display additions */
3285
+ /* ─────────────────────────────────────────────────────────────────────── */
3286
+
3287
+ function ChipSection() {
3288
+ const [filters, setFilters] = React.useState<string[]>([
3289
+ "Astoria Turf",
3290
+ "Sat 5/24",
3291
+ "5v5",
3292
+ ]);
3293
+ const remove = (label: string) =>
3294
+ setFilters(filters.filter((f) => f !== label));
3295
+
3296
+ return (
3297
+ <DocSection
3298
+ id="chip"
3299
+ title="Chip"
3300
+ description="Small removable token — use for active filter indicators, applied tags, selected options in a multi-select summary. Distinct from Badge: Chips are interactive."
3301
+ >
3302
+ <PreviewCode
3303
+ preview={
3304
+ <div className="space-y-3">
3305
+ <div className="flex flex-wrap items-center gap-1.5">
3306
+ <span className="text-xs text-text-muted mr-1">
3307
+ Active filters:
3308
+ </span>
3309
+ {filters.length === 0 && (
3310
+ <span className="text-xs text-text-subtle italic">
3311
+ no filters · click below to add some back
3312
+ </span>
3313
+ )}
3314
+ {filters.map((f) => (
3315
+ <Chip key={f} onRemove={() => remove(f)}>
3316
+ {f}
3317
+ </Chip>
3318
+ ))}
3319
+ </div>
3320
+ <div className="flex flex-wrap gap-1.5">
3321
+ <Chip variant="accent">accent</Chip>
3322
+ <Chip variant="neutral">neutral</Chip>
3323
+ <Chip variant="outline">outline</Chip>
3324
+ <Chip variant="accent" onRemove={() => {}}>
3325
+ with × button
3326
+ </Chip>
3327
+ </div>
3328
+ {filters.length < 3 && (
3329
+ <Button
3330
+ size="xs"
3331
+ variant="ghost"
3332
+ onClick={() => setFilters(["Astoria Turf", "Sat 5/24", "5v5"])}
3333
+ >
3334
+ Reset
3335
+ </Button>
3336
+ )}
3337
+ </div>
3338
+ }
3339
+ code={
3340
+ <CodeBlock
3341
+ language="tsx"
3342
+ code={`<Chip onRemove={() => removeFilter("astoria")}>
3343
+ Astoria Turf
3344
+ </Chip>
3345
+
3346
+ <Chip variant="neutral">Soccer</Chip>
3347
+ <Chip variant="outline">5v5</Chip>`}
3348
+ />
3349
+ }
3350
+ />
3351
+ </DocSection>
3352
+ );
3353
+ }
3354
+
3355
+ function SpecSection() {
3356
+ return (
3357
+ <DocSection
3358
+ id="spec"
3359
+ title="Spec list"
3360
+ description="Semantic key-value display (<dl> / <dt> / <dd>). Use for 'About this game' panels, settings summaries, receipts. Grid columns 1–4."
3361
+ >
3362
+ <PreviewCode
3363
+ preview={
3364
+ <Card className="max-w-2xl">
3365
+ <CardHeader>
3366
+ <CardTitle className="text-sm">About this game</CardTitle>
3367
+ </CardHeader>
3368
+ <CardContent>
3369
+ <SpecList columns={2}>
3370
+ <Spec label="Sport">Soccer</Spec>
3371
+ <Spec label="Format">5v5</Spec>
3372
+ <Spec label="Skill level">Mixed</Spec>
3373
+ <Spec label="Capacity">
3374
+ <span className="tabular">10 players (6 min to run)</span>
3375
+ </Spec>
3376
+ <Spec label="Venue">Astoria Turf · Field A</Spec>
3377
+ <Spec label="Address">19-04 30th Dr, Astoria, NY</Spec>
3378
+ </SpecList>
3379
+ </CardContent>
3380
+ </Card>
3381
+ }
3382
+ code={
3383
+ <CodeBlock
3384
+ language="tsx"
3385
+ code={`<SpecList columns={2}>
3386
+ <Spec label="Format">5v5</Spec>
3387
+ <Spec label="Capacity">10 players</Spec>
3388
+ <Spec label="Start"><Timestamp iso={…} /></Spec>
3389
+ </SpecList>`}
3390
+ />
3391
+ }
3392
+ />
3393
+ </DocSection>
3394
+ );
3395
+ }
3396
+
3397
+ function PaginationSection() {
3398
+ const [page, setPage] = React.useState(1);
3399
+ return (
3400
+ <DocSection
3401
+ id="pagination"
3402
+ title="Pagination"
3403
+ description="Use when total > 1. For URL-driven pagination per §3.9, wire onPageChange to router.replace(?page=N) — that gives you a shareable + back-button-safe URL."
3404
+ >
3405
+ <div className="space-y-3">
3406
+ <div className="flex items-center justify-between">
3407
+ <span className="text-xs text-text-muted tabular">
3408
+ Page {page} of 28
3409
+ </span>
3410
+ <Pagination
3411
+ currentPage={page}
3412
+ totalPages={28}
3413
+ onPageChange={setPage}
3414
+ />
3415
+ </div>
3416
+ <div className="text-xs text-text-subtle">
3417
+ Examples below for different total counts:
3418
+ </div>
3419
+ <div className="space-y-2">
3420
+ <Pagination currentPage={1} totalPages={3} onPageChange={() => {}} />
3421
+ <Pagination currentPage={1} totalPages={10} onPageChange={() => {}} />
3422
+ <Pagination
3423
+ currentPage={50}
3424
+ totalPages={100}
3425
+ onPageChange={() => {}}
3426
+ />
3427
+ </div>
3428
+ </div>
3429
+ <CodeBlock
3430
+ language="tsx"
3431
+ code={`<Pagination
3432
+ currentPage={page}
3433
+ totalPages={totalPages}
3434
+ onPageChange={setPage}
3435
+ />
3436
+
3437
+ {/* URL-driven (recommended) */}
3438
+ const setPage = (p: number) =>
3439
+ router.replace(\`?page=\${p}\`, { scroll: false });`}
3440
+ />
3441
+ </DocSection>
3442
+ );
3443
+ }
3444
+
3445
+ function AvatarGroupSection() {
3446
+ return (
3447
+ <DocSection
3448
+ id="avatar-group"
3449
+ title="Avatar group"
3450
+ description="Stacked overlapping avatars with a +N overflow pill. Use for roster previews and 'N people booked' indicators."
3451
+ >
3452
+ <PreviewCode
3453
+ preview={
3454
+ <div className="space-y-3">
3455
+ <AvatarGroup max={4} label="8 players booked">
3456
+ <Avatar name="Alex Chen" />
3457
+ <Avatar name="Jordan Lee" />
3458
+ <Avatar name="Sam Patel" status="checked-in" />
3459
+ <Avatar name="Maria Garcia" />
3460
+ <Avatar name="Danny Tran" />
3461
+ <Avatar name="Priya Singh" />
3462
+ <Avatar name="Ethan Brooks" />
3463
+ <Avatar name="Nina Park" />
3464
+ </AvatarGroup>
3465
+ <div className="flex items-center gap-3">
3466
+ <AvatarGroup max={3} size="sm">
3467
+ <Avatar name="Alex Chen" size="sm" />
3468
+ <Avatar name="Jordan Lee" size="sm" />
3469
+ <Avatar name="Sam Patel" size="sm" />
3470
+ <Avatar name="Maria Garcia" size="sm" />
3471
+ <Avatar name="Danny Tran" size="sm" />
3472
+ </AvatarGroup>
3473
+ <span className="text-xs text-text-muted">
3474
+ Compact size for table rows.
3475
+ </span>
3476
+ </div>
3477
+ </div>
3478
+ }
3479
+ code={
3480
+ <CodeBlock
3481
+ language="tsx"
3482
+ code={`<AvatarGroup max={4} label="8 players booked">
3483
+ <Avatar name="Alex Chen" />
3484
+ <Avatar name="Jordan Lee" />
3485
+ <Avatar name="Sam Patel" status="checked-in" />
3486
+
3487
+ </AvatarGroup>`}
3488
+ />
3489
+ }
3490
+ />
3491
+ </DocSection>
3492
+ );
3493
+ }
3494
+
3495
+ /* ─────────────────────────────────────────────────────────────────────── */
3496
+ /* Batch C — Layout */
3497
+ /* ─────────────────────────────────────────────────────────────────────── */
3498
+
3499
+ function AppShellSection() {
3500
+ return (
3501
+ <DocSection
3502
+ id="app-shell"
3503
+ title="App shell"
3504
+ description="The full-height flex frame. AppShell + AppMain compose any sidebar-and-content app. Use sticky TopBar inside AppMain so it pins on scroll. The /admin route uses exactly this pattern."
3505
+ >
3506
+ <CodeBlock
3507
+ language="tsx"
3508
+ code={`<AppShell>
3509
+ <Sidebar>…</Sidebar>
3510
+ <AppMain>
3511
+ <TopBar>
3512
+ <Breadcrumb>…</Breadcrumb>
3513
+ </TopBar>
3514
+ <main className="flex-1 px-6 py-6">
3515
+ {children}
3516
+ </main>
3517
+ </AppMain>
3518
+ </AppShell>`}
3519
+ />
3520
+ <p className="text-xs text-text-muted">
3521
+ Live in <code className="font-mono">/admin</code> — the route uses
3522
+ AppShell, Sidebar, and TopBar primitives.
3523
+ </p>
3524
+ </DocSection>
3525
+ );
3526
+ }
3527
+
3528
+ function SidebarComponentSection() {
3529
+ return (
3530
+ <DocSection
3531
+ id="sidebar-comp"
3532
+ title="Sidebar"
3533
+ description="Composable nav sidebar. Use Sidebar / SidebarHeader / SidebarBody / SidebarFooter for the shell; SidebarSection + SidebarItem for the nav. SidebarItem supports asChild for Next.js Link integration."
3534
+ >
3535
+ <Example label="Sidebar in a 280px frame">
3536
+ <div className="border border-border-strong rounded overflow-hidden h-[420px] flex bg-background">
3537
+ <div className="w-56 shrink-0 bg-bg-subtle/40 border-r border-border flex flex-col">
3538
+ <SidebarHeader>
3539
+ <div className="flex items-center gap-2">
3540
+ <div
3541
+ className="h-6 w-6 rounded-sm bg-brand-forest grid place-items-center text-brand-forest-foreground font-bold text-xs tabular"
3542
+ aria-hidden="true"
3543
+ >
3544
+ DT
3545
+ </div>
3546
+ <div>
3547
+ <div className="text-[10px] font-medium uppercase tracking-[0.06em] text-text-subtle">
3548
+ DT Sports
3549
+ </div>
3550
+ <div className="text-sm font-semibold leading-none">
3551
+ Admin
3552
+ </div>
3553
+ </div>
3554
+ </div>
3555
+ </SidebarHeader>
3556
+ <SidebarBody>
3557
+ <SidebarSection label="Today">
3558
+ <SidebarItem
3559
+ href="#today"
3560
+ active
3561
+ icon={
3562
+ <LayoutDashboard className="h-4 w-4" strokeWidth={1.75} />
3563
+ }
3564
+ >
3565
+ Today
3566
+ </SidebarItem>
3567
+ <SidebarItem
3568
+ href="#week"
3569
+ icon={<CalendarDays className="h-4 w-4" strokeWidth={1.75} />}
3570
+ >
3571
+ This week
3572
+ </SidebarItem>
3573
+ </SidebarSection>
3574
+ <SidebarSection label="Manage">
3575
+ <SidebarItem
3576
+ href="#games"
3577
+ icon={<Trophy className="h-4 w-4" strokeWidth={1.75} />}
3578
+ >
3579
+ Games
3580
+ </SidebarItem>
3581
+ <SidebarItem
3582
+ href="#series"
3583
+ icon={<Repeat className="h-4 w-4" strokeWidth={1.75} />}
3584
+ >
3585
+ Series
3586
+ </SidebarItem>
3587
+ <SidebarItem
3588
+ href="#players"
3589
+ icon={<Users className="h-4 w-4" strokeWidth={1.75} />}
3590
+ badge={
3591
+ <Badge variant="warning" size="xs">
3592
+ 4
3593
+ </Badge>
3594
+ }
3595
+ >
3596
+ Players
3597
+ </SidebarItem>
3598
+ </SidebarSection>
3599
+ </SidebarBody>
3600
+ <SidebarFooter>
3601
+ <div className="text-[10px] text-text-subtle font-mono tabular">
3602
+ v0.1 · main
3603
+ </div>
3604
+ </SidebarFooter>
3605
+ </div>
3606
+ <div className="flex-1 grid place-items-center text-text-subtle text-xs">
3607
+ (page content)
3608
+ </div>
3609
+ </div>
3610
+ </Example>
3611
+ <CodeBlock
3612
+ language="tsx"
3613
+ code={`<Sidebar>
3614
+ <SidebarHeader><BrandMark /></SidebarHeader>
3615
+ <SidebarBody>
3616
+ <SidebarSection label="Today">
3617
+ <SidebarItem asChild active>
3618
+ <Link href="/admin">
3619
+ <LayoutDashboard className="h-4 w-4" strokeWidth={1.75} />
3620
+ <span className="flex-1 truncate">Today</span>
3621
+ </Link>
3622
+ </SidebarItem>
3623
+ </SidebarSection>
3624
+ </SidebarBody>
3625
+ <SidebarFooter>v0.1</SidebarFooter>
3626
+ </Sidebar>`}
3627
+ />
3628
+ </DocSection>
3629
+ );
3630
+ }
3631
+
3632
+ function TopBarSection() {
3633
+ return (
3634
+ <DocSection
3635
+ id="top-bar"
3636
+ title="Top bar"
3637
+ description="Sticky 48px header. Renders the chrome; content layout is the consumer's. Common pattern: breadcrumb left, search center, actions right."
3638
+ >
3639
+ <Example>
3640
+ <div className="border border-border-strong rounded overflow-hidden bg-background">
3641
+ <TopBar className="static">
3642
+ <Breadcrumb>
3643
+ <BreadcrumbList>
3644
+ <BreadcrumbItem>
3645
+ <BreadcrumbLink href="#">Admin</BreadcrumbLink>
3646
+ </BreadcrumbItem>
3647
+ <BreadcrumbSeparator />
3648
+ <BreadcrumbItem>
3649
+ <BreadcrumbPage>Today</BreadcrumbPage>
3650
+ </BreadcrumbItem>
3651
+ </BreadcrumbList>
3652
+ </Breadcrumb>
3653
+ <div className="flex-1" />
3654
+ <Badge variant="forest" size="sm">
3655
+ v0.1
3656
+ </Badge>
3657
+ <Button size="sm" variant="ghost" aria-label="Settings">
3658
+ <Settings className="h-4 w-4" strokeWidth={1.75} />
3659
+ </Button>
3660
+ </TopBar>
3661
+ <div className="px-6 py-8 text-text-subtle text-xs">
3662
+ (page content here)
3663
+ </div>
3664
+ </div>
3665
+ </Example>
3666
+ <CodeBlock
3667
+ language="tsx"
3668
+ code={`<TopBar>
3669
+ <Breadcrumb>…</Breadcrumb>
3670
+ <div className="flex-1" />
3671
+ <ThemeToggle />
3672
+ <Avatar name="Danny Tran" size="sm" />
3673
+ </TopBar>`}
3674
+ />
3675
+ </DocSection>
3676
+ );
3677
+ }
3678
+
3679
+ function BreadcrumbSection() {
3680
+ return (
3681
+ <DocSection
3682
+ id="breadcrumb"
3683
+ title="Breadcrumb"
3684
+ description="Semantic <nav aria-label='Breadcrumb'> with ordered list, links, separator, and a non-link page indicator for the current location."
3685
+ >
3686
+ <PreviewCode
3687
+ preview={
3688
+ <div className="space-y-3">
3689
+ <Breadcrumb>
3690
+ <BreadcrumbList>
3691
+ <BreadcrumbItem>
3692
+ <BreadcrumbLink href="#">Admin</BreadcrumbLink>
3693
+ </BreadcrumbItem>
3694
+ <BreadcrumbSeparator />
3695
+ <BreadcrumbItem>
3696
+ <BreadcrumbPage>Today</BreadcrumbPage>
3697
+ </BreadcrumbItem>
3698
+ </BreadcrumbList>
3699
+ </Breadcrumb>
3700
+ <Breadcrumb>
3701
+ <BreadcrumbList>
3702
+ <BreadcrumbItem>
3703
+ <BreadcrumbLink href="#">Admin</BreadcrumbLink>
3704
+ </BreadcrumbItem>
3705
+ <BreadcrumbSeparator />
3706
+ <BreadcrumbItem>
3707
+ <BreadcrumbLink href="#">Games</BreadcrumbLink>
3708
+ </BreadcrumbItem>
3709
+ <BreadcrumbSeparator />
3710
+ <BreadcrumbItem>
3711
+ <BreadcrumbLink href="#">Astoria Turf</BreadcrumbLink>
3712
+ </BreadcrumbItem>
3713
+ <BreadcrumbSeparator />
3714
+ <BreadcrumbItem>
3715
+ <BreadcrumbPage>Field A · Sat 6 pm</BreadcrumbPage>
3716
+ </BreadcrumbItem>
3717
+ </BreadcrumbList>
3718
+ </Breadcrumb>
3719
+ </div>
3720
+ }
3721
+ code={
3722
+ <CodeBlock
3723
+ language="tsx"
3724
+ code={`<Breadcrumb>
3725
+ <BreadcrumbList>
3726
+ <BreadcrumbItem>
3727
+ <BreadcrumbLink asChild>
3728
+ <Link href="/admin">Admin</Link>
3729
+ </BreadcrumbLink>
3730
+ </BreadcrumbItem>
3731
+ <BreadcrumbSeparator />
3732
+ <BreadcrumbItem>
3733
+ <BreadcrumbPage>Today</BreadcrumbPage>
3734
+ </BreadcrumbItem>
3735
+ </BreadcrumbList>
3736
+ </Breadcrumb>`}
3737
+ />
3738
+ }
3739
+ />
3740
+ </DocSection>
3741
+ );
3742
+ }
3743
+
3744
+ function ContainerSection() {
3745
+ return (
3746
+ <DocSection
3747
+ id="container"
3748
+ title="Container"
3749
+ description="Max-width page wrapper with consistent horizontal padding (24/32px responsive). Sizes: sm (768) · default (1024) · lg (1280) · full."
3750
+ >
3751
+ <div className="space-y-3">
3752
+ {(["sm", "default", "lg", "full"] as const).map((size) => (
3753
+ <div key={size} className="border border-border rounded">
3754
+ <Container size={size} className="py-4">
3755
+ <div className="rounded bg-bg-subtle border border-border px-3 py-2 text-xs tabular text-text-muted">
3756
+ size=&quot;{size}&quot; — wraps content at the right width
3757
+ </div>
3758
+ </Container>
3759
+ </div>
3760
+ ))}
3761
+ </div>
3762
+ <CodeBlock
3763
+ language="tsx"
3764
+ code={`<Container size="lg">
3765
+ <h1>Page heading</h1>
3766
+
3767
+ </Container>`}
3768
+ />
3769
+ </DocSection>
3770
+ );
3771
+ }
3772
+
3773
+ function StackSection() {
3774
+ return (
3775
+ <DocSection
3776
+ id="stack"
3777
+ title="Stack"
3778
+ description="Flexbox with consistent gap. Documents intent (vs raw `flex flex-col gap-3`) and keeps gap tokens consistent. Both vertical and horizontal."
3779
+ >
3780
+ <PreviewCode
3781
+ preview={
3782
+ <div className="space-y-4">
3783
+ <Stack gap={2}>
3784
+ <div className="rounded border border-border bg-background px-3 py-2 text-xs">
3785
+ Vertical · gap=2
3786
+ </div>
3787
+ <div className="rounded border border-border bg-background px-3 py-2 text-xs">
3788
+ Vertical · gap=2
3789
+ </div>
3790
+ <div className="rounded border border-border bg-background px-3 py-2 text-xs">
3791
+ Vertical · gap=2
3792
+ </div>
3793
+ </Stack>
3794
+ <Stack direction="row" gap={3} align="center" justify="between">
3795
+ <span className="text-sm">
3796
+ Row · align=center · justify=between
3797
+ </span>
3798
+ <Button size="sm" variant="primary">
3799
+ Action
3800
+ </Button>
3801
+ </Stack>
3802
+ <Stack direction="row" gap={2} wrap>
3803
+ {Array.from({ length: 10 }).map((_, i) => (
3804
+ <Chip key={i} variant="neutral">
3805
+ Tag {i + 1}
3806
+ </Chip>
3807
+ ))}
3808
+ </Stack>
3809
+ </div>
3810
+ }
3811
+ code={
3812
+ <CodeBlock
3813
+ language="tsx"
3814
+ code={`<Stack gap={2}>…</Stack>
3815
+
3816
+ <Stack direction="row" gap={3} align="center" justify="between">
3817
+ <h2>Heading</h2>
3818
+ <Button>Action</Button>
3819
+ </Stack>
3820
+
3821
+ <Stack direction="row" gap={2} wrap>
3822
+ {tags.map(t => <Chip key={t}>{t}</Chip>)}
3823
+ </Stack>`}
3824
+ />
3825
+ }
3826
+ />
3827
+ </DocSection>
3828
+ );
3829
+ }
3830
+
3831
+ function AspectRatioSection() {
3832
+ return (
3833
+ <DocSection
3834
+ id="aspect-ratio"
3835
+ title="Aspect ratio"
3836
+ description="Locks a region to a given ratio without layout shift. Use for video embeds, image placeholders, banners. Pass `ratio={16 / 9}` etc."
3837
+ >
3838
+ <PreviewCode
3839
+ preview={
3840
+ <div className="grid grid-cols-2 gap-3 max-w-md">
3841
+ <div className="rounded border border-border overflow-hidden">
3842
+ <AspectRatio ratio={16 / 9}>
3843
+ <div className="h-full w-full bg-bg-subtle grid place-items-center text-xs text-text-muted">
3844
+ 16:9
3845
+ </div>
3846
+ </AspectRatio>
3847
+ </div>
3848
+ <div className="rounded border border-border overflow-hidden">
3849
+ <AspectRatio ratio={1}>
3850
+ <div className="h-full w-full bg-bg-subtle grid place-items-center text-xs text-text-muted">
3851
+ 1:1
3852
+ </div>
3853
+ </AspectRatio>
3854
+ </div>
3855
+ <div className="rounded border border-border overflow-hidden col-span-2">
3856
+ <AspectRatio ratio={21 / 9}>
3857
+ <div className="h-full w-full bg-bg-subtle grid place-items-center text-xs text-text-muted">
3858
+ 21:9
3859
+ </div>
3860
+ </AspectRatio>
3861
+ </div>
3862
+ </div>
3863
+ }
3864
+ code={
3865
+ <CodeBlock
3866
+ language="tsx"
3867
+ code={`<AspectRatio ratio={16 / 9}>
3868
+ <iframe src={url} className="h-full w-full" />
3869
+ </AspectRatio>`}
3870
+ />
3871
+ }
3872
+ />
3873
+ </DocSection>
3874
+ );
3875
+ }
3876
+
3877
+ function ScrollAreaSection() {
3878
+ return (
3879
+ <DocSection
3880
+ id="scroll-area"
3881
+ title="Scroll area"
3882
+ description="Styled, consistent custom scrollbars across browsers. Wraps bounded content that needs to scroll. Don't use for full-page scroll — that's the browser's job."
3883
+ >
3884
+ <PreviewCode
3885
+ preview={
3886
+ <ScrollArea className="h-48 w-full max-w-sm rounded border border-border bg-background p-3">
3887
+ <h4 className="text-sm font-semibold mb-2">
3888
+ Roster — Astoria Turf
3889
+ </h4>
3890
+ <ul className="space-y-1.5 text-sm">
3891
+ {[
3892
+ "Alex Chen",
3893
+ "Jordan Lee",
3894
+ "Sam Patel",
3895
+ "Maria Garcia",
3896
+ "Danny Tran",
3897
+ "Priya Singh",
3898
+ "Ethan Brooks",
3899
+ "Nina Park",
3900
+ "Oliver Stone",
3901
+ "Reese Wang",
3902
+ "Sophia Liu",
3903
+ "Tomás Vega",
3904
+ "Uma Patel",
3905
+ "Vince Holloway",
3906
+ "Wei Chen",
3907
+ "Xander Hayes",
3908
+ ].map((name) => (
3909
+ <li key={name} className="flex items-center gap-2">
3910
+ <Avatar name={name} size="sm" />
3911
+ <span>{name}</span>
3912
+ </li>
3913
+ ))}
3914
+ </ul>
3915
+ </ScrollArea>
3916
+ }
3917
+ code={
3918
+ <CodeBlock
3919
+ language="tsx"
3920
+ code={`<ScrollArea className="h-48 w-full rounded border">
3921
+ <RosterList players={…} />
3922
+ </ScrollArea>`}
3923
+ />
3924
+ }
3925
+ />
3926
+ </DocSection>
3927
+ );
3928
+ }
3929
+
3930
+ function ComboboxSection() {
3931
+ const [venue, setVenue] = React.useState<string | null>(null);
3932
+ return (
3933
+ <DocSection
3934
+ id="combobox"
3935
+ title="Combobox"
3936
+ description="Searchable single-select. Reach for this over Select once the list is long enough that typing beats scrolling (~10+ options)."
3937
+ >
3938
+ <PreviewCode
3939
+ preview={
3940
+ <div className="max-w-xs">
3941
+ <Combobox
3942
+ aria-label="Venue"
3943
+ placeholder="Select venue…"
3944
+ options={[
3945
+ { value: "astoria", label: "Astoria Turf" },
3946
+ { value: "mccarren", label: "McCarren Park" },
3947
+ { value: "greenpoint", label: "Greenpoint Dome" },
3948
+ { value: "redhook", label: "Red Hook Fields" },
3949
+ { value: "bushwick", label: "Bushwick Park", disabled: true },
3950
+ ]}
3951
+ value={venue}
3952
+ onValueChange={setVenue}
3953
+ />
3954
+ </div>
3955
+ }
3956
+ code={
3957
+ <CodeBlock
3958
+ language="tsx"
3959
+ code={`<Combobox
3960
+ aria-label="Venue"
3961
+ placeholder="Select venue…"
3962
+ options={venues.map(v => ({ value: v.id, label: v.name }))}
3963
+ value={venueId}
3964
+ onValueChange={setVenueId}
3965
+ />`}
3966
+ />
3967
+ }
3968
+ />
3969
+ </DocSection>
3970
+ );
3971
+ }
3972
+
3973
+ function SliderSection() {
3974
+ const [price, setPrice] = React.useState([20]);
3975
+ return (
3976
+ <DocSection
3977
+ id="slider"
3978
+ title="Slider"
3979
+ description="Range input for bounded numeric choices (max price, distance radius). Arrow keys step; PageUp/Down big-step; Home/End jump to the ends. 44px thumb targets on touch."
3980
+ >
3981
+ <PreviewCode
3982
+ preview={
3983
+ <div className="max-w-xs space-y-2">
3984
+ <Slider
3985
+ aria-label="Max price"
3986
+ min={0}
3987
+ max={50}
3988
+ step={5}
3989
+ value={price}
3990
+ onValueChange={setPrice}
3991
+ />
3992
+ <p className="text-xs text-text-muted tabular">
3993
+ Max price: ${price[0]}
3994
+ </p>
3995
+ </div>
3996
+ }
3997
+ code={
3998
+ <CodeBlock
3999
+ language="tsx"
4000
+ code={`<Slider aria-label="Max price" min={0} max={50} step={5}
4001
+ value={price} onValueChange={setPrice} />
4002
+
4003
+ {/* Two thumbs = range */}
4004
+ <Slider aria-label="Price range" defaultValue={[10, 30]} />`}
4005
+ />
4006
+ }
4007
+ />
4008
+ </DocSection>
4009
+ );
4010
+ }
4011
+
4012
+ function DatePickerSection() {
4013
+ const [date, setDate] = React.useState<Date | undefined>(undefined);
4014
+ return (
4015
+ <DocSection
4016
+ id="date-picker"
4017
+ title="Date picker"
4018
+ description="Single-date selection in a popover; closes on pick. The picked Date is midnight LOCAL time — convert against the venue tz before persisting (§3.16). Compose Calendar directly for inline or range use."
4019
+ >
4020
+ <PreviewCode
4021
+ preview={
4022
+ <div className="max-w-xs">
4023
+ <DatePicker
4024
+ aria-label="Game date"
4025
+ value={date}
4026
+ onValueChange={setDate}
4027
+ />
4028
+ </div>
4029
+ }
4030
+ code={
4031
+ <CodeBlock
4032
+ language="tsx"
4033
+ code={`<DatePicker
4034
+ aria-label="Game date"
4035
+ value={gameDate}
4036
+ onValueChange={setGameDate}
4037
+ disabledDates={{ before: new Date() }}
4038
+ />
4039
+
4040
+ {/* Inline calendar */}
4041
+ <Calendar mode="single" selected={date} onSelect={setDate} />`}
4042
+ />
4043
+ }
4044
+ />
4045
+ <DocSubsection
4046
+ title="Inline calendar"
4047
+ description="The same Calendar component, embedded (availability views, embedded scheduling)."
4048
+ >
4049
+ <div className="rounded border border-border bg-background inline-block">
4050
+ <Calendar mode="single" selected={date} onSelect={setDate} />
4051
+ </div>
4052
+ </DocSubsection>
4053
+ </DocSection>
4054
+ );
4055
+ }
4056
+
4057
+ function CollapsibleSection() {
4058
+ return (
4059
+ <DocSection
4060
+ id="collapsible"
4061
+ title="Collapsible"
4062
+ description="One independent disclosure. Accordion is for grouped sections with one-open-at-a-time semantics; Collapsible is a single 'show more'."
4063
+ >
4064
+ <PreviewCode
4065
+ preview={
4066
+ <div className="max-w-md rounded border border-border bg-background p-4">
4067
+ <Collapsible>
4068
+ <CollapsibleTrigger>Advanced options</CollapsibleTrigger>
4069
+ <CollapsibleContent>
4070
+ <div className="space-y-2 text-sm text-text-muted">
4071
+ <p>Waitlist auto-promotion, visibility, and refund rules.</p>
4072
+ </div>
4073
+ </CollapsibleContent>
4074
+ </Collapsible>
4075
+ </div>
4076
+ }
4077
+ code={
4078
+ <CodeBlock
4079
+ language="tsx"
4080
+ code={`<Collapsible>
4081
+ <CollapsibleTrigger>Advanced options</CollapsibleTrigger>
4082
+ <CollapsibleContent>…</CollapsibleContent>
4083
+ </Collapsible>`}
4084
+ />
4085
+ }
4086
+ />
4087
+ </DocSection>
4088
+ );
4089
+ }
4090
+
4091
+ function MarketingTierSection() {
4092
+ return (
4093
+ <DocSection
4094
+ id="marketing"
4095
+ title="Marketing tier"
4096
+ description="Primitives for the marketing site, sharing the same token source as the app tier. Display type is fluid (clamp 32→96px); spacing extends to 160px; the brand gradient is bg-gradient-brand."
4097
+ >
4098
+ <DocSubsection
4099
+ title="Display typography"
4100
+ description="text-display-sm / text-display / text-display-lg — fluid between phone and desktop. Resize the window to see them scale."
4101
+ >
4102
+ <div className="space-y-4 rounded border border-border bg-background p-6 overflow-hidden">
4103
+ <div className="text-display-lg font-bold">Aa 96</div>
4104
+ <div className="text-display font-bold">Book a game 64</div>
4105
+ <div className="text-display-sm font-bold">
4106
+ Three taps to kickoff 44
4107
+ </div>
4108
+ </div>
4109
+ </DocSubsection>
4110
+
4111
+ <DocSubsection
4112
+ title="Page composition"
4113
+ description="MarketingNav · Hero · Section · LogoCloud · Footer — a full page in five components. All server-safe except the nav."
4114
+ >
4115
+ <div className="rounded border border-border overflow-hidden">
4116
+ <MarketingNav
4117
+ brand={
4118
+ <a
4119
+ href="#marketing"
4120
+ aria-label="DT Sports home"
4121
+ className="font-bold tracking-[-0.01em]"
4122
+ >
4123
+ DT<span className="text-brand-forest">Sports</span>
4124
+ </a>
4125
+ }
4126
+ links={[
4127
+ { label: "Games", href: "#marketing" },
4128
+ { label: "Venues", href: "#marketing" },
4129
+ { label: "Pricing", href: "#marketing" },
4130
+ ]}
4131
+ cta={<Button variant="accent">Find a game</Button>}
4132
+ className="relative"
4133
+ />
4134
+ <Hero
4135
+ eyebrow="Pickup soccer, organized"
4136
+ title="Book a game in 30 seconds"
4137
+ description="Real fields, fair teams, instant refunds. Every game money-back if it doesn't run."
4138
+ actions={
4139
+ <>
4140
+ <Button variant="accent" size="touch">
4141
+ Find a game
4142
+ </Button>
4143
+ <Button variant="secondary" size="touch">
4144
+ Host with us
4145
+ </Button>
4146
+ </>
4147
+ }
4148
+ className="py-16 md:py-16 lg:py-16"
4149
+ />
4150
+ <LogoCloud label="Trusted by leagues across NYC">
4151
+ <span className="text-lg font-bold" aria-label="Astoria League">
4152
+ ASTORIA
4153
+ </span>
4154
+ <span className="text-lg font-bold" aria-label="McCarren FC">
4155
+ McCARREN FC
4156
+ </span>
4157
+ <span className="text-lg font-bold" aria-label="Greenpoint Dome">
4158
+ GP DOME
4159
+ </span>
4160
+ </LogoCloud>
4161
+ <Footer
4162
+ brand={<span className="font-bold">DT Sports</span>}
4163
+ tagline="Pickup sports, organized."
4164
+ columns={[
4165
+ {
4166
+ title: "Product",
4167
+ links: [
4168
+ { label: "Games", href: "#marketing" },
4169
+ { label: "Venues", href: "#marketing" },
4170
+ ],
4171
+ },
4172
+ {
4173
+ title: "Company",
4174
+ links: [{ label: "About", href: "#marketing" }],
4175
+ },
4176
+ ]}
4177
+ legal="© 2026 DT Sports"
4178
+ />
4179
+ </div>
4180
+ </DocSubsection>
4181
+
4182
+ <DocSubsection
4183
+ title="Prose"
4184
+ description="Long-form content styling for blog/legal/CMS pages — no typography plugin needed."
4185
+ >
4186
+ <div className="rounded border border-border bg-background p-6">
4187
+ <Prose>
4188
+ <h2>Refund policy</h2>
4189
+ <p>
4190
+ Cancel up to 2 hours before kickoff for a{" "}
4191
+ <a href="#marketing">full credit</a>. Weather cancellations refund
4192
+ automatically.
4193
+ </p>
4194
+ <ul>
4195
+ <li>Host cancellations refund to the original card.</li>
4196
+ <li>Credits never expire.</li>
4197
+ </ul>
4198
+ </Prose>
4199
+ </div>
4200
+ </DocSubsection>
4201
+
4202
+ <CodeBlock
4203
+ language="tsx"
4204
+ code={`import {
4205
+ MarketingNav, Hero, Section, Footer, LogoCloud, Prose,
4206
+ } from "@dtsports/design-system/marketing";
4207
+
4208
+ <Hero
4209
+ eyebrow="Pickup soccer, organized"
4210
+ title="Book a game in 30 seconds"
4211
+ actions={<Button variant="accent" size="touch">Find a game</Button>}
4212
+ />`}
4213
+ />
4214
+ </DocSection>
4215
+ );
4216
+ }