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({
) {