- pnpm workspace: apps/{web,server}, packages/{contracts,db,domain,adapters,tasks,agent,testkit}
- server: Hono + zod-openapi, API Key auth with scopes, redacting pino logger, SSE event bus with replay, worker heartbeat
- db: Kysely + pg, file migrator, 0001_init (settings/api_key/audit_event/outbox_event)
- web: React Router 7 SPA console shell with 12 workspace placeholders, uhd_web tokens/ui/guard scripts
- docs: ADR-001 (Node over Deno), dev runbook, design system rules
Co-Authored-By: Claude Fable 5.1 <[email protected]>
69 lines
2.9 KiB
TypeScript
69 lines
2.9 KiB
TypeScript
import { cva, type VariantProps } from "class-variance-authority";
|
|
import { Slot } from "radix-ui";
|
|
import type * as React from "react";
|
|
|
|
import { cn } from "~/lib/utils";
|
|
|
|
const buttonVariants = cva(
|
|
// 全站只有这一套按钮(与动漫站同一思路):形状一律胶囊 / 正圆,颜色与材质只由 variant 决定,
|
|
// 调用处只给布局(宽度、高度、间距),不改圆角、底色、边框、投影和字色。
|
|
// 不是 <button> 的元素(Link、a)用 buttonVariants() 拿同一套类名
|
|
"inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-full text-sm font-medium whitespace-nowrap transition-[transform,background-color,border-color,color,box-shadow,opacity] duration-base ease-spring outline-none active:scale-[0.96] focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
{
|
|
variants: {
|
|
variant: {
|
|
// 主操作:半透明胶囊 + 沿边框流转的棱镜光谱。样式见 app.css 的 .pill-primary(--mat-pill-primary-*、--prism-*)
|
|
default: "pill-primary",
|
|
// 次操作:描边胶囊。outline 与 secondary 是同一个样子,保留两个名字只为兼容旧调用
|
|
outline: "pill-secondary",
|
|
secondary: "pill-secondary",
|
|
// 危险操作:同一胶囊,淡红底红字
|
|
destructive:
|
|
"border border-destructive/35 bg-destructive/10 text-destructive hover:bg-destructive/15 focus-visible:ring-destructive/25 dark:focus-visible:ring-destructive/40",
|
|
// 只有文字 / 图标,悬停出淡底
|
|
ghost: "text-muted-foreground hover:bg-foreground/[0.06] hover:text-foreground",
|
|
link: "text-primary underline-offset-4 hover:underline active:scale-100",
|
|
},
|
|
size: {
|
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
xs: "h-6 gap-1 px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
lg: "h-11 px-6 has-[>svg]:px-5",
|
|
icon: "size-9",
|
|
"icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3",
|
|
"icon-sm": "size-8",
|
|
"icon-lg": "size-10",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: "default",
|
|
size: "default",
|
|
},
|
|
},
|
|
);
|
|
|
|
function Button({
|
|
className,
|
|
variant = "default",
|
|
size = "default",
|
|
asChild = false,
|
|
...props
|
|
}: React.ComponentProps<"button"> &
|
|
VariantProps<typeof buttonVariants> & {
|
|
asChild?: boolean;
|
|
}) {
|
|
const Comp = asChild ? Slot.Root : "button";
|
|
|
|
return (
|
|
<Comp
|
|
data-slot="button"
|
|
data-variant={variant}
|
|
data-size={size}
|
|
className={cn(buttonVariants({ variant, size, className }))}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export { Button, buttonVariants };
|