@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,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
|
5
|
+
import { ChevronDown } from "lucide-react";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Collapsible — a single expandable region. Accordion is for GROUPS of
|
|
10
|
+
* sections with one-open-at-a-time semantics; Collapsible is one
|
|
11
|
+
* independent disclosure ("Show advanced options", "View 12 more").
|
|
12
|
+
*
|
|
13
|
+
* <Collapsible>
|
|
14
|
+
* <CollapsibleTrigger>Advanced options</CollapsibleTrigger>
|
|
15
|
+
* <CollapsibleContent>…</CollapsibleContent>
|
|
16
|
+
* </Collapsible>
|
|
17
|
+
*/
|
|
18
|
+
export const Collapsible = CollapsiblePrimitive.Root;
|
|
19
|
+
|
|
20
|
+
export const CollapsibleTrigger = React.forwardRef<
|
|
21
|
+
React.ElementRef<typeof CollapsiblePrimitive.Trigger>,
|
|
22
|
+
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Trigger>
|
|
23
|
+
>(({ className, children, ...props }, ref) => (
|
|
24
|
+
<CollapsiblePrimitive.Trigger
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn(
|
|
27
|
+
"group flex items-center gap-1.5 text-sm font-medium text-foreground transition-colors hover:text-brand-forest",
|
|
28
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm",
|
|
29
|
+
"pointer-coarse:min-h-11",
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
{children}
|
|
35
|
+
<ChevronDown
|
|
36
|
+
className="h-3.5 w-3.5 text-text-muted transition-transform duration-150 group-data-[state=open]:rotate-180"
|
|
37
|
+
strokeWidth={1.75}
|
|
38
|
+
aria-hidden="true"
|
|
39
|
+
/>
|
|
40
|
+
</CollapsiblePrimitive.Trigger>
|
|
41
|
+
));
|
|
42
|
+
CollapsibleTrigger.displayName = "CollapsibleTrigger";
|
|
43
|
+
|
|
44
|
+
export const CollapsibleContent = React.forwardRef<
|
|
45
|
+
React.ElementRef<typeof CollapsiblePrimitive.Content>,
|
|
46
|
+
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Content>
|
|
47
|
+
>(({ className, ...props }, ref) => (
|
|
48
|
+
<CollapsiblePrimitive.Content
|
|
49
|
+
ref={ref}
|
|
50
|
+
className={cn("pt-2 data-[state=open]:animate-fade-in", className)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
));
|
|
54
|
+
CollapsibleContent.displayName = "CollapsibleContent";
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Check, ChevronsUpDown } from "lucide-react";
|
|
5
|
+
import { Command as CommandPrimitive } from "cmdk";
|
|
6
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
|
7
|
+
import { cn } from "../lib/utils";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Combobox — searchable single-select. Use instead of Select when the
|
|
11
|
+
* option list is long enough that scanning beats scrolling (venues,
|
|
12
|
+
* players, cities — roughly 10+ options).
|
|
13
|
+
*
|
|
14
|
+
* <Combobox
|
|
15
|
+
* options={venues.map(v => ({ value: v.id, label: v.name }))}
|
|
16
|
+
* value={venueId}
|
|
17
|
+
* onValueChange={setVenueId}
|
|
18
|
+
* placeholder="Select venue…"
|
|
19
|
+
* aria-label="Venue"
|
|
20
|
+
* />
|
|
21
|
+
*
|
|
22
|
+
* Keyboard: type to filter, ↑/↓ to move, Enter to select, Esc closes.
|
|
23
|
+
* Uncontrolled open state; controlled value.
|
|
24
|
+
*/
|
|
25
|
+
export interface ComboboxOption {
|
|
26
|
+
value: string;
|
|
27
|
+
label: string;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ComboboxProps {
|
|
32
|
+
options: ComboboxOption[];
|
|
33
|
+
value: string | null;
|
|
34
|
+
onValueChange: (value: string | null) => void;
|
|
35
|
+
/** Trigger text when nothing is selected. */
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
/** Search input placeholder. */
|
|
38
|
+
searchPlaceholder?: string;
|
|
39
|
+
/** Shown when the query matches nothing. */
|
|
40
|
+
emptyText?: string;
|
|
41
|
+
/** Accessible name for the trigger + listbox. */
|
|
42
|
+
"aria-label": string;
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
className?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function Combobox({
|
|
48
|
+
options,
|
|
49
|
+
value,
|
|
50
|
+
onValueChange,
|
|
51
|
+
placeholder = "Select…",
|
|
52
|
+
searchPlaceholder = "Search…",
|
|
53
|
+
emptyText = "No results.",
|
|
54
|
+
"aria-label": ariaLabel,
|
|
55
|
+
disabled,
|
|
56
|
+
className,
|
|
57
|
+
}: ComboboxProps) {
|
|
58
|
+
const [open, setOpen] = React.useState(false);
|
|
59
|
+
const listId = React.useId();
|
|
60
|
+
const selected = options.find((o) => o.value === value);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
64
|
+
<PopoverTrigger asChild>
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
role="combobox"
|
|
68
|
+
aria-expanded={open}
|
|
69
|
+
aria-controls={listId}
|
|
70
|
+
aria-haspopup="listbox"
|
|
71
|
+
aria-label={ariaLabel}
|
|
72
|
+
disabled={disabled}
|
|
73
|
+
className={cn(
|
|
74
|
+
"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",
|
|
75
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
76
|
+
"disabled:cursor-not-allowed disabled:bg-bg-muted disabled:opacity-60",
|
|
77
|
+
!selected && "text-text-subtle",
|
|
78
|
+
className
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
<span className="truncate">{selected?.label ?? placeholder}</span>
|
|
82
|
+
<ChevronsUpDown
|
|
83
|
+
className="h-3.5 w-3.5 shrink-0 text-text-muted"
|
|
84
|
+
strokeWidth={1.75}
|
|
85
|
+
aria-hidden="true"
|
|
86
|
+
/>
|
|
87
|
+
</button>
|
|
88
|
+
</PopoverTrigger>
|
|
89
|
+
<PopoverContent
|
|
90
|
+
aria-label={ariaLabel}
|
|
91
|
+
align="start"
|
|
92
|
+
className="w-[--radix-popover-trigger-width] min-w-[12rem] p-0"
|
|
93
|
+
>
|
|
94
|
+
<CommandPrimitive label={ariaLabel} className="outline-none">
|
|
95
|
+
<CommandPrimitive.Input
|
|
96
|
+
placeholder={searchPlaceholder}
|
|
97
|
+
className={cn(
|
|
98
|
+
"w-full border-b border-border bg-transparent px-3 h-8 pointer-coarse:h-11 text-sm text-foreground",
|
|
99
|
+
"placeholder:text-text-subtle focus:outline-none"
|
|
100
|
+
)}
|
|
101
|
+
/>
|
|
102
|
+
<CommandPrimitive.List
|
|
103
|
+
id={listId}
|
|
104
|
+
className="max-h-64 overflow-y-auto p-1 scrollbar-thin"
|
|
105
|
+
>
|
|
106
|
+
<CommandPrimitive.Empty className="px-3 py-6 text-center text-sm text-text-muted">
|
|
107
|
+
{emptyText}
|
|
108
|
+
</CommandPrimitive.Empty>
|
|
109
|
+
{options.map((option) => (
|
|
110
|
+
<CommandPrimitive.Item
|
|
111
|
+
key={option.value}
|
|
112
|
+
value={option.label}
|
|
113
|
+
disabled={option.disabled}
|
|
114
|
+
onSelect={() => {
|
|
115
|
+
onValueChange(option.value === value ? null : option.value);
|
|
116
|
+
setOpen(false);
|
|
117
|
+
}}
|
|
118
|
+
className={cn(
|
|
119
|
+
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 pointer-coarse:min-h-11 text-sm outline-none",
|
|
120
|
+
"data-[selected=true]:bg-bg-subtle data-[selected=true]:text-foreground",
|
|
121
|
+
"data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50"
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
<Check
|
|
125
|
+
className={cn(
|
|
126
|
+
"h-3.5 w-3.5 shrink-0 text-brand-forest",
|
|
127
|
+
option.value === value ? "opacity-100" : "opacity-0"
|
|
128
|
+
)}
|
|
129
|
+
strokeWidth={2}
|
|
130
|
+
aria-hidden="true"
|
|
131
|
+
/>
|
|
132
|
+
{option.label}
|
|
133
|
+
</CommandPrimitive.Item>
|
|
134
|
+
))}
|
|
135
|
+
</CommandPrimitive.List>
|
|
136
|
+
</CommandPrimitive>
|
|
137
|
+
</PopoverContent>
|
|
138
|
+
</Popover>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogBody,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
DialogFooter,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
DialogTrigger,
|
|
13
|
+
} from "./dialog";
|
|
14
|
+
import { Button, type ButtonProps } from "./button";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ConfirmAction — codifies design.md §3.11 Confirmation discipline.
|
|
18
|
+
*
|
|
19
|
+
* Use ONLY for destructive or financially irreversible actions. The body
|
|
20
|
+
* is required to be concrete ("8 players will be refunded $14 each"),
|
|
21
|
+
* never abstract ("Are you sure?").
|
|
22
|
+
*
|
|
23
|
+
* Wrap your trigger button (works with asChild).
|
|
24
|
+
*
|
|
25
|
+
* Example:
|
|
26
|
+
* <ConfirmAction
|
|
27
|
+
* title="Cancel this game?"
|
|
28
|
+
* description="This action is logged and cannot be undone."
|
|
29
|
+
* consequence="8 players will be refunded $14.00 each as credit and emailed."
|
|
30
|
+
* confirmLabel="Cancel + refund all"
|
|
31
|
+
* confirmVariant="destructive"
|
|
32
|
+
* onConfirm={async () => await cancelGame(id)}
|
|
33
|
+
* >
|
|
34
|
+
* <Button variant="destructive">Cancel game…</Button>
|
|
35
|
+
* </ConfirmAction>
|
|
36
|
+
*/
|
|
37
|
+
export interface ConfirmActionProps {
|
|
38
|
+
title: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
/** REQUIRED concrete consequence text — never abstract. */
|
|
41
|
+
consequence: React.ReactNode;
|
|
42
|
+
confirmLabel: string;
|
|
43
|
+
cancelLabel?: string;
|
|
44
|
+
confirmVariant?: ButtonProps["variant"];
|
|
45
|
+
onConfirm: () => void | Promise<void>;
|
|
46
|
+
children: React.ReactNode;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function ConfirmAction({
|
|
50
|
+
title,
|
|
51
|
+
description,
|
|
52
|
+
consequence,
|
|
53
|
+
confirmLabel,
|
|
54
|
+
cancelLabel = "Cancel",
|
|
55
|
+
confirmVariant = "destructive",
|
|
56
|
+
onConfirm,
|
|
57
|
+
children,
|
|
58
|
+
}: ConfirmActionProps) {
|
|
59
|
+
const [open, setOpen] = React.useState(false);
|
|
60
|
+
const [busy, setBusy] = React.useState(false);
|
|
61
|
+
|
|
62
|
+
const handleConfirm = async () => {
|
|
63
|
+
setBusy(true);
|
|
64
|
+
try {
|
|
65
|
+
await onConfirm();
|
|
66
|
+
setOpen(false);
|
|
67
|
+
} finally {
|
|
68
|
+
setBusy(false);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
74
|
+
<DialogTrigger asChild>{children}</DialogTrigger>
|
|
75
|
+
{/*
|
|
76
|
+
When no description prop is provided, explicitly opt out of
|
|
77
|
+
Radix's aria-describedby auto-wiring (passing `undefined`
|
|
78
|
+
disables the dev warning). The consequence text in the body
|
|
79
|
+
already explains the action; Radix only auto-wires DialogDescription.
|
|
80
|
+
When `description` IS provided, omit the prop so Radix can wire it.
|
|
81
|
+
*/}
|
|
82
|
+
<DialogContent
|
|
83
|
+
{...(description ? {} : { "aria-describedby": undefined })}
|
|
84
|
+
>
|
|
85
|
+
<DialogHeader>
|
|
86
|
+
<DialogTitle>{title}</DialogTitle>
|
|
87
|
+
{description && <DialogDescription>{description}</DialogDescription>}
|
|
88
|
+
</DialogHeader>
|
|
89
|
+
<DialogBody>
|
|
90
|
+
<p className="text-sm text-foreground">{consequence}</p>
|
|
91
|
+
</DialogBody>
|
|
92
|
+
<DialogFooter>
|
|
93
|
+
<Button
|
|
94
|
+
variant="ghost"
|
|
95
|
+
onClick={() => setOpen(false)}
|
|
96
|
+
disabled={busy}
|
|
97
|
+
>
|
|
98
|
+
{cancelLabel}
|
|
99
|
+
</Button>
|
|
100
|
+
<Button
|
|
101
|
+
variant={confirmVariant}
|
|
102
|
+
onClick={handleConfirm}
|
|
103
|
+
disabled={busy}
|
|
104
|
+
>
|
|
105
|
+
{busy ? "Working…" : confirmLabel}
|
|
106
|
+
</Button>
|
|
107
|
+
</DialogFooter>
|
|
108
|
+
</DialogContent>
|
|
109
|
+
</Dialog>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Container — max-width page wrapper with consistent horizontal padding.
|
|
6
|
+
* Sizes match the page layouts already used across the app:
|
|
7
|
+
* - sm: max-w-3xl (768px) — narrow content pages (FAQ, settings)
|
|
8
|
+
* - default: max-w-5xl (1024px) — design system docs
|
|
9
|
+
* - lg: max-w-7xl (1280px) — dashboards + game lists
|
|
10
|
+
* - full: max-w-none — edge-to-edge
|
|
11
|
+
*/
|
|
12
|
+
const SIZE_MAP = {
|
|
13
|
+
sm: "max-w-3xl",
|
|
14
|
+
default: "max-w-5xl",
|
|
15
|
+
lg: "max-w-7xl",
|
|
16
|
+
full: "max-w-none",
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export interface ContainerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
20
|
+
size?: keyof typeof SIZE_MAP;
|
|
21
|
+
/** Drop the horizontal padding (rare — only when nested). */
|
|
22
|
+
noPadding?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Container = React.forwardRef<HTMLDivElement, ContainerProps>(
|
|
26
|
+
({ className, size = "default", noPadding, ...props }, ref) => (
|
|
27
|
+
<div
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cn(
|
|
30
|
+
"mx-auto w-full",
|
|
31
|
+
SIZE_MAP[size],
|
|
32
|
+
!noPadding && "px-6 lg:px-8",
|
|
33
|
+
className
|
|
34
|
+
)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
38
|
+
);
|
|
39
|
+
Container.displayName = "Container";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { CalendarDays } from "lucide-react";
|
|
5
|
+
import { Calendar } from "./calendar";
|
|
6
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
|
7
|
+
import { cn } from "../lib/utils";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* DatePicker — single-date selection in a popover. Closes on pick.
|
|
11
|
+
*
|
|
12
|
+
* <DatePicker
|
|
13
|
+
* value={gameDate}
|
|
14
|
+
* onValueChange={setGameDate}
|
|
15
|
+
* aria-label="Game date"
|
|
16
|
+
* />
|
|
17
|
+
*
|
|
18
|
+
* Time-zone note (§3.16): the picked Date is midnight LOCAL time —
|
|
19
|
+
* convert against the venue timezone before persisting.
|
|
20
|
+
* For ranges or inline calendars, compose Calendar + Popover directly.
|
|
21
|
+
*/
|
|
22
|
+
export interface DatePickerProps {
|
|
23
|
+
value: Date | undefined;
|
|
24
|
+
onValueChange: (date: Date | undefined) => void;
|
|
25
|
+
/** Accessible name for the trigger + dialog. */
|
|
26
|
+
"aria-label": string;
|
|
27
|
+
/** Trigger text when nothing is selected. */
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
/** Intl date format options (default: weekday, month, day). */
|
|
30
|
+
formatOptions?: Intl.DateTimeFormatOptions;
|
|
31
|
+
/** Dates the calendar should refuse (react-day-picker Matcher). */
|
|
32
|
+
disabledDates?: React.ComponentProps<typeof Calendar>["disabled"];
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function DatePicker({
|
|
38
|
+
value,
|
|
39
|
+
onValueChange,
|
|
40
|
+
"aria-label": ariaLabel,
|
|
41
|
+
placeholder = "Pick a date…",
|
|
42
|
+
formatOptions = { weekday: "short", month: "short", day: "numeric" },
|
|
43
|
+
disabledDates,
|
|
44
|
+
disabled,
|
|
45
|
+
className,
|
|
46
|
+
}: DatePickerProps) {
|
|
47
|
+
const [open, setOpen] = React.useState(false);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
51
|
+
<PopoverTrigger asChild>
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
aria-label={ariaLabel}
|
|
55
|
+
disabled={disabled}
|
|
56
|
+
className={cn(
|
|
57
|
+
"flex h-8 pointer-coarse:h-11 w-full items-center gap-2 rounded-sm border border-border-strong bg-background px-2.5 text-sm",
|
|
58
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
59
|
+
"disabled:cursor-not-allowed disabled:bg-bg-muted disabled:opacity-60",
|
|
60
|
+
!value && "text-text-subtle",
|
|
61
|
+
className
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
<CalendarDays
|
|
65
|
+
className="h-3.5 w-3.5 shrink-0 text-text-muted"
|
|
66
|
+
strokeWidth={1.75}
|
|
67
|
+
aria-hidden="true"
|
|
68
|
+
/>
|
|
69
|
+
<span className="truncate tabular">
|
|
70
|
+
{value
|
|
71
|
+
? new Intl.DateTimeFormat(undefined, formatOptions).format(value)
|
|
72
|
+
: placeholder}
|
|
73
|
+
</span>
|
|
74
|
+
</button>
|
|
75
|
+
</PopoverTrigger>
|
|
76
|
+
<PopoverContent
|
|
77
|
+
aria-label={ariaLabel}
|
|
78
|
+
align="start"
|
|
79
|
+
className="w-auto p-0"
|
|
80
|
+
>
|
|
81
|
+
<Calendar
|
|
82
|
+
mode="single"
|
|
83
|
+
selected={value}
|
|
84
|
+
onSelect={(d) => {
|
|
85
|
+
onValueChange(d);
|
|
86
|
+
setOpen(false);
|
|
87
|
+
}}
|
|
88
|
+
disabled={disabledDates}
|
|
89
|
+
// react-day-picker's focus-management prop (not DOM autofocus):
|
|
90
|
+
// moving focus into the grid when the popover opens is the
|
|
91
|
+
// expected dialog pattern for keyboard users.
|
|
92
|
+
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
93
|
+
autoFocus
|
|
94
|
+
/>
|
|
95
|
+
</PopoverContent>
|
|
96
|
+
</Popover>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
5
|
+
import { X } from "lucide-react";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Dialog — per design.md §10
|
|
10
|
+
* - Max width 480px default, override via className
|
|
11
|
+
* - 6px radius, 1px strong border, shadow-md
|
|
12
|
+
* - Backdrop: rgba(0,0,0,0.4) with 4px blur
|
|
13
|
+
* - Header text-md semibold, body text-sm
|
|
14
|
+
* - Actions right-aligned; accent CTA on the right, secondary on left
|
|
15
|
+
*
|
|
16
|
+
* Accessibility (provided by Radix):
|
|
17
|
+
* - Focus trap inside dialog
|
|
18
|
+
* - Escape closes
|
|
19
|
+
* - aria-labelledby / aria-describedby auto-wired via Title / Description
|
|
20
|
+
* - Returns focus to trigger on close
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const Dialog = DialogPrimitive.Root;
|
|
24
|
+
export const DialogTrigger = DialogPrimitive.Trigger;
|
|
25
|
+
export const DialogPortal = DialogPrimitive.Portal;
|
|
26
|
+
export const DialogClose = DialogPrimitive.Close;
|
|
27
|
+
|
|
28
|
+
export const DialogOverlay = React.forwardRef<
|
|
29
|
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
30
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
31
|
+
>(({ className, ...props }, ref) => (
|
|
32
|
+
<DialogPrimitive.Overlay
|
|
33
|
+
ref={ref}
|
|
34
|
+
className={cn(
|
|
35
|
+
"fixed inset-0 z-50 bg-black/40 backdrop-blur-[4px]",
|
|
36
|
+
"data-[state=open]:animate-fade-in",
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
));
|
|
42
|
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|
43
|
+
|
|
44
|
+
export const DialogContent = React.forwardRef<
|
|
45
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
46
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
47
|
+
>(({ className, children, ...props }, ref) => (
|
|
48
|
+
<DialogPortal>
|
|
49
|
+
<DialogOverlay />
|
|
50
|
+
{/* Centering wrapper — uses Flexbox instead of the classic
|
|
51
|
+
`left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2`
|
|
52
|
+
trick. The translate-based approach is fragile because the
|
|
53
|
+
CSS `transform` property is single-valued: any animation or
|
|
54
|
+
Radix data-state class that touches `transform` would
|
|
55
|
+
override the centering and shove the dialog off-screen,
|
|
56
|
+
which is exactly the bug that previously made dialogs
|
|
57
|
+
appear as a dimmed-blurred page with no visible content.
|
|
58
|
+
Flexbox centering has no such conflict. Below sm we leave
|
|
59
|
+
the wrapper as a plain box so the inner Content can be a
|
|
60
|
+
full-screen sheet via `inset-0`. */}
|
|
61
|
+
<div className="fixed inset-0 z-50 flex sm:items-center sm:justify-center pointer-events-none">
|
|
62
|
+
<DialogPrimitive.Content
|
|
63
|
+
ref={ref}
|
|
64
|
+
className={cn(
|
|
65
|
+
// Mobile (<sm) — full-screen sheet. Phones don't have the
|
|
66
|
+
// height for a centered modal once a real form is in it;
|
|
67
|
+
// the keyboard would push the footer below the fold.
|
|
68
|
+
// Pointer events restored so the sheet is interactive
|
|
69
|
+
// even though its wrapper is pointer-events-none.
|
|
70
|
+
"pointer-events-auto bg-background outline-none flex flex-col",
|
|
71
|
+
"absolute inset-0",
|
|
72
|
+
// sm+ — centered modal. The flex wrapper handles
|
|
73
|
+
// centering; we just size + style the box. max-h prevents
|
|
74
|
+
// tall forms spilling past the viewport; DialogBody
|
|
75
|
+
// handles internal scroll.
|
|
76
|
+
"sm:static sm:w-full sm:max-w-[480px] sm:max-h-[85vh] sm:h-auto",
|
|
77
|
+
"sm:rounded-md sm:border sm:border-border-strong sm:shadow-md",
|
|
78
|
+
"data-[state=open]:animate-fade-in-up",
|
|
79
|
+
className
|
|
80
|
+
)}
|
|
81
|
+
{...props}
|
|
82
|
+
>
|
|
83
|
+
{children}
|
|
84
|
+
<DialogPrimitive.Close
|
|
85
|
+
className={cn(
|
|
86
|
+
"absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center rounded-sm text-text-muted",
|
|
87
|
+
"transition-colors hover:bg-bg-subtle hover:text-foreground",
|
|
88
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
89
|
+
// 44px hit target on touch devices; visual stays 24px.
|
|
90
|
+
"pointer-coarse:after:absolute pointer-coarse:after:-inset-2.5"
|
|
91
|
+
)}
|
|
92
|
+
aria-label="Close dialog"
|
|
93
|
+
>
|
|
94
|
+
<X className="h-4 w-4" strokeWidth={1.75} />
|
|
95
|
+
</DialogPrimitive.Close>
|
|
96
|
+
</DialogPrimitive.Content>
|
|
97
|
+
</div>
|
|
98
|
+
</DialogPortal>
|
|
99
|
+
));
|
|
100
|
+
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
|
101
|
+
|
|
102
|
+
export function DialogHeader({
|
|
103
|
+
className,
|
|
104
|
+
...props
|
|
105
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
106
|
+
return (
|
|
107
|
+
<div
|
|
108
|
+
className={cn("px-5 pt-4 pb-3 border-b border-border", className)}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const DialogTitle = React.forwardRef<
|
|
115
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
116
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
117
|
+
>(({ className, ...props }, ref) => (
|
|
118
|
+
<DialogPrimitive.Title
|
|
119
|
+
ref={ref}
|
|
120
|
+
className={cn(
|
|
121
|
+
"text-md font-semibold tracking-[-0.01em] text-foreground",
|
|
122
|
+
className
|
|
123
|
+
)}
|
|
124
|
+
{...props}
|
|
125
|
+
/>
|
|
126
|
+
));
|
|
127
|
+
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
|
128
|
+
|
|
129
|
+
export const DialogDescription = React.forwardRef<
|
|
130
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
131
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
132
|
+
>(({ className, ...props }, ref) => (
|
|
133
|
+
<DialogPrimitive.Description
|
|
134
|
+
ref={ref}
|
|
135
|
+
className={cn("text-xs text-text-muted mt-0.5", className)}
|
|
136
|
+
{...props}
|
|
137
|
+
/>
|
|
138
|
+
));
|
|
139
|
+
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
|
140
|
+
|
|
141
|
+
export function DialogBody({
|
|
142
|
+
className,
|
|
143
|
+
...props
|
|
144
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
145
|
+
// flex-1 + overflow-y-auto so the body absorbs the dialog's
|
|
146
|
+
// available height and scrolls internally — keeps the footer
|
|
147
|
+
// (Cancel / Save) pinned to the bottom of the viewport on phones
|
|
148
|
+
// and below the max-h cap on desktop. min-h-0 is required for
|
|
149
|
+
// flex children to scroll inside a flex column parent.
|
|
150
|
+
return (
|
|
151
|
+
<div
|
|
152
|
+
className={cn(
|
|
153
|
+
"px-5 py-4 text-sm text-foreground flex-1 min-h-0 overflow-y-auto",
|
|
154
|
+
className
|
|
155
|
+
)}
|
|
156
|
+
{...props}
|
|
157
|
+
/>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function DialogFooter({
|
|
162
|
+
className,
|
|
163
|
+
...props
|
|
164
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
165
|
+
return (
|
|
166
|
+
<div
|
|
167
|
+
className={cn(
|
|
168
|
+
"flex items-center justify-end gap-2 px-5 py-3 border-t border-border bg-bg-subtle/50",
|
|
169
|
+
className
|
|
170
|
+
)}
|
|
171
|
+
{...props}
|
|
172
|
+
/>
|
|
173
|
+
);
|
|
174
|
+
}
|