@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,100 @@
1
+ import { cn } from "../lib/utils";
2
+ import { Badge } from "./badge";
3
+ import { LivePill } from "./live-pill";
4
+
5
+ /**
6
+ * StatusPill — per design.md §3 (game-status colors)
7
+ *
8
+ * One source of truth for game status visualization across tables, cards,
9
+ * detail pages. Always includes an icon/dot signal plus text so it isn't
10
+ * color-only.
11
+ *
12
+ * status: draft | scheduled | filling | sold_out | live | completed | cancelled
13
+ */
14
+ export type GameStatus =
15
+ | "draft"
16
+ | "scheduled"
17
+ | "filling"
18
+ | "sold_out"
19
+ | "live"
20
+ | "completed"
21
+ | "cancelled";
22
+
23
+ const labels: Record<GameStatus, string> = {
24
+ draft: "Draft",
25
+ scheduled: "Scheduled",
26
+ filling: "Filling",
27
+ sold_out: "Sold out",
28
+ live: "Live",
29
+ completed: "Completed",
30
+ cancelled: "Cancelled",
31
+ };
32
+
33
+ export function StatusPill({
34
+ status,
35
+ className,
36
+ }: {
37
+ status: GameStatus;
38
+ className?: string;
39
+ }) {
40
+ if (status === "live") return <LivePill className={className} />;
41
+
42
+ if (status === "draft" || status === "completed") {
43
+ return (
44
+ <Badge
45
+ variant="muted-outline"
46
+ shape="pill"
47
+ className={cn("gap-1.5", className)}
48
+ >
49
+ <span
50
+ className="h-1.5 w-1.5 rounded-full bg-text-subtle"
51
+ aria-hidden="true"
52
+ />
53
+ {labels[status]}
54
+ </Badge>
55
+ );
56
+ }
57
+
58
+ if (status === "scheduled") {
59
+ return (
60
+ <Badge variant="forest" shape="pill" className={className}>
61
+ {labels[status]}
62
+ </Badge>
63
+ );
64
+ }
65
+
66
+ if (status === "filling") {
67
+ return (
68
+ <Badge variant="info" shape="pill" className={className}>
69
+ {labels[status]}
70
+ </Badge>
71
+ );
72
+ }
73
+
74
+ if (status === "sold_out") {
75
+ return (
76
+ <Badge
77
+ shape="pill"
78
+ className={cn("bg-foreground text-background", className)}
79
+ >
80
+ {labels[status]}
81
+ </Badge>
82
+ );
83
+ }
84
+
85
+ if (status === "cancelled") {
86
+ return (
87
+ <Badge
88
+ shape="pill"
89
+ className={cn(
90
+ "border border-danger bg-transparent text-danger",
91
+ className
92
+ )}
93
+ >
94
+ {labels[status]}
95
+ </Badge>
96
+ );
97
+ }
98
+
99
+ return null;
100
+ }
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as SwitchPrimitive from "@radix-ui/react-switch";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * Switch — for boolean settings. Use only for instantly-applied toggles
9
+ * (theme, notification preferences, "use credit at checkout"). Don't use
10
+ * Switch for form fields that need an explicit Save button — use Checkbox.
11
+ */
12
+ export const Switch = React.forwardRef<
13
+ React.ElementRef<typeof SwitchPrimitive.Root>,
14
+ React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
15
+ >(({ className, ...props }, ref) => (
16
+ <SwitchPrimitive.Root
17
+ ref={ref}
18
+ className={cn(
19
+ "peer inline-flex h-4 w-7 shrink-0 cursor-pointer items-center rounded-full transition-colors",
20
+ // Invisible hit-area extension: 24px desktop / 44px touch.
21
+ "relative after:absolute after:-inset-y-1 pointer-coarse:after:-inset-y-3.5 pointer-coarse:after:-inset-x-2",
22
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
23
+ "disabled:cursor-not-allowed disabled:opacity-50",
24
+ "data-[state=checked]:bg-brand-forest data-[state=unchecked]:bg-border-strong",
25
+ className
26
+ )}
27
+ {...props}
28
+ >
29
+ <SwitchPrimitive.Thumb
30
+ className={cn(
31
+ "pointer-events-none block h-3 w-3 rounded-full bg-background shadow-sm transition-transform",
32
+ "data-[state=checked]:translate-x-3.5 data-[state=unchecked]:translate-x-0.5"
33
+ )}
34
+ />
35
+ </SwitchPrimitive.Root>
36
+ ));
37
+ Switch.displayName = "Switch";
@@ -0,0 +1,129 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ /**
5
+ * Table primitives — per design.md §10
6
+ *
7
+ * Centerpiece component for the data-heavy UI.
8
+ * - Row height: 36px compact (default) / 40px comfortable
9
+ * - Header: text-xs uppercase tracking-wide muted on bg-subtle
10
+ * - Cell padding: 8px / 12px
11
+ * - Border: 1px bottom on rows, NO vertical borders
12
+ * - Numeric columns: add `tabular text-right` to cells
13
+ * - Sticky header: wrap <Table> in a scrollable parent and set <TableHeader sticky>
14
+ *
15
+ * Accessibility:
16
+ * - Always provide a <caption> via <TableCaption> for screen readers (it can be visually hidden)
17
+ * - Use scope="col" / scope="row" on <TableHead> / row headers
18
+ * - For sortable columns, pass aria-sort on <TableHead>
19
+ */
20
+
21
+ export const Table = React.forwardRef<
22
+ HTMLTableElement,
23
+ React.HTMLAttributes<HTMLTableElement>
24
+ >(({ className, ...props }, ref) => (
25
+ <div className="w-full overflow-auto scrollbar-thin">
26
+ <table
27
+ ref={ref}
28
+ className={cn("w-full caption-bottom text-sm border-collapse", className)}
29
+ {...props}
30
+ />
31
+ </div>
32
+ ));
33
+ Table.displayName = "Table";
34
+
35
+ export const TableHeader = React.forwardRef<
36
+ HTMLTableSectionElement,
37
+ React.HTMLAttributes<HTMLTableSectionElement> & { sticky?: boolean }
38
+ >(({ className, sticky, ...props }, ref) => (
39
+ <thead
40
+ ref={ref}
41
+ className={cn(
42
+ "bg-bg-subtle border-b border-border",
43
+ sticky && "sticky top-0 z-10",
44
+ className
45
+ )}
46
+ {...props}
47
+ />
48
+ ));
49
+ TableHeader.displayName = "TableHeader";
50
+
51
+ export const TableBody = React.forwardRef<
52
+ HTMLTableSectionElement,
53
+ React.HTMLAttributes<HTMLTableSectionElement>
54
+ >(({ className, ...props }, ref) => (
55
+ <tbody ref={ref} className={cn("", className)} {...props} />
56
+ ));
57
+ TableBody.displayName = "TableBody";
58
+
59
+ export const TableFooter = React.forwardRef<
60
+ HTMLTableSectionElement,
61
+ React.HTMLAttributes<HTMLTableSectionElement>
62
+ >(({ className, ...props }, ref) => (
63
+ <tfoot
64
+ ref={ref}
65
+ className={cn("bg-bg-subtle font-medium border-t border-border", className)}
66
+ {...props}
67
+ />
68
+ ));
69
+ TableFooter.displayName = "TableFooter";
70
+
71
+ export const TableRow = React.forwardRef<
72
+ HTMLTableRowElement,
73
+ React.HTMLAttributes<HTMLTableRowElement> & {
74
+ interactive?: boolean;
75
+ selected?: boolean;
76
+ }
77
+ >(({ className, interactive, selected, ...props }, ref) => (
78
+ <tr
79
+ ref={ref}
80
+ className={cn(
81
+ "border-b border-border transition-colors duration-150 ease-out",
82
+ interactive && "cursor-pointer hover:bg-bg-subtle",
83
+ selected && "bg-brand-lime/[0.06] border-l-2 border-l-brand-forest",
84
+ className
85
+ )}
86
+ {...props}
87
+ />
88
+ ));
89
+ TableRow.displayName = "TableRow";
90
+
91
+ export const TableHead = React.forwardRef<
92
+ HTMLTableCellElement,
93
+ React.ThHTMLAttributes<HTMLTableCellElement>
94
+ >(({ className, ...props }, ref) => (
95
+ <th
96
+ ref={ref}
97
+ scope="col"
98
+ className={cn(
99
+ "h-9 px-3 text-left align-middle text-xs font-medium uppercase tracking-[0.04em] text-text-muted",
100
+ className
101
+ )}
102
+ {...props}
103
+ />
104
+ ));
105
+ TableHead.displayName = "TableHead";
106
+
107
+ export const TableCell = React.forwardRef<
108
+ HTMLTableCellElement,
109
+ React.TdHTMLAttributes<HTMLTableCellElement>
110
+ >(({ className, ...props }, ref) => (
111
+ <td
112
+ ref={ref}
113
+ className={cn("h-9 px-3 py-2 align-middle text-sm", className)}
114
+ {...props}
115
+ />
116
+ ));
117
+ TableCell.displayName = "TableCell";
118
+
119
+ export const TableCaption = React.forwardRef<
120
+ HTMLTableCaptionElement,
121
+ React.HTMLAttributes<HTMLTableCaptionElement>
122
+ >(({ className, ...props }, ref) => (
123
+ <caption
124
+ ref={ref}
125
+ className={cn("mt-2 text-xs text-text-muted text-left", className)}
126
+ {...props}
127
+ />
128
+ ));
129
+ TableCaption.displayName = "TableCaption";
@@ -0,0 +1,72 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as TabsPrimitive from "@radix-ui/react-tabs";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * Tabs — underline-style horizontal tabs.
9
+ *
10
+ * <Tabs defaultValue="today">
11
+ * <TabsList>
12
+ * <TabsTrigger value="today">Today</TabsTrigger>
13
+ * <TabsTrigger value="week">This week</TabsTrigger>
14
+ * </TabsList>
15
+ * <TabsContent value="today">…</TabsContent>
16
+ * <TabsContent value="week">…</TabsContent>
17
+ * </Tabs>
18
+ *
19
+ * Use for: admin time-window filters, booking status filters, settings
20
+ * groupings. Keyboard nav (←/→/Home/End) handled by Radix.
21
+ */
22
+ export const Tabs = TabsPrimitive.Root;
23
+
24
+ export const TabsList = React.forwardRef<
25
+ React.ElementRef<typeof TabsPrimitive.List>,
26
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
27
+ >(({ className, ...props }, ref) => (
28
+ <TabsPrimitive.List
29
+ ref={ref}
30
+ className={cn(
31
+ "inline-flex h-9 items-center gap-1 border-b border-border w-full",
32
+ className
33
+ )}
34
+ {...props}
35
+ />
36
+ ));
37
+ TabsList.displayName = "TabsList";
38
+
39
+ export const TabsTrigger = React.forwardRef<
40
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
41
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
42
+ >(({ className, ...props }, ref) => (
43
+ <TabsPrimitive.Trigger
44
+ ref={ref}
45
+ className={cn(
46
+ "relative inline-flex items-center justify-center gap-1.5 whitespace-nowrap px-3 py-1.5 text-sm font-medium text-text-muted transition-colors",
47
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:rounded-sm",
48
+ "hover:text-foreground",
49
+ "disabled:opacity-50 disabled:cursor-not-allowed",
50
+ "data-[state=active]:text-foreground",
51
+ "data-[state=active]:after:absolute data-[state=active]:after:left-0 data-[state=active]:after:right-0 data-[state=active]:after:-bottom-px data-[state=active]:after:h-0.5 data-[state=active]:after:bg-brand-forest",
52
+ className
53
+ )}
54
+ {...props}
55
+ />
56
+ ));
57
+ TabsTrigger.displayName = "TabsTrigger";
58
+
59
+ export const TabsContent = React.forwardRef<
60
+ React.ElementRef<typeof TabsPrimitive.Content>,
61
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
62
+ >(({ className, ...props }, ref) => (
63
+ <TabsPrimitive.Content
64
+ ref={ref}
65
+ className={cn(
66
+ "mt-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:rounded",
67
+ className
68
+ )}
69
+ {...props}
70
+ />
71
+ ));
72
+ TabsContent.displayName = "TabsContent";
@@ -0,0 +1,27 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ /**
5
+ * Textarea — multi-line text input. Same focus/error pattern as Input.
6
+ * Use for: house rules markdown, broadcast composer, policy notes.
7
+ */
8
+ export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
9
+
10
+ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
11
+ ({ className, ...props }, ref) => (
12
+ <textarea
13
+ ref={ref}
14
+ className={cn(
15
+ "flex min-h-[80px] w-full rounded-sm border border-border-strong bg-background px-2.5 py-2 text-sm text-foreground transition-colors duration-150 ease-out",
16
+ "placeholder:text-text-subtle",
17
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
18
+ "disabled:cursor-not-allowed disabled:bg-bg-muted disabled:opacity-60",
19
+ "aria-[invalid=true]:border-danger aria-[invalid=true]:focus-visible:ring-danger",
20
+ "resize-y",
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ )
26
+ );
27
+ Textarea.displayName = "Textarea";
@@ -0,0 +1,131 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { TooltipProvider } from "./tooltip";
5
+
6
+ /**
7
+ * Theme = the effective look the page is rendered in (one of
8
+ * "light" | "dark"). ThemePreference = the user's chosen mode
9
+ * ("light", "dark", or "system"). When preference is "system" the
10
+ * effective theme tracks the OS `prefers-color-scheme` media query
11
+ * and updates live as the user toggles their laptop's appearance.
12
+ *
13
+ * Storage:
14
+ * - "dt-theme" persists the user's PREFERENCE (light/dark/system).
15
+ * Default if unset = "system" — out-of-the-box experience matches
16
+ * the laptop, no flash of wrong theme on first visit.
17
+ *
18
+ * Backward compatibility:
19
+ * - `theme` is still the effective theme (consumers that read
20
+ * `useTheme().theme` keep working unchanged).
21
+ * - `setTheme(t)` is preserved as a shortcut for setPreference(t)
22
+ * where t is "light" or "dark".
23
+ * - `toggle()` is preserved and now cycles light → dark → system.
24
+ */
25
+
26
+ type Theme = "light" | "dark";
27
+ type ThemePreference = "light" | "dark" | "system";
28
+
29
+ interface ThemeContextValue {
30
+ theme: Theme;
31
+ preference: ThemePreference;
32
+ setPreference: (p: ThemePreference) => void;
33
+ setTheme: (t: Theme) => void;
34
+ toggle: () => void;
35
+ }
36
+
37
+ const ThemeContext = React.createContext<ThemeContextValue | undefined>(
38
+ undefined
39
+ );
40
+
41
+ const STORAGE_KEY = "dt-theme";
42
+
43
+ function isPreference(v: unknown): v is ThemePreference {
44
+ return v === "light" || v === "dark" || v === "system";
45
+ }
46
+
47
+ export function ThemeProvider({ children }: { children: React.ReactNode }) {
48
+ const [preference, setPreferenceState] =
49
+ React.useState<ThemePreference>("system");
50
+ const [systemTheme, setSystemTheme] = React.useState<Theme>("light");
51
+
52
+ // On mount: read stored preference + current system theme, and
53
+ // subscribe to system theme changes so the page updates live when
54
+ // the user flips macOS / Windows appearance with the app open.
55
+ React.useEffect(() => {
56
+ try {
57
+ const stored = localStorage.getItem(STORAGE_KEY);
58
+ // SSR renders "system"; the stored preference can only be read on the
59
+ // client, so this one-time post-mount setState (and its extra render)
60
+ // is the hydration-safe path — a lazy initializer would mismatch.
61
+ // eslint-disable-next-line react-hooks/set-state-in-effect
62
+ if (isPreference(stored)) setPreferenceState(stored);
63
+ } catch {
64
+ /* localStorage blocked — fall back to default "system" */
65
+ }
66
+ const mql = window.matchMedia("(prefers-color-scheme: dark)");
67
+ setSystemTheme(mql.matches ? "dark" : "light");
68
+ const handler = (e: MediaQueryListEvent) => {
69
+ setSystemTheme(e.matches ? "dark" : "light");
70
+ };
71
+ // addEventListener is the modern API; older Safari needs addListener
72
+ if (mql.addEventListener) {
73
+ mql.addEventListener("change", handler);
74
+ return () => mql.removeEventListener("change", handler);
75
+ }
76
+ mql.addListener(handler);
77
+ return () => mql.removeListener(handler);
78
+ }, []);
79
+
80
+ // Resolved theme: explicit override wins, else follow system.
81
+ const theme: Theme = preference === "system" ? systemTheme : preference;
82
+
83
+ // Apply class to <html> whenever the effective theme changes.
84
+ React.useEffect(() => {
85
+ document.documentElement.classList.toggle("dark", theme === "dark");
86
+ }, [theme]);
87
+
88
+ const setPreference = React.useCallback((p: ThemePreference) => {
89
+ setPreferenceState(p);
90
+ try {
91
+ localStorage.setItem(STORAGE_KEY, p);
92
+ } catch {
93
+ /* ignore */
94
+ }
95
+ }, []);
96
+
97
+ // Legacy setter — accepts "light" | "dark", routes to preference.
98
+ const setTheme = React.useCallback(
99
+ (t: Theme) => setPreference(t),
100
+ [setPreference]
101
+ );
102
+
103
+ // Cycle light → dark → system → light. Keeps a single toggle
104
+ // button useful while still letting power users reach "system".
105
+ const toggle = React.useCallback(() => {
106
+ setPreference(
107
+ preference === "light"
108
+ ? "dark"
109
+ : preference === "dark"
110
+ ? "system"
111
+ : "light"
112
+ );
113
+ }, [preference, setPreference]);
114
+
115
+ return (
116
+ <ThemeContext.Provider
117
+ value={{ theme, preference, setPreference, setTheme, toggle }}
118
+ >
119
+ {/* TooltipProvider is co-located so consumers get tooltips for free. */}
120
+ <TooltipProvider delayDuration={200} skipDelayDuration={300}>
121
+ {children}
122
+ </TooltipProvider>
123
+ </ThemeContext.Provider>
124
+ );
125
+ }
126
+
127
+ export function useTheme() {
128
+ const ctx = React.useContext(ThemeContext);
129
+ if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
130
+ return ctx;
131
+ }
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import { Laptop, Moon, Sun } from "lucide-react";
4
+ import { Button } from "./button";
5
+ import { useTheme } from "./theme-provider";
6
+
7
+ /**
8
+ * ThemeToggle — single button that cycles light → dark → system,
9
+ * mirroring the macOS / Windows / iOS "Appearance" model. The icon
10
+ * reflects the user's PREFERENCE, not the resolved theme, so
11
+ * "system" shows a laptop glyph even when the effective theme is
12
+ * dark — that's how the user sees they're not in an explicit
13
+ * override.
14
+ */
15
+ export function ThemeToggle() {
16
+ const { preference, toggle } = useTheme();
17
+ const { Icon, label, next } =
18
+ preference === "light"
19
+ ? { Icon: Sun, label: "Light theme", next: "Switch to dark" }
20
+ : preference === "dark"
21
+ ? { Icon: Moon, label: "Dark theme", next: "Switch to system" }
22
+ : { Icon: Laptop, label: "System theme", next: "Switch to light" };
23
+ return (
24
+ <Button
25
+ variant="secondary"
26
+ size="icon"
27
+ onClick={toggle}
28
+ aria-label={`${label} — ${next}`}
29
+ title={`${label} — click to ${next.toLowerCase()}`}
30
+ >
31
+ <Icon className="h-4 w-4" strokeWidth={1.75} />
32
+ </Button>
33
+ );
34
+ }
@@ -0,0 +1,90 @@
1
+ import * as React from "react";
2
+ import { Skeleton } from "./skeleton";
3
+ import { EmptyState, type EmptyStateProps } from "./empty-state";
4
+ import { Button } from "./button";
5
+ import { AlertTriangle } from "lucide-react";
6
+
7
+ /**
8
+ * ThreeState — implements design.md §3.14 Empty/Loading/Full discipline.
9
+ *
10
+ * One wrapper for every list, every chart, every dashboard tile. Forces
11
+ * you to think about all three states (plus error) at design time.
12
+ *
13
+ * Usage:
14
+ * <ThreeState
15
+ * state={state}
16
+ * loading={<TableSkeleton rows={5} />}
17
+ * empty={{ title: "No games this week", description: "Host one?", action: <Button>Create</Button> }}
18
+ * error={(err, retry) => <ErrorView err={err} onRetry={retry} />}
19
+ * >
20
+ * <RealList items={data} />
21
+ * </ThreeState>
22
+ */
23
+ export type ListState<T> =
24
+ | { kind: "loading" }
25
+ | { kind: "empty" }
26
+ | { kind: "error"; error: Error; retry?: () => void }
27
+ | { kind: "data"; data: T };
28
+
29
+ export interface ThreeStateProps<T> {
30
+ state: ListState<T>;
31
+ loading?: React.ReactNode;
32
+ empty: EmptyStateProps;
33
+ error?: (err: Error, retry?: () => void) => React.ReactNode;
34
+ children: React.ReactNode;
35
+ }
36
+
37
+ export function ThreeState<T>({
38
+ state,
39
+ loading,
40
+ empty,
41
+ error,
42
+ children,
43
+ }: ThreeStateProps<T>) {
44
+ if (state.kind === "loading") {
45
+ return <>{loading ?? <DefaultLoading />}</>;
46
+ }
47
+ if (state.kind === "empty") {
48
+ return <EmptyState {...empty} />;
49
+ }
50
+ if (state.kind === "error") {
51
+ return (
52
+ <>
53
+ {error ? (
54
+ error(state.error, state.retry)
55
+ ) : (
56
+ <DefaultError err={state.error} retry={state.retry} />
57
+ )}
58
+ </>
59
+ );
60
+ }
61
+ return <>{children}</>;
62
+ }
63
+
64
+ function DefaultLoading() {
65
+ return (
66
+ <div className="space-y-2 p-2">
67
+ <Skeleton className="h-9 w-full" />
68
+ <Skeleton className="h-9 w-full" />
69
+ <Skeleton className="h-9 w-full" />
70
+ </div>
71
+ );
72
+ }
73
+
74
+ function DefaultError({ err, retry }: { err: Error; retry?: () => void }) {
75
+ return (
76
+ <div
77
+ role="alert"
78
+ className="flex flex-col items-center justify-center gap-2 px-6 py-10 text-center"
79
+ >
80
+ <AlertTriangle className="h-5 w-5 text-danger" strokeWidth={1.75} />
81
+ <div className="text-sm font-medium">Couldn&apos;t load this</div>
82
+ <div className="text-xs text-text-muted max-w-xs">{err.message}</div>
83
+ {retry && (
84
+ <Button variant="secondary" size="sm" onClick={retry} className="mt-2">
85
+ Try again
86
+ </Button>
87
+ )}
88
+ </div>
89
+ );
90
+ }