@rahulapgm/skyblue-ui 0.1.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/README.md +25 -0
- package/package.json +55 -0
- package/src/animation.tsx +117 -0
- package/src/autocomplete.tsx +271 -0
- package/src/badge.tsx +42 -0
- package/src/button.tsx +88 -0
- package/src/card.tsx +37 -0
- package/src/checkbox.tsx +36 -0
- package/src/chip.tsx +97 -0
- package/src/cluster.tsx +60 -0
- package/src/container.tsx +39 -0
- package/src/datepicker.tsx +59 -0
- package/src/drawer.tsx +126 -0
- package/src/dropdown.tsx +202 -0
- package/src/feature-card.tsx +33 -0
- package/src/floating-button.tsx +51 -0
- package/src/grid.tsx +94 -0
- package/src/hero-banner.tsx +82 -0
- package/src/icon-button.tsx +78 -0
- package/src/index.ts +32 -0
- package/src/input.tsx +62 -0
- package/src/label.tsx +20 -0
- package/src/loader.tsx +90 -0
- package/src/menu.tsx +100 -0
- package/src/message-box.tsx +46 -0
- package/src/metric.tsx +41 -0
- package/src/modal.tsx +110 -0
- package/src/navigation.tsx +147 -0
- package/src/number-input.tsx +127 -0
- package/src/pagination.tsx +102 -0
- package/src/phone-number-input.tsx +95 -0
- package/src/progress.tsx +65 -0
- package/src/radio.tsx +36 -0
- package/src/result.tsx +43 -0
- package/src/section.tsx +36 -0
- package/src/select.tsx +78 -0
- package/src/skeleton.tsx +17 -0
- package/src/stack.tsx +35 -0
- package/src/stat-card.tsx +69 -0
- package/src/steps.tsx +115 -0
- package/src/swatch.tsx +70 -0
- package/src/switch.tsx +60 -0
- package/src/table.tsx +88 -0
- package/src/tabs.tsx +100 -0
- package/src/textarea.tsx +52 -0
- package/src/timeline.tsx +60 -0
- package/src/toast.tsx +144 -0
- package/src/tooltip.tsx +56 -0
- package/src/utils.ts +3 -0
package/src/result.tsx
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Ban, CheckCircle2, CircleAlert, Inbox, Info } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { Card } from "./card";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
type ResultStatus = "success" | "info" | "warning" | "error" | "empty";
|
|
8
|
+
|
|
9
|
+
type ResultProps = {
|
|
10
|
+
status?: ResultStatus;
|
|
11
|
+
title: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
icon?: ReactNode;
|
|
14
|
+
actions?: ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const defaultIcons: Record<ResultStatus, ReactNode> = {
|
|
19
|
+
success: <CheckCircle2 className="h-8 w-8 text-(--color-status-success)" />,
|
|
20
|
+
info: <Info className="h-8 w-8 text-(--color-status-info)" />,
|
|
21
|
+
warning: <CircleAlert className="h-8 w-8 text-(--color-status-warning)" />,
|
|
22
|
+
error: <Ban className="h-8 w-8 text-(--color-status-error)" />,
|
|
23
|
+
empty: <Inbox className="h-8 w-8 text-(--ink-subtle)" />,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function Result({ status = "info", title, description, icon, actions, className }: ResultProps) {
|
|
27
|
+
return (
|
|
28
|
+
<Card
|
|
29
|
+
tone="surface"
|
|
30
|
+
padding="lg"
|
|
31
|
+
className={cn("flex flex-col items-start gap-4 text-left", className)}
|
|
32
|
+
>
|
|
33
|
+
<div className="flex h-14 w-14 items-center justify-center rounded-3xl bg-(--color-surface-muted)">
|
|
34
|
+
{icon ?? defaultIcons[status]}
|
|
35
|
+
</div>
|
|
36
|
+
<div>
|
|
37
|
+
<h3 className="type-subheading">{title}</h3>
|
|
38
|
+
{description ? <p className="type-body mt-2 text-(--ink-muted)">{description}</p> : null}
|
|
39
|
+
</div>
|
|
40
|
+
{actions ? <div className="flex flex-wrap gap-3">{actions}</div> : null}
|
|
41
|
+
</Card>
|
|
42
|
+
);
|
|
43
|
+
}
|
package/src/section.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
type SectionSpacing = "sm" | "md" | "lg" | "xl";
|
|
6
|
+
|
|
7
|
+
type SectionProps = HTMLAttributes<HTMLElement> & {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
as?: "section" | "div" | "main";
|
|
10
|
+
spacing?: SectionSpacing;
|
|
11
|
+
contained?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const spacingClasses: Record<SectionSpacing, string> = {
|
|
15
|
+
sm: "gutter-section-sm",
|
|
16
|
+
md: "gutter-section-md",
|
|
17
|
+
lg: "gutter-section-lg",
|
|
18
|
+
xl: "gutter-section-xl",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function Section({
|
|
22
|
+
children,
|
|
23
|
+
as = "section",
|
|
24
|
+
spacing = "lg",
|
|
25
|
+
contained = false,
|
|
26
|
+
className,
|
|
27
|
+
...props
|
|
28
|
+
}: SectionProps) {
|
|
29
|
+
const Component = as;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Component {...props} className={cn(spacingClasses[spacing], contained && "gutter-page", className)}>
|
|
33
|
+
{children}
|
|
34
|
+
</Component>
|
|
35
|
+
);
|
|
36
|
+
}
|
package/src/select.tsx
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ReactNode, SelectHTMLAttributes } from "react";
|
|
2
|
+
import { ChevronDown } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { Label } from "./label";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
type SelectOption = {
|
|
8
|
+
label: string;
|
|
9
|
+
value: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type SelectProps = Omit<SelectHTMLAttributes<HTMLSelectElement>, "children"> & {
|
|
14
|
+
label?: string;
|
|
15
|
+
helperText?: string;
|
|
16
|
+
errorText?: string;
|
|
17
|
+
options: SelectOption[];
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
leadingSlot?: ReactNode;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function Select({
|
|
23
|
+
label,
|
|
24
|
+
helperText,
|
|
25
|
+
errorText,
|
|
26
|
+
options,
|
|
27
|
+
placeholder,
|
|
28
|
+
leadingSlot,
|
|
29
|
+
className,
|
|
30
|
+
id,
|
|
31
|
+
required,
|
|
32
|
+
...props
|
|
33
|
+
}: SelectProps) {
|
|
34
|
+
const selectId = id ?? label?.toLowerCase().replace(/\s+/g, "-");
|
|
35
|
+
const hasError = Boolean(errorText);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="w-full">
|
|
39
|
+
{label ? (
|
|
40
|
+
<Label htmlFor={selectId} requiredMark={required} className="mb-2 block">
|
|
41
|
+
{label}
|
|
42
|
+
</Label>
|
|
43
|
+
) : null}
|
|
44
|
+
<div
|
|
45
|
+
className={cn(
|
|
46
|
+
"flex min-h-12 items-center gap-3 rounded-2xl border bg-(--surface-card) px-4 shadow-(--shadow-sm) transition-colors",
|
|
47
|
+
hasError
|
|
48
|
+
? "border-(--color-status-error)"
|
|
49
|
+
: "border-(--line-soft) hover:border-(--color-line-strong)",
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
{leadingSlot}
|
|
53
|
+
<select
|
|
54
|
+
{...props}
|
|
55
|
+
id={selectId}
|
|
56
|
+
required={required}
|
|
57
|
+
className={cn(
|
|
58
|
+
"type-body min-h-12 w-full appearance-none bg-transparent text-(--foreground) outline-none",
|
|
59
|
+
className,
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
{placeholder ? <option value="">{placeholder}</option> : null}
|
|
63
|
+
{options.map((option) => (
|
|
64
|
+
<option key={option.value} value={option.value} disabled={option.disabled}>
|
|
65
|
+
{option.label}
|
|
66
|
+
</option>
|
|
67
|
+
))}
|
|
68
|
+
</select>
|
|
69
|
+
<ChevronDown className="h-4 w-4 text-(--ink-subtle)" />
|
|
70
|
+
</div>
|
|
71
|
+
{errorText ? (
|
|
72
|
+
<p className="type-body mt-2 text-(--color-status-error)">{errorText}</p>
|
|
73
|
+
) : helperText ? (
|
|
74
|
+
<p className="type-body mt-2 text-(--ink-subtle)">{helperText}</p>
|
|
75
|
+
) : null}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
package/src/skeleton.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { cn } from "./utils";
|
|
2
|
+
|
|
3
|
+
type SkeletonProps = {
|
|
4
|
+
className?: string;
|
|
5
|
+
rounded?: "sm" | "md" | "lg" | "full";
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const roundedClasses = {
|
|
9
|
+
sm: "rounded-md",
|
|
10
|
+
md: "rounded-xl",
|
|
11
|
+
lg: "rounded-2xl",
|
|
12
|
+
full: "rounded-full",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export function Skeleton({ className, rounded = "md" }: SkeletonProps) {
|
|
16
|
+
return <div aria-hidden="true" className={cn("skeleton-shimmer", roundedClasses[rounded], className)} />;
|
|
17
|
+
}
|
package/src/stack.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
type StackGap = "xs" | "sm" | "md" | "lg" | "xl";
|
|
6
|
+
type StackAlign = "start" | "center" | "end" | "stretch";
|
|
7
|
+
|
|
8
|
+
type StackProps = HTMLAttributes<HTMLDivElement> & {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
gap?: StackGap;
|
|
11
|
+
align?: StackAlign;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const gapClasses: Record<StackGap, string> = {
|
|
15
|
+
xs: "gutter-grid-xs",
|
|
16
|
+
sm: "gutter-grid-sm",
|
|
17
|
+
md: "gutter-grid-md",
|
|
18
|
+
lg: "gutter-grid-lg",
|
|
19
|
+
xl: "gutter-grid-xl",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const alignClasses: Record<StackAlign, string> = {
|
|
23
|
+
start: "items-start",
|
|
24
|
+
center: "items-center",
|
|
25
|
+
end: "items-end",
|
|
26
|
+
stretch: "items-stretch",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function Stack({ children, gap = "md", align = "stretch", className, ...props }: StackProps) {
|
|
30
|
+
return (
|
|
31
|
+
<div {...props} className={cn("flex flex-col", gapClasses[gap], alignClasses[align], className)}>
|
|
32
|
+
{children}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { Badge } from "./badge";
|
|
4
|
+
import { Card } from "./card";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
type StatCardProps = {
|
|
8
|
+
label: string;
|
|
9
|
+
value: ReactNode;
|
|
10
|
+
change?: string;
|
|
11
|
+
tone?: "default" | "brand" | "success" | "warning";
|
|
12
|
+
icon?: ReactNode;
|
|
13
|
+
size?: "sm" | "md";
|
|
14
|
+
className?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const toneClasses = {
|
|
18
|
+
default: "text-(--foreground)",
|
|
19
|
+
brand: "text-(--color-brand-primary)",
|
|
20
|
+
success: "text-(--color-status-success)",
|
|
21
|
+
warning: "text-(--color-status-warning)",
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
export function StatCard({
|
|
25
|
+
label,
|
|
26
|
+
value,
|
|
27
|
+
change,
|
|
28
|
+
tone = "default",
|
|
29
|
+
icon,
|
|
30
|
+
size = "md",
|
|
31
|
+
className,
|
|
32
|
+
}: StatCardProps) {
|
|
33
|
+
return (
|
|
34
|
+
<Card
|
|
35
|
+
className={cn("min-w-0 overflow-hidden", size === "sm" ? "min-h-42" : "min-h-49", className)}
|
|
36
|
+
tone="surface"
|
|
37
|
+
padding="md"
|
|
38
|
+
>
|
|
39
|
+
<div className="flex items-start justify-between gap-3 sm:gap-4">
|
|
40
|
+
<div className="min-w-0 flex-1">
|
|
41
|
+
<p className="type-caption text-(--ink-subtle)">{label}</p>
|
|
42
|
+
<p
|
|
43
|
+
className={cn(
|
|
44
|
+
"mt-2 whitespace-normal text-balance font-extrabold leading-[0.95]",
|
|
45
|
+
size === "sm"
|
|
46
|
+
? "text-[1.5rem] sm:text-[1.75rem]"
|
|
47
|
+
: "text-[1.625rem] sm:text-[2rem] lg:text-[2.25rem]",
|
|
48
|
+
toneClasses[tone],
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
{value}
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
{icon ? (
|
|
55
|
+
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl bg-(--color-brand-primary-light) text-(--color-brand-primary) sm:h-11 sm:w-11">
|
|
56
|
+
{icon}
|
|
57
|
+
</div>
|
|
58
|
+
) : null}
|
|
59
|
+
</div>
|
|
60
|
+
{change ? (
|
|
61
|
+
<div className="mt-3 sm:mt-4">
|
|
62
|
+
<Badge tone={tone === "success" ? "success" : tone === "warning" ? "warning" : "info"} size="sm">
|
|
63
|
+
{change}
|
|
64
|
+
</Badge>
|
|
65
|
+
</div>
|
|
66
|
+
) : null}
|
|
67
|
+
</Card>
|
|
68
|
+
);
|
|
69
|
+
}
|
package/src/steps.tsx
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Fragment } from "react";
|
|
2
|
+
import { ArrowDown, ArrowRight } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { Badge } from "./badge";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
export type StepItem = {
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type StepsProps = {
|
|
13
|
+
items: StepItem[];
|
|
14
|
+
current: number;
|
|
15
|
+
className?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const statusLabels = {
|
|
19
|
+
completed: "Completed",
|
|
20
|
+
current: "Ongoing",
|
|
21
|
+
upcoming: "Upcoming",
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
function getConnectorClass(stepNumber: number, current: number) {
|
|
25
|
+
if (stepNumber < current - 1) {
|
|
26
|
+
return "bg-(--color-status-success)";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (stepNumber === current - 1) {
|
|
30
|
+
return "bg-(--color-brand-primary)";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return "bg-(--line-soft)";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getConnectorIconClass(stepNumber: number, current: number) {
|
|
37
|
+
if (stepNumber < current - 1) {
|
|
38
|
+
return "text-(--color-status-success)";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (stepNumber === current - 1) {
|
|
42
|
+
return "text-(--color-brand-primary)";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return "text-(--ink-subtle)";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function Steps({ items, current, className }: StepsProps) {
|
|
49
|
+
return (
|
|
50
|
+
<div role="list" className={cn("w-full overflow-hidden", className)}>
|
|
51
|
+
<div className="flex flex-col gap-4 xl:flex-row xl:items-stretch xl:gap-5">
|
|
52
|
+
{items.map((item, index) => {
|
|
53
|
+
const status = index + 1 < current ? "completed" : index + 1 === current ? "current" : "upcoming";
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Fragment key={item.title}>
|
|
57
|
+
<div className="min-w-0 flex-1">
|
|
58
|
+
<div
|
|
59
|
+
aria-current={status === "current" ? "step" : undefined}
|
|
60
|
+
className={cn(
|
|
61
|
+
"relative min-w-0 rounded-3xl border px-5 py-5 shadow-(--shadow-sm) sm:px-6 sm:py-6",
|
|
62
|
+
status === "completed" &&
|
|
63
|
+
"border-(--color-status-success-light) bg-(--color-status-success-light)/45",
|
|
64
|
+
status === "current" &&
|
|
65
|
+
"border-(--color-brand-primary-light) bg-(--color-brand-primary-light)/5 ring-2 ring-(--color-brand-primary-light)/80",
|
|
66
|
+
status === "upcoming" && "border-(--line-soft) bg-(--surface-card)",
|
|
67
|
+
)}
|
|
68
|
+
role="listitem"
|
|
69
|
+
>
|
|
70
|
+
<div className="absolute right-3 top-2">
|
|
71
|
+
<Badge
|
|
72
|
+
tone={status === "completed" ? "success" : status === "current" ? "brand" : "neutral"}
|
|
73
|
+
size="sm"
|
|
74
|
+
>
|
|
75
|
+
{statusLabels[status]}
|
|
76
|
+
</Badge>
|
|
77
|
+
</div>
|
|
78
|
+
<div className="flex items-start gap-4 sm:gap-5">
|
|
79
|
+
<div className="min-w-0 flex-1">
|
|
80
|
+
<h3 className="mt-1 pr-28 text-base font-extrabold leading-tight text-(--foreground) sm:pr-32 sm:text-lg">
|
|
81
|
+
{item.title}
|
|
82
|
+
</h3>
|
|
83
|
+
{item.description ? (
|
|
84
|
+
<p className="mt-2 max-w-[22ch] text-sm font-semibold leading-6 text-(--ink-muted)">
|
|
85
|
+
{item.description}
|
|
86
|
+
</p>
|
|
87
|
+
) : null}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{index < items.length - 1 ? (
|
|
94
|
+
<div className="flex justify-center py-1 xl:w-20 xl:flex-none xl:items-center xl:py-0">
|
|
95
|
+
<div className="flex flex-col items-center gap-2 xl:w-full xl:flex-row xl:gap-2">
|
|
96
|
+
<span className={cn("h-8 w-px xl:hidden", getConnectorClass(index + 1, current))} />
|
|
97
|
+
<ArrowDown
|
|
98
|
+
className={cn("h-4 w-4 xl:hidden", getConnectorIconClass(index + 1, current))}
|
|
99
|
+
/>
|
|
100
|
+
<span
|
|
101
|
+
className={cn("hidden h-px flex-1 xl:block", getConnectorClass(index + 1, current))}
|
|
102
|
+
/>
|
|
103
|
+
<ArrowRight
|
|
104
|
+
className={cn("hidden h-4 w-4 xl:block", getConnectorIconClass(index + 1, current))}
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
) : null}
|
|
109
|
+
</Fragment>
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
package/src/swatch.tsx
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Check, Copy } from "lucide-react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "./utils";
|
|
7
|
+
|
|
8
|
+
type SwatchProps = {
|
|
9
|
+
name: string;
|
|
10
|
+
value: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
shape?: "square" | "circle";
|
|
13
|
+
size?: "sm" | "md" | "lg";
|
|
14
|
+
className?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const previewSizeClasses = {
|
|
18
|
+
sm: "h-10 w-10",
|
|
19
|
+
md: "h-14 w-14",
|
|
20
|
+
lg: "h-20 w-20",
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export function Swatch({
|
|
24
|
+
name,
|
|
25
|
+
value,
|
|
26
|
+
description,
|
|
27
|
+
shape = "square",
|
|
28
|
+
size = "md",
|
|
29
|
+
className,
|
|
30
|
+
}: SwatchProps) {
|
|
31
|
+
const [copied, setCopied] = useState(false);
|
|
32
|
+
|
|
33
|
+
async function handleCopy() {
|
|
34
|
+
await navigator.clipboard.writeText(value);
|
|
35
|
+
setCopied(true);
|
|
36
|
+
window.setTimeout(() => setCopied(false), 1200);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
className={cn(
|
|
42
|
+
"surface-glass flex items-center gap-4 rounded-3xl border border-(--line-soft) p-4 shadow-(--shadow-sm)",
|
|
43
|
+
className,
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
<div
|
|
47
|
+
className={cn(
|
|
48
|
+
"shrink-0 border border-(--line-soft) shadow-(--shadow-sm)",
|
|
49
|
+
previewSizeClasses[size],
|
|
50
|
+
shape === "circle" ? "rounded-full" : "rounded-2xl",
|
|
51
|
+
)}
|
|
52
|
+
style={{ backgroundColor: value }}
|
|
53
|
+
aria-hidden
|
|
54
|
+
/>
|
|
55
|
+
<div className="min-w-0 flex-1">
|
|
56
|
+
<p className="type-title">{name}</p>
|
|
57
|
+
<p className="type-caption mt-1 text-(--ink-subtle)">{value}</p>
|
|
58
|
+
{description ? <p className="type-body mt-2 text-(--ink-muted)">{description}</p> : null}
|
|
59
|
+
</div>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={handleCopy}
|
|
63
|
+
className="inline-flex h-10 w-10 items-center justify-center rounded-2xl border border-(--line-soft) bg-(--surface-card) text-(--ink-muted) transition-colors hover:bg-(--color-surface-hover)"
|
|
64
|
+
aria-label={`Copy ${name} color value`}
|
|
65
|
+
>
|
|
66
|
+
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
package/src/switch.tsx
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { InputHTMLAttributes } from "react";
|
|
4
|
+
|
|
5
|
+
import { Label } from "./label";
|
|
6
|
+
import { cn } from "./utils";
|
|
7
|
+
|
|
8
|
+
type SwitchProps = Omit<InputHTMLAttributes<HTMLInputElement>, "type"> & {
|
|
9
|
+
label: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function Switch({
|
|
14
|
+
label,
|
|
15
|
+
description,
|
|
16
|
+
className,
|
|
17
|
+
id,
|
|
18
|
+
checked,
|
|
19
|
+
defaultChecked,
|
|
20
|
+
...props
|
|
21
|
+
}: SwitchProps) {
|
|
22
|
+
const switchId = id ?? label.toLowerCase().replace(/\s+/g, "-");
|
|
23
|
+
const isChecked = checked ?? defaultChecked;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<label
|
|
27
|
+
htmlFor={switchId}
|
|
28
|
+
className={cn(
|
|
29
|
+
"flex cursor-pointer items-center justify-between gap-4 rounded-2xl border border-(--line-soft) bg-(--surface-card) p-4 shadow-(--shadow-sm)",
|
|
30
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
<span>
|
|
34
|
+
<Label htmlFor={switchId}>{label}</Label>
|
|
35
|
+
{description ? (
|
|
36
|
+
<span suppressHydrationWarning className="type-body mt-1 block text-(--ink-subtle)">
|
|
37
|
+
{description}
|
|
38
|
+
</span>
|
|
39
|
+
) : null}
|
|
40
|
+
</span>
|
|
41
|
+
<span className="relative inline-flex h-7 w-12 shrink-0 items-center">
|
|
42
|
+
<input
|
|
43
|
+
{...props}
|
|
44
|
+
id={switchId}
|
|
45
|
+
checked={checked}
|
|
46
|
+
defaultChecked={defaultChecked}
|
|
47
|
+
type="checkbox"
|
|
48
|
+
className="peer sr-only"
|
|
49
|
+
/>
|
|
50
|
+
<span className="absolute inset-0 rounded-full bg-(--color-line-strong) transition-colors peer-checked:bg-(--color-brand-primary)" />
|
|
51
|
+
<span
|
|
52
|
+
className={cn(
|
|
53
|
+
"absolute left-1 h-5 w-5 rounded-full bg-white shadow-(--shadow-sm) transition-transform peer-checked:translate-x-5",
|
|
54
|
+
isChecked && "translate-x-5",
|
|
55
|
+
)}
|
|
56
|
+
/>
|
|
57
|
+
</span>
|
|
58
|
+
</label>
|
|
59
|
+
);
|
|
60
|
+
}
|
package/src/table.tsx
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
export type TableColumn<T> = {
|
|
6
|
+
key: string;
|
|
7
|
+
header: ReactNode;
|
|
8
|
+
align?: "left" | "center" | "right";
|
|
9
|
+
width?: string;
|
|
10
|
+
render: (row: T, rowIndex: number) => ReactNode;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type TableProps<T> = {
|
|
14
|
+
columns: Array<TableColumn<T>>;
|
|
15
|
+
rows: T[];
|
|
16
|
+
rowKey: (row: T, rowIndex: number) => string;
|
|
17
|
+
emptyState?: ReactNode;
|
|
18
|
+
className?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const alignClasses = {
|
|
22
|
+
left: "text-left",
|
|
23
|
+
center: "text-center",
|
|
24
|
+
right: "text-right",
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
export function Table<T>({
|
|
28
|
+
columns,
|
|
29
|
+
rows,
|
|
30
|
+
rowKey,
|
|
31
|
+
emptyState = "No records found.",
|
|
32
|
+
className,
|
|
33
|
+
}: TableProps<T>) {
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
className={cn(
|
|
37
|
+
"overflow-hidden rounded-xl border border-(--line-soft) bg-(--surface-card) shadow-(--shadow-sm)",
|
|
38
|
+
className,
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
<div className="overflow-x-auto">
|
|
42
|
+
<table className="min-w-full border-collapse">
|
|
43
|
+
<thead className="bg-(--color-surface-muted)">
|
|
44
|
+
<tr>
|
|
45
|
+
{columns.map((column) => (
|
|
46
|
+
<th
|
|
47
|
+
key={column.key}
|
|
48
|
+
className={cn(
|
|
49
|
+
"type-overline px-4 py-3 text-(--ink-subtle)",
|
|
50
|
+
alignClasses[column.align ?? "left"],
|
|
51
|
+
)}
|
|
52
|
+
style={column.width ? { width: column.width } : undefined}
|
|
53
|
+
>
|
|
54
|
+
{column.header}
|
|
55
|
+
</th>
|
|
56
|
+
))}
|
|
57
|
+
</tr>
|
|
58
|
+
</thead>
|
|
59
|
+
<tbody>
|
|
60
|
+
{rows.length ? (
|
|
61
|
+
rows.map((row, rowIndex) => (
|
|
62
|
+
<tr key={rowKey(row, rowIndex)} className="border-t border-(--line-soft)">
|
|
63
|
+
{columns.map((column) => (
|
|
64
|
+
<td
|
|
65
|
+
key={column.key}
|
|
66
|
+
className={cn(
|
|
67
|
+
"type-body px-4 py-4 align-top text-(--foreground)",
|
|
68
|
+
alignClasses[column.align ?? "left"],
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
{column.render(row, rowIndex)}
|
|
72
|
+
</td>
|
|
73
|
+
))}
|
|
74
|
+
</tr>
|
|
75
|
+
))
|
|
76
|
+
) : (
|
|
77
|
+
<tr>
|
|
78
|
+
<td colSpan={columns.length} className="type-body px-4 py-10 text-center text-(--ink-subtle)">
|
|
79
|
+
{emptyState}
|
|
80
|
+
</td>
|
|
81
|
+
</tr>
|
|
82
|
+
)}
|
|
83
|
+
</tbody>
|
|
84
|
+
</table>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|