@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,54 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
5
+ import { ChevronDown } from "lucide-react";
6
+ import { cn } from "../lib/utils";
7
+
8
+ /**
9
+ * Collapsible — a single expandable region. Accordion is for GROUPS of
10
+ * sections with one-open-at-a-time semantics; Collapsible is one
11
+ * independent disclosure ("Show advanced options", "View 12 more").
12
+ *
13
+ * <Collapsible>
14
+ * <CollapsibleTrigger>Advanced options</CollapsibleTrigger>
15
+ * <CollapsibleContent>…</CollapsibleContent>
16
+ * </Collapsible>
17
+ */
18
+ export const Collapsible = CollapsiblePrimitive.Root;
19
+
20
+ export const CollapsibleTrigger = React.forwardRef<
21
+ React.ElementRef<typeof CollapsiblePrimitive.Trigger>,
22
+ React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Trigger>
23
+ >(({ className, children, ...props }, ref) => (
24
+ <CollapsiblePrimitive.Trigger
25
+ ref={ref}
26
+ className={cn(
27
+ "group flex items-center gap-1.5 text-sm font-medium text-foreground transition-colors hover:text-brand-forest",
28
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm",
29
+ "pointer-coarse:min-h-11",
30
+ className
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronDown
36
+ className="h-3.5 w-3.5 text-text-muted transition-transform duration-150 group-data-[state=open]:rotate-180"
37
+ strokeWidth={1.75}
38
+ aria-hidden="true"
39
+ />
40
+ </CollapsiblePrimitive.Trigger>
41
+ ));
42
+ CollapsibleTrigger.displayName = "CollapsibleTrigger";
43
+
44
+ export const CollapsibleContent = React.forwardRef<
45
+ React.ElementRef<typeof CollapsiblePrimitive.Content>,
46
+ React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Content>
47
+ >(({ className, ...props }, ref) => (
48
+ <CollapsiblePrimitive.Content
49
+ ref={ref}
50
+ className={cn("pt-2 data-[state=open]:animate-fade-in", className)}
51
+ {...props}
52
+ />
53
+ ));
54
+ CollapsibleContent.displayName = "CollapsibleContent";
@@ -0,0 +1,140 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Check, ChevronsUpDown } from "lucide-react";
5
+ import { Command as CommandPrimitive } from "cmdk";
6
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover";
7
+ import { cn } from "../lib/utils";
8
+
9
+ /**
10
+ * Combobox — searchable single-select. Use instead of Select when the
11
+ * option list is long enough that scanning beats scrolling (venues,
12
+ * players, cities — roughly 10+ options).
13
+ *
14
+ * <Combobox
15
+ * options={venues.map(v => ({ value: v.id, label: v.name }))}
16
+ * value={venueId}
17
+ * onValueChange={setVenueId}
18
+ * placeholder="Select venue…"
19
+ * aria-label="Venue"
20
+ * />
21
+ *
22
+ * Keyboard: type to filter, ↑/↓ to move, Enter to select, Esc closes.
23
+ * Uncontrolled open state; controlled value.
24
+ */
25
+ export interface ComboboxOption {
26
+ value: string;
27
+ label: string;
28
+ disabled?: boolean;
29
+ }
30
+
31
+ export interface ComboboxProps {
32
+ options: ComboboxOption[];
33
+ value: string | null;
34
+ onValueChange: (value: string | null) => void;
35
+ /** Trigger text when nothing is selected. */
36
+ placeholder?: string;
37
+ /** Search input placeholder. */
38
+ searchPlaceholder?: string;
39
+ /** Shown when the query matches nothing. */
40
+ emptyText?: string;
41
+ /** Accessible name for the trigger + listbox. */
42
+ "aria-label": string;
43
+ disabled?: boolean;
44
+ className?: string;
45
+ }
46
+
47
+ export function Combobox({
48
+ options,
49
+ value,
50
+ onValueChange,
51
+ placeholder = "Select…",
52
+ searchPlaceholder = "Search…",
53
+ emptyText = "No results.",
54
+ "aria-label": ariaLabel,
55
+ disabled,
56
+ className,
57
+ }: ComboboxProps) {
58
+ const [open, setOpen] = React.useState(false);
59
+ const listId = React.useId();
60
+ const selected = options.find((o) => o.value === value);
61
+
62
+ return (
63
+ <Popover open={open} onOpenChange={setOpen}>
64
+ <PopoverTrigger asChild>
65
+ <button
66
+ type="button"
67
+ role="combobox"
68
+ aria-expanded={open}
69
+ aria-controls={listId}
70
+ aria-haspopup="listbox"
71
+ aria-label={ariaLabel}
72
+ disabled={disabled}
73
+ className={cn(
74
+ "flex h-8 pointer-coarse:h-11 w-full items-center justify-between gap-2 rounded-sm border border-border-strong bg-background px-2.5 text-sm",
75
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
76
+ "disabled:cursor-not-allowed disabled:bg-bg-muted disabled:opacity-60",
77
+ !selected && "text-text-subtle",
78
+ className
79
+ )}
80
+ >
81
+ <span className="truncate">{selected?.label ?? placeholder}</span>
82
+ <ChevronsUpDown
83
+ className="h-3.5 w-3.5 shrink-0 text-text-muted"
84
+ strokeWidth={1.75}
85
+ aria-hidden="true"
86
+ />
87
+ </button>
88
+ </PopoverTrigger>
89
+ <PopoverContent
90
+ aria-label={ariaLabel}
91
+ align="start"
92
+ className="w-[--radix-popover-trigger-width] min-w-[12rem] p-0"
93
+ >
94
+ <CommandPrimitive label={ariaLabel} className="outline-none">
95
+ <CommandPrimitive.Input
96
+ placeholder={searchPlaceholder}
97
+ className={cn(
98
+ "w-full border-b border-border bg-transparent px-3 h-8 pointer-coarse:h-11 text-sm text-foreground",
99
+ "placeholder:text-text-subtle focus:outline-none"
100
+ )}
101
+ />
102
+ <CommandPrimitive.List
103
+ id={listId}
104
+ className="max-h-64 overflow-y-auto p-1 scrollbar-thin"
105
+ >
106
+ <CommandPrimitive.Empty className="px-3 py-6 text-center text-sm text-text-muted">
107
+ {emptyText}
108
+ </CommandPrimitive.Empty>
109
+ {options.map((option) => (
110
+ <CommandPrimitive.Item
111
+ key={option.value}
112
+ value={option.label}
113
+ disabled={option.disabled}
114
+ onSelect={() => {
115
+ onValueChange(option.value === value ? null : option.value);
116
+ setOpen(false);
117
+ }}
118
+ className={cn(
119
+ "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 pointer-coarse:min-h-11 text-sm outline-none",
120
+ "data-[selected=true]:bg-bg-subtle data-[selected=true]:text-foreground",
121
+ "data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50"
122
+ )}
123
+ >
124
+ <Check
125
+ className={cn(
126
+ "h-3.5 w-3.5 shrink-0 text-brand-forest",
127
+ option.value === value ? "opacity-100" : "opacity-0"
128
+ )}
129
+ strokeWidth={2}
130
+ aria-hidden="true"
131
+ />
132
+ {option.label}
133
+ </CommandPrimitive.Item>
134
+ ))}
135
+ </CommandPrimitive.List>
136
+ </CommandPrimitive>
137
+ </PopoverContent>
138
+ </Popover>
139
+ );
140
+ }
@@ -0,0 +1,111 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import {
5
+ Dialog,
6
+ DialogBody,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogFooter,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ DialogTrigger,
13
+ } from "./dialog";
14
+ import { Button, type ButtonProps } from "./button";
15
+
16
+ /**
17
+ * ConfirmAction — codifies design.md §3.11 Confirmation discipline.
18
+ *
19
+ * Use ONLY for destructive or financially irreversible actions. The body
20
+ * is required to be concrete ("8 players will be refunded $14 each"),
21
+ * never abstract ("Are you sure?").
22
+ *
23
+ * Wrap your trigger button (works with asChild).
24
+ *
25
+ * Example:
26
+ * <ConfirmAction
27
+ * title="Cancel this game?"
28
+ * description="This action is logged and cannot be undone."
29
+ * consequence="8 players will be refunded $14.00 each as credit and emailed."
30
+ * confirmLabel="Cancel + refund all"
31
+ * confirmVariant="destructive"
32
+ * onConfirm={async () => await cancelGame(id)}
33
+ * >
34
+ * <Button variant="destructive">Cancel game…</Button>
35
+ * </ConfirmAction>
36
+ */
37
+ export interface ConfirmActionProps {
38
+ title: string;
39
+ description?: string;
40
+ /** REQUIRED concrete consequence text — never abstract. */
41
+ consequence: React.ReactNode;
42
+ confirmLabel: string;
43
+ cancelLabel?: string;
44
+ confirmVariant?: ButtonProps["variant"];
45
+ onConfirm: () => void | Promise<void>;
46
+ children: React.ReactNode;
47
+ }
48
+
49
+ export function ConfirmAction({
50
+ title,
51
+ description,
52
+ consequence,
53
+ confirmLabel,
54
+ cancelLabel = "Cancel",
55
+ confirmVariant = "destructive",
56
+ onConfirm,
57
+ children,
58
+ }: ConfirmActionProps) {
59
+ const [open, setOpen] = React.useState(false);
60
+ const [busy, setBusy] = React.useState(false);
61
+
62
+ const handleConfirm = async () => {
63
+ setBusy(true);
64
+ try {
65
+ await onConfirm();
66
+ setOpen(false);
67
+ } finally {
68
+ setBusy(false);
69
+ }
70
+ };
71
+
72
+ return (
73
+ <Dialog open={open} onOpenChange={setOpen}>
74
+ <DialogTrigger asChild>{children}</DialogTrigger>
75
+ {/*
76
+ When no description prop is provided, explicitly opt out of
77
+ Radix's aria-describedby auto-wiring (passing `undefined`
78
+ disables the dev warning). The consequence text in the body
79
+ already explains the action; Radix only auto-wires DialogDescription.
80
+ When `description` IS provided, omit the prop so Radix can wire it.
81
+ */}
82
+ <DialogContent
83
+ {...(description ? {} : { "aria-describedby": undefined })}
84
+ >
85
+ <DialogHeader>
86
+ <DialogTitle>{title}</DialogTitle>
87
+ {description && <DialogDescription>{description}</DialogDescription>}
88
+ </DialogHeader>
89
+ <DialogBody>
90
+ <p className="text-sm text-foreground">{consequence}</p>
91
+ </DialogBody>
92
+ <DialogFooter>
93
+ <Button
94
+ variant="ghost"
95
+ onClick={() => setOpen(false)}
96
+ disabled={busy}
97
+ >
98
+ {cancelLabel}
99
+ </Button>
100
+ <Button
101
+ variant={confirmVariant}
102
+ onClick={handleConfirm}
103
+ disabled={busy}
104
+ >
105
+ {busy ? "Working…" : confirmLabel}
106
+ </Button>
107
+ </DialogFooter>
108
+ </DialogContent>
109
+ </Dialog>
110
+ );
111
+ }
@@ -0,0 +1,39 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ /**
5
+ * Container — max-width page wrapper with consistent horizontal padding.
6
+ * Sizes match the page layouts already used across the app:
7
+ * - sm: max-w-3xl (768px) — narrow content pages (FAQ, settings)
8
+ * - default: max-w-5xl (1024px) — design system docs
9
+ * - lg: max-w-7xl (1280px) — dashboards + game lists
10
+ * - full: max-w-none — edge-to-edge
11
+ */
12
+ const SIZE_MAP = {
13
+ sm: "max-w-3xl",
14
+ default: "max-w-5xl",
15
+ lg: "max-w-7xl",
16
+ full: "max-w-none",
17
+ } as const;
18
+
19
+ export interface ContainerProps extends React.HTMLAttributes<HTMLDivElement> {
20
+ size?: keyof typeof SIZE_MAP;
21
+ /** Drop the horizontal padding (rare — only when nested). */
22
+ noPadding?: boolean;
23
+ }
24
+
25
+ export const Container = React.forwardRef<HTMLDivElement, ContainerProps>(
26
+ ({ className, size = "default", noPadding, ...props }, ref) => (
27
+ <div
28
+ ref={ref}
29
+ className={cn(
30
+ "mx-auto w-full",
31
+ SIZE_MAP[size],
32
+ !noPadding && "px-6 lg:px-8",
33
+ className
34
+ )}
35
+ {...props}
36
+ />
37
+ )
38
+ );
39
+ Container.displayName = "Container";
@@ -0,0 +1,98 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { CalendarDays } from "lucide-react";
5
+ import { Calendar } from "./calendar";
6
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover";
7
+ import { cn } from "../lib/utils";
8
+
9
+ /**
10
+ * DatePicker — single-date selection in a popover. Closes on pick.
11
+ *
12
+ * <DatePicker
13
+ * value={gameDate}
14
+ * onValueChange={setGameDate}
15
+ * aria-label="Game date"
16
+ * />
17
+ *
18
+ * Time-zone note (§3.16): the picked Date is midnight LOCAL time —
19
+ * convert against the venue timezone before persisting.
20
+ * For ranges or inline calendars, compose Calendar + Popover directly.
21
+ */
22
+ export interface DatePickerProps {
23
+ value: Date | undefined;
24
+ onValueChange: (date: Date | undefined) => void;
25
+ /** Accessible name for the trigger + dialog. */
26
+ "aria-label": string;
27
+ /** Trigger text when nothing is selected. */
28
+ placeholder?: string;
29
+ /** Intl date format options (default: weekday, month, day). */
30
+ formatOptions?: Intl.DateTimeFormatOptions;
31
+ /** Dates the calendar should refuse (react-day-picker Matcher). */
32
+ disabledDates?: React.ComponentProps<typeof Calendar>["disabled"];
33
+ disabled?: boolean;
34
+ className?: string;
35
+ }
36
+
37
+ export function DatePicker({
38
+ value,
39
+ onValueChange,
40
+ "aria-label": ariaLabel,
41
+ placeholder = "Pick a date…",
42
+ formatOptions = { weekday: "short", month: "short", day: "numeric" },
43
+ disabledDates,
44
+ disabled,
45
+ className,
46
+ }: DatePickerProps) {
47
+ const [open, setOpen] = React.useState(false);
48
+
49
+ return (
50
+ <Popover open={open} onOpenChange={setOpen}>
51
+ <PopoverTrigger asChild>
52
+ <button
53
+ type="button"
54
+ aria-label={ariaLabel}
55
+ disabled={disabled}
56
+ className={cn(
57
+ "flex h-8 pointer-coarse:h-11 w-full items-center gap-2 rounded-sm border border-border-strong bg-background px-2.5 text-sm",
58
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
59
+ "disabled:cursor-not-allowed disabled:bg-bg-muted disabled:opacity-60",
60
+ !value && "text-text-subtle",
61
+ className
62
+ )}
63
+ >
64
+ <CalendarDays
65
+ className="h-3.5 w-3.5 shrink-0 text-text-muted"
66
+ strokeWidth={1.75}
67
+ aria-hidden="true"
68
+ />
69
+ <span className="truncate tabular">
70
+ {value
71
+ ? new Intl.DateTimeFormat(undefined, formatOptions).format(value)
72
+ : placeholder}
73
+ </span>
74
+ </button>
75
+ </PopoverTrigger>
76
+ <PopoverContent
77
+ aria-label={ariaLabel}
78
+ align="start"
79
+ className="w-auto p-0"
80
+ >
81
+ <Calendar
82
+ mode="single"
83
+ selected={value}
84
+ onSelect={(d) => {
85
+ onValueChange(d);
86
+ setOpen(false);
87
+ }}
88
+ disabled={disabledDates}
89
+ // react-day-picker's focus-management prop (not DOM autofocus):
90
+ // moving focus into the grid when the popover opens is the
91
+ // expected dialog pattern for keyboard users.
92
+ // eslint-disable-next-line jsx-a11y/no-autofocus
93
+ autoFocus
94
+ />
95
+ </PopoverContent>
96
+ </Popover>
97
+ );
98
+ }
@@ -0,0 +1,174 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
5
+ import { X } from "lucide-react";
6
+ import { cn } from "../lib/utils";
7
+
8
+ /**
9
+ * Dialog — per design.md §10
10
+ * - Max width 480px default, override via className
11
+ * - 6px radius, 1px strong border, shadow-md
12
+ * - Backdrop: rgba(0,0,0,0.4) with 4px blur
13
+ * - Header text-md semibold, body text-sm
14
+ * - Actions right-aligned; accent CTA on the right, secondary on left
15
+ *
16
+ * Accessibility (provided by Radix):
17
+ * - Focus trap inside dialog
18
+ * - Escape closes
19
+ * - aria-labelledby / aria-describedby auto-wired via Title / Description
20
+ * - Returns focus to trigger on close
21
+ */
22
+
23
+ export const Dialog = DialogPrimitive.Root;
24
+ export const DialogTrigger = DialogPrimitive.Trigger;
25
+ export const DialogPortal = DialogPrimitive.Portal;
26
+ export const DialogClose = DialogPrimitive.Close;
27
+
28
+ export const DialogOverlay = React.forwardRef<
29
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
30
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
31
+ >(({ className, ...props }, ref) => (
32
+ <DialogPrimitive.Overlay
33
+ ref={ref}
34
+ className={cn(
35
+ "fixed inset-0 z-50 bg-black/40 backdrop-blur-[4px]",
36
+ "data-[state=open]:animate-fade-in",
37
+ className
38
+ )}
39
+ {...props}
40
+ />
41
+ ));
42
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
43
+
44
+ export const DialogContent = React.forwardRef<
45
+ React.ElementRef<typeof DialogPrimitive.Content>,
46
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
47
+ >(({ className, children, ...props }, ref) => (
48
+ <DialogPortal>
49
+ <DialogOverlay />
50
+ {/* Centering wrapper — uses Flexbox instead of the classic
51
+ `left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2`
52
+ trick. The translate-based approach is fragile because the
53
+ CSS `transform` property is single-valued: any animation or
54
+ Radix data-state class that touches `transform` would
55
+ override the centering and shove the dialog off-screen,
56
+ which is exactly the bug that previously made dialogs
57
+ appear as a dimmed-blurred page with no visible content.
58
+ Flexbox centering has no such conflict. Below sm we leave
59
+ the wrapper as a plain box so the inner Content can be a
60
+ full-screen sheet via `inset-0`. */}
61
+ <div className="fixed inset-0 z-50 flex sm:items-center sm:justify-center pointer-events-none">
62
+ <DialogPrimitive.Content
63
+ ref={ref}
64
+ className={cn(
65
+ // Mobile (<sm) — full-screen sheet. Phones don't have the
66
+ // height for a centered modal once a real form is in it;
67
+ // the keyboard would push the footer below the fold.
68
+ // Pointer events restored so the sheet is interactive
69
+ // even though its wrapper is pointer-events-none.
70
+ "pointer-events-auto bg-background outline-none flex flex-col",
71
+ "absolute inset-0",
72
+ // sm+ — centered modal. The flex wrapper handles
73
+ // centering; we just size + style the box. max-h prevents
74
+ // tall forms spilling past the viewport; DialogBody
75
+ // handles internal scroll.
76
+ "sm:static sm:w-full sm:max-w-[480px] sm:max-h-[85vh] sm:h-auto",
77
+ "sm:rounded-md sm:border sm:border-border-strong sm:shadow-md",
78
+ "data-[state=open]:animate-fade-in-up",
79
+ className
80
+ )}
81
+ {...props}
82
+ >
83
+ {children}
84
+ <DialogPrimitive.Close
85
+ className={cn(
86
+ "absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center rounded-sm text-text-muted",
87
+ "transition-colors hover:bg-bg-subtle hover:text-foreground",
88
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
89
+ // 44px hit target on touch devices; visual stays 24px.
90
+ "pointer-coarse:after:absolute pointer-coarse:after:-inset-2.5"
91
+ )}
92
+ aria-label="Close dialog"
93
+ >
94
+ <X className="h-4 w-4" strokeWidth={1.75} />
95
+ </DialogPrimitive.Close>
96
+ </DialogPrimitive.Content>
97
+ </div>
98
+ </DialogPortal>
99
+ ));
100
+ DialogContent.displayName = DialogPrimitive.Content.displayName;
101
+
102
+ export function DialogHeader({
103
+ className,
104
+ ...props
105
+ }: React.HTMLAttributes<HTMLDivElement>) {
106
+ return (
107
+ <div
108
+ className={cn("px-5 pt-4 pb-3 border-b border-border", className)}
109
+ {...props}
110
+ />
111
+ );
112
+ }
113
+
114
+ export const DialogTitle = React.forwardRef<
115
+ React.ElementRef<typeof DialogPrimitive.Title>,
116
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
117
+ >(({ className, ...props }, ref) => (
118
+ <DialogPrimitive.Title
119
+ ref={ref}
120
+ className={cn(
121
+ "text-md font-semibold tracking-[-0.01em] text-foreground",
122
+ className
123
+ )}
124
+ {...props}
125
+ />
126
+ ));
127
+ DialogTitle.displayName = DialogPrimitive.Title.displayName;
128
+
129
+ export const DialogDescription = React.forwardRef<
130
+ React.ElementRef<typeof DialogPrimitive.Description>,
131
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
132
+ >(({ className, ...props }, ref) => (
133
+ <DialogPrimitive.Description
134
+ ref={ref}
135
+ className={cn("text-xs text-text-muted mt-0.5", className)}
136
+ {...props}
137
+ />
138
+ ));
139
+ DialogDescription.displayName = DialogPrimitive.Description.displayName;
140
+
141
+ export function DialogBody({
142
+ className,
143
+ ...props
144
+ }: React.HTMLAttributes<HTMLDivElement>) {
145
+ // flex-1 + overflow-y-auto so the body absorbs the dialog's
146
+ // available height and scrolls internally — keeps the footer
147
+ // (Cancel / Save) pinned to the bottom of the viewport on phones
148
+ // and below the max-h cap on desktop. min-h-0 is required for
149
+ // flex children to scroll inside a flex column parent.
150
+ return (
151
+ <div
152
+ className={cn(
153
+ "px-5 py-4 text-sm text-foreground flex-1 min-h-0 overflow-y-auto",
154
+ className
155
+ )}
156
+ {...props}
157
+ />
158
+ );
159
+ }
160
+
161
+ export function DialogFooter({
162
+ className,
163
+ ...props
164
+ }: React.HTMLAttributes<HTMLDivElement>) {
165
+ return (
166
+ <div
167
+ className={cn(
168
+ "flex items-center justify-end gap-2 px-5 py-3 border-t border-border bg-bg-subtle/50",
169
+ className
170
+ )}
171
+ {...props}
172
+ />
173
+ );
174
+ }