@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,154 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$description": "DT Sports design tokens — single source of truth. Edit this file, then run `npm run build:tokens`. Never edit the generated blocks in globals.css, src/styles/generated/, or src/tokens/index.ts by hand.",
|
|
3
|
+
"color": {
|
|
4
|
+
"$format": "HSL channel triplets (\"H S% L%\") so CSS can compose hsl(var(--x) / alpha).",
|
|
5
|
+
"light": {
|
|
6
|
+
"brand-lime": "72 65% 52%",
|
|
7
|
+
"brand-lime-foreground": "0 0% 4%",
|
|
8
|
+
"brand-forest": "147 66% 26%",
|
|
9
|
+
"brand-forest-foreground": "0 0% 100%",
|
|
10
|
+
"background": "0 0% 100%",
|
|
11
|
+
"bg-subtle": "0 0% 98%",
|
|
12
|
+
"bg-muted": "240 5% 96%",
|
|
13
|
+
"border": "0 0% 90%",
|
|
14
|
+
"border-strong": "0 0% 83%",
|
|
15
|
+
"foreground": "0 0% 4%",
|
|
16
|
+
"text-muted": "0 0% 32%",
|
|
17
|
+
"text-subtle": "0 0% 45%",
|
|
18
|
+
"success": "142 72% 29%",
|
|
19
|
+
"success-foreground": "0 0% 100%",
|
|
20
|
+
"warning": "35 90% 33%",
|
|
21
|
+
"warning-foreground": "0 0% 100%",
|
|
22
|
+
"danger": "0 72% 49%",
|
|
23
|
+
"danger-foreground": "0 0% 100%",
|
|
24
|
+
"info": "217 91% 50%",
|
|
25
|
+
"info-foreground": "0 0% 100%",
|
|
26
|
+
"ring": "147 66% 26%"
|
|
27
|
+
},
|
|
28
|
+
"dark": {
|
|
29
|
+
"$comment": "Overrides only — anything not listed inherits the light value.",
|
|
30
|
+
"background": "0 0% 4%",
|
|
31
|
+
"bg-subtle": "0 0% 8%",
|
|
32
|
+
"bg-muted": "0 0% 12%",
|
|
33
|
+
"border": "0 0% 15%",
|
|
34
|
+
"border-strong": "240 5% 26%",
|
|
35
|
+
"foreground": "0 0% 98%",
|
|
36
|
+
"text-muted": "240 5% 65%",
|
|
37
|
+
"text-subtle": "240 4% 52%",
|
|
38
|
+
"success": "142 60% 45%",
|
|
39
|
+
"success-foreground": "0 0% 4%",
|
|
40
|
+
"warning": "38 92% 50%",
|
|
41
|
+
"warning-foreground": "0 0% 4%",
|
|
42
|
+
"danger": "0 84% 62%",
|
|
43
|
+
"danger-foreground": "0 0% 4%",
|
|
44
|
+
"info": "217 91% 62%",
|
|
45
|
+
"info-foreground": "0 0% 4%",
|
|
46
|
+
"ring": "72 65% 52%"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"fontFamily": {
|
|
50
|
+
"sans": ["\"Exo 2\"", "system-ui", "sans-serif"],
|
|
51
|
+
"mono": [
|
|
52
|
+
"ui-monospace",
|
|
53
|
+
"SFMono-Regular",
|
|
54
|
+
"\"JetBrains Mono\"",
|
|
55
|
+
"Menlo",
|
|
56
|
+
"monospace"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
"fontSize": {
|
|
60
|
+
"xs": { "size": 11, "lineHeight": 16 },
|
|
61
|
+
"sm": { "size": 13, "lineHeight": 18 },
|
|
62
|
+
"base": { "size": 14, "lineHeight": 20 },
|
|
63
|
+
"md": { "size": 15, "lineHeight": 22 },
|
|
64
|
+
"lg": { "size": 17, "lineHeight": 24 },
|
|
65
|
+
"xl": { "size": 20, "lineHeight": 28 },
|
|
66
|
+
"2xl": { "size": 24, "lineHeight": 32 },
|
|
67
|
+
"3xl": { "size": 30, "lineHeight": 36 }
|
|
68
|
+
},
|
|
69
|
+
"fontSizeFluid": {
|
|
70
|
+
"$comment": "Marketing display scale — clamp(minPx, basePx + vw, maxPx). Web gets fluid clamp(); React Native gets {min,max}.",
|
|
71
|
+
"display-sm": {
|
|
72
|
+
"min": 32,
|
|
73
|
+
"max": 44,
|
|
74
|
+
"base": 24,
|
|
75
|
+
"vw": 2.5,
|
|
76
|
+
"lineHeight": 1.1,
|
|
77
|
+
"letterSpacing": "-0.02em"
|
|
78
|
+
},
|
|
79
|
+
"display": {
|
|
80
|
+
"min": 40,
|
|
81
|
+
"max": 64,
|
|
82
|
+
"base": 26,
|
|
83
|
+
"vw": 4,
|
|
84
|
+
"lineHeight": 1.05,
|
|
85
|
+
"letterSpacing": "-0.02em"
|
|
86
|
+
},
|
|
87
|
+
"display-lg": {
|
|
88
|
+
"min": 48,
|
|
89
|
+
"max": 96,
|
|
90
|
+
"base": 28,
|
|
91
|
+
"vw": 6,
|
|
92
|
+
"lineHeight": 1,
|
|
93
|
+
"letterSpacing": "-0.025em"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"fontWeight": {
|
|
97
|
+
"normal": 400,
|
|
98
|
+
"medium": 500,
|
|
99
|
+
"semibold": 600,
|
|
100
|
+
"bold": 700
|
|
101
|
+
},
|
|
102
|
+
"spacing": {
|
|
103
|
+
"0.5": 2,
|
|
104
|
+
"1": 4,
|
|
105
|
+
"1.5": 6,
|
|
106
|
+
"2": 8,
|
|
107
|
+
"2.5": 10,
|
|
108
|
+
"3": 12,
|
|
109
|
+
"4": 16,
|
|
110
|
+
"5": 20,
|
|
111
|
+
"6": 24,
|
|
112
|
+
"8": 32,
|
|
113
|
+
"10": 40,
|
|
114
|
+
"12": 48,
|
|
115
|
+
"16": 64,
|
|
116
|
+
"20": 80,
|
|
117
|
+
"24": 96,
|
|
118
|
+
"32": 128,
|
|
119
|
+
"40": 160
|
|
120
|
+
},
|
|
121
|
+
"radius": {
|
|
122
|
+
"sm": 2,
|
|
123
|
+
"DEFAULT": 4,
|
|
124
|
+
"md": 6
|
|
125
|
+
},
|
|
126
|
+
"shadow": {
|
|
127
|
+
"sm": "0 1px 2px rgba(0,0,0,0.04), 0 1px 1px rgba(0,0,0,0.04)",
|
|
128
|
+
"md": "0 4px 12px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.06)",
|
|
129
|
+
"lg": "0 12px 32px rgba(0,0,0,0.12), 0 2px 6px rgba(0,0,0,0.08)"
|
|
130
|
+
},
|
|
131
|
+
"gradient": {
|
|
132
|
+
"brand": "linear-gradient(135deg, hsl(var(--brand-forest)) 0%, hsl(100 55% 38%) 55%, hsl(var(--brand-lime)) 100%)",
|
|
133
|
+
"forest-fade": "linear-gradient(180deg, hsl(var(--brand-forest) / 0.08) 0%, transparent 100%)"
|
|
134
|
+
},
|
|
135
|
+
"motion": {
|
|
136
|
+
"duration": { "DEFAULT": 150, "slow": 200, "pulse": 1500 },
|
|
137
|
+
"easing": { "out": "cubic-bezier(0.2, 0, 0, 1)" }
|
|
138
|
+
},
|
|
139
|
+
"zIndex": {
|
|
140
|
+
"sticky": 20,
|
|
141
|
+
"overlay": 40,
|
|
142
|
+
"modal": 50,
|
|
143
|
+
"popover": 50,
|
|
144
|
+
"toast": 60
|
|
145
|
+
},
|
|
146
|
+
"breakpoint": {
|
|
147
|
+
"$comment": "Matches Tailwind defaults; declared so RN and marketing share them.",
|
|
148
|
+
"sm": 640,
|
|
149
|
+
"md": 768,
|
|
150
|
+
"lg": 1024,
|
|
151
|
+
"xl": 1280,
|
|
152
|
+
"2xl": 1536
|
|
153
|
+
}
|
|
154
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dtsports/design-system",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "DT Sports — shared design system. Tokens, components, and UX patterns for hosts and players.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"**/*.css"
|
|
9
|
+
],
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./showcase": {
|
|
18
|
+
"types": "./dist/showcase/index.d.ts",
|
|
19
|
+
"default": "./dist/showcase/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./marketing": {
|
|
22
|
+
"types": "./dist/marketing/index.d.ts",
|
|
23
|
+
"default": "./dist/marketing/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./styles/globals.css": "./dist/styles/globals.css",
|
|
26
|
+
"./tailwind-preset": {
|
|
27
|
+
"types": "./dist/styles/tailwind.preset.d.ts",
|
|
28
|
+
"default": "./dist/styles/tailwind.preset.js"
|
|
29
|
+
},
|
|
30
|
+
"./tokens": {
|
|
31
|
+
"types": "./dist/tokens/index.d.ts",
|
|
32
|
+
"default": "./dist/tokens/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./tokens.json": "./dist/tokens/tokens.json"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "npm run build:tokens && rm -rf dist && tsc -p tsconfig.build.json && node scripts/fix-extensions.mjs && cp src/styles/globals.css dist/styles/globals.css && cp src/tokens/tokens.json dist/tokens/tokens.json",
|
|
45
|
+
"dev": "vite --config dev/vite.config.ts",
|
|
46
|
+
"test:visual": "playwright test",
|
|
47
|
+
"test:visual:update": "playwright test --update-snapshots",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"test:watch": "vitest",
|
|
51
|
+
"check:contrast": "node scripts/check-contrast.mjs",
|
|
52
|
+
"build:tokens": "node scripts/build-tokens.mjs",
|
|
53
|
+
"check:tokens": "node scripts/build-tokens.mjs --check",
|
|
54
|
+
"lint": "eslint src",
|
|
55
|
+
"format": "prettier --write \"src/**/*.{ts,tsx,css}\" \"*.{js,json,md}\"",
|
|
56
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx,css}\" \"*.{js,json,md}\""
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@radix-ui/react-accordion": "^1.2.1",
|
|
60
|
+
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
|
61
|
+
"@radix-ui/react-checkbox": "^1.1.2",
|
|
62
|
+
"@radix-ui/react-collapsible": "^1.1.15",
|
|
63
|
+
"@radix-ui/react-dialog": "^1.1.2",
|
|
64
|
+
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
|
65
|
+
"@radix-ui/react-label": "^2.1.0",
|
|
66
|
+
"@radix-ui/react-popover": "^1.1.2",
|
|
67
|
+
"@radix-ui/react-progress": "^1.1.0",
|
|
68
|
+
"@radix-ui/react-radio-group": "^1.2.1",
|
|
69
|
+
"@radix-ui/react-scroll-area": "^1.2.1",
|
|
70
|
+
"@radix-ui/react-select": "^2.1.2",
|
|
71
|
+
"@radix-ui/react-separator": "^1.1.0",
|
|
72
|
+
"@radix-ui/react-slider": "^1.4.2",
|
|
73
|
+
"@radix-ui/react-slot": "^1.1.0",
|
|
74
|
+
"@radix-ui/react-switch": "^1.1.1",
|
|
75
|
+
"@radix-ui/react-tabs": "^1.1.1",
|
|
76
|
+
"@radix-ui/react-tooltip": "^1.1.3",
|
|
77
|
+
"class-variance-authority": "^0.7.1",
|
|
78
|
+
"clsx": "^2.1.1",
|
|
79
|
+
"cmdk": "^1.1.1",
|
|
80
|
+
"lucide-react": "^0.460.0",
|
|
81
|
+
"react-day-picker": "^10.0.1",
|
|
82
|
+
"sonner": "^1.7.0",
|
|
83
|
+
"tailwind-merge": "^2.5.4"
|
|
84
|
+
},
|
|
85
|
+
"peerDependencies": {
|
|
86
|
+
"react": ">=19",
|
|
87
|
+
"react-dom": ">=19",
|
|
88
|
+
"tailwindcss": ">=3.4"
|
|
89
|
+
},
|
|
90
|
+
"peerDependenciesMeta": {
|
|
91
|
+
"react-dom": {
|
|
92
|
+
"optional": true
|
|
93
|
+
},
|
|
94
|
+
"tailwindcss": {
|
|
95
|
+
"optional": true
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"devDependencies": {
|
|
99
|
+
"@changesets/cli": "^2.31.0",
|
|
100
|
+
"@eslint/js": "^9.39.4",
|
|
101
|
+
"@playwright/test": "^1.61.1",
|
|
102
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
103
|
+
"@testing-library/react": "^16.3.2",
|
|
104
|
+
"@testing-library/user-event": "^14.6.1",
|
|
105
|
+
"@types/jest-axe": "^3.5.9",
|
|
106
|
+
"@types/react": "^19.2.15",
|
|
107
|
+
"@types/react-dom": "^19.2.3",
|
|
108
|
+
"@vitejs/plugin-react": "^5.2.0",
|
|
109
|
+
"autoprefixer": "^10.5.2",
|
|
110
|
+
"eslint": "^9.39.4",
|
|
111
|
+
"eslint-config-prettier": "^10.1.8",
|
|
112
|
+
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
113
|
+
"eslint-plugin-react": "^7.37.5",
|
|
114
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
115
|
+
"jest-axe": "^10.0.0",
|
|
116
|
+
"jsdom": "^27.0.1",
|
|
117
|
+
"prettier": "^3.9.4",
|
|
118
|
+
"tailwindcss": "^3.4.15",
|
|
119
|
+
"typescript": "^5.7.2",
|
|
120
|
+
"typescript-eslint": "^8.62.1",
|
|
121
|
+
"vite": "^7.3.6",
|
|
122
|
+
"vitest": "^3.2.6"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
|
5
|
+
import { ChevronDown } from "lucide-react";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Accordion — for grouped expandable sections (House rules · Policy ·
|
|
10
|
+
* About). Use `type="single"` for FAQ-style (one-open-at-a-time) and
|
|
11
|
+
* `type="multiple"` for settings panels.
|
|
12
|
+
*
|
|
13
|
+
* <Accordion type="single" collapsible>
|
|
14
|
+
* <AccordionItem value="a">
|
|
15
|
+
* <AccordionTrigger>House rules</AccordionTrigger>
|
|
16
|
+
* <AccordionContent>…</AccordionContent>
|
|
17
|
+
* </AccordionItem>
|
|
18
|
+
* </Accordion>
|
|
19
|
+
*/
|
|
20
|
+
export const Accordion = AccordionPrimitive.Root;
|
|
21
|
+
|
|
22
|
+
export const AccordionItem = React.forwardRef<
|
|
23
|
+
React.ElementRef<typeof AccordionPrimitive.Item>,
|
|
24
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
25
|
+
>(({ className, ...props }, ref) => (
|
|
26
|
+
<AccordionPrimitive.Item
|
|
27
|
+
ref={ref}
|
|
28
|
+
className={cn("border-b border-border last:border-b-0", className)}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
));
|
|
32
|
+
AccordionItem.displayName = "AccordionItem";
|
|
33
|
+
|
|
34
|
+
export const AccordionTrigger = React.forwardRef<
|
|
35
|
+
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
36
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
|
37
|
+
>(({ className, children, ...props }, ref) => (
|
|
38
|
+
<AccordionPrimitive.Header className="flex">
|
|
39
|
+
<AccordionPrimitive.Trigger
|
|
40
|
+
ref={ref}
|
|
41
|
+
className={cn(
|
|
42
|
+
"flex flex-1 items-center justify-between py-3 text-sm font-medium text-foreground transition-all duration-150",
|
|
43
|
+
"hover:text-brand-forest",
|
|
44
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:rounded-sm",
|
|
45
|
+
"[&[data-state=open]>svg]:rotate-180",
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
<ChevronDown
|
|
52
|
+
className="h-3.5 w-3.5 shrink-0 text-text-muted transition-transform duration-150"
|
|
53
|
+
strokeWidth={1.75}
|
|
54
|
+
/>
|
|
55
|
+
</AccordionPrimitive.Trigger>
|
|
56
|
+
</AccordionPrimitive.Header>
|
|
57
|
+
));
|
|
58
|
+
AccordionTrigger.displayName = "AccordionTrigger";
|
|
59
|
+
|
|
60
|
+
export const AccordionContent = React.forwardRef<
|
|
61
|
+
React.ElementRef<typeof AccordionPrimitive.Content>,
|
|
62
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
63
|
+
>(({ className, children, ...props }, ref) => (
|
|
64
|
+
<AccordionPrimitive.Content
|
|
65
|
+
ref={ref}
|
|
66
|
+
className={cn(
|
|
67
|
+
"overflow-hidden text-sm text-foreground",
|
|
68
|
+
"data-[state=closed]:animate-fade-in data-[state=open]:animate-fade-in",
|
|
69
|
+
className
|
|
70
|
+
)}
|
|
71
|
+
{...props}
|
|
72
|
+
>
|
|
73
|
+
<div className="pb-3 pt-0">{children}</div>
|
|
74
|
+
</AccordionPrimitive.Content>
|
|
75
|
+
));
|
|
76
|
+
AccordionContent.displayName = "AccordionContent";
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { AlertTriangle, CheckCircle2, Info, X, XCircle } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Alert / inline notice — for status messages that aren't transient.
|
|
8
|
+
* Pair with the right variant:
|
|
9
|
+
* - info neutral notices
|
|
10
|
+
* - success confirmations that stick (booking saved, etc.)
|
|
11
|
+
* - warning needs attention (below min to run, low credit)
|
|
12
|
+
* - danger error / failure
|
|
13
|
+
*
|
|
14
|
+
* Use Toast for transient feedback; use Alert when the message belongs
|
|
15
|
+
* inline with the content.
|
|
16
|
+
*/
|
|
17
|
+
export const alertVariants = cva(
|
|
18
|
+
"relative w-full rounded border px-3 py-2.5 text-sm",
|
|
19
|
+
{
|
|
20
|
+
variants: {
|
|
21
|
+
variant: {
|
|
22
|
+
info: "border-info/30 bg-info/5 text-foreground",
|
|
23
|
+
success: "border-success/30 bg-success/5 text-foreground",
|
|
24
|
+
warning: "border-warning/40 bg-warning/5 text-foreground",
|
|
25
|
+
danger: "border-danger/30 bg-danger/5 text-foreground",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: { variant: "info" },
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const ICON_MAP = {
|
|
33
|
+
info: Info,
|
|
34
|
+
success: CheckCircle2,
|
|
35
|
+
warning: AlertTriangle,
|
|
36
|
+
danger: XCircle,
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
const ICON_COLOR_MAP = {
|
|
40
|
+
info: "text-info",
|
|
41
|
+
success: "text-success",
|
|
42
|
+
warning: "text-warning",
|
|
43
|
+
danger: "text-danger",
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
export interface AlertProps
|
|
47
|
+
extends
|
|
48
|
+
Omit<React.HTMLAttributes<HTMLDivElement>, "title">,
|
|
49
|
+
VariantProps<typeof alertVariants> {
|
|
50
|
+
title?: React.ReactNode;
|
|
51
|
+
/** Set false to hide the leading status icon */
|
|
52
|
+
showIcon?: boolean;
|
|
53
|
+
/** Show a close (×) button. Provide onDismiss to handle the click. */
|
|
54
|
+
dismissible?: boolean;
|
|
55
|
+
onDismiss?: () => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function Alert({
|
|
59
|
+
className,
|
|
60
|
+
variant,
|
|
61
|
+
title,
|
|
62
|
+
showIcon = true,
|
|
63
|
+
dismissible,
|
|
64
|
+
onDismiss,
|
|
65
|
+
children,
|
|
66
|
+
...props
|
|
67
|
+
}: AlertProps) {
|
|
68
|
+
const v = variant ?? "info";
|
|
69
|
+
const Icon = ICON_MAP[v];
|
|
70
|
+
const iconColor = ICON_COLOR_MAP[v];
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
role={v === "danger" ? "alert" : "status"}
|
|
74
|
+
className={cn(alertVariants({ variant: v }), className)}
|
|
75
|
+
{...props}
|
|
76
|
+
>
|
|
77
|
+
<div className="flex items-start gap-2.5">
|
|
78
|
+
{showIcon && (
|
|
79
|
+
<Icon
|
|
80
|
+
className={cn("h-4 w-4 shrink-0 mt-0.5", iconColor)}
|
|
81
|
+
strokeWidth={2}
|
|
82
|
+
aria-hidden="true"
|
|
83
|
+
/>
|
|
84
|
+
)}
|
|
85
|
+
<div className="flex-1 min-w-0">
|
|
86
|
+
{title && (
|
|
87
|
+
<div className="font-medium leading-tight text-sm">{title}</div>
|
|
88
|
+
)}
|
|
89
|
+
{children && (
|
|
90
|
+
<div
|
|
91
|
+
className={cn(
|
|
92
|
+
title && "mt-1",
|
|
93
|
+
"text-xs text-text-muted [&_a]:text-brand-forest [&_a]:underline-offset-2 [&_a]:hover:underline"
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
96
|
+
{children}
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
{dismissible && (
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
onClick={onDismiss}
|
|
104
|
+
aria-label="Dismiss"
|
|
105
|
+
className="shrink-0 -mr-1 -mt-0.5 h-5 w-5 grid place-items-center rounded-sm text-text-muted hover:bg-bg-subtle hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring relative after:absolute after:-inset-0.5 pointer-coarse:after:-inset-3"
|
|
106
|
+
>
|
|
107
|
+
<X className="h-3 w-3" strokeWidth={1.75} />
|
|
108
|
+
</button>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* AppShell — flex layout for sidebar + main content.
|
|
6
|
+
* The actual sidebar and content are children; AppShell just provides
|
|
7
|
+
* the full-height flex frame.
|
|
8
|
+
*
|
|
9
|
+
* <AppShell>
|
|
10
|
+
* <Sidebar>…</Sidebar>
|
|
11
|
+
* <AppMain>
|
|
12
|
+
* <TopBar>…</TopBar>
|
|
13
|
+
* <PageContent>…</PageContent>
|
|
14
|
+
* </AppMain>
|
|
15
|
+
* </AppShell>
|
|
16
|
+
*/
|
|
17
|
+
export const AppShell = React.forwardRef<
|
|
18
|
+
HTMLDivElement,
|
|
19
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
20
|
+
>(({ className, ...props }, ref) => (
|
|
21
|
+
<div
|
|
22
|
+
ref={ref}
|
|
23
|
+
className={cn("flex min-h-screen w-full", className)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
));
|
|
27
|
+
AppShell.displayName = "AppShell";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* AppMain — main content area, scrollable, takes remaining width.
|
|
31
|
+
* Pair with TopBar (sticky) and your page content inside.
|
|
32
|
+
*/
|
|
33
|
+
export const AppMain = React.forwardRef<
|
|
34
|
+
HTMLDivElement,
|
|
35
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
36
|
+
>(({ className, ...props }, ref) => (
|
|
37
|
+
<div
|
|
38
|
+
ref={ref}
|
|
39
|
+
className={cn("flex-1 min-w-0 flex flex-col", className)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
));
|
|
43
|
+
AppMain.displayName = "AppMain";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AspectRatio — locks a region to a given ratio without layout shift.
|
|
7
|
+
*
|
|
8
|
+
* <AspectRatio ratio={16 / 9}>
|
|
9
|
+
* <iframe src={…} className="h-full w-full" />
|
|
10
|
+
* </AspectRatio>
|
|
11
|
+
*
|
|
12
|
+
* Common ratios:
|
|
13
|
+
* - 16/9 video
|
|
14
|
+
* - 4/3 older video / photos
|
|
15
|
+
* - 1/1 square
|
|
16
|
+
* - 21/9 ultrawide / banner
|
|
17
|
+
*/
|
|
18
|
+
export const AspectRatio = AspectRatioPrimitive.Root;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* AvatarGroup — stacked overlapping avatars. Shows up to `max` avatars
|
|
6
|
+
* followed by a "+N" pill for the overflow.
|
|
7
|
+
*
|
|
8
|
+
* <AvatarGroup max={4}>
|
|
9
|
+
* <Avatar name="Alex Chen" />
|
|
10
|
+
* <Avatar name="Jordan Lee" />
|
|
11
|
+
* <Avatar name="Sam Patel" />
|
|
12
|
+
* <Avatar name="Maria Garcia" />
|
|
13
|
+
* <Avatar name="Danny Tran" />
|
|
14
|
+
* </AvatarGroup>
|
|
15
|
+
*
|
|
16
|
+
* Use for: roster previews, "8 people booked" inline indicators.
|
|
17
|
+
*/
|
|
18
|
+
const SIZE_CLASSES = {
|
|
19
|
+
sm: "h-6 w-6 text-[10px]",
|
|
20
|
+
default: "h-8 w-8 text-xs",
|
|
21
|
+
lg: "h-10 w-10 text-sm",
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
export interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
25
|
+
/** Max avatars shown before collapsing to "+N". */
|
|
26
|
+
max?: number;
|
|
27
|
+
/** Size of the overflow pill — should match the avatars inside. */
|
|
28
|
+
size?: keyof typeof SIZE_CLASSES;
|
|
29
|
+
/** Accessible label for screen readers (e.g. "8 players booked"). */
|
|
30
|
+
label?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
|
|
34
|
+
(
|
|
35
|
+
{ className, max = 4, size = "default", label, children, ...props },
|
|
36
|
+
ref
|
|
37
|
+
) => {
|
|
38
|
+
const all = React.Children.toArray(children).filter(Boolean);
|
|
39
|
+
const visible = all.slice(0, max);
|
|
40
|
+
const overflow = all.length - visible.length;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
ref={ref}
|
|
45
|
+
role={label ? "img" : undefined}
|
|
46
|
+
aria-label={label}
|
|
47
|
+
className={cn("inline-flex -space-x-1.5", className)}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
{visible.map((child, i) => (
|
|
51
|
+
<span
|
|
52
|
+
key={i}
|
|
53
|
+
className="ring-2 ring-background rounded-full inline-flex"
|
|
54
|
+
>
|
|
55
|
+
{child}
|
|
56
|
+
</span>
|
|
57
|
+
))}
|
|
58
|
+
{overflow > 0 && (
|
|
59
|
+
<span
|
|
60
|
+
aria-hidden={label ? "true" : undefined}
|
|
61
|
+
className={cn(
|
|
62
|
+
"ring-2 ring-background rounded-full bg-bg-muted text-text-muted font-medium tabular grid place-items-center",
|
|
63
|
+
SIZE_CLASSES[size]
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
+{overflow}
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
AvatarGroup.displayName = "AvatarGroup";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Avatar — per design.md §10
|
|
6
|
+
* Sizes: sm 24 / default 32 / lg 64
|
|
7
|
+
* Fallback: brand-forest bg, white initials (first letter of first + last)
|
|
8
|
+
*/
|
|
9
|
+
export interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
10
|
+
name: string;
|
|
11
|
+
src?: string;
|
|
12
|
+
size?: "sm" | "default" | "lg";
|
|
13
|
+
/** Optional status dot: lime = checked-in, gray = booked */
|
|
14
|
+
status?: "checked-in" | "booked";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function initials(name: string) {
|
|
18
|
+
const parts = name.trim().split(/\s+/);
|
|
19
|
+
const first = parts[0]?.[0] ?? "";
|
|
20
|
+
const last = parts.length > 1 ? parts[parts.length - 1][0] : "";
|
|
21
|
+
return (first + last).toUpperCase() || "?";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const sizeMap = {
|
|
25
|
+
sm: { box: "h-6 w-6", text: "text-[10px]", dot: "h-1.5 w-1.5" },
|
|
26
|
+
default: { box: "h-8 w-8", text: "text-xs", dot: "h-2 w-2" },
|
|
27
|
+
lg: { box: "h-16 w-16", text: "text-lg", dot: "h-3 w-3" },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function Avatar({
|
|
31
|
+
name,
|
|
32
|
+
src,
|
|
33
|
+
size = "default",
|
|
34
|
+
status,
|
|
35
|
+
className,
|
|
36
|
+
...props
|
|
37
|
+
}: AvatarProps) {
|
|
38
|
+
const s = sizeMap[size];
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
role="img"
|
|
42
|
+
// Descendants of role="img" are presentational, so the status must
|
|
43
|
+
// live in the container's label — a labelled dot would be ignored.
|
|
44
|
+
aria-label={
|
|
45
|
+
status
|
|
46
|
+
? `${name} (${status === "checked-in" ? "checked in" : "booked"})`
|
|
47
|
+
: name
|
|
48
|
+
}
|
|
49
|
+
className={cn("relative inline-flex shrink-0", className)}
|
|
50
|
+
{...props}
|
|
51
|
+
>
|
|
52
|
+
{src ? (
|
|
53
|
+
<img
|
|
54
|
+
src={src}
|
|
55
|
+
alt={name}
|
|
56
|
+
className={cn("rounded-full object-cover", s.box)}
|
|
57
|
+
/>
|
|
58
|
+
) : (
|
|
59
|
+
<div
|
|
60
|
+
className={cn(
|
|
61
|
+
"flex items-center justify-center rounded-full bg-brand-forest text-brand-forest-foreground font-semibold tabular",
|
|
62
|
+
s.box,
|
|
63
|
+
s.text
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
{initials(name)}
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
{status && (
|
|
70
|
+
<span
|
|
71
|
+
aria-hidden="true"
|
|
72
|
+
className={cn(
|
|
73
|
+
"absolute -bottom-0.5 -right-0.5 rounded-full ring-2 ring-background",
|
|
74
|
+
s.dot,
|
|
75
|
+
status === "checked-in" ? "bg-brand-lime" : "bg-text-subtle"
|
|
76
|
+
)}
|
|
77
|
+
/>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|