@olympusoss/canvas 2.6.19
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/package.json +179 -0
- package/src/components/atoms/README.md +11 -0
- package/src/components/atoms/aspect-ratio.tsx +32 -0
- package/src/components/atoms/avatar.tsx +98 -0
- package/src/components/atoms/badge.tsx +44 -0
- package/src/components/atoms/brand-mark.tsx +74 -0
- package/src/components/atoms/button.tsx +104 -0
- package/src/components/atoms/checkbox.tsx +63 -0
- package/src/components/atoms/flex-box.tsx +105 -0
- package/src/components/atoms/icon.tsx +34 -0
- package/src/components/atoms/input.tsx +91 -0
- package/src/components/atoms/label.tsx +41 -0
- package/src/components/atoms/logo.tsx +89 -0
- package/src/components/atoms/progress.tsx +55 -0
- package/src/components/atoms/radio-group.tsx +122 -0
- package/src/components/atoms/scroll-area.tsx +106 -0
- package/src/components/atoms/section.tsx +48 -0
- package/src/components/atoms/separator.tsx +45 -0
- package/src/components/atoms/skeleton.tsx +17 -0
- package/src/components/atoms/slider.tsx +93 -0
- package/src/components/atoms/switch.tsx +60 -0
- package/src/components/atoms/textarea.tsx +78 -0
- package/src/components/atoms/toggle.tsx +80 -0
- package/src/components/charts/activity-heatmap.tsx +96 -0
- package/src/components/charts/axes.tsx +21 -0
- package/src/components/charts/chart-container.tsx +195 -0
- package/src/components/charts/chart-legend.tsx +67 -0
- package/src/components/charts/chart-tooltip.tsx +161 -0
- package/src/components/charts/chart-types.tsx +49 -0
- package/src/components/charts/containers.tsx +11 -0
- package/src/components/charts/data.tsx +16 -0
- package/src/components/charts/details.tsx +25 -0
- package/src/components/charts/gauge.tsx +106 -0
- package/src/components/charts/grids.tsx +8 -0
- package/src/components/charts/index.ts +62 -0
- package/src/components/charts/labeled-bar-list.tsx +85 -0
- package/src/components/charts/references.tsx +8 -0
- package/src/components/charts/service-health-list.tsx +73 -0
- package/src/components/charts/sparkline.tsx +52 -0
- package/src/components/charts/stacked-bar.tsx +104 -0
- package/src/components/charts/text.tsx +10 -0
- package/src/components/charts/world-heat-map-inner.tsx +317 -0
- package/src/components/charts/world-heat-map.tsx +184 -0
- package/src/components/molecules/README.md +12 -0
- package/src/components/molecules/action-bar.tsx +73 -0
- package/src/components/molecules/activity-item.tsx +74 -0
- package/src/components/molecules/alert.tsx +80 -0
- package/src/components/molecules/animated-background.tsx +92 -0
- package/src/components/molecules/brand-lockup.tsx +48 -0
- package/src/components/molecules/breadcrumb.tsx +161 -0
- package/src/components/molecules/button-group.tsx +104 -0
- package/src/components/molecules/calendar.tsx +216 -0
- package/src/components/molecules/card.tsx +101 -0
- package/src/components/molecules/code-block.tsx +48 -0
- package/src/components/molecules/empty-state.tsx +55 -0
- package/src/components/molecules/error-state.tsx +42 -0
- package/src/components/molecules/field-display.tsx +35 -0
- package/src/components/molecules/input-otp.tsx +74 -0
- package/src/components/molecules/loading-state.tsx +36 -0
- package/src/components/molecules/notification-item.tsx +67 -0
- package/src/components/molecules/notification-list.tsx +45 -0
- package/src/components/molecules/number-badge.tsx +53 -0
- package/src/components/molecules/page-header.tsx +88 -0
- package/src/components/molecules/page-tabs.tsx +94 -0
- package/src/components/molecules/pagination.tsx +150 -0
- package/src/components/molecules/phone-input.tsx +200 -0
- package/src/components/molecules/search-bar.tsx +64 -0
- package/src/components/molecules/secret-field.tsx +158 -0
- package/src/components/molecules/section-card.tsx +91 -0
- package/src/components/molecules/stat-card.tsx +96 -0
- package/src/components/molecules/status-badge.tsx +42 -0
- package/src/components/molecules/stepper.tsx +96 -0
- package/src/components/molecules/table.tsx +157 -0
- package/src/components/molecules/toggle-group.tsx +145 -0
- package/src/components/molecules/tooltip.tsx +150 -0
- package/src/components/molecules/user-avatar-chip.tsx +71 -0
- package/src/components/organisms/README.md +14 -0
- package/src/components/organisms/accordion.tsx +149 -0
- package/src/components/organisms/alert-dialog.tsx +269 -0
- package/src/components/organisms/carousel.tsx +244 -0
- package/src/components/organisms/collapsible.tsx +69 -0
- package/src/components/organisms/command.tsx +143 -0
- package/src/components/organisms/context-menu.tsx +333 -0
- package/src/components/organisms/dashboard-grid.tsx +360 -0
- package/src/components/organisms/data-table.tsx +330 -0
- package/src/components/organisms/dialog.tsx +304 -0
- package/src/components/organisms/drawer.tsx +100 -0
- package/src/components/organisms/dropdown-menu.tsx +434 -0
- package/src/components/organisms/editors/code-editor.tsx +144 -0
- package/src/components/organisms/editors/index.ts +4 -0
- package/src/components/organisms/editors/markdown-editor.tsx +153 -0
- package/src/components/organisms/editors/markdown-renderer.ts +27 -0
- package/src/components/organisms/editors/prose-canvas-classes.ts +45 -0
- package/src/components/organisms/editors/rich-text-editor.tsx +126 -0
- package/src/components/organisms/editors/toolbar/md-toolbar.tsx +129 -0
- package/src/components/organisms/editors/toolbar/rte-toolbar.tsx +211 -0
- package/src/components/organisms/editors/toolbar/toolbar-shell.tsx +45 -0
- package/src/components/organisms/editors/use-codemirror-theme.ts +61 -0
- package/src/components/organisms/error-boundary.tsx +61 -0
- package/src/components/organisms/form.tsx +174 -0
- package/src/components/organisms/hover-card.tsx +114 -0
- package/src/components/organisms/menubar.tsx +491 -0
- package/src/components/organisms/navbar.tsx +101 -0
- package/src/components/organisms/navigation-menu.tsx +234 -0
- package/src/components/organisms/popover.tsx +144 -0
- package/src/components/organisms/resizable.tsx +39 -0
- package/src/components/organisms/schema-form.tsx +232 -0
- package/src/components/organisms/select.tsx +303 -0
- package/src/components/organisms/sheet.tsx +256 -0
- package/src/components/organisms/sidebar.tsx +1037 -0
- package/src/components/organisms/sonner.tsx +96 -0
- package/src/components/organisms/tabs.tsx +132 -0
- package/src/components/organisms/theme-provider.tsx +101 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/index.ts +547 -0
- package/src/lib/portal-container.tsx +35 -0
- package/src/lib/utils.ts +6 -0
- package/src/native.ts +23 -0
- package/src/tokens/colors.ts +91 -0
- package/src/tokens/index.ts +3 -0
- package/src/tokens/spacing.ts +55 -0
- package/src/tokens/typography.ts +27 -0
- package/styles/canvas.css +55 -0
- package/styles/dashboard-grid.css +47 -0
- package/styles/leaflet.css +13 -0
- package/styles/tokens.css +234 -0
- package/tailwind.config.ts +70 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
import { Icon } from "../atoms/icon";
|
|
7
|
+
import { Input } from "../atoms/input";
|
|
8
|
+
import { Label } from "../atoms/label";
|
|
9
|
+
|
|
10
|
+
export type SecretFieldStatus = "idle" | "validating" | "valid" | "invalid";
|
|
11
|
+
|
|
12
|
+
export interface SecretFieldProps {
|
|
13
|
+
id: string;
|
|
14
|
+
value: string;
|
|
15
|
+
onChange: (value: string) => void;
|
|
16
|
+
/**
|
|
17
|
+
* Async validator. Return `true` (or any string) on success; throw or return
|
|
18
|
+
* `false` on failure. The thrown message is shown below the input.
|
|
19
|
+
*/
|
|
20
|
+
onValidate?: (value: string) => Promise<boolean | string>;
|
|
21
|
+
/**
|
|
22
|
+
* Called after successful validation with the validated value.
|
|
23
|
+
* Use this to persist the value once it's confirmed valid.
|
|
24
|
+
*/
|
|
25
|
+
onSave?: (value: string) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Notifies when the validation status changes. Useful for parent state.
|
|
28
|
+
*/
|
|
29
|
+
onStatusChange?: (status: SecretFieldStatus) => void;
|
|
30
|
+
placeholder?: string;
|
|
31
|
+
label?: string;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
/** Minimum length before auto-validation starts. Default 3. */
|
|
34
|
+
minLength?: number;
|
|
35
|
+
/** Show the reveal (eye) toggle. Default true. */
|
|
36
|
+
revealable?: boolean;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function SecretField({
|
|
41
|
+
id,
|
|
42
|
+
value,
|
|
43
|
+
onChange,
|
|
44
|
+
onValidate,
|
|
45
|
+
onSave,
|
|
46
|
+
onStatusChange,
|
|
47
|
+
placeholder,
|
|
48
|
+
label,
|
|
49
|
+
disabled,
|
|
50
|
+
minLength = 3,
|
|
51
|
+
revealable = true,
|
|
52
|
+
className,
|
|
53
|
+
}: SecretFieldProps) {
|
|
54
|
+
const [visible, setVisible] = React.useState(false);
|
|
55
|
+
const [status, setStatus] = React.useState<SecretFieldStatus>("idle");
|
|
56
|
+
const [error, setError] = React.useState("");
|
|
57
|
+
const debounceRef = React.useRef(0);
|
|
58
|
+
const lastValidatedRef = React.useRef("");
|
|
59
|
+
|
|
60
|
+
React.useEffect(() => {
|
|
61
|
+
onStatusChange?.(status);
|
|
62
|
+
}, [status, onStatusChange]);
|
|
63
|
+
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
if (!onValidate) return;
|
|
66
|
+
if (!value || value.length < minLength) {
|
|
67
|
+
setStatus("idle");
|
|
68
|
+
setError("");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
/* c8 ignore next -- race-condition guard: only triggered by rapid input */
|
|
72
|
+
if (value === lastValidatedRef.current) return;
|
|
73
|
+
|
|
74
|
+
const id = ++debounceRef.current;
|
|
75
|
+
setStatus("validating");
|
|
76
|
+
setError("");
|
|
77
|
+
|
|
78
|
+
const timer = setTimeout(async () => {
|
|
79
|
+
/* c8 ignore next -- race-condition guard: debounce re-entry unreachable in tests */
|
|
80
|
+
if (id !== debounceRef.current) return;
|
|
81
|
+
try {
|
|
82
|
+
const result = await onValidate(value);
|
|
83
|
+
/* c8 ignore next -- race-condition guard: await-resolved stale id unreachable in tests */
|
|
84
|
+
if (id !== debounceRef.current) return;
|
|
85
|
+
if (result === true || typeof result === "string") {
|
|
86
|
+
setStatus("valid");
|
|
87
|
+
setError("");
|
|
88
|
+
lastValidatedRef.current = value;
|
|
89
|
+
onSave?.(value);
|
|
90
|
+
} else {
|
|
91
|
+
setStatus("invalid");
|
|
92
|
+
setError("Validation failed");
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
/* c8 ignore next -- race-condition guard: await-rejected stale id unreachable in tests */
|
|
96
|
+
if (id !== debounceRef.current) return;
|
|
97
|
+
setStatus("invalid");
|
|
98
|
+
setError(err instanceof Error ? err.message : "Validation failed");
|
|
99
|
+
}
|
|
100
|
+
}, 800);
|
|
101
|
+
|
|
102
|
+
return () => clearTimeout(timer);
|
|
103
|
+
}, [value, minLength, onValidate, onSave]);
|
|
104
|
+
|
|
105
|
+
const handleChange = (v: string) => {
|
|
106
|
+
onChange(v);
|
|
107
|
+
if (error) setError("");
|
|
108
|
+
lastValidatedRef.current = "";
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className={cn("space-y-1", className)}>
|
|
113
|
+
{label && (
|
|
114
|
+
<Label htmlFor={id} className="text-xs">
|
|
115
|
+
{label}
|
|
116
|
+
</Label>
|
|
117
|
+
)}
|
|
118
|
+
<div className="relative">
|
|
119
|
+
<Input
|
|
120
|
+
id={id}
|
|
121
|
+
type={visible ? "text" : "password"}
|
|
122
|
+
value={value}
|
|
123
|
+
onChange={(e) => handleChange(e.target.value)}
|
|
124
|
+
placeholder={placeholder}
|
|
125
|
+
className={cn(
|
|
126
|
+
"font-mono text-sm",
|
|
127
|
+
revealable && "pr-16",
|
|
128
|
+
!revealable && "pr-10",
|
|
129
|
+
status === "invalid" && "border-destructive",
|
|
130
|
+
status === "valid" && "border-green-500/50",
|
|
131
|
+
)}
|
|
132
|
+
disabled={disabled}
|
|
133
|
+
/>
|
|
134
|
+
<div className="absolute right-2.5 top-1/2 -translate-y-1/2 flex items-center gap-1.5">
|
|
135
|
+
{status === "validating" && (
|
|
136
|
+
<Icon name="LoaderCircle" className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
|
|
137
|
+
)}
|
|
138
|
+
{status === "valid" && <Icon name="CircleCheck" className="h-3.5 w-3.5 text-green-500" />}
|
|
139
|
+
{status === "invalid" && <Icon name="CircleX" className="h-3.5 w-3.5 text-destructive" />}
|
|
140
|
+
{revealable && (
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
onClick={() => setVisible((v) => !v)}
|
|
144
|
+
aria-label={visible ? "Hide value" : "Show value"}
|
|
145
|
+
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
146
|
+
tabIndex={-1}
|
|
147
|
+
>
|
|
148
|
+
<Icon name={visible ? "EyeOff" : "Eye"} className="h-4 w-4" />
|
|
149
|
+
</button>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
SecretField.displayName = "SecretField";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
import { Icon } from "../atoms/icon";
|
|
5
|
+
import { Alert, AlertDescription } from "./alert";
|
|
6
|
+
import { Card, CardContent, CardHeader } from "./card";
|
|
7
|
+
|
|
8
|
+
export interface SectionCardProps {
|
|
9
|
+
title?: string | React.ReactNode;
|
|
10
|
+
subtitle?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Icon rendered to the left of the title (16px Lucide via the Canvas
|
|
13
|
+
* `<Icon>` atom is the canonical pattern).
|
|
14
|
+
*/
|
|
15
|
+
icon?: React.ReactNode;
|
|
16
|
+
/** Right-aligned action slot inside the header. */
|
|
17
|
+
actions?: React.ReactNode;
|
|
18
|
+
/** @deprecated Use `actions`. */
|
|
19
|
+
headerActions?: React.ReactNode;
|
|
20
|
+
children?: React.ReactNode;
|
|
21
|
+
loading?: boolean;
|
|
22
|
+
error?: string | boolean | null;
|
|
23
|
+
emptyMessage?: string;
|
|
24
|
+
padding?: boolean;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function SectionCard({
|
|
29
|
+
title,
|
|
30
|
+
subtitle,
|
|
31
|
+
icon,
|
|
32
|
+
actions,
|
|
33
|
+
headerActions,
|
|
34
|
+
children,
|
|
35
|
+
loading = false,
|
|
36
|
+
error,
|
|
37
|
+
emptyMessage,
|
|
38
|
+
padding = true,
|
|
39
|
+
className,
|
|
40
|
+
}: SectionCardProps) {
|
|
41
|
+
const resolvedActions = actions ?? headerActions;
|
|
42
|
+
const hasHeader = title || subtitle || icon || resolvedActions;
|
|
43
|
+
return (
|
|
44
|
+
<Card className={className}>
|
|
45
|
+
{hasHeader && (
|
|
46
|
+
<>
|
|
47
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 px-5 pb-3 pt-[18px]">
|
|
48
|
+
<div className="flex flex-1 flex-col gap-1">
|
|
49
|
+
{(title || icon) && (
|
|
50
|
+
<div className="flex items-center gap-2">
|
|
51
|
+
{icon}
|
|
52
|
+
{title &&
|
|
53
|
+
(typeof title === "string" ? (
|
|
54
|
+
<span className="text-[15px] font-semibold leading-none">{title}</span>
|
|
55
|
+
) : (
|
|
56
|
+
title
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
{subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
|
|
61
|
+
</div>
|
|
62
|
+
{resolvedActions && <div className="flex items-center gap-2">{resolvedActions}</div>}
|
|
63
|
+
</CardHeader>
|
|
64
|
+
<div className="mx-5 mb-3.5 h-px bg-border" />
|
|
65
|
+
</>
|
|
66
|
+
)}
|
|
67
|
+
<CardContent className={cn(padding ? "px-5 pb-[18px] pt-0" : "p-0")}>
|
|
68
|
+
{loading ? (
|
|
69
|
+
<div className="flex items-center justify-center py-8">
|
|
70
|
+
<Icon name="LoaderCircle" className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
71
|
+
</div>
|
|
72
|
+
) : error ? (
|
|
73
|
+
<Alert variant="destructive">
|
|
74
|
+
<Icon name="CircleX" className="h-4 w-4" />
|
|
75
|
+
<AlertDescription>
|
|
76
|
+
{typeof error === "string" ? error : "An error occurred"}
|
|
77
|
+
</AlertDescription>
|
|
78
|
+
</Alert>
|
|
79
|
+
) : emptyMessage && !children ? (
|
|
80
|
+
<div className="flex items-center justify-center py-8 text-center">
|
|
81
|
+
<p className="text-sm text-muted-foreground">{emptyMessage}</p>
|
|
82
|
+
</div>
|
|
83
|
+
) : (
|
|
84
|
+
children
|
|
85
|
+
)}
|
|
86
|
+
</CardContent>
|
|
87
|
+
</Card>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
SectionCard.displayName = "SectionCard";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ArrowDown, ArrowUp } from "lucide-react";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
import { Card, CardContent } from "./card";
|
|
6
|
+
|
|
7
|
+
export interface StatCardProps {
|
|
8
|
+
title: string;
|
|
9
|
+
value: React.ReactNode;
|
|
10
|
+
icon?: React.ReactNode;
|
|
11
|
+
colorVariant?: "primary" | "blue" | "purple" | "success" | "warning" | "amber" | "destructive";
|
|
12
|
+
/** Optional delta line shown under the value (e.g. "+4.2%"). */
|
|
13
|
+
delta?: React.ReactNode;
|
|
14
|
+
/** Tone for the delta — `up` is green, `down` is red, `neutral` is muted. */
|
|
15
|
+
deltaTone?: "up" | "down" | "neutral";
|
|
16
|
+
/** Caption shown next to the delta (e.g. "vs. last 7d"). */
|
|
17
|
+
deltaCaption?: React.ReactNode;
|
|
18
|
+
/**
|
|
19
|
+
* When `true` (default) and `delta` is a string, prepend an ArrowUp /
|
|
20
|
+
* ArrowDown icon based on `deltaTone`. Pass `false` to opt out, or pass
|
|
21
|
+
* `delta` as ReactNode to fully control rendering yourself.
|
|
22
|
+
*/
|
|
23
|
+
deltaArrow?: boolean;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const COLOR: Record<NonNullable<StatCardProps["colorVariant"]>, string> = {
|
|
28
|
+
primary: "bg-primary/10 text-primary",
|
|
29
|
+
blue: "bg-[hsl(var(--stat-blue)/0.1)] text-[hsl(var(--stat-blue))]",
|
|
30
|
+
purple: "bg-[hsl(var(--stat-purple)/0.1)] text-[hsl(var(--stat-purple))]",
|
|
31
|
+
success: "bg-[hsl(var(--stat-success)/0.1)] text-[hsl(var(--stat-success))]",
|
|
32
|
+
warning: "bg-[hsl(var(--stat-amber)/0.1)] text-[hsl(var(--stat-amber))]",
|
|
33
|
+
amber: "bg-[hsl(var(--stat-amber)/0.1)] text-[hsl(var(--stat-amber))]",
|
|
34
|
+
destructive: "bg-[hsl(var(--stat-destructive)/0.1)] text-[hsl(var(--stat-destructive))]",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const DELTA_TONE: Record<NonNullable<StatCardProps["deltaTone"]>, string> = {
|
|
38
|
+
up: "text-green-600 dark:text-green-500",
|
|
39
|
+
down: "text-red-600 dark:text-red-500",
|
|
40
|
+
neutral: "text-muted-foreground",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function StatCard({
|
|
44
|
+
title,
|
|
45
|
+
value,
|
|
46
|
+
icon,
|
|
47
|
+
colorVariant = "primary",
|
|
48
|
+
delta,
|
|
49
|
+
deltaTone = "up",
|
|
50
|
+
deltaCaption,
|
|
51
|
+
deltaArrow = true,
|
|
52
|
+
className,
|
|
53
|
+
}: StatCardProps) {
|
|
54
|
+
const showArrow = deltaArrow && typeof delta === "string" && deltaTone !== "neutral";
|
|
55
|
+
const ArrowGlyph = deltaTone === "down" ? ArrowDown : ArrowUp;
|
|
56
|
+
return (
|
|
57
|
+
<Card className={className}>
|
|
58
|
+
<CardContent className="p-5">
|
|
59
|
+
<div className="flex items-start justify-between gap-3">
|
|
60
|
+
<div className="min-w-0 flex-1">
|
|
61
|
+
<p className="truncate text-[13px] font-medium text-muted-foreground">{title}</p>
|
|
62
|
+
<p className="mt-1 text-[28px] font-semibold leading-tight tracking-[-0.02em]">
|
|
63
|
+
{value}
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
{icon && (
|
|
67
|
+
<div
|
|
68
|
+
className={cn(
|
|
69
|
+
"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg",
|
|
70
|
+
COLOR[colorVariant],
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
{icon}
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
{delta != null && (
|
|
78
|
+
<div className="mt-3 flex items-center gap-1.5 text-xs">
|
|
79
|
+
<span
|
|
80
|
+
className={cn(
|
|
81
|
+
"inline-flex items-center gap-0.5 font-mono font-medium",
|
|
82
|
+
DELTA_TONE[deltaTone],
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
{showArrow && <ArrowGlyph className="h-3 w-3" aria-hidden />}
|
|
86
|
+
{delta}
|
|
87
|
+
</span>
|
|
88
|
+
{deltaCaption != null && <span className="text-muted-foreground">{deltaCaption}</span>}
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</CardContent>
|
|
92
|
+
</Card>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
StatCard.displayName = "StatCard";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
|
|
6
|
+
const statusBadgeVariants = cva(
|
|
7
|
+
"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
status: {
|
|
11
|
+
success: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
|
|
12
|
+
warning: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400",
|
|
13
|
+
error: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400",
|
|
14
|
+
info: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
|
|
15
|
+
neutral: "bg-muted text-muted-foreground",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: {
|
|
19
|
+
status: "neutral",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export interface StatusBadgeProps
|
|
25
|
+
extends React.HTMLAttributes<HTMLSpanElement>,
|
|
26
|
+
VariantProps<typeof statusBadgeVariants> {
|
|
27
|
+
dot?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const StatusBadge = React.forwardRef<HTMLSpanElement, StatusBadgeProps>(
|
|
31
|
+
({ status, dot = true, className, children, ...props }, ref) => {
|
|
32
|
+
return (
|
|
33
|
+
<span ref={ref} className={cn(statusBadgeVariants({ status }), className)} {...props}>
|
|
34
|
+
{dot && <span className="h-1.5 w-1.5 rounded-full bg-current" />}
|
|
35
|
+
{children}
|
|
36
|
+
</span>
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
StatusBadge.displayName = "StatusBadge";
|
|
41
|
+
|
|
42
|
+
export { StatusBadge, statusBadgeVariants };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
import { Icon } from "../atoms/icon";
|
|
7
|
+
|
|
8
|
+
export type StepStatus = "pending" | "active" | "complete" | "error";
|
|
9
|
+
|
|
10
|
+
export interface StepperStep {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
status: StepStatus;
|
|
14
|
+
/** Optional icon override (atoms/icon IconName or React node). */
|
|
15
|
+
icon?: React.ReactNode;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface StepperProps {
|
|
20
|
+
steps: StepperStep[];
|
|
21
|
+
orientation?: "horizontal" | "vertical";
|
|
22
|
+
onStepClick?: (id: string) => void;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const STATUS_STYLES: Record<StepStatus, { dot: string; label: string }> = {
|
|
27
|
+
pending: {
|
|
28
|
+
dot: "border-muted-foreground/30 text-muted-foreground",
|
|
29
|
+
label: "text-muted-foreground",
|
|
30
|
+
},
|
|
31
|
+
active: {
|
|
32
|
+
dot: "border-primary bg-primary text-primary-foreground",
|
|
33
|
+
label: "text-foreground font-medium",
|
|
34
|
+
},
|
|
35
|
+
complete: {
|
|
36
|
+
dot: "border-green-500 bg-green-500 text-white",
|
|
37
|
+
label: "text-foreground",
|
|
38
|
+
},
|
|
39
|
+
error: {
|
|
40
|
+
dot: "border-destructive bg-destructive text-destructive-foreground",
|
|
41
|
+
label: "text-destructive",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function Stepper({ steps, orientation = "vertical", onStepClick, className }: StepperProps) {
|
|
46
|
+
const vertical = orientation === "vertical";
|
|
47
|
+
return (
|
|
48
|
+
<ol
|
|
49
|
+
className={cn("flex", vertical ? "flex-col gap-1" : "flex-row items-center gap-2", className)}
|
|
50
|
+
>
|
|
51
|
+
{steps.map((step, i) => {
|
|
52
|
+
const styles = STATUS_STYLES[step.status];
|
|
53
|
+
const clickable = !!onStepClick && !step.disabled;
|
|
54
|
+
return (
|
|
55
|
+
<li key={step.id} className={cn("flex items-center gap-3", vertical ? "py-2" : "flex-1")}>
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
disabled={!clickable}
|
|
59
|
+
onClick={() => clickable && onStepClick(step.id)}
|
|
60
|
+
className={cn(
|
|
61
|
+
"flex items-center gap-3 rounded-md transition-colors",
|
|
62
|
+
vertical && "w-full px-2 py-1 text-left",
|
|
63
|
+
clickable && "hover:bg-accent/50",
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
<span
|
|
67
|
+
className={cn(
|
|
68
|
+
"flex h-6 w-6 shrink-0 items-center justify-center rounded-full border text-xs",
|
|
69
|
+
styles.dot,
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
{step.icon ? (
|
|
73
|
+
typeof step.icon === "string" ? (
|
|
74
|
+
<Icon name={step.icon as never} className="h-3.5 w-3.5" />
|
|
75
|
+
) : (
|
|
76
|
+
step.icon
|
|
77
|
+
)
|
|
78
|
+
) : step.status === "complete" ? (
|
|
79
|
+
<Icon name="Check" className="h-3.5 w-3.5" />
|
|
80
|
+
) : step.status === "error" ? (
|
|
81
|
+
<Icon name="X" className="h-3.5 w-3.5" />
|
|
82
|
+
) : (
|
|
83
|
+
i + 1
|
|
84
|
+
)}
|
|
85
|
+
</span>
|
|
86
|
+
<span className={cn("text-sm", styles.label)}>{step.label}</span>
|
|
87
|
+
</button>
|
|
88
|
+
{!vertical && i < steps.length - 1 && <span className="mx-2 h-px flex-1 bg-border" />}
|
|
89
|
+
</li>
|
|
90
|
+
);
|
|
91
|
+
})}
|
|
92
|
+
</ol>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
Stepper.displayName = "Stepper";
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
export interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
|
|
6
|
+
/** `<TableHeader>` + `<TableBody>` + optional `<TableFooter>` + optional `<TableCaption>`. */
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
/** Tailwind / CSS classes merged onto the `<table>` via `cn()`. */
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const Table = React.forwardRef<HTMLTableElement, TableProps>(({ className, ...props }, ref) => (
|
|
13
|
+
<div className="relative w-full overflow-auto">
|
|
14
|
+
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
|
15
|
+
</div>
|
|
16
|
+
));
|
|
17
|
+
Table.displayName = "Table";
|
|
18
|
+
|
|
19
|
+
export interface TableHeaderProps extends React.HTMLAttributes<HTMLTableSectionElement> {
|
|
20
|
+
/** A single `<TableRow>` of `<TableHead>` cells. */
|
|
21
|
+
children?: React.ReactNode;
|
|
22
|
+
/** Tailwind / CSS classes merged onto the `<thead>` via `cn()`. */
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const TableHeader = React.forwardRef<HTMLTableSectionElement, TableHeaderProps>(
|
|
27
|
+
({ className, ...props }, ref) => (
|
|
28
|
+
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
TableHeader.displayName = "TableHeader";
|
|
32
|
+
|
|
33
|
+
export interface TableBodyProps extends React.HTMLAttributes<HTMLTableSectionElement> {
|
|
34
|
+
/** A list of `<TableRow>`s of `<TableCell>`s. */
|
|
35
|
+
children?: React.ReactNode;
|
|
36
|
+
/** Tailwind / CSS classes merged onto the `<tbody>` via `cn()`. */
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const TableBody = React.forwardRef<HTMLTableSectionElement, TableBodyProps>(
|
|
41
|
+
({ className, ...props }, ref) => (
|
|
42
|
+
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
TableBody.displayName = "TableBody";
|
|
46
|
+
|
|
47
|
+
export interface TableFooterProps extends React.HTMLAttributes<HTMLTableSectionElement> {
|
|
48
|
+
/** A `<TableRow>` of summary `<TableCell>`s (totals, etc.). */
|
|
49
|
+
children?: React.ReactNode;
|
|
50
|
+
/** Tailwind / CSS classes merged onto the `<tfoot>` via `cn()`. */
|
|
51
|
+
className?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const TableFooter = React.forwardRef<HTMLTableSectionElement, TableFooterProps>(
|
|
55
|
+
({ className, ...props }, ref) => (
|
|
56
|
+
<tfoot
|
|
57
|
+
ref={ref}
|
|
58
|
+
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
TableFooter.displayName = "TableFooter";
|
|
64
|
+
|
|
65
|
+
export interface TableRowProps extends React.HTMLAttributes<HTMLTableRowElement> {
|
|
66
|
+
/** `<TableHead>` or `<TableCell>` children. */
|
|
67
|
+
children?: React.ReactNode;
|
|
68
|
+
/** Tailwind / CSS classes merged onto the `<tr>` via `cn()`. */
|
|
69
|
+
className?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Optional data-state — pass `"selected"` to highlight selected rows
|
|
72
|
+
* (e.g. checkbox-driven multi-select tables). Mirrors Radix's pattern.
|
|
73
|
+
*/
|
|
74
|
+
"data-state"?: "selected";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const TableRow = React.forwardRef<HTMLTableRowElement, TableRowProps>(
|
|
78
|
+
({ className, ...props }, ref) => (
|
|
79
|
+
<tr
|
|
80
|
+
ref={ref}
|
|
81
|
+
className={cn(
|
|
82
|
+
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
|
83
|
+
className,
|
|
84
|
+
)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
TableRow.displayName = "TableRow";
|
|
90
|
+
|
|
91
|
+
export interface TableHeadProps extends React.ThHTMLAttributes<HTMLTableCellElement> {
|
|
92
|
+
/** Column header text. */
|
|
93
|
+
children?: React.ReactNode;
|
|
94
|
+
/** Tailwind / CSS classes merged onto the `<th>` via `cn()`. */
|
|
95
|
+
className?: string;
|
|
96
|
+
/** Number of columns this header spans. */
|
|
97
|
+
colSpan?: number;
|
|
98
|
+
/** Number of rows this header spans. */
|
|
99
|
+
rowSpan?: number;
|
|
100
|
+
/** Scope of the header — `"col"` (default) or `"row"` for row-headers. */
|
|
101
|
+
scope?: "col" | "row" | "rowgroup" | "colgroup";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const TableHead = React.forwardRef<HTMLTableCellElement, TableHeadProps>(
|
|
105
|
+
({ className, ...props }, ref) => (
|
|
106
|
+
<th
|
|
107
|
+
ref={ref}
|
|
108
|
+
className={cn(
|
|
109
|
+
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
110
|
+
className,
|
|
111
|
+
)}
|
|
112
|
+
{...props}
|
|
113
|
+
/>
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
TableHead.displayName = "TableHead";
|
|
117
|
+
|
|
118
|
+
export interface TableCellProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
|
|
119
|
+
/** Cell content. */
|
|
120
|
+
children?: React.ReactNode;
|
|
121
|
+
/** Tailwind / CSS classes merged onto the `<td>` via `cn()`. */
|
|
122
|
+
className?: string;
|
|
123
|
+
/** Number of columns this cell spans. */
|
|
124
|
+
colSpan?: number;
|
|
125
|
+
/** Number of rows this cell spans. */
|
|
126
|
+
rowSpan?: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const TableCell = React.forwardRef<HTMLTableCellElement, TableCellProps>(
|
|
130
|
+
({ className, ...props }, ref) => (
|
|
131
|
+
<td
|
|
132
|
+
ref={ref}
|
|
133
|
+
className={cn(
|
|
134
|
+
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
135
|
+
className,
|
|
136
|
+
)}
|
|
137
|
+
{...props}
|
|
138
|
+
/>
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
TableCell.displayName = "TableCell";
|
|
142
|
+
|
|
143
|
+
export interface TableCaptionProps extends React.HTMLAttributes<HTMLTableCaptionElement> {
|
|
144
|
+
/** A short label describing the table — read by screen readers. */
|
|
145
|
+
children?: React.ReactNode;
|
|
146
|
+
/** Tailwind / CSS classes merged onto the `<caption>` via `cn()`. */
|
|
147
|
+
className?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const TableCaption = React.forwardRef<HTMLTableCaptionElement, TableCaptionProps>(
|
|
151
|
+
({ className, ...props }, ref) => (
|
|
152
|
+
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
TableCaption.displayName = "TableCaption";
|
|
156
|
+
|
|
157
|
+
export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow };
|