Files
next-tjwater-frontend/src/shared/ui/button.tsx
T
jiang f4318a9b9c feat: refine workbench visuals and map controls
Unify the Agent history extension with the header acrylic surface, preserve the full conversation body, and consolidate shared control and status styling.

Restore map flow and SCADA controller behavior, remove obsolete rendering paths, and extend regression coverage. Button press coverage now releases outside the target so state assertions cannot accidentally toggle the control.
2026-07-28 16:39:36 +08:00

122 lines
5.7 KiB
TypeScript

import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { BUTTON_INTERACTION_CLASS_NAME } from "@/shared/ui/button-interaction"
import { cn } from "@/shared/ui/cn"
import { Spinner } from "@/shared/ui/spinner"
const buttonVariants = cva(
cn(
BUTTON_INTERACTION_CLASS_NAME,
"inline-flex items-center justify-center gap-2 whitespace-nowrap border border-transparent text-sm font-semibold [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0"
),
{
variants: {
variant: {
default:
"border-[var(--action-blue)] bg-[var(--action-blue)] text-white shadow-[var(--action-shadow-primary)] [@media(hover:hover)]:hover:border-[var(--action-blue-hover)] [@media(hover:hover)]:hover:bg-[var(--action-blue-hover)] [&:active:not(:disabled)]:border-[var(--action-blue-pressed)] [&:active:not(:disabled)]:bg-[var(--action-blue-pressed)] [&:active:not(:disabled)]:shadow-[var(--action-shadow-primary-pressed)]",
destructive:
"border-[var(--action-danger)] bg-[var(--action-danger)] text-white shadow-[var(--action-shadow-secondary)] [@media(hover:hover)]:hover:border-[var(--action-danger-hover)] [@media(hover:hover)]:hover:bg-[var(--action-danger-hover)] [&:active:not(:disabled)]:border-[var(--action-danger-pressed)] [&:active:not(:disabled)]:bg-[var(--action-danger-pressed)] [&:active:not(:disabled)]:shadow-[var(--action-shadow-secondary-pressed)]",
outline:
"border-[var(--action-border)] bg-[var(--surface-reading)] text-slate-700 shadow-[var(--action-shadow-secondary)] [@media(hover:hover)]:hover:border-[var(--action-border-hover)] [@media(hover:hover)]:hover:bg-[var(--action-soft)] [@media(hover:hover)]:hover:text-[var(--action-blue-hover)] [&:active:not(:disabled)]:border-[var(--action-border-pressed)] [&:active:not(:disabled)]:bg-[var(--action-soft-pressed)] [&:active:not(:disabled)]:text-[var(--action-blue-pressed)] [&:active:not(:disabled)]:shadow-[var(--action-shadow-secondary-pressed)]",
secondary:
"border-[var(--action-border)] bg-[var(--surface-control)] text-slate-700 shadow-[var(--action-shadow-secondary)] [@media(hover:hover)]:hover:border-[var(--action-border-hover)] [@media(hover:hover)]:hover:bg-[var(--action-soft)] [@media(hover:hover)]:hover:text-[var(--action-blue-hover)] [&:active:not(:disabled)]:border-[var(--action-border-pressed)] [&:active:not(:disabled)]:bg-[var(--action-soft-pressed)] [&:active:not(:disabled)]:text-[var(--action-blue-pressed)] [&:active:not(:disabled)]:shadow-[var(--action-shadow-secondary-pressed)]",
ghost:
"bg-transparent text-slate-600 shadow-none [@media(hover:hover)]:hover:bg-[var(--action-soft)] [@media(hover:hover)]:hover:text-[var(--action-blue-hover)] [&:active:not(:disabled)]:bg-[var(--action-soft-pressed)] [&:active:not(:disabled)]:text-[var(--action-blue-pressed)] [&:active:not(:disabled)]:shadow-none",
link:
"h-auto border-0 bg-transparent p-0 text-[var(--action-blue)] shadow-none underline-offset-4 [@media(hover:hover)]:hover:text-[var(--action-blue-hover)] [@media(hover:hover)]:hover:underline [&:active:not(:disabled)]:text-[var(--action-blue-pressed)] [&:active:not(:disabled)]:shadow-none"
},
size: {
default: "h-10 rounded-xl px-4 py-2",
sm: "h-8 rounded-lg px-3 text-xs after:absolute after:-inset-1 after:content-['']",
lg: "h-11 rounded-xl px-6",
icon: "h-10 w-10 rounded-xl",
"icon-sm":
"h-8 w-8 rounded-lg after:absolute after:-inset-1 after:content-['']"
}
},
defaultVariants: {
variant: "default",
size: "default"
}
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
loading?: boolean
loadingLabel?: string
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
children,
className,
variant,
size,
asChild = false,
disabled,
loading = false,
loadingLabel = "处理中",
onClick,
tabIndex,
...props
},
ref
) => {
const Comp = asChild ? Slot : "button"
const blocked = disabled || loading
const childContent =
asChild && React.isValidElement<{ children?: React.ReactNode }>(children)
? children.props.children
: children
const loadingContent = loading ? (
<>
<span aria-hidden="true" className="invisible inline-flex items-center gap-2">
{childContent}
</span>
<Spinner
aria-hidden="true"
className="pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
/>
<span className="sr-only">{loadingLabel}</span>
</>
) : null
const renderedChildren =
loading && asChild && React.isValidElement(children)
? React.cloneElement(children, undefined, loadingContent)
: loadingContent ?? children
return (
<Comp
aria-busy={loading || undefined}
aria-disabled={asChild && blocked ? true : undefined}
className={cn(buttonVariants({ variant, size, className }))}
data-loading={loading || undefined}
data-slot="button"
disabled={asChild ? undefined : blocked}
onClick={(event) => {
if (asChild && blocked) {
event.preventDefault()
event.stopPropagation()
return
}
onClick?.(event)
}}
ref={ref}
tabIndex={asChild && blocked ? -1 : tabIndex}
{...props}
>
{renderedChildren}
</Comp>
)
}
)
Button.displayName = "Button"
export { Button }