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