@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,83 @@
1
+ import * as React from "react";
2
+ import { ArrowDown, ArrowUp, Minus } from "lucide-react";
3
+ import { cn } from "../lib/utils";
4
+
5
+ /**
6
+ * MetricTile — per design.md §10
7
+ *
8
+ * Three lines exactly: label / value / delta.
9
+ * - label: text-xs uppercase muted
10
+ * - value: text-2xl semibold tabular
11
+ * - delta: text-xs forest (up) / danger (down) / muted (flat)
12
+ *
13
+ * Use 3–4 across in dashboard rows.
14
+ */
15
+ export interface MetricTileProps extends React.HTMLAttributes<HTMLDivElement> {
16
+ label: string;
17
+ value: React.ReactNode;
18
+ /** Numeric delta vs prior period. Sign and direction are derived. */
19
+ delta?: number;
20
+ /** Human-readable suffix appended to the delta, e.g. "vs last week" */
21
+ deltaLabel?: string;
22
+ /** Override the delta direction (e.g. when "up" is bad like refunds) */
23
+ invertDelta?: boolean;
24
+ }
25
+
26
+ export function MetricTile({
27
+ label,
28
+ value,
29
+ delta,
30
+ deltaLabel = "vs last week",
31
+ invertDelta = false,
32
+ className,
33
+ ...props
34
+ }: MetricTileProps) {
35
+ const direction: "up" | "down" | "flat" =
36
+ delta === undefined || delta === 0 ? "flat" : delta > 0 ? "up" : "down";
37
+
38
+ const positive =
39
+ direction === "up"
40
+ ? !invertDelta
41
+ : direction === "down"
42
+ ? invertDelta
43
+ : null;
44
+
45
+ return (
46
+ <div
47
+ className={cn(
48
+ "rounded border border-border bg-background p-4",
49
+ className
50
+ )}
51
+ {...props}
52
+ >
53
+ <div className="text-xs font-medium uppercase tracking-[0.04em] text-text-muted">
54
+ {label}
55
+ </div>
56
+ <div className="tabular mt-1 text-2xl font-semibold tracking-[-0.01em] text-foreground">
57
+ {value}
58
+ </div>
59
+ {delta !== undefined && (
60
+ <div
61
+ className={cn(
62
+ "mt-1 inline-flex items-center gap-1 text-xs",
63
+ positive === null
64
+ ? "text-text-muted"
65
+ : positive
66
+ ? "text-brand-forest"
67
+ : "text-danger"
68
+ )}
69
+ >
70
+ {direction === "up" ? (
71
+ <ArrowUp className="h-3 w-3" strokeWidth={2} />
72
+ ) : direction === "down" ? (
73
+ <ArrowDown className="h-3 w-3" strokeWidth={2} />
74
+ ) : (
75
+ <Minus className="h-3 w-3" strokeWidth={2} />
76
+ )}
77
+ <span className="tabular">{Math.abs(delta)}%</span>
78
+ <span className="text-text-muted">{deltaLabel}</span>
79
+ </div>
80
+ )}
81
+ </div>
82
+ );
83
+ }
@@ -0,0 +1,92 @@
1
+ import { cn } from "../lib/utils";
2
+
3
+ /**
4
+ * MoneyBreakdown — implements design.md §3.15 Money UX.
5
+ *
6
+ * Always shown before any charge. Every line is named, signed, and
7
+ * tabular-aligned. The receipt component should render the same lines
8
+ * in the same order.
9
+ *
10
+ * Usage:
11
+ * <MoneyBreakdown
12
+ * lines={[
13
+ * { label: "Game fee", cents: 1400 },
14
+ * { label: "Credit applied", cents: -600, muted: true },
15
+ * { label: "Sales tax", cents: 70 },
16
+ * ]}
17
+ * totalLabel="Charged to card"
18
+ * totalCents={870}
19
+ * />
20
+ */
21
+ export interface MoneyLine {
22
+ label: string;
23
+ cents: number;
24
+ /** Render in muted text (for credit applied, taxes, etc.). */
25
+ muted?: boolean;
26
+ /** Optional hint shown under the label. */
27
+ hint?: string;
28
+ }
29
+
30
+ export interface MoneyBreakdownProps {
31
+ lines: MoneyLine[];
32
+ totalLabel?: string;
33
+ totalCents: number;
34
+ /** Currency code; default USD. */
35
+ currency?: string;
36
+ className?: string;
37
+ }
38
+
39
+ function fmt(cents: number, currency = "USD") {
40
+ return new Intl.NumberFormat("en-US", {
41
+ style: "currency",
42
+ currency,
43
+ }).format(cents / 100);
44
+ }
45
+
46
+ export function MoneyBreakdown({
47
+ lines,
48
+ totalLabel = "Total",
49
+ totalCents,
50
+ currency = "USD",
51
+ className,
52
+ }: MoneyBreakdownProps) {
53
+ return (
54
+ <dl className={cn("rounded border border-border bg-background", className)}>
55
+ {lines.map((line, i) => (
56
+ <div
57
+ key={i}
58
+ className="flex items-start justify-between gap-4 px-4 py-2.5 border-b border-border last:border-b-0"
59
+ >
60
+ <dt
61
+ className={cn(
62
+ "text-sm",
63
+ line.muted ? "text-text-muted" : "text-foreground"
64
+ )}
65
+ >
66
+ {line.label}
67
+ {line.hint && (
68
+ <span className="block text-xs text-text-subtle mt-0.5">
69
+ {line.hint}
70
+ </span>
71
+ )}
72
+ </dt>
73
+ <dd
74
+ className={cn(
75
+ "tabular text-sm",
76
+ line.muted ? "text-text-muted" : "text-foreground"
77
+ )}
78
+ >
79
+ {line.cents < 0 ? "−" : ""}
80
+ {fmt(Math.abs(line.cents), currency)}
81
+ </dd>
82
+ </div>
83
+ ))}
84
+ <div className="flex items-center justify-between gap-4 px-4 py-3 bg-bg-subtle border-t border-border">
85
+ <dt className="text-sm font-semibold">{totalLabel}</dt>
86
+ <dd className="tabular text-md font-semibold">
87
+ {fmt(totalCents, currency)}
88
+ </dd>
89
+ </div>
90
+ </dl>
91
+ );
92
+ }
@@ -0,0 +1,113 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Minus, Plus } from "lucide-react";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * NumberInput — stepper-style numeric input with explicit +/− buttons.
9
+ * Use for: capacity, min-to-run, price (in cents or units), quantity.
10
+ *
11
+ * Decimals: pass `step={0.5}` etc. Always set min/max for safe bounds.
12
+ */
13
+ export interface NumberInputProps extends Omit<
14
+ React.InputHTMLAttributes<HTMLInputElement>,
15
+ "onChange" | "value" | "type"
16
+ > {
17
+ value?: number;
18
+ onChange?: (value: number) => void;
19
+ min?: number;
20
+ max?: number;
21
+ step?: number;
22
+ }
23
+
24
+ export const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
25
+ (
26
+ {
27
+ value,
28
+ onChange,
29
+ min,
30
+ max,
31
+ step = 1,
32
+ className,
33
+ disabled,
34
+ "aria-label": ariaLabel,
35
+ "aria-describedby": ariaDescribedBy,
36
+ "aria-invalid": ariaInvalid,
37
+ id,
38
+ ...rest
39
+ },
40
+ ref
41
+ ) => {
42
+ const clamp = (v: number) => {
43
+ let next = v;
44
+ if (min !== undefined && next < min) next = min;
45
+ if (max !== undefined && next > max) next = max;
46
+ return next;
47
+ };
48
+
49
+ const handleChange = (next: number) => {
50
+ onChange?.(clamp(next));
51
+ };
52
+
53
+ const cur = value ?? 0;
54
+ const decDisabled = disabled || (min !== undefined && cur <= min);
55
+ const incDisabled = disabled || (max !== undefined && cur >= max);
56
+
57
+ return (
58
+ <div
59
+ className={cn(
60
+ "inline-flex h-8 items-stretch rounded-sm border border-border-strong overflow-hidden",
61
+ // 44px control height + stepper width on touch devices.
62
+ "pointer-coarse:h-11",
63
+ "aria-[invalid=true]:border-danger",
64
+ disabled && "opacity-50 pointer-events-none",
65
+ className
66
+ )}
67
+ aria-invalid={ariaInvalid}
68
+ >
69
+ <button
70
+ type="button"
71
+ aria-label="Decrease"
72
+ onClick={() => handleChange(cur - step)}
73
+ disabled={decDisabled}
74
+ className="w-7 pointer-coarse:w-11 grid place-items-center text-text-muted hover:bg-bg-subtle hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed border-r border-border-strong"
75
+ >
76
+ <Minus className="h-3.5 w-3.5" strokeWidth={1.75} />
77
+ </button>
78
+ <input
79
+ ref={ref}
80
+ id={id}
81
+ type="number"
82
+ inputMode="numeric"
83
+ value={value ?? ""}
84
+ onChange={(e) => {
85
+ const n = e.target.value === "" ? NaN : Number(e.target.value);
86
+ if (!Number.isNaN(n)) handleChange(n);
87
+ }}
88
+ min={min}
89
+ max={max}
90
+ step={step}
91
+ aria-label={ariaLabel}
92
+ aria-describedby={ariaDescribedBy}
93
+ className={cn(
94
+ "w-16 text-center text-sm font-medium tabular bg-background text-foreground",
95
+ "focus:outline-none focus-visible:bg-bg-subtle",
96
+ "[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
97
+ )}
98
+ {...rest}
99
+ />
100
+ <button
101
+ type="button"
102
+ aria-label="Increase"
103
+ onClick={() => handleChange(cur + step)}
104
+ disabled={incDisabled}
105
+ className="w-7 pointer-coarse:w-11 grid place-items-center text-text-muted hover:bg-bg-subtle hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed border-l border-border-strong"
106
+ >
107
+ <Plus className="h-3.5 w-3.5" strokeWidth={1.75} />
108
+ </button>
109
+ </div>
110
+ );
111
+ }
112
+ );
113
+ NumberInput.displayName = "NumberInput";
@@ -0,0 +1,157 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * Pagination — page navigation for tables and lists.
9
+ *
10
+ * <Pagination
11
+ * currentPage={page}
12
+ * totalPages={totalPages}
13
+ * onPageChange={setPage}
14
+ * />
15
+ *
16
+ * Renders previous / page numbers (with ellipses for large counts) / next.
17
+ * Use only when total > 1; for <= 10 pages no ellipses are inserted.
18
+ *
19
+ * For URL-driven pagination (per design.md §3.9), pass an onPageChange
20
+ * that calls router.replace(?page=N).
21
+ */
22
+ export interface PaginationProps extends React.HTMLAttributes<HTMLElement> {
23
+ currentPage: number;
24
+ totalPages: number;
25
+ onPageChange: (page: number) => void;
26
+ /** Number of pages to show on each side of the current page. */
27
+ siblingCount?: number;
28
+ /** Disable interaction (e.g., while fetching). */
29
+ disabled?: boolean;
30
+ }
31
+
32
+ export const Pagination = React.forwardRef<HTMLElement, PaginationProps>(
33
+ (
34
+ {
35
+ currentPage,
36
+ totalPages,
37
+ onPageChange,
38
+ siblingCount = 1,
39
+ disabled,
40
+ className,
41
+ ...rest
42
+ },
43
+ ref
44
+ ) => {
45
+ if (totalPages <= 1) return null;
46
+
47
+ const pages = buildPageList(currentPage, totalPages, siblingCount);
48
+
49
+ const go = (p: number) => {
50
+ if (disabled) return;
51
+ const clamped = Math.max(1, Math.min(totalPages, p));
52
+ if (clamped !== currentPage) onPageChange(clamped);
53
+ };
54
+
55
+ return (
56
+ <nav
57
+ ref={ref}
58
+ aria-label="Pagination"
59
+ className={cn("flex items-center gap-1", className)}
60
+ {...rest}
61
+ >
62
+ <PageButton
63
+ aria-label="Previous page"
64
+ onClick={() => go(currentPage - 1)}
65
+ disabled={disabled || currentPage <= 1}
66
+ >
67
+ <ChevronLeft className="h-3.5 w-3.5" strokeWidth={1.75} />
68
+ </PageButton>
69
+
70
+ {pages.map((p, i) =>
71
+ p === "…" ? (
72
+ <span
73
+ key={`ellipsis-${i}`}
74
+ role="presentation"
75
+ aria-hidden="true"
76
+ className="h-7 w-7 grid place-items-center text-text-subtle"
77
+ >
78
+ <MoreHorizontal className="h-3 w-3" strokeWidth={1.75} />
79
+ </span>
80
+ ) : (
81
+ <PageButton
82
+ key={p}
83
+ aria-label={`Page ${p}`}
84
+ aria-current={p === currentPage ? "page" : undefined}
85
+ active={p === currentPage}
86
+ onClick={() => go(p)}
87
+ disabled={disabled}
88
+ >
89
+ {p}
90
+ </PageButton>
91
+ )
92
+ )}
93
+
94
+ <PageButton
95
+ aria-label="Next page"
96
+ onClick={() => go(currentPage + 1)}
97
+ disabled={disabled || currentPage >= totalPages}
98
+ >
99
+ <ChevronRight className="h-3.5 w-3.5" strokeWidth={1.75} />
100
+ </PageButton>
101
+ </nav>
102
+ );
103
+ }
104
+ );
105
+ Pagination.displayName = "Pagination";
106
+
107
+ function PageButton({
108
+ active,
109
+ className,
110
+ ...props
111
+ }: React.ButtonHTMLAttributes<HTMLButtonElement> & { active?: boolean }) {
112
+ return (
113
+ <button
114
+ type="button"
115
+ className={cn(
116
+ "min-w-7 h-7 px-2 rounded-sm text-xs font-medium tabular transition-colors",
117
+ // 44px targets on touch devices; dense 28px on desktop.
118
+ "pointer-coarse:min-w-11 pointer-coarse:h-11",
119
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
120
+ active
121
+ ? "bg-brand-forest text-brand-forest-foreground"
122
+ : "text-text-muted hover:bg-bg-subtle hover:text-foreground",
123
+ "disabled:opacity-40 disabled:pointer-events-none",
124
+ className
125
+ )}
126
+ {...props}
127
+ />
128
+ );
129
+ }
130
+
131
+ function buildPageList(
132
+ current: number,
133
+ total: number,
134
+ siblings: number
135
+ ): Array<number | "…"> {
136
+ // Always include first + last; show ellipsis when there's a gap.
137
+ const range = (a: number, b: number): number[] => {
138
+ const out: number[] = [];
139
+ for (let i = a; i <= b; i++) out.push(i);
140
+ return out;
141
+ };
142
+
143
+ const totalShown = siblings * 2 + 5; // first + last + current + 2 ellipses
144
+ if (total <= totalShown) return range(1, total);
145
+
146
+ const leftBound = Math.max(2, current - siblings);
147
+ const rightBound = Math.min(total - 1, current + siblings);
148
+ const showLeftDots = leftBound > 2;
149
+ const showRightDots = rightBound < total - 1;
150
+
151
+ const list: Array<number | "…"> = [1];
152
+ if (showLeftDots) list.push("…");
153
+ for (const p of range(leftBound, rightBound)) list.push(p);
154
+ if (showRightDots) list.push("…");
155
+ list.push(total);
156
+ return list;
157
+ }
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * Popover — generic click-to-open floating panel.
9
+ *
10
+ * Use for:
11
+ * - Date pickers
12
+ * - Filter mini-menus
13
+ * - "Quick edit" inline form panels
14
+ * - Help text that's too long for a Tooltip
15
+ *
16
+ * Tooltip is hover-only and small; Popover is click + can hold any
17
+ * content (form, list, mini-chart). DropdownMenu is for action menus —
18
+ * if your popover content is "items you click," use DropdownMenu instead.
19
+ *
20
+ * Accessibility: PopoverContent renders with role="dialog", which needs
21
+ * an accessible name — always pass `aria-label` (or `aria-labelledby`
22
+ * pointing at a heading inside). The a11y test suite enforces this shape.
23
+ */
24
+ export const Popover = PopoverPrimitive.Root;
25
+ export const PopoverTrigger = PopoverPrimitive.Trigger;
26
+ export const PopoverAnchor = PopoverPrimitive.Anchor;
27
+
28
+ export const PopoverContent = React.forwardRef<
29
+ React.ElementRef<typeof PopoverPrimitive.Content>,
30
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
31
+ >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
32
+ <PopoverPrimitive.Portal>
33
+ <PopoverPrimitive.Content
34
+ ref={ref}
35
+ align={align}
36
+ sideOffset={sideOffset}
37
+ className={cn(
38
+ "z-50 w-72 rounded border border-border-strong bg-background p-3 text-sm text-foreground shadow-md outline-none",
39
+ "data-[state=open]:animate-fade-in",
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ </PopoverPrimitive.Portal>
45
+ ));
46
+ PopoverContent.displayName = "PopoverContent";
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as ProgressPrimitive from "@radix-ui/react-progress";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * Progress — for multi-step flows (waiver → emergency contact → done),
9
+ * upload bars, anything with a known percent completion.
10
+ *
11
+ * If you don't know the percent (indeterminate), use Skeleton or Spinner.
12
+ *
13
+ * For "spots filled" use CapacityBar — same shape but with a min-to-run
14
+ * warning color and tabular count alongside.
15
+ */
16
+ export const Progress = React.forwardRef<
17
+ React.ElementRef<typeof ProgressPrimitive.Root>,
18
+ React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
19
+ >(({ className, value, ...props }, ref) => (
20
+ <ProgressPrimitive.Root
21
+ ref={ref}
22
+ className={cn(
23
+ "relative h-1.5 w-full overflow-hidden rounded-full bg-bg-muted",
24
+ className
25
+ )}
26
+ {...props}
27
+ >
28
+ <ProgressPrimitive.Indicator
29
+ className="h-full w-full flex-1 bg-brand-forest transition-transform duration-150"
30
+ style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
31
+ />
32
+ </ProgressPrimitive.Root>
33
+ ));
34
+ Progress.displayName = "Progress";
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * RadioGroup + RadioGroupItem — Radix-backed, keyboard-navigable
9
+ * (Arrow / Home / End). Always wrap items in a <RadioGroup> with a
10
+ * matching set of <Label htmlFor=> wiring.
11
+ */
12
+ export const RadioGroup = React.forwardRef<
13
+ React.ElementRef<typeof RadioGroupPrimitive.Root>,
14
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
15
+ >(({ className, ...props }, ref) => (
16
+ <RadioGroupPrimitive.Root
17
+ ref={ref}
18
+ className={cn("grid gap-2", className)}
19
+ {...props}
20
+ />
21
+ ));
22
+ RadioGroup.displayName = "RadioGroup";
23
+
24
+ export const RadioGroupItem = React.forwardRef<
25
+ React.ElementRef<typeof RadioGroupPrimitive.Item>,
26
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
27
+ >(({ className, ...props }, ref) => (
28
+ <RadioGroupPrimitive.Item
29
+ ref={ref}
30
+ className={cn(
31
+ "aspect-square h-4 w-4 rounded-full border border-border-strong bg-background transition-colors",
32
+ // Invisible hit-area extension: 24px desktop / 44px touch.
33
+ "relative after:absolute after:-inset-1 pointer-coarse:after:-inset-3.5",
34
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
35
+ "disabled:cursor-not-allowed disabled:opacity-50",
36
+ "data-[state=checked]:border-brand-forest",
37
+ className
38
+ )}
39
+ {...props}
40
+ >
41
+ <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
42
+ <span className="h-2 w-2 rounded-full bg-brand-forest" />
43
+ </RadioGroupPrimitive.Indicator>
44
+ </RadioGroupPrimitive.Item>
45
+ ));
46
+ RadioGroupItem.displayName = "RadioGroupItem";
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
5
+ import { cn } from "../lib/utils";
6
+
7
+ /**
8
+ * ScrollArea — styled, consistent custom scrollbars across browsers.
9
+ * Wraps content that needs to scroll within a bounded region.
10
+ *
11
+ * <ScrollArea className="h-72 w-48 rounded border">
12
+ * <div className="p-4">… long content …</div>
13
+ * </ScrollArea>
14
+ *
15
+ * For full-page scroll, use the browser default — don't wrap <body>.
16
+ */
17
+ export const ScrollArea = React.forwardRef<
18
+ React.ElementRef<typeof ScrollAreaPrimitive.Root>,
19
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
20
+ >(({ className, children, ...props }, ref) => (
21
+ <ScrollAreaPrimitive.Root
22
+ ref={ref}
23
+ className={cn("relative overflow-hidden", className)}
24
+ {...props}
25
+ >
26
+ <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
27
+ {children}
28
+ </ScrollAreaPrimitive.Viewport>
29
+ <ScrollBar />
30
+ <ScrollAreaPrimitive.Corner />
31
+ </ScrollAreaPrimitive.Root>
32
+ ));
33
+ ScrollArea.displayName = "ScrollArea";
34
+
35
+ export const ScrollBar = React.forwardRef<
36
+ React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
37
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
38
+ >(({ className, orientation = "vertical", ...props }, ref) => (
39
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
40
+ ref={ref}
41
+ orientation={orientation}
42
+ className={cn(
43
+ "flex touch-none select-none transition-colors",
44
+ orientation === "vertical" &&
45
+ "h-full w-2 border-l border-l-transparent p-[1px]",
46
+ orientation === "horizontal" &&
47
+ "h-2 flex-col border-t border-t-transparent p-[1px]",
48
+ className
49
+ )}
50
+ {...props}
51
+ >
52
+ <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border-strong" />
53
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
54
+ ));
55
+ ScrollBar.displayName = "ScrollBar";