refactor(platform): 统一控件 - 统一输入按钮与图标控件样式表现

This commit is contained in:
ddcat666
2026-09-25 06:34:42 +08:00
parent a755102b09
commit 84e5be0bdb
15 changed files with 100 additions and 111 deletions

View File

@@ -359,7 +359,7 @@ function BatchReferenceHandle({ column, index, theme, commonStyle, onConnectStar
}, []);
return (
<Tooltip title={`连接到${column.label}`} placement="left">
<button type="button" aria-label={`${column.label}连线点`} className="group absolute z-[var(--node-z-handle)] grid place-items-center rounded-full outline-none" style={{ ...commonStyle, top: BATCH_REFERENCE_HANDLE_TOP + index * BATCH_REFERENCE_HANDLE_GAP, cursor: "crosshair" }} onPointerEnter={(event) => { setHovered(true); update(event); }} onPointerMove={update} onPointerLeave={reset} onPointerDown={(event) => { event.stopPropagation(); onConnectStart(event, handleId); }} onPointerUp={(event) => { event.stopPropagation(); onConnectDrop?.(event, handleId); }}>
<button type="button" data-icon-only aria-label={`${column.label}连线点`} className="group absolute z-[var(--node-z-handle)] grid place-items-center rounded-full outline-none" style={{ ...commonStyle, top: BATCH_REFERENCE_HANDLE_TOP + index * BATCH_REFERENCE_HANDLE_GAP, cursor: "crosshair" }} onPointerEnter={(event) => { setHovered(true); update(event); }} onPointerMove={update} onPointerLeave={reset} onPointerDown={(event) => { event.stopPropagation(); onConnectStart(event, handleId); }} onPointerUp={(event) => { event.stopPropagation(); onConnectDrop?.(event, handleId); }}>
<span className="grid size-[18px] place-items-center rounded-full border text-[8px] font-semibold shadow-sm transition-transform duration-100 group-hover:scale-125 group-focus-visible:scale-125" style={{ transform: `translate(${offset.x}px, ${offset.y}px) scale(${hovered ? 1.06 : 1})`, background: theme.node.panel, borderColor: theme.accent.primary, color: theme.accent.primary }}>{index + 1}</span>
</button>
</Tooltip>

View File

@@ -1051,6 +1051,7 @@ export function AgentChatComposer({
whileTap={canSubmit && !reducedMotion ? { scale: 0.9, y: 1 } : undefined}
animate={stopping && !reducedMotion ? { scale: [1, 0.94, 1] } : { scale: 1, rotate: 0 }}
transition={sending && !reducedMotion ? { duration: 0.42, ease: "easeOut" } : { type: "spring", stiffness: 420, damping: 24 }}
data-icon-only
className="agent-composer-send grid size-7 shrink-0 place-items-center rounded-full p-0 outline-none transition-[background-color,box-shadow,color,transform] duration-200 focus-visible:ring-2 focus-visible:ring-current/35 disabled:cursor-not-allowed"
style={{
background: canSubmit || sending ? theme.accent.primary : theme.spatial.surface,

View File

@@ -1038,10 +1038,7 @@ function formatContextBytes(bytes: number | undefined) {
}
function AgentContextRing({ view }: { view: AgentContextUsageView }) {
const reducedMotion = useReducedMotion();
const [open, setOpen] = useState(false);
const radius = 15;
const circumference = 2 * Math.PI * radius;
const marker = view.compactRatio && view.compactRatio > 0 && view.compactRatio < 1 ? view.compactRatio : undefined;
const percent = view.ratio === undefined ? view.label : `${Math.round(view.ratio * 100)}%`;
const meterLabel = view.ratio === undefined ? "—" : percent;
@@ -1147,20 +1144,15 @@ function AgentContextRing({ view }: { view: AgentContextUsageView }) {
title="查看上下文用量"
onPointerDown={(event) => event.stopPropagation()}
>
<span className="agent-context-ring-visual" aria-hidden="true">
<svg viewBox="0 0 40 40">
<circle className="agent-context-ring-track" cx="20" cy="20" r={radius} />
<circle
className="agent-context-ring-value"
cx="20"
cy="20"
r={radius}
strokeDasharray={`${circumference} ${circumference}`}
strokeDashoffset={circumference * (1 - view.ring)}
style={{ transition: reducedMotion ? "none" : undefined }}
/>
{marker ? <circle className="agent-context-ring-marker" cx="20" cy={20 - radius} r="1.35" transform={`rotate(${marker * 360} 20 20)`} /> : null}
</svg>
<span
className="agent-context-ring-visual"
aria-hidden="true"
style={{
"--agent-context-progress": `${view.ring * 100}%`,
"--agent-context-marker-angle": `${(marker || 0) * 360}deg`,
} as CSSProperties}
>
{marker ? <span className="agent-context-ring-marker" /> : null}
</span>
<span className="agent-context-meter-copy">
<strong>{meterLabel}</strong>

View File

@@ -51,26 +51,26 @@
}
.agent-context-ring {
--agent-context-ring-color: var(--agent-accent, currentColor);
position: relative;
display: inline-flex;
flex: none;
align-items: center;
gap: 7px;
min-width: 68px;
height: 32px;
padding: 3px 8px 3px 4px;
border: 1px solid color-mix(in srgb, var(--agent-ink) 10%, transparent);
gap: 6px;
min-width: 64px;
height: 30px;
padding: 2px 7px 2px 3px;
border: 0;
border-radius: 999px;
background: color-mix(in srgb, var(--agent-ink) 4%, transparent);
background: color-mix(in srgb, var(--agent-ink) 5%, transparent);
color: var(--agent-caption);
cursor: pointer;
transition: border-color 160ms ease, background-color 160ms ease, color 160ms ease;
transition: background-color 160ms ease, color 160ms ease;
}
.agent-context-ring:hover,
.agent-context-ring[aria-expanded="true"] {
border-color: color-mix(in srgb, var(--agent-accent) 48%, transparent);
background: var(--agent-surface-raised);
background: color-mix(in srgb, var(--agent-ink) 9%, transparent);
color: var(--agent-ink);
}
@@ -79,45 +79,61 @@
outline-offset: 2px;
}
.agent-context-ring-visual,
.agent-context-ring-visual svg {
.agent-context-ring-visual {
position: relative;
display: block;
width: 25px;
height: 25px;
width: 26px;
height: 26px;
flex: none;
overflow: hidden;
border-radius: 50%;
background: conic-gradient(
from -90deg,
var(--agent-context-ring-color) 0 var(--agent-context-progress),
color-mix(in srgb, var(--agent-ink) 11%, transparent) var(--agent-context-progress) 100%
);
}
.agent-context-ring-visual svg {
transform: rotate(-90deg);
}
.agent-context-ring circle {
fill: none;
stroke-linecap: round;
}
.agent-context-ring-track {
stroke: color-mix(in srgb, var(--agent-ink) 18%, transparent);
stroke-width: 2.4;
}
.agent-context-ring-value {
stroke: var(--agent-accent, currentColor);
stroke-width: 2.4;
transition: stroke-dashoffset 240ms ease;
.agent-context-ring-visual::after {
position: absolute;
inset: 5px;
border-radius: 50%;
background: color-mix(in srgb, var(--agent-surface-raised) 92%, transparent);
content: "";
}
.agent-context-ring-marker {
fill: var(--agent-ink);
stroke: none;
position: absolute;
inset: 2px;
z-index: 1;
border-radius: 50%;
transform: rotate(var(--agent-context-marker-angle));
}
.agent-context-ring.is-watch .agent-context-ring-value { stroke: #d9973b; }
.agent-context-ring.is-compress .agent-context-ring-value,
.agent-context-ring.is-compacting .agent-context-ring-value { stroke: #d4534e; }
.agent-context-ring.is-compacting .agent-context-ring-visual svg { animation: agent-context-spin 1.4s linear infinite; }
.agent-context-ring.is-unknown .agent-context-ring-value,
.agent-context-ring.is-stale .agent-context-ring-value { stroke-dasharray: 4 6; }
.agent-context-ring-marker::after {
position: absolute;
top: -1px;
left: 50%;
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--agent-context-ring-color);
content: "";
transform: translateX(-50%);
}
.agent-context-ring.is-watch {
--agent-context-ring-color: #d9973b;
}
.agent-context-ring.is-compress,
.agent-context-ring.is-compacting {
--agent-context-ring-color: #d4534e;
}
.agent-context-ring.is-compacting .agent-context-ring-visual { animation: agent-context-spin 1.4s linear infinite; }
.agent-context-ring.is-unknown .agent-context-ring-visual,
.agent-context-ring.is-stale .agent-context-ring-visual { opacity: 0.72; }
.agent-context-meter-copy {
display: grid;
@@ -267,7 +283,6 @@
border-radius: inherit;
background: #8b93ff;
background: linear-gradient(90deg, color-mix(in srgb, var(--agent-accent, #8b93ff) 58%, white), var(--agent-accent, #8b93ff));
box-shadow: 0 0 12px color-mix(in srgb, var(--agent-accent, #8b93ff) 48%, transparent);
transition: width 240ms ease;
}
@@ -359,8 +374,8 @@
}
@media (prefers-reduced-motion: reduce) {
.agent-context-ring.is-compacting .agent-context-ring-visual svg { animation: none; }
.agent-context-ring-value,
.agent-context-ring.is-compacting .agent-context-ring-visual { animation: none; }
.agent-context-ring-visual,
.agent-context-progress > span { transition: none; }
}

View File

@@ -173,7 +173,7 @@ function EmotionPad({ preset, onChange }: { preset: CanvasEmotionPreset; onChang
const distance = pointer ? Math.hypot(pointer.x - column, pointer.y - row) : 4;
const proximity = dragging ? Math.max(0, 1 - distance / 1.7) : 0;
return (
<button key={item.id} type="button" aria-label={item.label} title={item.label} className="relative m-auto grid size-7 place-items-center rounded-full outline-none" onClick={() => onChange(item)}>
<button key={item.id} type="button" data-icon-only aria-label={item.label} title={item.label} className="relative m-auto grid size-7 place-items-center rounded-full outline-none" onClick={() => onChange(item)}>
<motion.span
animate={{ scale: active ? 1.65 : 1 + proximity * 0.42, opacity: active ? 1 : onPath ? 0.88 : 0.42 + proximity * 0.45 }}
transition={reducedMotion ? { duration: 0 } : aceternityMotion.spring.dock}

View File

@@ -1,6 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
import { Select } from "@/components/ui/base/select";

View File

@@ -35,6 +35,7 @@ export function StoryboardAssetsCell({ bindings, nodes, limit = 4 }: { bindings:
<Tooltip key={binding.nodeId} title={`${node?.title || "资产已失效"} · ${ROLE_LABELS[binding.role]}`}>
<button
type="button"
data-icon-only
disabled={!node}
className="relative grid size-9 shrink-0 place-items-center overflow-hidden rounded-md border border-foreground/10 bg-foreground/[0.035] text-foreground/45 outline-none transition enabled:hover:border-foreground/30 enabled:hover:text-foreground/70 focus-visible:ring-2 focus-visible:ring-[var(--color-primary)] disabled:cursor-not-allowed"
aria-label={`预览${node?.title || "失效资产"}`}

View File

@@ -90,6 +90,7 @@ export function SystemAnnouncementCenter({ userId, className, style, showLabel =
onClick={() => void openAnnouncements()}
aria-label={unreadCount ? `系统公告,${unreadCount} 条未读` : "系统公告"}
title="系统公告"
data-icon-only={showLabel ? undefined : true}
>
<span className="relative shrink-0">
<Bell className="size-4" />

View File

@@ -259,6 +259,7 @@ function DockCommandButton({ command, mouseX, metrics, motionEnabled, compact, s
type="button"
aria-label={command.label}
title={nativeTitle}
data-icon-only
aria-expanded={command.expands ? command.active || undefined : undefined}
aria-pressed={command.expands ? undefined : command.active || undefined}
disabled={command.disabled}

View File

@@ -19,7 +19,12 @@ import { cn } from "@/lib/utils";
* 明暗:全部颜色走 token;active 按压底用 bg-surface-active,明暗自动适配。
*/
export const iconButtonVariants = cva(
["inline-flex shrink-0 items-center justify-center rounded-md", "outline-none focus-visible:outline-none data-[input-modality=keyboard]:focus-visible:ring-2 data-[input-modality=keyboard]:focus-visible:ring-ring", "disabled:pointer-events-none disabled:opacity-45", "motion-safe:active:scale-[0.96]"],
[
"inline-flex shrink-0 items-center justify-center rounded-md",
"outline-none focus-visible:outline-none data-[input-modality=keyboard]:focus-visible:ring-2 data-[input-modality=keyboard]:focus-visible:ring-ring",
"disabled:pointer-events-none disabled:opacity-45",
"motion-safe:active:scale-[0.96]",
],
{
variants: {
variant: {

View File

@@ -21,7 +21,12 @@ import { cn } from "@/lib/utils";
* 明暗:全部颜色走 token;active 用按压底语义 token,明暗自动适配。
*/
export const toolButtonVariants = cva(
["inline-flex select-none items-center justify-center gap-1.5 rounded-md px-3", "font-medium transition-colors", "outline-none focus-visible:outline-none data-[input-modality=keyboard]:focus-visible:ring-2 data-[input-modality=keyboard]:focus-visible:ring-ring", "disabled:pointer-events-none disabled:opacity-45"],
[
"inline-flex select-none items-center justify-center gap-1.5 rounded-md px-3",
"font-medium transition-colors",
"outline-none focus-visible:outline-none data-[input-modality=keyboard]:focus-visible:ring-2 data-[input-modality=keyboard]:focus-visible:ring-ring",
"disabled:pointer-events-none disabled:opacity-45",
],
{
variants: {
variant: {

View File

@@ -56,7 +56,7 @@ export const Select = forwardRef<RefSelectProps, SelectProps>(function Select({
onKeyDown?.(event);
}}
onFocus={(event) => {
setInputModality((current) => current === "unknown" ? "keyboard" : current);
setInputModality((current) => (current === "unknown" ? "keyboard" : current));
onFocus?.(event);
}}
onBlur={(event) => {

View File

@@ -245,6 +245,7 @@ function BatchConnectionHandle({ scale, count, active, onPointerDown }: { scale:
<button
type="button"
data-canvas-no-zoom
data-icon-only
className="pointer-events-auto absolute grid -translate-y-1/2 translate-x-1/2 place-items-center rounded-full border shadow-md transition hover:scale-110 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
style={buttonStyle}
title={`批量连接 ${count} 个节点`}

View File

@@ -11,8 +11,10 @@
box-shadow: none !important;
}
/* 带文字的按钮不要贴着左边缘。纯图标按钮保持正方形,不额外加水平内边距。 */
:where(button:not(.ant-btn-icon-only), .ant-btn:not(.ant-btn-icon-only)):not(:has(> svg:only-child)) {
/* 带文字的按钮不要贴着左边缘。纯图标按钮保持正方形,不额外加水平内边距。
:has 只能识别 svg 是直接唯一子元素的情况;图标被动画 span、角标、sr-only 等
wrapper 包住时检测不到,这类按钮用 data-icon-only 显式声明豁免。 */
:where(button:not(.ant-btn-icon-only):not([data-icon-only]), .ant-btn:not(.ant-btn-icon-only):not([data-icon-only])):not(:has(> svg:only-child)) {
padding-inline: max(12px, var(--space-3));
}

View File

@@ -20,19 +20,14 @@
--unified-input-addon: color-mix(in srgb, var(--foreground) 4%, transparent);
}
:where(
.ant-input:not(textarea),
.ant-input-affix-wrapper,
.ant-input-number,
.ant-input-number-affix-wrapper,
.ant-input-group-wrapper .ant-input-group-addon,
.ant-otp-input
) {
:where(.ant-input:not(textarea), .ant-input-affix-wrapper, .ant-input-number, .ant-input-number-affix-wrapper, .ant-input-group-wrapper .ant-input-group-addon, .ant-otp-input) {
border-color: transparent !important;
border-radius: var(--r-lg) !important;
background: var(--unified-input-surface) !important;
box-shadow: none !important;
transition: background-color var(--motion-state, 180ms) ease, color var(--motion-state, 180ms) ease !important;
transition:
background-color var(--motion-state, 180ms) ease,
color var(--motion-state, 180ms) ease !important;
}
:where(textarea.ant-input, .ant-input-textarea textarea.ant-input) {
@@ -53,25 +48,14 @@
box-shadow: none !important;
}
:where(
.ant-input:focus,
.ant-input-focused,
.ant-input-affix-wrapper-focused,
.ant-input-number-focused,
.ant-input-number-affix-wrapper-focused
) {
:where(.ant-input:focus, .ant-input-focused, .ant-input-affix-wrapper-focused, .ant-input-number-focused, .ant-input-number-affix-wrapper-focused) {
border-color: transparent !important;
background: var(--unified-input-surface-focus) !important;
outline: none !important;
box-shadow: none !important;
}
:where(
.ant-input-affix-wrapper > .ant-input,
.ant-input-number .ant-input-number-input,
.ant-input-number-affix-wrapper .ant-input-number-input,
.ant-input-search .ant-input
) {
:where(.ant-input-affix-wrapper > .ant-input, .ant-input-number .ant-input-number-input, .ant-input-number-affix-wrapper .ant-input-number-input, .ant-input-search .ant-input) {
border: 0 !important;
border-radius: 0 !important;
background: transparent !important;
@@ -98,40 +82,22 @@
}
/* 外壳已经承担视觉时,内部输入不再形成第二层线条。 */
:where(
.ant-input-affix-wrapper .ant-input,
.ant-input-group .ant-input,
.ant-input-group .ant-input-affix-wrapper,
.ant-otp .ant-otp-input-wrapper .ant-input
) {
:where(.ant-input-affix-wrapper .ant-input, .ant-input-group .ant-input, .ant-input-group .ant-input-affix-wrapper, .ant-otp .ant-otp-input-wrapper .ant-input) {
border-color: transparent !important;
background: transparent !important;
box-shadow: none !important;
}
/* 错误和警告仍需要可识别的状态,但不再叠加发光描边。 */
:where(
.ant-input-status-error:not(.ant-input-disabled),
.ant-input-affix-wrapper-status-error:not(.ant-input-affix-wrapper-disabled),
.ant-input-number-status-error:not(.ant-input-number-disabled)
) {
:where(.ant-input-status-error:not(.ant-input-disabled), .ant-input-affix-wrapper-status-error:not(.ant-input-affix-wrapper-disabled), .ant-input-number-status-error:not(.ant-input-number-disabled)) {
background: color-mix(in srgb, var(--destructive, #ef4444) 10%, transparent) !important;
}
:where(
.ant-input-status-warning:not(.ant-input-disabled),
.ant-input-affix-wrapper-status-warning:not(.ant-input-affix-wrapper-disabled),
.ant-input-number-status-warning:not(.ant-input-number-disabled)
) {
:where(.ant-input-status-warning:not(.ant-input-disabled), .ant-input-affix-wrapper-status-warning:not(.ant-input-affix-wrapper-disabled), .ant-input-number-status-warning:not(.ant-input-number-disabled)) {
background: color-mix(in srgb, var(--warning, #f59e0b) 12%, transparent) !important;
}
:where(
.ant-input-disabled,
.ant-input-affix-wrapper-disabled,
.ant-input-number-disabled,
.ant-input-number-affix-wrapper-disabled
) {
:where(.ant-input-disabled, .ant-input-affix-wrapper-disabled, .ant-input-number-disabled, .ant-input-number-affix-wrapper-disabled) {
background: color-mix(in srgb, var(--foreground) 4%, transparent) !important;
color: color-mix(in srgb, var(--foreground) 42%, transparent) !important;
box-shadow: none !important;