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