@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,83 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ArrowDown, ArrowUp, Minus } from "lucide-react";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MetricTile — per design.md §10
|
|
7
|
+
*
|
|
8
|
+
* Three lines exactly: label / value / delta.
|
|
9
|
+
* - label: text-xs uppercase muted
|
|
10
|
+
* - value: text-2xl semibold tabular
|
|
11
|
+
* - delta: text-xs forest (up) / danger (down) / muted (flat)
|
|
12
|
+
*
|
|
13
|
+
* Use 3–4 across in dashboard rows.
|
|
14
|
+
*/
|
|
15
|
+
export interface MetricTileProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
16
|
+
label: string;
|
|
17
|
+
value: React.ReactNode;
|
|
18
|
+
/** Numeric delta vs prior period. Sign and direction are derived. */
|
|
19
|
+
delta?: number;
|
|
20
|
+
/** Human-readable suffix appended to the delta, e.g. "vs last week" */
|
|
21
|
+
deltaLabel?: string;
|
|
22
|
+
/** Override the delta direction (e.g. when "up" is bad like refunds) */
|
|
23
|
+
invertDelta?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function MetricTile({
|
|
27
|
+
label,
|
|
28
|
+
value,
|
|
29
|
+
delta,
|
|
30
|
+
deltaLabel = "vs last week",
|
|
31
|
+
invertDelta = false,
|
|
32
|
+
className,
|
|
33
|
+
...props
|
|
34
|
+
}: MetricTileProps) {
|
|
35
|
+
const direction: "up" | "down" | "flat" =
|
|
36
|
+
delta === undefined || delta === 0 ? "flat" : delta > 0 ? "up" : "down";
|
|
37
|
+
|
|
38
|
+
const positive =
|
|
39
|
+
direction === "up"
|
|
40
|
+
? !invertDelta
|
|
41
|
+
: direction === "down"
|
|
42
|
+
? invertDelta
|
|
43
|
+
: null;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
className={cn(
|
|
48
|
+
"rounded border border-border bg-background p-4",
|
|
49
|
+
className
|
|
50
|
+
)}
|
|
51
|
+
{...props}
|
|
52
|
+
>
|
|
53
|
+
<div className="text-xs font-medium uppercase tracking-[0.04em] text-text-muted">
|
|
54
|
+
{label}
|
|
55
|
+
</div>
|
|
56
|
+
<div className="tabular mt-1 text-2xl font-semibold tracking-[-0.01em] text-foreground">
|
|
57
|
+
{value}
|
|
58
|
+
</div>
|
|
59
|
+
{delta !== undefined && (
|
|
60
|
+
<div
|
|
61
|
+
className={cn(
|
|
62
|
+
"mt-1 inline-flex items-center gap-1 text-xs",
|
|
63
|
+
positive === null
|
|
64
|
+
? "text-text-muted"
|
|
65
|
+
: positive
|
|
66
|
+
? "text-brand-forest"
|
|
67
|
+
: "text-danger"
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
{direction === "up" ? (
|
|
71
|
+
<ArrowUp className="h-3 w-3" strokeWidth={2} />
|
|
72
|
+
) : direction === "down" ? (
|
|
73
|
+
<ArrowDown className="h-3 w-3" strokeWidth={2} />
|
|
74
|
+
) : (
|
|
75
|
+
<Minus className="h-3 w-3" strokeWidth={2} />
|
|
76
|
+
)}
|
|
77
|
+
<span className="tabular">{Math.abs(delta)}%</span>
|
|
78
|
+
<span className="text-text-muted">{deltaLabel}</span>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { cn } from "../lib/utils";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MoneyBreakdown — implements design.md §3.15 Money UX.
|
|
5
|
+
*
|
|
6
|
+
* Always shown before any charge. Every line is named, signed, and
|
|
7
|
+
* tabular-aligned. The receipt component should render the same lines
|
|
8
|
+
* in the same order.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* <MoneyBreakdown
|
|
12
|
+
* lines={[
|
|
13
|
+
* { label: "Game fee", cents: 1400 },
|
|
14
|
+
* { label: "Credit applied", cents: -600, muted: true },
|
|
15
|
+
* { label: "Sales tax", cents: 70 },
|
|
16
|
+
* ]}
|
|
17
|
+
* totalLabel="Charged to card"
|
|
18
|
+
* totalCents={870}
|
|
19
|
+
* />
|
|
20
|
+
*/
|
|
21
|
+
export interface MoneyLine {
|
|
22
|
+
label: string;
|
|
23
|
+
cents: number;
|
|
24
|
+
/** Render in muted text (for credit applied, taxes, etc.). */
|
|
25
|
+
muted?: boolean;
|
|
26
|
+
/** Optional hint shown under the label. */
|
|
27
|
+
hint?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MoneyBreakdownProps {
|
|
31
|
+
lines: MoneyLine[];
|
|
32
|
+
totalLabel?: string;
|
|
33
|
+
totalCents: number;
|
|
34
|
+
/** Currency code; default USD. */
|
|
35
|
+
currency?: string;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fmt(cents: number, currency = "USD") {
|
|
40
|
+
return new Intl.NumberFormat("en-US", {
|
|
41
|
+
style: "currency",
|
|
42
|
+
currency,
|
|
43
|
+
}).format(cents / 100);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function MoneyBreakdown({
|
|
47
|
+
lines,
|
|
48
|
+
totalLabel = "Total",
|
|
49
|
+
totalCents,
|
|
50
|
+
currency = "USD",
|
|
51
|
+
className,
|
|
52
|
+
}: MoneyBreakdownProps) {
|
|
53
|
+
return (
|
|
54
|
+
<dl className={cn("rounded border border-border bg-background", className)}>
|
|
55
|
+
{lines.map((line, i) => (
|
|
56
|
+
<div
|
|
57
|
+
key={i}
|
|
58
|
+
className="flex items-start justify-between gap-4 px-4 py-2.5 border-b border-border last:border-b-0"
|
|
59
|
+
>
|
|
60
|
+
<dt
|
|
61
|
+
className={cn(
|
|
62
|
+
"text-sm",
|
|
63
|
+
line.muted ? "text-text-muted" : "text-foreground"
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
{line.label}
|
|
67
|
+
{line.hint && (
|
|
68
|
+
<span className="block text-xs text-text-subtle mt-0.5">
|
|
69
|
+
{line.hint}
|
|
70
|
+
</span>
|
|
71
|
+
)}
|
|
72
|
+
</dt>
|
|
73
|
+
<dd
|
|
74
|
+
className={cn(
|
|
75
|
+
"tabular text-sm",
|
|
76
|
+
line.muted ? "text-text-muted" : "text-foreground"
|
|
77
|
+
)}
|
|
78
|
+
>
|
|
79
|
+
{line.cents < 0 ? "−" : ""}
|
|
80
|
+
{fmt(Math.abs(line.cents), currency)}
|
|
81
|
+
</dd>
|
|
82
|
+
</div>
|
|
83
|
+
))}
|
|
84
|
+
<div className="flex items-center justify-between gap-4 px-4 py-3 bg-bg-subtle border-t border-border">
|
|
85
|
+
<dt className="text-sm font-semibold">{totalLabel}</dt>
|
|
86
|
+
<dd className="tabular text-md font-semibold">
|
|
87
|
+
{fmt(totalCents, currency)}
|
|
88
|
+
</dd>
|
|
89
|
+
</div>
|
|
90
|
+
</dl>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Minus, Plus } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* NumberInput — stepper-style numeric input with explicit +/− buttons.
|
|
9
|
+
* Use for: capacity, min-to-run, price (in cents or units), quantity.
|
|
10
|
+
*
|
|
11
|
+
* Decimals: pass `step={0.5}` etc. Always set min/max for safe bounds.
|
|
12
|
+
*/
|
|
13
|
+
export interface NumberInputProps extends Omit<
|
|
14
|
+
React.InputHTMLAttributes<HTMLInputElement>,
|
|
15
|
+
"onChange" | "value" | "type"
|
|
16
|
+
> {
|
|
17
|
+
value?: number;
|
|
18
|
+
onChange?: (value: number) => void;
|
|
19
|
+
min?: number;
|
|
20
|
+
max?: number;
|
|
21
|
+
step?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
|
|
25
|
+
(
|
|
26
|
+
{
|
|
27
|
+
value,
|
|
28
|
+
onChange,
|
|
29
|
+
min,
|
|
30
|
+
max,
|
|
31
|
+
step = 1,
|
|
32
|
+
className,
|
|
33
|
+
disabled,
|
|
34
|
+
"aria-label": ariaLabel,
|
|
35
|
+
"aria-describedby": ariaDescribedBy,
|
|
36
|
+
"aria-invalid": ariaInvalid,
|
|
37
|
+
id,
|
|
38
|
+
...rest
|
|
39
|
+
},
|
|
40
|
+
ref
|
|
41
|
+
) => {
|
|
42
|
+
const clamp = (v: number) => {
|
|
43
|
+
let next = v;
|
|
44
|
+
if (min !== undefined && next < min) next = min;
|
|
45
|
+
if (max !== undefined && next > max) next = max;
|
|
46
|
+
return next;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const handleChange = (next: number) => {
|
|
50
|
+
onChange?.(clamp(next));
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const cur = value ?? 0;
|
|
54
|
+
const decDisabled = disabled || (min !== undefined && cur <= min);
|
|
55
|
+
const incDisabled = disabled || (max !== undefined && cur >= max);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
className={cn(
|
|
60
|
+
"inline-flex h-8 items-stretch rounded-sm border border-border-strong overflow-hidden",
|
|
61
|
+
// 44px control height + stepper width on touch devices.
|
|
62
|
+
"pointer-coarse:h-11",
|
|
63
|
+
"aria-[invalid=true]:border-danger",
|
|
64
|
+
disabled && "opacity-50 pointer-events-none",
|
|
65
|
+
className
|
|
66
|
+
)}
|
|
67
|
+
aria-invalid={ariaInvalid}
|
|
68
|
+
>
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
aria-label="Decrease"
|
|
72
|
+
onClick={() => handleChange(cur - step)}
|
|
73
|
+
disabled={decDisabled}
|
|
74
|
+
className="w-7 pointer-coarse:w-11 grid place-items-center text-text-muted hover:bg-bg-subtle hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed border-r border-border-strong"
|
|
75
|
+
>
|
|
76
|
+
<Minus className="h-3.5 w-3.5" strokeWidth={1.75} />
|
|
77
|
+
</button>
|
|
78
|
+
<input
|
|
79
|
+
ref={ref}
|
|
80
|
+
id={id}
|
|
81
|
+
type="number"
|
|
82
|
+
inputMode="numeric"
|
|
83
|
+
value={value ?? ""}
|
|
84
|
+
onChange={(e) => {
|
|
85
|
+
const n = e.target.value === "" ? NaN : Number(e.target.value);
|
|
86
|
+
if (!Number.isNaN(n)) handleChange(n);
|
|
87
|
+
}}
|
|
88
|
+
min={min}
|
|
89
|
+
max={max}
|
|
90
|
+
step={step}
|
|
91
|
+
aria-label={ariaLabel}
|
|
92
|
+
aria-describedby={ariaDescribedBy}
|
|
93
|
+
className={cn(
|
|
94
|
+
"w-16 text-center text-sm font-medium tabular bg-background text-foreground",
|
|
95
|
+
"focus:outline-none focus-visible:bg-bg-subtle",
|
|
96
|
+
"[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
97
|
+
)}
|
|
98
|
+
{...rest}
|
|
99
|
+
/>
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
aria-label="Increase"
|
|
103
|
+
onClick={() => handleChange(cur + step)}
|
|
104
|
+
disabled={incDisabled}
|
|
105
|
+
className="w-7 pointer-coarse:w-11 grid place-items-center text-text-muted hover:bg-bg-subtle hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed border-l border-border-strong"
|
|
106
|
+
>
|
|
107
|
+
<Plus className="h-3.5 w-3.5" strokeWidth={1.75} />
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
NumberInput.displayName = "NumberInput";
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pagination — page navigation for tables and lists.
|
|
9
|
+
*
|
|
10
|
+
* <Pagination
|
|
11
|
+
* currentPage={page}
|
|
12
|
+
* totalPages={totalPages}
|
|
13
|
+
* onPageChange={setPage}
|
|
14
|
+
* />
|
|
15
|
+
*
|
|
16
|
+
* Renders previous / page numbers (with ellipses for large counts) / next.
|
|
17
|
+
* Use only when total > 1; for <= 10 pages no ellipses are inserted.
|
|
18
|
+
*
|
|
19
|
+
* For URL-driven pagination (per design.md §3.9), pass an onPageChange
|
|
20
|
+
* that calls router.replace(?page=N).
|
|
21
|
+
*/
|
|
22
|
+
export interface PaginationProps extends React.HTMLAttributes<HTMLElement> {
|
|
23
|
+
currentPage: number;
|
|
24
|
+
totalPages: number;
|
|
25
|
+
onPageChange: (page: number) => void;
|
|
26
|
+
/** Number of pages to show on each side of the current page. */
|
|
27
|
+
siblingCount?: number;
|
|
28
|
+
/** Disable interaction (e.g., while fetching). */
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const Pagination = React.forwardRef<HTMLElement, PaginationProps>(
|
|
33
|
+
(
|
|
34
|
+
{
|
|
35
|
+
currentPage,
|
|
36
|
+
totalPages,
|
|
37
|
+
onPageChange,
|
|
38
|
+
siblingCount = 1,
|
|
39
|
+
disabled,
|
|
40
|
+
className,
|
|
41
|
+
...rest
|
|
42
|
+
},
|
|
43
|
+
ref
|
|
44
|
+
) => {
|
|
45
|
+
if (totalPages <= 1) return null;
|
|
46
|
+
|
|
47
|
+
const pages = buildPageList(currentPage, totalPages, siblingCount);
|
|
48
|
+
|
|
49
|
+
const go = (p: number) => {
|
|
50
|
+
if (disabled) return;
|
|
51
|
+
const clamped = Math.max(1, Math.min(totalPages, p));
|
|
52
|
+
if (clamped !== currentPage) onPageChange(clamped);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<nav
|
|
57
|
+
ref={ref}
|
|
58
|
+
aria-label="Pagination"
|
|
59
|
+
className={cn("flex items-center gap-1", className)}
|
|
60
|
+
{...rest}
|
|
61
|
+
>
|
|
62
|
+
<PageButton
|
|
63
|
+
aria-label="Previous page"
|
|
64
|
+
onClick={() => go(currentPage - 1)}
|
|
65
|
+
disabled={disabled || currentPage <= 1}
|
|
66
|
+
>
|
|
67
|
+
<ChevronLeft className="h-3.5 w-3.5" strokeWidth={1.75} />
|
|
68
|
+
</PageButton>
|
|
69
|
+
|
|
70
|
+
{pages.map((p, i) =>
|
|
71
|
+
p === "…" ? (
|
|
72
|
+
<span
|
|
73
|
+
key={`ellipsis-${i}`}
|
|
74
|
+
role="presentation"
|
|
75
|
+
aria-hidden="true"
|
|
76
|
+
className="h-7 w-7 grid place-items-center text-text-subtle"
|
|
77
|
+
>
|
|
78
|
+
<MoreHorizontal className="h-3 w-3" strokeWidth={1.75} />
|
|
79
|
+
</span>
|
|
80
|
+
) : (
|
|
81
|
+
<PageButton
|
|
82
|
+
key={p}
|
|
83
|
+
aria-label={`Page ${p}`}
|
|
84
|
+
aria-current={p === currentPage ? "page" : undefined}
|
|
85
|
+
active={p === currentPage}
|
|
86
|
+
onClick={() => go(p)}
|
|
87
|
+
disabled={disabled}
|
|
88
|
+
>
|
|
89
|
+
{p}
|
|
90
|
+
</PageButton>
|
|
91
|
+
)
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
<PageButton
|
|
95
|
+
aria-label="Next page"
|
|
96
|
+
onClick={() => go(currentPage + 1)}
|
|
97
|
+
disabled={disabled || currentPage >= totalPages}
|
|
98
|
+
>
|
|
99
|
+
<ChevronRight className="h-3.5 w-3.5" strokeWidth={1.75} />
|
|
100
|
+
</PageButton>
|
|
101
|
+
</nav>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
Pagination.displayName = "Pagination";
|
|
106
|
+
|
|
107
|
+
function PageButton({
|
|
108
|
+
active,
|
|
109
|
+
className,
|
|
110
|
+
...props
|
|
111
|
+
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { active?: boolean }) {
|
|
112
|
+
return (
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
className={cn(
|
|
116
|
+
"min-w-7 h-7 px-2 rounded-sm text-xs font-medium tabular transition-colors",
|
|
117
|
+
// 44px targets on touch devices; dense 28px on desktop.
|
|
118
|
+
"pointer-coarse:min-w-11 pointer-coarse:h-11",
|
|
119
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
120
|
+
active
|
|
121
|
+
? "bg-brand-forest text-brand-forest-foreground"
|
|
122
|
+
: "text-text-muted hover:bg-bg-subtle hover:text-foreground",
|
|
123
|
+
"disabled:opacity-40 disabled:pointer-events-none",
|
|
124
|
+
className
|
|
125
|
+
)}
|
|
126
|
+
{...props}
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildPageList(
|
|
132
|
+
current: number,
|
|
133
|
+
total: number,
|
|
134
|
+
siblings: number
|
|
135
|
+
): Array<number | "…"> {
|
|
136
|
+
// Always include first + last; show ellipsis when there's a gap.
|
|
137
|
+
const range = (a: number, b: number): number[] => {
|
|
138
|
+
const out: number[] = [];
|
|
139
|
+
for (let i = a; i <= b; i++) out.push(i);
|
|
140
|
+
return out;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const totalShown = siblings * 2 + 5; // first + last + current + 2 ellipses
|
|
144
|
+
if (total <= totalShown) return range(1, total);
|
|
145
|
+
|
|
146
|
+
const leftBound = Math.max(2, current - siblings);
|
|
147
|
+
const rightBound = Math.min(total - 1, current + siblings);
|
|
148
|
+
const showLeftDots = leftBound > 2;
|
|
149
|
+
const showRightDots = rightBound < total - 1;
|
|
150
|
+
|
|
151
|
+
const list: Array<number | "…"> = [1];
|
|
152
|
+
if (showLeftDots) list.push("…");
|
|
153
|
+
for (const p of range(leftBound, rightBound)) list.push(p);
|
|
154
|
+
if (showRightDots) list.push("…");
|
|
155
|
+
list.push(total);
|
|
156
|
+
return list;
|
|
157
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Popover — generic click-to-open floating panel.
|
|
9
|
+
*
|
|
10
|
+
* Use for:
|
|
11
|
+
* - Date pickers
|
|
12
|
+
* - Filter mini-menus
|
|
13
|
+
* - "Quick edit" inline form panels
|
|
14
|
+
* - Help text that's too long for a Tooltip
|
|
15
|
+
*
|
|
16
|
+
* Tooltip is hover-only and small; Popover is click + can hold any
|
|
17
|
+
* content (form, list, mini-chart). DropdownMenu is for action menus —
|
|
18
|
+
* if your popover content is "items you click," use DropdownMenu instead.
|
|
19
|
+
*
|
|
20
|
+
* Accessibility: PopoverContent renders with role="dialog", which needs
|
|
21
|
+
* an accessible name — always pass `aria-label` (or `aria-labelledby`
|
|
22
|
+
* pointing at a heading inside). The a11y test suite enforces this shape.
|
|
23
|
+
*/
|
|
24
|
+
export const Popover = PopoverPrimitive.Root;
|
|
25
|
+
export const PopoverTrigger = PopoverPrimitive.Trigger;
|
|
26
|
+
export const PopoverAnchor = PopoverPrimitive.Anchor;
|
|
27
|
+
|
|
28
|
+
export const PopoverContent = React.forwardRef<
|
|
29
|
+
React.ElementRef<typeof PopoverPrimitive.Content>,
|
|
30
|
+
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
|
31
|
+
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
|
32
|
+
<PopoverPrimitive.Portal>
|
|
33
|
+
<PopoverPrimitive.Content
|
|
34
|
+
ref={ref}
|
|
35
|
+
align={align}
|
|
36
|
+
sideOffset={sideOffset}
|
|
37
|
+
className={cn(
|
|
38
|
+
"z-50 w-72 rounded border border-border-strong bg-background p-3 text-sm text-foreground shadow-md outline-none",
|
|
39
|
+
"data-[state=open]:animate-fade-in",
|
|
40
|
+
className
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
</PopoverPrimitive.Portal>
|
|
45
|
+
));
|
|
46
|
+
PopoverContent.displayName = "PopoverContent";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Progress — for multi-step flows (waiver → emergency contact → done),
|
|
9
|
+
* upload bars, anything with a known percent completion.
|
|
10
|
+
*
|
|
11
|
+
* If you don't know the percent (indeterminate), use Skeleton or Spinner.
|
|
12
|
+
*
|
|
13
|
+
* For "spots filled" use CapacityBar — same shape but with a min-to-run
|
|
14
|
+
* warning color and tabular count alongside.
|
|
15
|
+
*/
|
|
16
|
+
export const Progress = React.forwardRef<
|
|
17
|
+
React.ElementRef<typeof ProgressPrimitive.Root>,
|
|
18
|
+
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
|
19
|
+
>(({ className, value, ...props }, ref) => (
|
|
20
|
+
<ProgressPrimitive.Root
|
|
21
|
+
ref={ref}
|
|
22
|
+
className={cn(
|
|
23
|
+
"relative h-1.5 w-full overflow-hidden rounded-full bg-bg-muted",
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
>
|
|
28
|
+
<ProgressPrimitive.Indicator
|
|
29
|
+
className="h-full w-full flex-1 bg-brand-forest transition-transform duration-150"
|
|
30
|
+
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
|
31
|
+
/>
|
|
32
|
+
</ProgressPrimitive.Root>
|
|
33
|
+
));
|
|
34
|
+
Progress.displayName = "Progress";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* RadioGroup + RadioGroupItem — Radix-backed, keyboard-navigable
|
|
9
|
+
* (Arrow / Home / End). Always wrap items in a <RadioGroup> with a
|
|
10
|
+
* matching set of <Label htmlFor=> wiring.
|
|
11
|
+
*/
|
|
12
|
+
export const RadioGroup = React.forwardRef<
|
|
13
|
+
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
|
14
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
|
15
|
+
>(({ className, ...props }, ref) => (
|
|
16
|
+
<RadioGroupPrimitive.Root
|
|
17
|
+
ref={ref}
|
|
18
|
+
className={cn("grid gap-2", className)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
));
|
|
22
|
+
RadioGroup.displayName = "RadioGroup";
|
|
23
|
+
|
|
24
|
+
export const RadioGroupItem = React.forwardRef<
|
|
25
|
+
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
|
26
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
|
27
|
+
>(({ className, ...props }, ref) => (
|
|
28
|
+
<RadioGroupPrimitive.Item
|
|
29
|
+
ref={ref}
|
|
30
|
+
className={cn(
|
|
31
|
+
"aspect-square h-4 w-4 rounded-full border border-border-strong bg-background transition-colors",
|
|
32
|
+
// Invisible hit-area extension: 24px desktop / 44px touch.
|
|
33
|
+
"relative after:absolute after:-inset-1 pointer-coarse:after:-inset-3.5",
|
|
34
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
35
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
36
|
+
"data-[state=checked]:border-brand-forest",
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
|
42
|
+
<span className="h-2 w-2 rounded-full bg-brand-forest" />
|
|
43
|
+
</RadioGroupPrimitive.Indicator>
|
|
44
|
+
</RadioGroupPrimitive.Item>
|
|
45
|
+
));
|
|
46
|
+
RadioGroupItem.displayName = "RadioGroupItem";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ScrollArea — styled, consistent custom scrollbars across browsers.
|
|
9
|
+
* Wraps content that needs to scroll within a bounded region.
|
|
10
|
+
*
|
|
11
|
+
* <ScrollArea className="h-72 w-48 rounded border">
|
|
12
|
+
* <div className="p-4">… long content …</div>
|
|
13
|
+
* </ScrollArea>
|
|
14
|
+
*
|
|
15
|
+
* For full-page scroll, use the browser default — don't wrap <body>.
|
|
16
|
+
*/
|
|
17
|
+
export const ScrollArea = React.forwardRef<
|
|
18
|
+
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
|
19
|
+
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
|
20
|
+
>(({ className, children, ...props }, ref) => (
|
|
21
|
+
<ScrollAreaPrimitive.Root
|
|
22
|
+
ref={ref}
|
|
23
|
+
className={cn("relative overflow-hidden", className)}
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
|
27
|
+
{children}
|
|
28
|
+
</ScrollAreaPrimitive.Viewport>
|
|
29
|
+
<ScrollBar />
|
|
30
|
+
<ScrollAreaPrimitive.Corner />
|
|
31
|
+
</ScrollAreaPrimitive.Root>
|
|
32
|
+
));
|
|
33
|
+
ScrollArea.displayName = "ScrollArea";
|
|
34
|
+
|
|
35
|
+
export const ScrollBar = React.forwardRef<
|
|
36
|
+
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
|
37
|
+
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
38
|
+
>(({ className, orientation = "vertical", ...props }, ref) => (
|
|
39
|
+
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
40
|
+
ref={ref}
|
|
41
|
+
orientation={orientation}
|
|
42
|
+
className={cn(
|
|
43
|
+
"flex touch-none select-none transition-colors",
|
|
44
|
+
orientation === "vertical" &&
|
|
45
|
+
"h-full w-2 border-l border-l-transparent p-[1px]",
|
|
46
|
+
orientation === "horizontal" &&
|
|
47
|
+
"h-2 flex-col border-t border-t-transparent p-[1px]",
|
|
48
|
+
className
|
|
49
|
+
)}
|
|
50
|
+
{...props}
|
|
51
|
+
>
|
|
52
|
+
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border-strong" />
|
|
53
|
+
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
54
|
+
));
|
|
55
|
+
ScrollBar.displayName = "ScrollBar";
|