From 0999f1905eb626b11dac780b4079c1ba0e2616fc Mon Sep 17 00:00:00 2001 From: zetaloop Date: Sat, 25 Apr 2026 19:56:24 +0800 Subject: [PATCH] feat(ui): add linear design foundation --- app/globals.css | 41 ++++++++++++++++++++++++++++------ components/ui/badge.tsx | 4 ++++ components/ui/button.tsx | 2 +- components/ui/empty-state.tsx | 37 ++++++++++++++++++++++++++++++ components/ui/status-badge.tsx | 37 ++++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 components/ui/empty-state.tsx create mode 100644 components/ui/status-badge.tsx diff --git a/app/globals.css b/app/globals.css index 94fd5ac..70f7fb8 100644 --- a/app/globals.css +++ b/app/globals.css @@ -7,8 +7,12 @@ @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); + --font-sans: + var(--font-geist-sans), "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", + "Microsoft YaHei", sans-serif; + --font-mono: + var(--font-geist-mono), "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", + "Microsoft YaHei", monospace; --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -39,6 +43,14 @@ --color-popover: var(--popover); --color-card-foreground: var(--card-foreground); --color-card: var(--card); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); + --color-info: var(--info); + --color-info-foreground: var(--info-foreground); + --color-neutral: var(--neutral); + --color-neutral-foreground: var(--neutral-foreground); --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); @@ -84,9 +96,16 @@ --sidebar-accent-foreground: oklch(0.36 0.1 46.65); --sidebar-border: oklch(0.94 0 0); --sidebar-ring: oklch(0.701 0.189 46.65); - --card-shadow: 0 1px 3px 0 rgba(30, 40, 55, 0.04), 0 4px 12px -2px rgba(30, 40, 55, 0.06); - --card-shadow-hover: - 0 4px 16px -2px rgba(30, 40, 55, 0.07), 0 8px 24px -4px rgba(30, 40, 55, 0.09); + --success: oklch(0.45 0.14 150); + --success-foreground: oklch(0.99 0 0); + --warning: oklch(0.48 0.14 70); + --warning-foreground: oklch(0.14 0 0); + --info: oklch(0.48 0.16 260); + --info-foreground: oklch(0.99 0 0); + --neutral: oklch(0.42 0 0); + --neutral-foreground: oklch(0.99 0 0); + --card-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.02); + --card-shadow-hover: 0 2px 4px 0 rgba(0, 0, 0, 0.04); } .dark { @@ -122,8 +141,16 @@ --sidebar-accent-foreground: oklch(0.839 0.01 264.51); --sidebar-border: oklch(0.291 0.018 264.24); --sidebar-ring: oklch(0.701 0.189 46.66); - --card-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08), 0 4px 12px -2px rgba(0, 0, 0, 0.12); - --card-shadow-hover: 0 4px 16px -2px rgba(0, 0, 0, 0.14), 0 8px 24px -4px rgba(0, 0, 0, 0.18); + --success: oklch(0.74 0.14 150); + --success-foreground: oklch(0.14 0 0); + --warning: oklch(0.78 0.14 70); + --warning-foreground: oklch(0.14 0 0); + --info: oklch(0.75 0.14 260); + --info-foreground: oklch(0.14 0 0); + --neutral: oklch(0.78 0 0); + --neutral-foreground: oklch(0.14 0 0); + --card-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.04); + --card-shadow-hover: 0 2px 4px 0 rgba(0, 0, 0, 0.06); } @layer base { diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx index c720719..d5edc18 100644 --- a/components/ui/badge.tsx +++ b/components/ui/badge.tsx @@ -11,6 +11,10 @@ const badgeVariants = cva( variant: { default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + success: "border-success/20 bg-success/10 text-success [a&]:hover:bg-success/15", + warning: "border-warning/25 bg-warning/10 text-warning [a&]:hover:bg-warning/15", + info: "border-info/20 bg-info/10 text-info [a&]:hover:bg-info/15", + neutral: "border-neutral/20 bg-neutral/10 text-neutral [a&]:hover:bg-neutral/15", destructive: "bg-destructive text-destructive-foreground [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 377bed1..7f24a8b 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -13,7 +13,7 @@ const buttonVariants = cva( destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: - "border bg-background shadow-sm hover:shadow-md hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + "border bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", diff --git a/components/ui/empty-state.tsx b/components/ui/empty-state.tsx new file mode 100644 index 0000000..b7db6e4 --- /dev/null +++ b/components/ui/empty-state.tsx @@ -0,0 +1,37 @@ +import { Inbox } from "lucide-react" +import type * as React from "react" + +import { cn } from "@/lib/utils" + +export interface EmptyStateProps extends React.HTMLAttributes { + title: string + description?: string + icon?: React.ElementType + action?: React.ReactNode +} + +export function EmptyState({ + title, + description, + icon: Icon = Inbox, + action, + className, + ...props +}: EmptyStateProps) { + return ( +
+
+ +
+

{title}

+ {description &&

{description}

} + {action &&
{action}
} +
+ ) +} diff --git a/components/ui/status-badge.tsx b/components/ui/status-badge.tsx new file mode 100644 index 0000000..f73799b --- /dev/null +++ b/components/ui/status-badge.tsx @@ -0,0 +1,37 @@ +import type { VariantProps } from "class-variance-authority" +import { AlertCircle, CheckCircle2, HelpCircle, Info } from "lucide-react" +import type * as React from "react" + +import { Badge, type badgeVariants } from "@/components/ui/badge" + +type StatusBadgeVariant = NonNullable["variant"]> + +export interface StatusBadgeProps extends React.ComponentProps<"span"> { + status: StatusBadgeVariant + icon?: boolean +} + +const statusConfig: Partial> = { + success: CheckCircle2, + warning: AlertCircle, + info: Info, + destructive: AlertCircle, + neutral: HelpCircle, +} + +export function StatusBadge({ + status, + icon = true, + className, + children, + ...props +}: StatusBadgeProps) { + const Icon = icon ? statusConfig[status] : undefined + + return ( + + {Icon && } + {children} + + ) +}