feat: adopt kumo green ui system
This commit is contained in:
@@ -1,31 +1,26 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Badge as KumoBadge } from "@cloudflare/kumo/components/badge";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
type KumoBadgeProps = React.ComponentProps<typeof KumoBadge>;
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-1 text-xs font-medium transition-colors",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-transparent bg-primary text-primary-foreground",
|
||||
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
||||
outline: "text-foreground",
|
||||
success: "border-emerald-200 bg-emerald-50 text-emerald-700",
|
||||
warning: "border-amber-200 bg-amber-50 text-amber-700",
|
||||
danger: "border-red-200 bg-red-50 text-red-700",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
type LegacyBadgeVariant = "default" | "secondary" | "outline" | "success" | "warning" | "danger";
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
const variantMap: Record<LegacyBadgeVariant, KumoBadgeProps["variant"]> = {
|
||||
default: "green",
|
||||
secondary: "secondary",
|
||||
outline: "outline",
|
||||
success: "success",
|
||||
warning: "warning",
|
||||
danger: "error",
|
||||
};
|
||||
|
||||
export function Badge({ className, variant, ...props }: BadgeProps): React.ReactElement {
|
||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
||||
export type BadgeProps = Omit<KumoBadgeProps, "variant" | "children"> & {
|
||||
variant?: LegacyBadgeVariant | KumoBadgeProps["variant"];
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function Badge({ variant = "default", ...props }: BadgeProps): React.ReactElement {
|
||||
const mappedVariant = variant in variantMap ? variantMap[variant as LegacyBadgeVariant] : (variant as KumoBadgeProps["variant"]);
|
||||
|
||||
return <KumoBadge variant={mappedVariant} {...(props as KumoBadgeProps)} />;
|
||||
}
|
||||
|
||||
@@ -1,50 +1,59 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import {
|
||||
Button as KumoButton,
|
||||
LinkButton as KumoLinkButton,
|
||||
type ButtonProps as KumoButtonProps,
|
||||
type LinkButtonProps as KumoLinkButtonProps,
|
||||
} from "@cloudflare/kumo/components/button";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
type ButtonVariant = "default" | "secondary" | "outline" | "ghost" | "destructive";
|
||||
type ButtonSize = "default" | "sm" | "lg" | "icon";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex min-h-11 shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
outline: "border bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
},
|
||||
size: {
|
||||
default: "h-11 px-4 py-2",
|
||||
sm: "h-9 min-h-9 px-3",
|
||||
lg: "h-12 px-6",
|
||||
icon: "h-11 w-11",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
const variantMap: Record<ButtonVariant, KumoButtonProps["variant"]> = {
|
||||
default: "primary",
|
||||
secondary: "secondary",
|
||||
outline: "outline",
|
||||
ghost: "ghost",
|
||||
destructive: "destructive",
|
||||
};
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
const sizeMap: Record<ButtonSize, KumoButtonProps["size"]> = {
|
||||
default: "base",
|
||||
sm: "sm",
|
||||
lg: "lg",
|
||||
icon: "base",
|
||||
};
|
||||
|
||||
export function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: ButtonProps): React.ReactElement {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
export type ButtonProps = Omit<KumoButtonProps, "variant" | "size" | "shape"> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
|
||||
return <Comp className={cn(buttonVariants({ variant, size, className }))} {...props} />;
|
||||
}
|
||||
export type LinkButtonProps = Omit<KumoLinkButtonProps, "variant" | "size"> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: Exclude<ButtonSize, "icon">;
|
||||
};
|
||||
|
||||
export { buttonVariants };
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
{ variant = "default", size = "default", ...props },
|
||||
ref,
|
||||
): React.ReactElement {
|
||||
const kumoProps = {
|
||||
ref,
|
||||
shape: size === "icon" ? "square" : "base",
|
||||
size: sizeMap[size],
|
||||
variant: variantMap[variant],
|
||||
...props,
|
||||
} as KumoButtonProps & React.RefAttributes<HTMLButtonElement>;
|
||||
|
||||
return (
|
||||
<KumoButton {...kumoProps} />
|
||||
);
|
||||
});
|
||||
|
||||
export const LinkButton = React.forwardRef<HTMLAnchorElement, LinkButtonProps>(function LinkButton(
|
||||
{ variant = "default", size = "default", ...props },
|
||||
ref,
|
||||
): React.ReactElement {
|
||||
return <KumoLinkButton ref={ref} size={sizeMap[size]} variant={variantMap[variant]} {...props} />;
|
||||
});
|
||||
|
||||
@@ -1,43 +1,29 @@
|
||||
import * as React from "react";
|
||||
import { LayerCard } from "@cloudflare/kumo/components/layer-card";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Card({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>): React.ReactElement {
|
||||
export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>): React.ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
|
||||
<LayerCard
|
||||
className={cn("rounded-lg border border-kumo-line bg-kumo-base text-card-foreground shadow-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CardHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>): React.ReactElement {
|
||||
return <div className={cn("flex flex-col gap-1.5 p-5", className)} {...props} />;
|
||||
export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>): React.ReactElement {
|
||||
return <LayerCard.Secondary className={cn("flex flex-col gap-1.5 p-5", className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CardTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLHeadingElement>): React.ReactElement {
|
||||
return <h3 className={cn("text-base font-semibold leading-none", className)} {...props} />;
|
||||
export function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>): React.ReactElement {
|
||||
return <h3 className={cn("text-base font-semibold leading-none text-kumo-strong", className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CardDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLParagraphElement>): React.ReactElement {
|
||||
return <p className={cn("text-sm text-muted-foreground", className)} {...props} />;
|
||||
export function CardDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>): React.ReactElement {
|
||||
return <p className={cn("text-sm text-kumo-subtle", className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CardContent({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>): React.ReactElement {
|
||||
return <div className={cn("p-5 pt-0", className)} {...props} />;
|
||||
export function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>): React.ReactElement {
|
||||
return <LayerCard.Primary className={cn("p-5 pt-0", className)} {...props} />;
|
||||
}
|
||||
|
||||
39
components/ui/checkbox.tsx
Normal file
39
components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Checkbox as KumoCheckbox, type CheckboxProps as KumoCheckboxProps } from "@cloudflare/kumo/components/checkbox";
|
||||
|
||||
type CheckboxProps = Omit<KumoCheckboxProps, "checked" | "onCheckedChange"> & {
|
||||
checked?: boolean;
|
||||
defaultChecked?: boolean;
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
};
|
||||
|
||||
export function Checkbox({
|
||||
checked,
|
||||
defaultChecked = false,
|
||||
name,
|
||||
onCheckedChange,
|
||||
...props
|
||||
}: CheckboxProps): React.ReactElement {
|
||||
const [uncontrolledChecked, setUncontrolledChecked] = React.useState(defaultChecked);
|
||||
const isControlled = checked !== undefined;
|
||||
const currentChecked = isControlled ? checked : uncontrolledChecked;
|
||||
|
||||
return (
|
||||
<>
|
||||
<KumoCheckbox
|
||||
checked={currentChecked}
|
||||
onCheckedChange={(nextChecked) => {
|
||||
const booleanValue = Boolean(nextChecked);
|
||||
if (!isControlled) {
|
||||
setUncontrolledChecked(booleanValue);
|
||||
}
|
||||
onCheckedChange?.(booleanValue);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
{name && currentChecked ? <input name={name} type="hidden" value="on" /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { X } from "lucide-react";
|
||||
import { Dialog as KumoDialog } from "@cloudflare/kumo/components/dialog";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type DialogProps = {
|
||||
@@ -21,38 +25,35 @@ export function Dialog({
|
||||
footer,
|
||||
onClose,
|
||||
className,
|
||||
}: DialogProps): React.ReactElement | null {
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}: DialogProps): React.ReactElement {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/32 p-3 backdrop-blur-sm sm:items-center">
|
||||
<section
|
||||
aria-modal="true"
|
||||
<KumoDialog.Root
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (!nextOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
open={open}
|
||||
>
|
||||
<KumoDialog
|
||||
className={cn(
|
||||
"flex max-h-[92svh] w-full max-w-3xl flex-col overflow-hidden rounded-lg border bg-card shadow-xl",
|
||||
"flex max-h-[92svh] w-[min(calc(100vw-1.5rem),48rem)] flex-col overflow-hidden rounded-lg border border-kumo-line bg-kumo-base p-0 shadow-xl",
|
||||
className,
|
||||
)}
|
||||
role="dialog"
|
||||
size="xl"
|
||||
>
|
||||
<header className="flex shrink-0 items-start justify-between gap-4 border-b p-5">
|
||||
<header className="flex shrink-0 items-start justify-between gap-4 border-b border-kumo-line p-5">
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-lg font-semibold">{title}</h2>
|
||||
{description ? <p className="mt-1 text-sm text-muted-foreground">{description}</p> : null}
|
||||
<KumoDialog.Title>{title}</KumoDialog.Title>
|
||||
{description ? <KumoDialog.Description className="mt-1">{description}</KumoDialog.Description> : null}
|
||||
</div>
|
||||
<button
|
||||
aria-label="关闭弹窗"
|
||||
className="inline-flex size-10 shrink-0 items-center justify-center rounded-md border bg-background text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
>
|
||||
<Button aria-label="关闭弹窗" onClick={onClose} size="icon" type="button" variant="outline">
|
||||
<X className="size-4" aria-hidden="true" />
|
||||
</button>
|
||||
</Button>
|
||||
</header>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-5">{children}</div>
|
||||
{footer ? <footer className="shrink-0 border-t bg-secondary/35 p-4">{footer}</footer> : null}
|
||||
</section>
|
||||
</div>
|
||||
{footer ? <footer className="shrink-0 border-t border-kumo-line bg-kumo-recessed p-4">{footer}</footer> : null}
|
||||
</KumoDialog>
|
||||
</KumoDialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
export function Input({ className, type, ...props }: InputProps): React.ReactElement {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export { Input, Textarea, type InputProps, type InputAreaProps as TextareaProps } from "@cloudflare/kumo/components/input";
|
||||
|
||||
49
components/ui/select.tsx
Normal file
49
components/ui/select.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Select as KumoSelect } from "@cloudflare/kumo/components/select";
|
||||
|
||||
export type SelectOption = {
|
||||
value: string;
|
||||
label: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type SelectProps = {
|
||||
"aria-label"?: string;
|
||||
className?: string;
|
||||
defaultValue?: string;
|
||||
disabled?: boolean;
|
||||
label?: React.ReactNode;
|
||||
name?: string;
|
||||
onValueChange?: (value: string) => void;
|
||||
options: SelectOption[];
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
export function Select({
|
||||
onValueChange,
|
||||
options,
|
||||
placeholder,
|
||||
value,
|
||||
defaultValue,
|
||||
...props
|
||||
}: SelectProps): React.ReactElement {
|
||||
return (
|
||||
<KumoSelect<string>
|
||||
defaultValue={defaultValue}
|
||||
onValueChange={(nextValue) => onValueChange?.(String(nextValue))}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
{...props}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<KumoSelect.Option key={option.value} disabled={option.disabled} value={option.value}>
|
||||
{option.label}
|
||||
</KumoSelect.Option>
|
||||
))}
|
||||
</KumoSelect>
|
||||
);
|
||||
}
|
||||
1
components/ui/table.tsx
Normal file
1
components/ui/table.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { Table, type KumoTableLayout, type KumoTableRowVariant } from "@cloudflare/kumo/components/table";
|
||||
Reference in New Issue
Block a user