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.
This commit is contained in:
+80
-17
@@ -2,36 +2,44 @@ 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(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
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:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
"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:
|
||||
"bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90",
|
||||
"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 border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||
"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:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
"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-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
"icon-sm": "h-8 w-8",
|
||||
},
|
||||
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",
|
||||
},
|
||||
size: "default"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -39,17 +47,72 @@ export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
loading?: boolean
|
||||
loadingLabel?: string
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
(
|
||||
{
|
||||
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>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user