@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,169 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
5
|
+
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Select — keyboard-accessible dropdown built on Radix.
|
|
10
|
+
*
|
|
11
|
+
* <Select value={v} onValueChange={setV}>
|
|
12
|
+
* <SelectTrigger><SelectValue placeholder="Pick a venue" /></SelectTrigger>
|
|
13
|
+
* <SelectContent>
|
|
14
|
+
* <SelectItem value="astoria">Astoria Turf</SelectItem>
|
|
15
|
+
* …
|
|
16
|
+
* </SelectContent>
|
|
17
|
+
* </Select>
|
|
18
|
+
*
|
|
19
|
+
* Native <select> is fine for tiny lists in dense forms; reach for this
|
|
20
|
+
* when you want typeahead, item groups, or styled portals.
|
|
21
|
+
*/
|
|
22
|
+
export const Select = SelectPrimitive.Root;
|
|
23
|
+
export const SelectGroup = SelectPrimitive.Group;
|
|
24
|
+
export const SelectValue = SelectPrimitive.Value;
|
|
25
|
+
|
|
26
|
+
export const SelectTrigger = React.forwardRef<
|
|
27
|
+
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
28
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
|
29
|
+
>(({ className, children, ...props }, ref) => (
|
|
30
|
+
<SelectPrimitive.Trigger
|
|
31
|
+
ref={ref}
|
|
32
|
+
className={cn(
|
|
33
|
+
"flex h-8 pointer-coarse:h-11 w-full items-center justify-between gap-2 rounded-sm border border-border-strong bg-background px-2.5 text-sm",
|
|
34
|
+
"placeholder:text-text-subtle",
|
|
35
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
36
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
37
|
+
"[&>span]:line-clamp-1",
|
|
38
|
+
className
|
|
39
|
+
)}
|
|
40
|
+
{...props}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
<SelectPrimitive.Icon asChild>
|
|
44
|
+
<ChevronDown
|
|
45
|
+
className="h-3.5 w-3.5 opacity-60 shrink-0"
|
|
46
|
+
strokeWidth={1.75}
|
|
47
|
+
/>
|
|
48
|
+
</SelectPrimitive.Icon>
|
|
49
|
+
</SelectPrimitive.Trigger>
|
|
50
|
+
));
|
|
51
|
+
SelectTrigger.displayName = "SelectTrigger";
|
|
52
|
+
|
|
53
|
+
export const SelectScrollUpButton = React.forwardRef<
|
|
54
|
+
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
|
55
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
|
56
|
+
>(({ className, ...props }, ref) => (
|
|
57
|
+
<SelectPrimitive.ScrollUpButton
|
|
58
|
+
ref={ref}
|
|
59
|
+
className={cn(
|
|
60
|
+
"flex cursor-default items-center justify-center py-1",
|
|
61
|
+
className
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
>
|
|
65
|
+
<ChevronUp className="h-3 w-3" strokeWidth={1.75} />
|
|
66
|
+
</SelectPrimitive.ScrollUpButton>
|
|
67
|
+
));
|
|
68
|
+
SelectScrollUpButton.displayName = "SelectScrollUpButton";
|
|
69
|
+
|
|
70
|
+
export const SelectScrollDownButton = React.forwardRef<
|
|
71
|
+
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
|
72
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
|
73
|
+
>(({ className, ...props }, ref) => (
|
|
74
|
+
<SelectPrimitive.ScrollDownButton
|
|
75
|
+
ref={ref}
|
|
76
|
+
className={cn(
|
|
77
|
+
"flex cursor-default items-center justify-center py-1",
|
|
78
|
+
className
|
|
79
|
+
)}
|
|
80
|
+
{...props}
|
|
81
|
+
>
|
|
82
|
+
<ChevronDown className="h-3 w-3" strokeWidth={1.75} />
|
|
83
|
+
</SelectPrimitive.ScrollDownButton>
|
|
84
|
+
));
|
|
85
|
+
SelectScrollDownButton.displayName = "SelectScrollDownButton";
|
|
86
|
+
|
|
87
|
+
export const SelectContent = React.forwardRef<
|
|
88
|
+
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
89
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
90
|
+
>(({ className, children, position = "popper", ...props }, ref) => (
|
|
91
|
+
<SelectPrimitive.Portal>
|
|
92
|
+
<SelectPrimitive.Content
|
|
93
|
+
ref={ref}
|
|
94
|
+
className={cn(
|
|
95
|
+
"relative z-50 max-h-80 min-w-[8rem] overflow-hidden rounded-md border border-border-strong bg-background text-foreground shadow-md",
|
|
96
|
+
"data-[state=open]:animate-fade-in",
|
|
97
|
+
position === "popper" &&
|
|
98
|
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
99
|
+
className
|
|
100
|
+
)}
|
|
101
|
+
position={position}
|
|
102
|
+
{...props}
|
|
103
|
+
>
|
|
104
|
+
<SelectScrollUpButton />
|
|
105
|
+
<SelectPrimitive.Viewport
|
|
106
|
+
className={cn(
|
|
107
|
+
"p-1",
|
|
108
|
+
position === "popper" &&
|
|
109
|
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
|
110
|
+
)}
|
|
111
|
+
>
|
|
112
|
+
{children}
|
|
113
|
+
</SelectPrimitive.Viewport>
|
|
114
|
+
<SelectScrollDownButton />
|
|
115
|
+
</SelectPrimitive.Content>
|
|
116
|
+
</SelectPrimitive.Portal>
|
|
117
|
+
));
|
|
118
|
+
SelectContent.displayName = "SelectContent";
|
|
119
|
+
|
|
120
|
+
export const SelectLabel = React.forwardRef<
|
|
121
|
+
React.ElementRef<typeof SelectPrimitive.Label>,
|
|
122
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
|
123
|
+
>(({ className, ...props }, ref) => (
|
|
124
|
+
<SelectPrimitive.Label
|
|
125
|
+
ref={ref}
|
|
126
|
+
className={cn(
|
|
127
|
+
"py-1.5 pl-7 pr-2 text-[10px] font-semibold uppercase tracking-[0.06em] text-text-muted",
|
|
128
|
+
className
|
|
129
|
+
)}
|
|
130
|
+
{...props}
|
|
131
|
+
/>
|
|
132
|
+
));
|
|
133
|
+
SelectLabel.displayName = "SelectLabel";
|
|
134
|
+
|
|
135
|
+
export const SelectItem = React.forwardRef<
|
|
136
|
+
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
137
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
|
138
|
+
>(({ className, children, ...props }, ref) => (
|
|
139
|
+
<SelectPrimitive.Item
|
|
140
|
+
ref={ref}
|
|
141
|
+
className={cn(
|
|
142
|
+
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pointer-coarse:min-h-11 pl-7 pr-2 text-sm outline-none transition-colors",
|
|
143
|
+
"focus:bg-bg-subtle focus:text-foreground",
|
|
144
|
+
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
145
|
+
className
|
|
146
|
+
)}
|
|
147
|
+
{...props}
|
|
148
|
+
>
|
|
149
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
150
|
+
<SelectPrimitive.ItemIndicator>
|
|
151
|
+
<Check className="h-3 w-3" strokeWidth={2.5} />
|
|
152
|
+
</SelectPrimitive.ItemIndicator>
|
|
153
|
+
</span>
|
|
154
|
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
155
|
+
</SelectPrimitive.Item>
|
|
156
|
+
));
|
|
157
|
+
SelectItem.displayName = "SelectItem";
|
|
158
|
+
|
|
159
|
+
export const SelectSeparator = React.forwardRef<
|
|
160
|
+
React.ElementRef<typeof SelectPrimitive.Separator>,
|
|
161
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
|
162
|
+
>(({ className, ...props }, ref) => (
|
|
163
|
+
<SelectPrimitive.Separator
|
|
164
|
+
ref={ref}
|
|
165
|
+
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
166
|
+
{...props}
|
|
167
|
+
/>
|
|
168
|
+
));
|
|
169
|
+
SelectSeparator.displayName = "SelectSeparator";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Separator — a thin divider (`<hr>` for horizontal, vertical bar for
|
|
9
|
+
* vertical). Decorative by default. Set `decorative={false}` when the
|
|
10
|
+
* separator semantically separates content groups (screen readers will
|
|
11
|
+
* announce it).
|
|
12
|
+
*/
|
|
13
|
+
export const Separator = React.forwardRef<
|
|
14
|
+
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
|
15
|
+
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
|
16
|
+
>(
|
|
17
|
+
(
|
|
18
|
+
{ className, orientation = "horizontal", decorative = true, ...props },
|
|
19
|
+
ref
|
|
20
|
+
) => (
|
|
21
|
+
<SeparatorPrimitive.Root
|
|
22
|
+
ref={ref}
|
|
23
|
+
decorative={decorative}
|
|
24
|
+
orientation={orientation}
|
|
25
|
+
className={cn(
|
|
26
|
+
"shrink-0 bg-border",
|
|
27
|
+
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
|
|
28
|
+
className
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
Separator.displayName = "Separator";
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sidebar primitives — composable nav sidebar.
|
|
9
|
+
*
|
|
10
|
+
* <Sidebar>
|
|
11
|
+
* <SidebarHeader><BrandMark /></SidebarHeader>
|
|
12
|
+
* <SidebarBody>
|
|
13
|
+
* <SidebarSection label="Today">
|
|
14
|
+
* <SidebarItem asChild active>
|
|
15
|
+
* <Link href="/admin">
|
|
16
|
+
* <LayoutDashboard className="h-4 w-4" strokeWidth={1.75} />
|
|
17
|
+
* <span className="flex-1 truncate">Today</span>
|
|
18
|
+
* </Link>
|
|
19
|
+
* </SidebarItem>
|
|
20
|
+
* </SidebarSection>
|
|
21
|
+
* </SidebarBody>
|
|
22
|
+
* <SidebarFooter>v0.1</SidebarFooter>
|
|
23
|
+
* </Sidebar>
|
|
24
|
+
*
|
|
25
|
+
* For non-Next contexts, pass `href` prop directly; SidebarItem renders
|
|
26
|
+
* an <a> by default.
|
|
27
|
+
*/
|
|
28
|
+
export const Sidebar = React.forwardRef<
|
|
29
|
+
HTMLElement,
|
|
30
|
+
React.HTMLAttributes<HTMLElement>
|
|
31
|
+
>(({ className, ...props }, ref) => (
|
|
32
|
+
<aside
|
|
33
|
+
ref={ref}
|
|
34
|
+
aria-label="Sidebar navigation"
|
|
35
|
+
className={cn(
|
|
36
|
+
"hidden lg:flex flex-col w-56 shrink-0 border-r border-border bg-bg-subtle/40 sticky top-0 h-screen",
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
));
|
|
42
|
+
Sidebar.displayName = "Sidebar";
|
|
43
|
+
|
|
44
|
+
export const SidebarHeader = React.forwardRef<
|
|
45
|
+
HTMLDivElement,
|
|
46
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
47
|
+
>(({ className, ...props }, ref) => (
|
|
48
|
+
<div
|
|
49
|
+
ref={ref}
|
|
50
|
+
className={cn("px-5 py-4 border-b border-border", className)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
));
|
|
54
|
+
SidebarHeader.displayName = "SidebarHeader";
|
|
55
|
+
|
|
56
|
+
export const SidebarBody = React.forwardRef<
|
|
57
|
+
HTMLElement,
|
|
58
|
+
React.HTMLAttributes<HTMLElement>
|
|
59
|
+
>(({ className, ...props }, ref) => (
|
|
60
|
+
<nav
|
|
61
|
+
ref={ref}
|
|
62
|
+
className={cn("flex-1 overflow-y-auto scrollbar-thin py-3 px-2", className)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
));
|
|
66
|
+
SidebarBody.displayName = "SidebarBody";
|
|
67
|
+
|
|
68
|
+
export const SidebarFooter = React.forwardRef<
|
|
69
|
+
HTMLDivElement,
|
|
70
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
71
|
+
>(({ className, ...props }, ref) => (
|
|
72
|
+
<div
|
|
73
|
+
ref={ref}
|
|
74
|
+
className={cn("px-3 py-3 border-t border-border", className)}
|
|
75
|
+
{...props}
|
|
76
|
+
/>
|
|
77
|
+
));
|
|
78
|
+
SidebarFooter.displayName = "SidebarFooter";
|
|
79
|
+
|
|
80
|
+
export interface SidebarSectionProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
81
|
+
label?: React.ReactNode;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const SidebarSection = React.forwardRef<
|
|
85
|
+
HTMLDivElement,
|
|
86
|
+
SidebarSectionProps
|
|
87
|
+
>(({ className, label, children, ...props }, ref) => (
|
|
88
|
+
<div ref={ref} className={cn("mt-4 first:mt-0", className)} {...props}>
|
|
89
|
+
{label && (
|
|
90
|
+
<div className="px-2.5 pb-1 text-[10px] font-semibold uppercase tracking-[0.06em] text-text-subtle">
|
|
91
|
+
{label}
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
<ul className="space-y-0.5">
|
|
95
|
+
{React.Children.map(children, (child) =>
|
|
96
|
+
child == null || child === false ? null : <li>{child}</li>
|
|
97
|
+
)}
|
|
98
|
+
</ul>
|
|
99
|
+
</div>
|
|
100
|
+
));
|
|
101
|
+
SidebarSection.displayName = "SidebarSection";
|
|
102
|
+
|
|
103
|
+
export interface SidebarItemProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
104
|
+
active?: boolean;
|
|
105
|
+
/** Lucide icon (or any node). Rendered before the label. */
|
|
106
|
+
icon?: React.ReactNode;
|
|
107
|
+
/** Right-aligned badge (e.g., warning count). */
|
|
108
|
+
badge?: React.ReactNode;
|
|
109
|
+
/** Use Radix Slot to render a custom element (e.g., Next.js Link). */
|
|
110
|
+
asChild?: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const SidebarItem = React.forwardRef<
|
|
114
|
+
HTMLAnchorElement,
|
|
115
|
+
SidebarItemProps
|
|
116
|
+
>(
|
|
117
|
+
(
|
|
118
|
+
{ asChild, active, icon, badge, className, children, href, ...rest },
|
|
119
|
+
ref
|
|
120
|
+
) => {
|
|
121
|
+
const baseClasses = cn(
|
|
122
|
+
"group flex items-center gap-2.5 rounded-sm px-2.5 py-1.5 text-sm transition-colors",
|
|
123
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
124
|
+
active
|
|
125
|
+
? "bg-bg-subtle text-foreground font-medium border-l-2 border-l-brand-forest -ml-px pl-[10px]"
|
|
126
|
+
: "text-text-muted hover:bg-bg-muted hover:text-foreground",
|
|
127
|
+
className
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (asChild) {
|
|
131
|
+
const SlotComp = Slot as React.ElementType;
|
|
132
|
+
return (
|
|
133
|
+
<SlotComp
|
|
134
|
+
ref={ref}
|
|
135
|
+
className={baseClasses}
|
|
136
|
+
aria-current={active ? "page" : undefined}
|
|
137
|
+
{...rest}
|
|
138
|
+
>
|
|
139
|
+
{children}
|
|
140
|
+
</SlotComp>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<a
|
|
146
|
+
ref={ref}
|
|
147
|
+
href={href}
|
|
148
|
+
className={baseClasses}
|
|
149
|
+
aria-current={active ? "page" : undefined}
|
|
150
|
+
{...rest}
|
|
151
|
+
>
|
|
152
|
+
{icon && (
|
|
153
|
+
<span
|
|
154
|
+
className={cn(
|
|
155
|
+
"shrink-0 flex items-center",
|
|
156
|
+
active ? "text-brand-forest" : "text-text-muted"
|
|
157
|
+
)}
|
|
158
|
+
>
|
|
159
|
+
{icon}
|
|
160
|
+
</span>
|
|
161
|
+
)}
|
|
162
|
+
<span className="flex-1 truncate">{children}</span>
|
|
163
|
+
{badge && <span className="shrink-0">{badge}</span>}
|
|
164
|
+
</a>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
SidebarItem.displayName = "SidebarItem";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Skeleton — per design.md §12
|
|
6
|
+
* Use over spinners. Pulse opacity 0.5 → 1, 1.5s. Respects prefers-reduced-motion.
|
|
7
|
+
*/
|
|
8
|
+
export function Skeleton({
|
|
9
|
+
className,
|
|
10
|
+
...props
|
|
11
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
className={cn("animate-pulse rounded bg-bg-muted", className)}
|
|
15
|
+
aria-hidden="true"
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* SkeletonList — loading placeholder echoing a row list (§3.14:
|
|
23
|
+
* skeletons echo the final shape). Use as the `loading` slot of
|
|
24
|
+
* ThreeState for list views.
|
|
25
|
+
*/
|
|
26
|
+
export function SkeletonList({
|
|
27
|
+
rows = 3,
|
|
28
|
+
className,
|
|
29
|
+
...props
|
|
30
|
+
}: React.HTMLAttributes<HTMLDivElement> & { rows?: number }) {
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
className={cn("space-y-2", className)}
|
|
34
|
+
role="status"
|
|
35
|
+
aria-label="Loading"
|
|
36
|
+
{...props}
|
|
37
|
+
>
|
|
38
|
+
{Array.from({ length: rows }, (_, i) => (
|
|
39
|
+
<div key={i} className="flex items-center gap-3">
|
|
40
|
+
<Skeleton className="h-8 w-8 rounded-full shrink-0" />
|
|
41
|
+
<div className="flex-1 space-y-1.5">
|
|
42
|
+
<Skeleton className="h-3 w-2/5" />
|
|
43
|
+
<Skeleton className="h-3 w-3/5" />
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* SkeletonTable — loading placeholder echoing a data table. Match
|
|
53
|
+
* `columns` to the real table so the layout doesn't jump on load.
|
|
54
|
+
*/
|
|
55
|
+
export function SkeletonTable({
|
|
56
|
+
rows = 5,
|
|
57
|
+
columns = 4,
|
|
58
|
+
className,
|
|
59
|
+
...props
|
|
60
|
+
}: React.HTMLAttributes<HTMLDivElement> & {
|
|
61
|
+
rows?: number;
|
|
62
|
+
columns?: number;
|
|
63
|
+
}) {
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
className={cn("space-y-2", className)}
|
|
67
|
+
role="status"
|
|
68
|
+
aria-label="Loading"
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
<div className="flex gap-4 border-b border-border pb-2">
|
|
72
|
+
{Array.from({ length: columns }, (_, i) => (
|
|
73
|
+
<Skeleton key={i} className="h-3 flex-1" />
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
{Array.from({ length: rows }, (_, r) => (
|
|
77
|
+
<div key={r} className="flex gap-4 py-1">
|
|
78
|
+
{Array.from({ length: columns }, (_, c) => (
|
|
79
|
+
<Skeleton key={c} className="h-4 flex-1" />
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as SliderPrimitive from "@radix-ui/react-slider";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Slider — Radix-backed range input. Keyboard: arrows step, PageUp/Down
|
|
9
|
+
* big-step, Home/End min/max. Pass `aria-label` (or wire a Label to the
|
|
10
|
+
* thumb via `aria-labelledby`) — the a11y suite enforces a name.
|
|
11
|
+
*
|
|
12
|
+
* <Slider aria-label="Max price" min={0} max={50} step={5}
|
|
13
|
+
* defaultValue={[20]} />
|
|
14
|
+
*
|
|
15
|
+
* Supports multiple thumbs by passing more values: defaultValue={[10, 30]}.
|
|
16
|
+
*/
|
|
17
|
+
export const Slider = React.forwardRef<
|
|
18
|
+
React.ElementRef<typeof SliderPrimitive.Root>,
|
|
19
|
+
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
|
20
|
+
>(({ className, "aria-label": ariaLabel, ...props }, ref) => {
|
|
21
|
+
const values = props.value ?? props.defaultValue ?? [0];
|
|
22
|
+
// role="slider" lives on each Radix THUMB, not the root — a label on
|
|
23
|
+
// the root is invisible to screen readers, so name the thumbs.
|
|
24
|
+
const thumbLabel = (i: number) =>
|
|
25
|
+
values.length > 1
|
|
26
|
+
? `${ariaLabel} (${i + 1} of ${values.length})`
|
|
27
|
+
: ariaLabel;
|
|
28
|
+
return (
|
|
29
|
+
<SliderPrimitive.Root
|
|
30
|
+
ref={ref}
|
|
31
|
+
className={cn(
|
|
32
|
+
"relative flex w-full touch-none select-none items-center",
|
|
33
|
+
// Comfortable grab zone without visual bulk.
|
|
34
|
+
"h-5 pointer-coarse:h-11",
|
|
35
|
+
"data-[disabled]:opacity-50 data-[disabled]:pointer-events-none",
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
>
|
|
40
|
+
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-bg-muted">
|
|
41
|
+
<SliderPrimitive.Range className="absolute h-full bg-brand-forest" />
|
|
42
|
+
</SliderPrimitive.Track>
|
|
43
|
+
{values.map((_, i) => (
|
|
44
|
+
<SliderPrimitive.Thumb
|
|
45
|
+
key={i}
|
|
46
|
+
aria-label={thumbLabel(i)}
|
|
47
|
+
className={cn(
|
|
48
|
+
"block h-4 w-4 rounded-full border-2 border-brand-forest bg-background shadow-sm transition-colors",
|
|
49
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
50
|
+
// Invisible hit-area extension: 24px desktop / 44px touch.
|
|
51
|
+
"relative after:absolute after:-inset-1 pointer-coarse:after:-inset-3.5"
|
|
52
|
+
)}
|
|
53
|
+
/>
|
|
54
|
+
))}
|
|
55
|
+
</SliderPrimitive.Root>
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
Slider.displayName = "Slider";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SpecList + Spec — semantic key-value display (`<dl>` / `<dt>` / `<dd>`).
|
|
6
|
+
* Use for game detail "About this game" panels, settings summaries,
|
|
7
|
+
* receipts.
|
|
8
|
+
*
|
|
9
|
+
* <SpecList columns={2}>
|
|
10
|
+
* <Spec label="Format">5v5</Spec>
|
|
11
|
+
* <Spec label="Capacity">10 players</Spec>
|
|
12
|
+
* <Spec label="Start"><Timestamp iso={…} /></Spec>
|
|
13
|
+
* </SpecList>
|
|
14
|
+
*/
|
|
15
|
+
const COLUMNS_MAP = {
|
|
16
|
+
1: "grid-cols-1",
|
|
17
|
+
2: "grid-cols-2",
|
|
18
|
+
3: "grid-cols-3",
|
|
19
|
+
4: "grid-cols-4",
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
const GAP_MAP = {
|
|
23
|
+
2: "gap-2",
|
|
24
|
+
3: "gap-3",
|
|
25
|
+
4: "gap-4",
|
|
26
|
+
6: "gap-6",
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
export interface SpecListProps extends React.HTMLAttributes<HTMLDListElement> {
|
|
30
|
+
columns?: keyof typeof COLUMNS_MAP;
|
|
31
|
+
gap?: keyof typeof GAP_MAP;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const SpecList = React.forwardRef<HTMLDListElement, SpecListProps>(
|
|
35
|
+
({ className, columns = 2, gap = 4, ...props }, ref) => (
|
|
36
|
+
<dl
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn("grid", COLUMNS_MAP[columns], GAP_MAP[gap], className)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
SpecList.displayName = "SpecList";
|
|
44
|
+
|
|
45
|
+
export interface SpecProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
46
|
+
label: React.ReactNode;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const Spec = React.forwardRef<HTMLDivElement, SpecProps>(
|
|
50
|
+
({ className, label, children, ...props }, ref) => (
|
|
51
|
+
<div ref={ref} className={cn("", className)} {...props}>
|
|
52
|
+
<dt className="text-[10px] font-medium uppercase tracking-[0.04em] text-text-muted">
|
|
53
|
+
{label}
|
|
54
|
+
</dt>
|
|
55
|
+
<dd className="text-sm mt-0.5 text-foreground">{children}</dd>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
Spec.displayName = "Spec";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Loader2 } from "lucide-react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Spinner — tiny inline loader for buttons + per-row "working" state.
|
|
6
|
+
* Prefer Skeleton for full-region loading (per design.md §3.14).
|
|
7
|
+
*
|
|
8
|
+
* Honors prefers-reduced-motion via globals.css.
|
|
9
|
+
*/
|
|
10
|
+
const SIZE_MAP = {
|
|
11
|
+
sm: "h-3 w-3",
|
|
12
|
+
default: "h-4 w-4",
|
|
13
|
+
lg: "h-5 w-5",
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
export interface SpinnerProps {
|
|
17
|
+
size?: keyof typeof SIZE_MAP;
|
|
18
|
+
className?: string;
|
|
19
|
+
/** Accessible label (default "Loading") */
|
|
20
|
+
label?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function Spinner({
|
|
24
|
+
size = "default",
|
|
25
|
+
className,
|
|
26
|
+
label = "Loading",
|
|
27
|
+
}: SpinnerProps) {
|
|
28
|
+
return (
|
|
29
|
+
<Loader2
|
|
30
|
+
role="status"
|
|
31
|
+
aria-label={label}
|
|
32
|
+
className={cn("animate-spin", SIZE_MAP[size], className)}
|
|
33
|
+
strokeWidth={2}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stack — flexbox with consistent gap. Use Stack when you'd otherwise
|
|
6
|
+
* reach for `flex flex-col gap-3` (or `space-y-3`); it documents intent
|
|
7
|
+
* and keeps gap tokens consistent across the app.
|
|
8
|
+
*
|
|
9
|
+
* <Stack gap={2}> // vertical, gap-2
|
|
10
|
+
* <Stack direction="row" gap={4} align="center" justify="between">
|
|
11
|
+
*/
|
|
12
|
+
const GAP_MAP = {
|
|
13
|
+
0: "gap-0",
|
|
14
|
+
0.5: "gap-0.5",
|
|
15
|
+
1: "gap-1",
|
|
16
|
+
1.5: "gap-1.5",
|
|
17
|
+
2: "gap-2",
|
|
18
|
+
2.5: "gap-2.5",
|
|
19
|
+
3: "gap-3",
|
|
20
|
+
4: "gap-4",
|
|
21
|
+
5: "gap-5",
|
|
22
|
+
6: "gap-6",
|
|
23
|
+
8: "gap-8",
|
|
24
|
+
10: "gap-10",
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
const ALIGN_MAP = {
|
|
28
|
+
start: "items-start",
|
|
29
|
+
center: "items-center",
|
|
30
|
+
end: "items-end",
|
|
31
|
+
baseline: "items-baseline",
|
|
32
|
+
stretch: "items-stretch",
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
const JUSTIFY_MAP = {
|
|
36
|
+
start: "justify-start",
|
|
37
|
+
center: "justify-center",
|
|
38
|
+
end: "justify-end",
|
|
39
|
+
between: "justify-between",
|
|
40
|
+
around: "justify-around",
|
|
41
|
+
evenly: "justify-evenly",
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
export interface StackProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
45
|
+
direction?: "column" | "row";
|
|
46
|
+
gap?: keyof typeof GAP_MAP;
|
|
47
|
+
align?: keyof typeof ALIGN_MAP;
|
|
48
|
+
justify?: keyof typeof JUSTIFY_MAP;
|
|
49
|
+
wrap?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const Stack = React.forwardRef<HTMLDivElement, StackProps>(
|
|
53
|
+
(
|
|
54
|
+
{
|
|
55
|
+
className,
|
|
56
|
+
direction = "column",
|
|
57
|
+
gap = 3,
|
|
58
|
+
align,
|
|
59
|
+
justify,
|
|
60
|
+
wrap,
|
|
61
|
+
...props
|
|
62
|
+
},
|
|
63
|
+
ref
|
|
64
|
+
) => (
|
|
65
|
+
<div
|
|
66
|
+
ref={ref}
|
|
67
|
+
className={cn(
|
|
68
|
+
"flex",
|
|
69
|
+
direction === "column" ? "flex-col" : "flex-row",
|
|
70
|
+
GAP_MAP[gap],
|
|
71
|
+
align && ALIGN_MAP[align],
|
|
72
|
+
justify && JUSTIFY_MAP[justify],
|
|
73
|
+
wrap && "flex-wrap",
|
|
74
|
+
className
|
|
75
|
+
)}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
Stack.displayName = "Stack";
|