From 5f558832f1e52fdb15341d79675b384b966b326d Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 1 Jul 2026 11:52:07 +0800 Subject: [PATCH] feat: polish solution ui and design docs --- docs/frontend/design-system.md | 27 +++- frontend/src/app/router.tsx | 4 + frontend/src/components/ui/badge.tsx | 2 +- frontend/src/components/ui/button.tsx | 20 ++- frontend/src/components/ui/card.tsx | 10 +- frontend/src/components/ui/input.tsx | 2 +- frontend/src/components/ui/select.tsx | 8 +- frontend/src/components/ui/textarea.tsx | 2 +- .../features/solutions/SolutionLinkPanel.tsx | 27 ++-- .../features/solutions/SolutionListPage.tsx | 129 +++++++++++++++--- frontend/src/styles/globals.css | 108 ++++----------- frontend/tailwind.config.ts | 19 +++ 12 files changed, 216 insertions(+), 142 deletions(-) diff --git a/docs/frontend/design-system.md b/docs/frontend/design-system.md index 1c9ca69..e2b6fa7 100644 --- a/docs/frontend/design-system.md +++ b/docs/frontend/design-system.md @@ -2,9 +2,11 @@ ## Principles -- Build product screens from shared tokens and primitives. +- Build product screens from shadcn/ui primitives and project-owned App components. - Keep operational pages dense, quiet, and optimized for repeated use. - Use semantic status colors for ticket state, priority, audit, AI, and system feedback. +- User-facing frontend text must be Chinese-first. Internal TypeScript names and API field names may stay English. +- Business pages must not create one-off table, filter, detail, status, or priority patterns. ## Tokens @@ -19,18 +21,35 @@ Core tokens: ## Components -Base UI primitives: +shadcn/ui primitives live in `frontend/src/components/ui` and should remain close to generated shadcn APIs: - `Button` - `Badge` -- `Card`, `CardHeader`, `CardContent` +- `Card`, `CardHeader`, `CardContent`, `CardTitle`, `CardDescription` +- `Input`, `Textarea`, `Select` +- `Table` +- `Dialog`, `Sheet`, `DropdownMenu`, `Tabs`, `Tooltip`, `Skeleton`, `Sonner` -Product components: +Product App components live in `frontend/src/components/app` and are the preferred page-level API: +- `AppSidebar` +- `AppTopbar` - `AppPageHeader` +- `AppDataTable` +- `AppFilterBar` +- `AppDetailLayout` +- `AppSection` +- `AppEmptyState` - `AppStatusBadge` - `AppPriorityBadge` +Business pages should use: + +- `AppDataTable` for repeated table/list surfaces. +- `AppFilterBar` for search and filter controls. +- `AppDetailLayout` for detail pages with main content and side panels. +- `AppStatusBadge` and `AppPriorityBadge` for ticket lifecycle and priority presentation. + Preview page: - `frontend/src/features/design-system/DesignSystemPreview.tsx` diff --git a/frontend/src/app/router.tsx b/frontend/src/app/router.tsx index ba3164e..b7707dd 100644 --- a/frontend/src/app/router.tsx +++ b/frontend/src/app/router.tsx @@ -32,6 +32,10 @@ export function Router() { return } + if (route === '/tickets') { + return + } + if (route === '/tickets/new') { return } diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx index 8c4dc43..4d19928 100644 --- a/frontend/src/components/ui/badge.tsx +++ b/frontend/src/components/ui/badge.tsx @@ -5,7 +5,7 @@ import { Slot } from "radix-ui" import { cn } from "@/lib/utils" const badgeVariants = cva( - "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", + "inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-md border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-colors focus-visible:ring-2 focus-visible:ring-ring/40 [&>svg]:pointer-events-none [&>svg]:size-3", { variants: { variant: { diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 75b8c3d..078040f 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { Slot } from "radix-ui" import { cn } from "@/lib/utils" const buttonVariants = cva( - "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "inline-flex shrink-0 select-none items-center justify-center whitespace-nowrap rounded-md border border-transparent text-sm font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", { variants: { variant: { @@ -13,24 +13,22 @@ const buttonVariants = cva( outline: "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", secondary: - "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + "bg-secondary text-secondary-foreground hover:bg-muted", ghost: "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", destructive: - "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", + "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:ring-destructive/20", link: "text-primary underline-offset-4 hover:underline", }, size: { default: - "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", - lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + "h-8 gap-1.5 px-3", + xs: "h-6 gap-1 px-2 text-xs", + sm: "h-7 gap-1 px-2.5 text-xs", + lg: "h-9 gap-1.5 px-4", icon: "size-8", - "icon-xs": - "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", - "icon-sm": - "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg", + "icon-xs": "size-6", + "icon-sm": "size-7", "icon-lg": "size-9", }, }, diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx index 4458dae..1b393b0 100644 --- a/frontend/src/components/ui/card.tsx +++ b/frontend/src/components/ui/card.tsx @@ -12,7 +12,7 @@ function Card({ data-slot="card" data-size={size} className={cn( - "group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", + "flex flex-col gap-4 overflow-hidden rounded-lg border border-border bg-card py-4 text-sm text-card-foreground shadow-sm", className )} {...props} @@ -25,7 +25,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
) {
) { return (
) @@ -84,7 +84,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
) { type={type} data-slot="input" className={cn( - "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", + "h-8 w-full min-w-0 rounded-md border border-input bg-transparent px-2.5 py-1 text-base outline-none transition-colors file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 md:text-sm", className )} {...props} diff --git a/frontend/src/components/ui/select.tsx b/frontend/src/components/ui/select.tsx index 8333850..2a36264 100644 --- a/frontend/src/components/ui/select.tsx +++ b/frontend/src/components/ui/select.tsx @@ -42,7 +42,7 @@ function SelectTrigger({ data-slot="select-trigger" data-size={size} className={cn( - "flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "flex h-8 w-fit select-none items-center justify-between gap-1.5 whitespace-nowrap rounded-md border border-input bg-transparent px-2.5 py-1 text-sm outline-none transition-colors focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-50 data-placeholder:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0", className )} {...props} @@ -67,7 +67,7 @@ function SelectContent({ @@ -110,7 +110,7 @@ function SelectItem({ ) {