feat(ui): refine gem-blue acrylic palette and focus borders
Derive the gem-blue palette with OKLCH tonal and complementary-color rules while preserving acrylic opacity, blur and surface hierarchy. Darker focus tokens exposed stacked field rings, fallback outlines and persistent basemap selection rings. Keep fallback focus styles in the base layer and use one themed field boundary or inset card stroke. Preserve keyboard focus, invalid states and native input semantics. Add browser regression coverage for pointer and keyboard focus, all basemap presentations, narrow layouts and rendered contrast. Validation in an isolated checkout: pnpm lint, pnpm typecheck, pnpm test (393 tests), focused browser suites and pnpm build.
This commit is contained in:
@@ -13,6 +13,9 @@ import { AnimatePresence, motion } from "motion/react";
|
||||
import { useId, useState, type ReactNode } from "react";
|
||||
import { cn } from "@/shared/ui/cn";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Checkbox } from "@/shared/ui/checkbox";
|
||||
import { Radio } from "@/shared/ui/radio";
|
||||
import { Textarea } from "@/shared/ui/textarea";
|
||||
import {
|
||||
StatusBadge,
|
||||
StatusDot,
|
||||
@@ -218,9 +221,9 @@ function ProgressList({ progress, running }: { progress: AgentChatProgress[]; ru
|
||||
return (
|
||||
<div className="agent-panel-nested rounded-xl p-2.5" data-agent-progress>
|
||||
<div className="flex min-h-8 items-center gap-2 px-0.5">
|
||||
<ListTree size={15} className="shrink-0 text-blue-600" aria-hidden="true" />
|
||||
<ListTree size={15} className="shrink-0 text-[var(--action-blue-hover)]" aria-hidden="true" />
|
||||
<span className="min-w-0 flex-1 truncate text-xs font-semibold text-slate-700">执行进度</span>
|
||||
<span className="flex shrink-0 items-center gap-1.5 text-xs font-medium text-slate-500">
|
||||
<span className="flex shrink-0 items-center gap-1.5 text-xs font-medium text-[var(--ink-muted)]">
|
||||
{running ? (
|
||||
<StatusDot tone="info" activity="live" aria-hidden="true" />
|
||||
) : null}
|
||||
@@ -236,8 +239,8 @@ function ProgressList({ progress, running }: { progress: AgentChatProgress[]; ru
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"-my-1 -mr-1.5 grid h-10 w-10 shrink-0 place-items-center rounded-md text-slate-400 transition-colors [@media(hover:hover)]:hover:text-blue-600",
|
||||
expanded && "text-blue-500"
|
||||
"-my-1 -mr-1.5 grid h-10 w-10 shrink-0 place-items-center rounded-md text-[var(--ink-muted)] transition-colors [@media(hover:hover)]:hover:text-[var(--action-blue-hover)]",
|
||||
expanded && "text-[var(--action-blue-hover)]"
|
||||
)}
|
||||
aria-controls={listId}
|
||||
aria-expanded={expanded}
|
||||
@@ -296,7 +299,7 @@ function ProgressList({ progress, running }: { progress: AgentChatProgress[]; ru
|
||||
{item.detail ? (
|
||||
<span
|
||||
className={cn(
|
||||
"block leading-5 text-slate-500",
|
||||
"block leading-5 text-[var(--ink-muted)]",
|
||||
compact ? "truncate" : "break-words"
|
||||
)}
|
||||
title={item.detail}
|
||||
@@ -312,7 +315,7 @@ function ProgressList({ progress, running }: { progress: AgentChatProgress[]; ru
|
||||
className={cn(
|
||||
"min-w-12 pt-0.5 text-right text-xs font-semibold",
|
||||
item.status === "running"
|
||||
? "text-blue-600"
|
||||
? "text-[var(--action-blue-hover)]"
|
||||
: [
|
||||
`status-tone-${progressStatusMeta[item.status].tone}`,
|
||||
"text-[var(--status-foreground)]"
|
||||
@@ -429,7 +432,7 @@ function PermissionRequestCard({
|
||||
<p className="truncate font-semibold text-slate-800" title={permission.target ?? permission.permission}>
|
||||
{getPermissionTitle(permission)}
|
||||
</p>
|
||||
<p className="mt-0.5 truncate font-mono text-xs text-slate-500" title={target}>
|
||||
<p className="mt-0.5 truncate font-mono text-xs text-[var(--ink-muted)]" title={target}>
|
||||
{target}
|
||||
</p>
|
||||
{permission.error ? (
|
||||
@@ -470,7 +473,7 @@ function PermissionRequestCard({
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 border-blue-200 bg-blue-50 px-2 text-xs text-blue-700 hover:bg-blue-100"
|
||||
className="h-8 border-[var(--surface-border)] bg-[var(--action-soft)] px-2 text-xs text-[var(--action-blue-hover)] hover:bg-[var(--action-selection-soft)]"
|
||||
disabled={disabled}
|
||||
onClick={() => reply("always")}
|
||||
>
|
||||
@@ -506,7 +509,7 @@ function QuestionList({
|
||||
onRejectQuestion?: (request: AgentQuestionRequest) => Promise<void> | void;
|
||||
}) {
|
||||
return (
|
||||
<AgentNestedBlock icon={HelpCircle} iconClassName="text-blue-600" title="补充信息" floating>
|
||||
<AgentNestedBlock icon={HelpCircle} iconClassName="text-[var(--action-blue-hover)]" title="补充信息" floating>
|
||||
<motion.div className="flex flex-col gap-1.5" variants={agentPanelListVariants} animate="animate">
|
||||
<AnimatePresence initial={false} mode="popLayout">
|
||||
{questions.slice(-3).map((request) => (
|
||||
@@ -587,7 +590,7 @@ function QuestionRequestCard({
|
||||
<p className="status-tone-danger mt-2 line-clamp-2 text-xs leading-4 text-[var(--status-foreground)]">{request.error}</p>
|
||||
) : null}
|
||||
{request.answers?.length ? (
|
||||
<p className="mt-2 truncate text-slate-500" title={request.answers.map((answer) => answer.join("、")).join(";")}>
|
||||
<p className="mt-2 truncate text-[var(--ink-muted)]" title={request.answers.map((answer) => answer.join("、")).join(";")}>
|
||||
已答:{request.answers.map((answer) => answer.join("、")).join(";")}
|
||||
</p>
|
||||
) : null}
|
||||
@@ -617,7 +620,7 @@ function QuestionRequestCard({
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 border-slate-200 bg-white px-2 text-xs text-slate-600 hover:bg-slate-50"
|
||||
className="h-8 border-[var(--surface-divider)] bg-[var(--surface-reading)] px-2 text-xs text-slate-600 hover:bg-[var(--surface-reading)]"
|
||||
disabled={submitting || !onRejectQuestion}
|
||||
onClick={reject}
|
||||
>
|
||||
@@ -686,21 +689,20 @@ function QuestionInput({
|
||||
<div className="space-y-1">
|
||||
{question.options.map((option) => {
|
||||
const checked = value.selected.includes(option.label);
|
||||
const Choice = question.multiple ? Checkbox : Radio;
|
||||
return (
|
||||
<label
|
||||
key={option.label}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-start gap-2 rounded-md border px-2 py-1.5 transition-colors",
|
||||
checked
|
||||
? "border-blue-300 bg-blue-50/70 text-blue-700"
|
||||
: "border-slate-200/80 text-slate-600 hover:border-slate-300 hover:bg-slate-50/70",
|
||||
? "border-[var(--palette-brand)] bg-[var(--action-soft)]/70 text-[var(--action-blue-hover)]"
|
||||
: "border-[var(--surface-divider)]/80 text-slate-600 hover:border-[var(--surface-border)] hover:bg-[var(--surface-reading)]/70",
|
||||
disabled && "cursor-default opacity-60"
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type={question.multiple ? "checkbox" : "radio"}
|
||||
<Choice
|
||||
name={name}
|
||||
className="mt-0.5 h-3.5 w-3.5 accent-blue-600"
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={(event) => toggleOption(option.label, event.currentTarget.checked)}
|
||||
@@ -708,7 +710,7 @@ function QuestionInput({
|
||||
<span className="min-w-0">
|
||||
<span className="block font-semibold text-slate-700">{option.label}</span>
|
||||
{option.description ? (
|
||||
<span className="block leading-4 text-slate-500">{option.description}</span>
|
||||
<span className="block leading-4 text-[var(--ink-muted)]">{option.description}</span>
|
||||
) : null}
|
||||
</span>
|
||||
</label>
|
||||
@@ -719,27 +721,24 @@ function QuestionInput({
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center gap-2 rounded-md border px-2 py-2 transition-colors",
|
||||
value.customSelected
|
||||
? "border-blue-300 bg-blue-50/70 text-blue-700"
|
||||
: "border-slate-200/80 text-slate-600 hover:border-slate-300 hover:bg-slate-50/70",
|
||||
? "border-[var(--palette-brand)] bg-[var(--action-soft)]/70 text-[var(--action-blue-hover)]"
|
||||
: "border-[var(--surface-divider)]/80 text-slate-600 hover:border-[var(--surface-border)] hover:bg-[var(--surface-reading)]/70",
|
||||
disabled && "cursor-default opacity-60"
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type={question.multiple ? "checkbox" : "radio"}
|
||||
name={name}
|
||||
className="h-3.5 w-3.5 accent-blue-600"
|
||||
checked={value.customSelected}
|
||||
disabled={disabled}
|
||||
onChange={(event) => toggleCustom(event.currentTarget.checked)}
|
||||
/>
|
||||
{question.multiple ? (
|
||||
<Checkbox name={name} checked={value.customSelected} disabled={disabled} onChange={(event) => toggleCustom(event.currentTarget.checked)} />
|
||||
) : (
|
||||
<Radio name={name} checked={value.customSelected} disabled={disabled} onChange={(event) => toggleCustom(event.currentTarget.checked)} />
|
||||
)}
|
||||
<span className="font-semibold text-slate-700">自定义回答</span>
|
||||
</label>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{showCustomInput ? (
|
||||
<textarea
|
||||
className="min-h-16 w-full resize-none rounded-md border border-slate-200/80 bg-transparent px-2 py-1.5 text-xs leading-5 text-slate-700 outline-hidden transition focus:border-blue-300 focus:ring-2 focus:ring-blue-100 disabled:opacity-60"
|
||||
<Textarea
|
||||
className="min-h-16 resize-none border-[var(--surface-divider)]/80 px-2 py-1.5 text-xs leading-5 text-slate-700 shadow-none md:text-xs disabled:opacity-60"
|
||||
aria-label="自定义回答内容"
|
||||
placeholder="输入自定义回答"
|
||||
value={value.custom}
|
||||
|
||||
Reference in New Issue
Block a user