From 025ae31f9ffb4650a6fcf440e044598ccc95f075 Mon Sep 17 00:00:00 2001 From: afei A <57030625+NewHubBoy@users.noreply.github.com> Date: Sat, 28 Mar 2026 00:16:01 +0800 Subject: [PATCH] feat: add dense console ui primitives --- src/components/ui/button.tsx | 6 +-- src/components/ui/card.tsx | 10 ++-- src/components/ui/input.tsx | 26 ++++++++++ src/components/ui/metric-chip.tsx | 32 ++++++++++++ src/components/ui/page-toolbar.tsx | 23 +++++++++ src/components/ui/select.tsx | 27 +++++++++++ src/components/ui/separator.tsx | 15 ++++++ src/components/ui/status-badge.tsx | 2 +- src/components/ui/table.tsx | 78 ++++++++++++++++++++++++++++++ tests/ui/page-toolbar.test.tsx | 19 ++++++++ tests/ui/status-badge.test.tsx | 7 +++ 11 files changed, 236 insertions(+), 9 deletions(-) create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/metric-chip.tsx create mode 100644 src/components/ui/page-toolbar.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/separator.tsx create mode 100644 src/components/ui/table.tsx create mode 100644 tests/ui/page-toolbar.test.tsx diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index adf7a23..2151e7f 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -26,9 +26,9 @@ const VARIANT_STYLES: Record = { }; const SIZE_STYLES: Record = { - sm: "min-h-9 rounded-full px-3.5 text-sm", - md: "min-h-11 rounded-full px-4 text-sm", - lg: "min-h-12 rounded-full px-5 text-base", + sm: "h-8 rounded-md px-2.5 text-xs", + md: "h-9 rounded-md px-3 text-sm", + lg: "h-10 rounded-md px-4 text-sm", }; function joinClasses(...values: Array) { diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 911e17d..1add002 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -19,7 +19,7 @@ export const Card = forwardRef(
(
(

); @@ -99,7 +99,7 @@ export const CardFooter = forwardRef(
; + +function joinClasses(...values: Array) { + return values.filter(Boolean).join(" "); +} + +export const Input = forwardRef( + ({ className, type = "text", ...props }, ref) => { + return ( + + ); + }, +); + +Input.displayName = "Input"; diff --git a/src/components/ui/metric-chip.tsx b/src/components/ui/metric-chip.tsx new file mode 100644 index 0000000..d232859 --- /dev/null +++ b/src/components/ui/metric-chip.tsx @@ -0,0 +1,32 @@ +import type { HTMLAttributes, ReactNode } from "react"; + +type MetricChipProps = HTMLAttributes & { + label: string; + value: ReactNode; +}; + +function joinClasses(...values: Array) { + return values.filter(Boolean).join(" "); +} + +export function MetricChip({ + className, + label, + value, + ...props +}: MetricChipProps) { + return ( +
+ + {label} + + {value} +
+ ); +} diff --git a/src/components/ui/page-toolbar.tsx b/src/components/ui/page-toolbar.tsx new file mode 100644 index 0000000..adf34b1 --- /dev/null +++ b/src/components/ui/page-toolbar.tsx @@ -0,0 +1,23 @@ +import type { HTMLAttributes, PropsWithChildren } from "react"; + +function joinClasses(...values: Array) { + return values.filter(Boolean).join(" "); +} + +export function PageToolbar({ + children, + className, + ...props +}: PropsWithChildren>) { + return ( +
+ {children} +
+ ); +} diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..d68dfa0 --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,27 @@ +import { forwardRef, type SelectHTMLAttributes } from "react"; + +type SelectProps = SelectHTMLAttributes; + +function joinClasses(...values: Array) { + return values.filter(Boolean).join(" "); +} + +export const Select = forwardRef( + ({ className, children, ...props }, ref) => { + return ( + + ); + }, +); + +Select.displayName = "Select"; diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..5293901 --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,15 @@ +import type { HTMLAttributes } from "react"; + +function joinClasses(...values: Array) { + return values.filter(Boolean).join(" "); +} + +export function Separator({ className, ...props }: HTMLAttributes) { + return ( +
+ ); +} diff --git a/src/components/ui/status-badge.tsx b/src/components/ui/status-badge.tsx index 3af4435..3e3077b 100644 --- a/src/components/ui/status-badge.tsx +++ b/src/components/ui/status-badge.tsx @@ -104,7 +104,7 @@ export function StatusBadge({ className, ...props }: StatusBadgeProps) { ) { + return values.filter(Boolean).join(" "); +} + +export const Table = forwardRef>( + ({ className, ...props }, ref) => ( +
+ + + ), +); + +Table.displayName = "Table"; + +export const TableHeader = forwardRef>( + ({ className, ...props }, ref) => ( + + ), +); + +TableHeader.displayName = "TableHeader"; + +export const TableBody = forwardRef>( + ({ className, ...props }, ref) => ( + + ), +); + +TableBody.displayName = "TableBody"; + +export const TableRow = forwardRef>( + ({ className, ...props }, ref) => ( + + ), +); + +TableRow.displayName = "TableRow"; + +export const TableHead = forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); + +TableHead.displayName = "TableHead"; + +export const TableCell = forwardRef>( + ({ className, ...props }, ref) => ( + + ), +); + +TableCell.displayName = "TableCell"; diff --git a/tests/ui/page-toolbar.test.tsx b/tests/ui/page-toolbar.test.tsx new file mode 100644 index 0000000..e506fba --- /dev/null +++ b/tests/ui/page-toolbar.test.tsx @@ -0,0 +1,19 @@ +import { render, screen } from "@testing-library/react"; + +import { PageToolbar } from "@/components/ui/page-toolbar"; +import { Input } from "@/components/ui/input"; +import { Select } from "@/components/ui/select"; + +test("renders a dense toolbar row with compact controls", () => { + render( + + + + , + ); + + expect(screen.getByLabelText("search").className).toContain("h-9"); + expect(screen.getByLabelText("status").className).toContain("h-9"); +}); diff --git a/tests/ui/status-badge.test.tsx b/tests/ui/status-badge.test.tsx index 566169f..2f1b38f 100644 --- a/tests/ui/status-badge.test.tsx +++ b/tests/ui/status-badge.test.tsx @@ -21,6 +21,13 @@ test("uses order status metadata for the rendered tone", () => { expect(screen.getByText("失败")).toHaveAttribute("data-tone", "danger"); }); +test("uses compact dense-console badge sizing", () => { + render(); + + expect(screen.getByText("待审核").className).toContain("px-2"); + expect(screen.getByText("待审核").className).toContain("py-0.5"); +}); + test("can render review decision metadata when a variant is provided", () => { render();