From 7a3274fe31edd406604ea108ea745d2ace322f9a Mon Sep 17 00:00:00 2001 From: BingZi-233 Date: Fri, 7 Aug 2026 23:57:19 +0800 Subject: [PATCH 01/14] style(gui): adopt Apple design tokens and chat materials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce motion curves, neutral system colors, and material styles for the chat shell—header, sidebar, composer glass, floor nav, markdown typography, and reduced-motion/transparency fallbacks. --- crates/agent-gui/src/index.css | 1026 ++++++++++++++++++++++++++------ 1 file changed, 857 insertions(+), 169 deletions(-) diff --git a/crates/agent-gui/src/index.css b/crates/agent-gui/src/index.css index e4bac3587..69d9410c5 100644 --- a/crates/agent-gui/src/index.css +++ b/crates/agent-gui/src/index.css @@ -167,63 +167,83 @@ @layer base { :root { color-scheme: light; - --app-font-family: ui-sans-serif, system-ui, "PingFang SC", "Microsoft YaHei", sans-serif; - --chat-font-family: "OpenAI Sans Semibold", "PingFang SC", "Microsoft YaHei", sans-serif; + /* Prefer platform system face (SF Pro / Segoe / PingFang) for optical sizing + and tracking tables — Apple typography default before custom faces. */ + --app-font-family: + -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", ui-sans-serif, system-ui, + "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; + --chat-font-family: + "OpenAI Sans Semibold", -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", + sans-serif; --code-font-family: "SF Mono", SFMono-Regular, Menlo, Monaco, "Cascadia Code", Consolas, "Liberation Mono", monospace; + + /* Apple motion vocabulary (WWDC Designing Fluid Interfaces / Emil curves) */ + --ease-out: cubic-bezier(0.23, 1, 0.32, 1); + --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); + --ease-apple: cubic-bezier(0.32, 0.72, 0, 1); + --duration-press: 100ms; + --duration-ui: 200ms; + --duration-sheet: 320ms; + --press-scale: 0.97; + /* ChatHeader row (py-2.5 + h-8) — keep in sync with overlay pad / composer top */ + --chat-header-height: 3.25rem; + + /* Light canvas: near-pure white content, cooler structural sidebar (#F5F5F7) */ --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; + --foreground: 240 10% 8%; --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; + --card-foreground: 240 10% 8%; --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; + --popover-foreground: 240 10% 8%; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; + --primary: 240 8% 12%; + --primary-foreground: 0 0% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; + --secondary: 240 6% 96%; + --secondary-foreground: 240 8% 12%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; + --muted: 240 6% 96%; + --muted-foreground: 240 5% 42%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; + --accent: 240 6% 96%; + --accent-foreground: 240 8% 12%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; + --destructive: 0 72% 51%; + --destructive-foreground: 0 0% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; + --border: 240 6% 90%; + --input: 240 6% 90%; + --ring: 240 6% 40%; - --radius: 0.5rem; + --radius: 0.625rem; - /* Sidebar */ - --sidebar-bg: 220 14% 96%; + /* Sidebar — heavier material encodes structure (Apple materials hierarchy) */ + --sidebar-bg: 240 6% 96%; + --sidebar-separator: 240 6% 88%; /* Hub pages (MCP/Skills) canvas — in dark it sits below --background so background-tinted glass panels read as elevated surfaces. */ --hub-canvas: var(--background); /* Chip / pill buttons (MCP, Skills, 新对话) */ - --chip-bg: 220 13% 91%; - --chip-bg-hover: 220 13% 86%; + --chip-bg: 240 6% 91%; + --chip-bg-hover: 240 5% 86%; /* Send button disabled */ - --send-disabled-bg: 220 13% 91%; + --send-disabled-bg: 240 6% 91%; - /* Chat-specific tokens */ - --chat-user-bg: 220 9% 91%; - --chat-user-fg: 222 84% 5%; + /* Chat-specific tokens — user bubble closer to Messages gray fill */ + --chat-user-bg: 240 6% 92%; + --chat-user-fg: 240 10% 8%; --chat-assistant-bg: 0 0% 100%; - --chat-assistant-fg: 222.2 84% 4.9%; + --chat-assistant-fg: 240 10% 8%; --chat-tool-bg: 0 0% 100%; - --chat-tool-border: 220 13% 91%; - --chat-thinking-bg: 220 14% 97%; - --chat-thinking-border: 220 13% 90%; + --chat-tool-border: 240 6% 90%; + --chat-thinking-bg: 240 6% 97%; + --chat-thinking-border: 240 6% 90%; --chat-success: 152 60% 42%; --chat-error: 0 72% 51%; --chat-running: 252 56% 57%; @@ -252,53 +272,55 @@ .dark { color-scheme: dark; - --background: 224 22% 9%; - --foreground: 210 30% 96%; + /* Apple dark canvas (~#1C1C1E content, slightly elevated chrome) */ + --background: 240 6% 7%; + --foreground: 240 8% 96%; - --card: 224 22% 10%; - --card-foreground: 210 30% 96%; + --card: 240 5% 9%; + --card-foreground: 240 8% 96%; - --popover: 224 20% 12%; - --popover-foreground: 210 30% 96%; + --popover: 240 5% 11%; + --popover-foreground: 240 8% 96%; - --primary: 210 30% 96%; - --primary-foreground: 222.2 47.4% 11.2%; + --primary: 240 8% 96%; + --primary-foreground: 240 8% 10%; - --secondary: 220 18% 17%; - --secondary-foreground: 210 30% 96%; + --secondary: 240 5% 14%; + --secondary-foreground: 240 8% 96%; - --muted: 220 18% 17%; - --muted-foreground: 215 18% 76%; + --muted: 240 5% 14%; + --muted-foreground: 240 5% 68%; - --accent: 220 18% 17%; - --accent-foreground: 210 30% 96%; + --accent: 240 5% 14%; + --accent-foreground: 240 8% 96%; - --destructive: 0 62.8% 36%; - --destructive-foreground: 210 30% 96%; + --destructive: 0 55% 38%; + --destructive-foreground: 240 8% 96%; - --border: 220 14% 26%; - --input: 220 14% 26%; - --ring: 212.7 26.8% 83.9%; + --border: 240 4% 18%; + --input: 240 4% 18%; + --ring: 240 5% 55%; /* Chip / pill buttons (dark) */ - --chip-bg: 220 16% 24%; - --chip-bg-hover: 220 16% 30%; + --chip-bg: 240 5% 18%; + --chip-bg-hover: 240 5% 22%; /* Send button disabled (dark) */ - --send-disabled-bg: 220 16% 24%; + --send-disabled-bg: 240 5% 18%; /* Hub pages canvas (dark) — deeper than --background for glass elevation */ - --hub-canvas: 224 24% 6.5%; + --hub-canvas: 240 8% 5%; /* Chat-specific tokens (dark) */ - --sidebar-bg: 224 22% 11%; - --chat-user-bg: 220 14% 28%; - --chat-user-fg: 210 30% 96%; - --chat-assistant-bg: 224 22% 9%; - --chat-assistant-fg: 210 30% 96%; - --chat-tool-bg: 224 22% 9%; - --chat-tool-border: 220 14% 24%; - --chat-thinking-bg: 220 18% 13%; - --chat-thinking-border: 220 14% 22%; + --sidebar-bg: 240 5% 9%; + --sidebar-separator: 240 4% 16%; + --chat-user-bg: 240 5% 18%; + --chat-user-fg: 240 8% 96%; + --chat-assistant-bg: 240 6% 7%; + --chat-assistant-fg: 240 8% 96%; + --chat-tool-bg: 240 6% 7%; + --chat-tool-border: 240 4% 16%; + --chat-thinking-bg: 240 5% 11%; + --chat-thinking-border: 240 4% 16%; --chat-success: 152 56% 52%; --chat-error: 0 75% 65%; --chat-running: 252 60% 70%; @@ -719,21 +741,55 @@ --text-2xl: calc(1.5rem * var(--zone-font-scale, 1)); } + /* + * Assistant markdown — Apple reading rhythm (WWDC UI Typography): + * size-specific tracking, inverse leading (tight headings / open body), + * hierarchy from weight+size+leading together. Values are intentional. + */ .chat-markdown { - @apply text-[calc(15px*var(--zone-font-scale,1))] leading-8 text-foreground; + --chat-md-size: calc(15.5px * var(--zone-font-scale, 1)); + --chat-md-leading: 1.52; + --chat-md-tracking: -0.011em; + --chat-md-para-gap: 0.7em; + --chat-md-block-gap: 0.95em; + font-size: var(--chat-md-size); + line-height: var(--chat-md-leading); + letter-spacing: var(--chat-md-tracking); + color: hsl(var(--foreground) / 0.94); + font-optical-sizing: auto; + -webkit-font-smoothing: antialiased; } .chat-markdown p { - @apply my-2 min-w-0 max-w-full break-words text-[calc(15px*var(--zone-font-scale,1))] leading-8 text-foreground [overflow-wrap:anywhere]; + margin-block: var(--chat-md-para-gap); + min-width: 0; + max-width: 100%; + overflow-wrap: anywhere; + word-break: break-word; + font-size: var(--chat-md-size); + line-height: var(--chat-md-leading); + letter-spacing: var(--chat-md-tracking); + color: hsl(var(--foreground) / 0.94); + } + + /* Tighter first paragraph under a heading; open air between prose blocks */ + .chat-markdown [data-streamdown^="heading-"] + p { + margin-top: 0.35em; + } + .chat-markdown p + p { + margin-top: var(--chat-md-para-gap); } .chat-markdown strong, .chat-markdown [data-streamdown="strong"] { - @apply font-medium text-foreground; + font-weight: 600; + color: hsl(var(--foreground) / 0.98); + letter-spacing: -0.005em; } .chat-markdown em { - @apply text-foreground/90; + font-style: italic; + color: hsl(var(--foreground) / 0.9); } /* Hide the retained Streamdown caret without removing its inline slot. */ @@ -741,49 +797,106 @@ opacity: 0; } - /* Headings */ + /* Headings — larger → tighter leading + more negative tracking */ .chat-markdown [data-streamdown="heading-1"] { - @apply mb-2 mt-6 text-xl font-semibold tracking-tight text-foreground; + margin: 1.35em 0 0.4em; + font-size: calc(1.35rem * var(--zone-font-scale, 1)); + font-weight: 600; + line-height: 1.18; + letter-spacing: -0.025em; + color: hsl(var(--foreground)); } .chat-markdown [data-streamdown="heading-2"] { - @apply mb-2 mt-5 text-lg font-semibold tracking-tight text-foreground; + margin: 1.2em 0 0.35em; + font-size: calc(1.18rem * var(--zone-font-scale, 1)); + font-weight: 600; + line-height: 1.22; + letter-spacing: -0.02em; + color: hsl(var(--foreground)); } .chat-markdown [data-streamdown="heading-3"] { - @apply mb-1.5 mt-4 text-base font-semibold tracking-tight text-foreground; + margin: 1.05em 0 0.3em; + font-size: calc(1.05rem * var(--zone-font-scale, 1)); + font-weight: 600; + line-height: 1.28; + letter-spacing: -0.015em; + color: hsl(var(--foreground)); } .chat-markdown [data-streamdown="heading-4"], .chat-markdown [data-streamdown="heading-5"] { - @apply mb-1 mt-4 text-sm font-semibold tracking-tight text-foreground; + margin: 0.95em 0 0.25em; + font-size: calc(0.95rem * var(--zone-font-scale, 1)); + font-weight: 600; + line-height: 1.32; + letter-spacing: -0.01em; + color: hsl(var(--foreground) / 0.96); } .chat-markdown [data-streamdown="heading-6"] { - @apply mb-1 mt-4 text-xs font-semibold tracking-wide text-muted-foreground; + margin: 0.9em 0 0.2em; + font-size: calc(0.82rem * var(--zone-font-scale, 1)); + font-weight: 600; + line-height: 1.35; + letter-spacing: 0.01em; + text-transform: none; + color: hsl(var(--muted-foreground)); } - /* Links */ + /* Links — calm, readable; underline is secondary */ .chat-markdown [data-streamdown="link"] { - @apply rounded-xs font-medium text-primary underline decoration-primary/30 underline-offset-2 transition-colors hover:decoration-primary/70; + border-radius: 2px; + font-weight: 500; + color: hsl(var(--primary)); + text-decoration: underline; + text-decoration-color: hsl(var(--primary) / 0.28); + text-underline-offset: 0.18em; + text-decoration-thickness: 1px; + transition: text-decoration-color 150ms var(--ease-out); + } + .chat-markdown [data-streamdown="link"]:hover { + text-decoration-color: hsl(var(--primary) / 0.65); } /* Inline code */ .chat-markdown [data-streamdown="inline-code"] { - @apply whitespace-pre-wrap break-words rounded-md border-0 bg-foreground/[0.05] px-1.5 py-0.5 font-mono text-[0.92em] font-normal text-foreground [overflow-wrap:anywhere]; + white-space: pre-wrap; + overflow-wrap: anywhere; + word-break: break-word; + border-radius: 0.35rem; + border: 0; + background: hsl(var(--foreground) / 0.055); + padding: 0.12em 0.38em; + font-family: var(--code-font-family); + font-size: 0.9em; + font-weight: 400; + letter-spacing: 0; + line-height: 1.35; + color: hsl(var(--foreground) / 0.95); } /* Code blocks — container, header, body (safe in components layer) */ .chat-markdown [data-streamdown="code-block"] { @apply relative my-4 w-full min-w-0 overflow-visible rounded-none border-0 bg-transparent p-0 shadow-none outline-none ring-0; + margin-block: var(--chat-md-block-gap); } .chat-markdown [data-streamdown="code-block-header"] { - @apply h-8 border-0 bg-transparent px-2 py-0 text-[calc(11px*var(--zone-font-scale,1))] font-medium tracking-[0.06em] text-muted-foreground/85 shadow-none; + @apply h-8 border-0 bg-transparent px-2 py-0 shadow-none; + font-size: calc(11px * var(--zone-font-scale, 1)); + font-weight: 500; + letter-spacing: 0.04em; + color: hsl(var(--muted-foreground) / 0.88); } .chat-markdown [data-streamdown="code-block-header"] > span { - @apply ml-0 font-mono tracking-[0.06em]; + @apply ml-0 font-mono; + letter-spacing: 0.04em; } .chat-markdown [data-streamdown="code-block-body"] { - @apply w-full min-w-0 overflow-x-auto rounded-xl border-0 bg-muted/40 p-4 text-[calc(13px*var(--zone-font-scale,1))] shadow-none outline-none ring-0; + @apply w-full min-w-0 overflow-x-auto rounded-xl border-0 bg-muted/40 p-4 shadow-none outline-none ring-0; + font-size: calc(13px * var(--zone-font-scale, 1)); + letter-spacing: 0; } .chat-markdown [data-streamdown="code-block-body"] pre { - @apply leading-6; + line-height: 1.55; + letter-spacing: 0; } .chat-markdown--streaming [data-streamdown="code-block"][data-incomplete="true"] { @apply relative border-0 bg-transparent; @@ -797,37 +910,74 @@ @apply border-0 bg-transparent text-muted-foreground/85; } - /* Blockquotes */ + /* Blockquotes — secondary voice, not heavy italic (Notes-like) */ .chat-markdown [data-streamdown="blockquote"] { - @apply my-3 min-w-0 max-w-full border-l-2 border-border/70 pl-4 italic text-muted-foreground [overflow-wrap:anywhere]; + margin-block: var(--chat-md-block-gap); + min-width: 0; + max-width: 100%; + overflow-wrap: anywhere; + border-left: 2px solid hsl(var(--border) / 0.85); + padding-left: 0.9em; + font-style: normal; + color: hsl(var(--muted-foreground)); + letter-spacing: -0.005em; } .chat-markdown [data-streamdown="blockquote"] p { - @apply my-0; + margin-block: 0.35em; + color: inherit; } - /* Lists */ + /* Lists — slightly tighter than body paragraphs, same type size */ .chat-markdown [data-streamdown="unordered-list"] { - @apply my-2 ml-1 list-disc space-y-0.5 pl-5; + margin-block: 0.65em; + margin-left: 0.15em; + list-style-type: disc; + padding-left: 1.35em; } .chat-markdown [data-streamdown="ordered-list"] { - @apply my-2 ml-1 list-decimal space-y-0.5 pl-5; + margin-block: 0.65em; + margin-left: 0.15em; + list-style-type: decimal; + padding-left: 1.35em; } .chat-markdown [data-streamdown="list-item"] { - @apply min-w-0 max-w-full break-words pl-1 text-[calc(15px*var(--zone-font-scale,1))] leading-8 [overflow-wrap:anywhere]; + min-width: 0; + max-width: 100%; + overflow-wrap: anywhere; + word-break: break-word; + margin-block: 0.2em; + padding-left: 0.2em; + font-size: var(--chat-md-size); + line-height: var(--chat-md-leading); + letter-spacing: var(--chat-md-tracking); + } + .chat-markdown [data-streamdown="list-item"] > p { + margin-block: 0.2em; } .chat-markdown .contains-task-list { - @apply list-none pl-0; + list-style: none; + padding-left: 0; } .chat-markdown .task-list-item { - @apply flex items-start gap-2 pl-0; + display: flex; + align-items: flex-start; + gap: 0.5rem; + padding-left: 0; } .chat-markdown .task-list-item input[type="checkbox"] { - @apply mt-[0.45rem] h-3.5 w-3.5 rounded border border-border align-middle accent-emerald-500; + margin-top: 0.4em; + height: 0.875rem; + width: 0.875rem; + border-radius: 0.25rem; + border: 1px solid hsl(var(--border)); + vertical-align: middle; + accent-color: hsl(152 55% 42%); } /* Tables */ .chat-markdown [data-streamdown="table-wrapper"] { @apply my-4 w-full min-w-0 gap-0 rounded-none border-0 bg-transparent p-0 shadow-none outline-none ring-0; + margin-block: var(--chat-md-block-gap); } .chat-markdown [data-streamdown="table-wrapper"] > div:first-child { @apply justify-end gap-1 px-0 pt-0; @@ -839,31 +989,49 @@ @apply rounded-md border-0 bg-transparent shadow-none transition-colors hover:bg-foreground/[0.04] hover:text-foreground; } .chat-markdown [data-streamdown="table"] { - @apply my-2 w-full min-w-full table-auto border-collapse border-0 bg-transparent text-sm shadow-none outline-none ring-0; + @apply my-2 w-full min-w-full table-auto border-collapse border-0 bg-transparent shadow-none outline-none ring-0; + font-size: calc(0.9em * var(--zone-font-scale, 1)); + letter-spacing: -0.005em; } .chat-markdown [data-streamdown="table-header"] { @apply bg-transparent; } .chat-markdown [data-streamdown="table-header-cell"] { - @apply border-0 border-b border-border/50 px-0 py-2 pr-8 text-left align-bottom text-sm font-semibold tracking-tight text-foreground; + border: 0; + border-bottom: 1px solid hsl(var(--border) / 0.55); + padding: 0.45em 2em 0.45em 0; + text-align: left; + vertical-align: bottom; + font-size: 0.92em; + font-weight: 600; + line-height: 1.35; + letter-spacing: -0.012em; + color: hsl(var(--foreground)); } .chat-markdown [data-streamdown="table-cell"] { - @apply border-0 border-b border-border/30 px-0 py-1 pr-8 text-left align-middle leading-8 text-foreground/90; + border: 0; + border-bottom: 1px solid hsl(var(--border) / 0.32); + padding: 0.4em 2em 0.4em 0; + text-align: left; + vertical-align: middle; + line-height: 1.45; + color: hsl(var(--foreground) / 0.9); } .chat-markdown [data-streamdown="table-body"] [data-streamdown="table-row"]:nth-child(even) { @apply bg-transparent; } .chat-markdown [data-streamdown="table-header-cell"]:last-child, .chat-markdown [data-streamdown="table-cell"]:last-child { - @apply pr-0; + padding-right: 0; } .chat-markdown [data-streamdown="table-row"]:last-child [data-streamdown="table-cell"] { - @apply border-b-0; + border-bottom: 0; } /* Mermaid */ .chat-markdown [data-streamdown="mermaid-block"] { @apply my-4 flex w-full min-w-0 gap-2 rounded-none border-0 bg-transparent p-0 shadow-none; + margin-block: var(--chat-md-block-gap); } .chat-markdown [data-streamdown="mermaid-block"] > div:last-child, .chat-markdown [data-streamdown="mermaid"] { @@ -881,50 +1049,84 @@ /* Math */ .chat-markdown .katex { - @apply text-[1.04em]; + font-size: 1.05em; } .chat-markdown .katex-display { - @apply my-3.5 block max-w-full overflow-x-auto overflow-y-hidden px-1 py-1.5 text-center; + margin-block: 0.9em; + display: block; + max-width: 100%; + overflow-x: auto; + overflow-y: hidden; + padding: 0.35em 0.25em; + text-align: center; } - /* Thinking markdown */ + /* Thinking markdown — denser secondary voice; stays subordinate to answer */ .chat-markdown.thinking-markdown { - @apply text-[calc(12px*var(--zone-font-scale,1))] leading-6 text-muted-foreground/80; + --chat-md-size: calc(12.5px * var(--zone-font-scale, 1)); + --chat-md-leading: 1.45; + --chat-md-tracking: 0; + --chat-md-para-gap: 0.4em; + font-size: var(--chat-md-size); + line-height: var(--chat-md-leading); + letter-spacing: var(--chat-md-tracking); + color: hsl(var(--muted-foreground) / 0.82); } .chat-markdown.thinking-markdown p { - @apply my-1 text-[calc(12px*var(--zone-font-scale,1))] leading-6 text-muted-foreground/80; + margin-block: var(--chat-md-para-gap); + font-size: var(--chat-md-size); + line-height: var(--chat-md-leading); + color: hsl(var(--muted-foreground) / 0.82); } .chat-markdown.thinking-markdown [data-streamdown^="heading-"] { - @apply mb-0 mt-1 text-[calc(12px*var(--zone-font-scale,1))] font-medium leading-6 tracking-normal text-muted-foreground/90; + margin: 0.45em 0 0.15em; + font-size: var(--chat-md-size); + font-weight: 500; + line-height: 1.4; + letter-spacing: 0; + color: hsl(var(--muted-foreground) / 0.92); } .chat-markdown.thinking-markdown [data-streamdown="unordered-list"], .chat-markdown.thinking-markdown [data-streamdown="ordered-list"] { - @apply my-1 pl-4; + margin-block: 0.35em; + padding-left: 1.1em; } .chat-markdown.thinking-markdown [data-streamdown="list-item"] { - @apply text-[calc(12px*var(--zone-font-scale,1))] leading-5 text-muted-foreground/80; + font-size: var(--chat-md-size); + line-height: 1.4; + color: hsl(var(--muted-foreground) / 0.82); } .chat-markdown.thinking-markdown [data-streamdown="blockquote"] { - @apply my-1 border-l-0 pl-0 not-italic text-muted-foreground/75; + margin-block: 0.35em; + border-left: 0; + padding-left: 0; + font-style: normal; + color: hsl(var(--muted-foreground) / 0.75); } .chat-markdown.thinking-markdown [data-streamdown="inline-code"] { - @apply bg-foreground/[0.03] text-[calc(11px*var(--zone-font-scale,1))] text-muted-foreground/90; + background: hsl(var(--foreground) / 0.03); + font-size: calc(11px * var(--zone-font-scale, 1)); + color: hsl(var(--muted-foreground) / 0.9); } .chat-markdown.thinking-markdown [data-streamdown="code-block-body"] { - @apply bg-muted/20 p-3 text-[calc(11px*var(--zone-font-scale,1))]; + @apply bg-muted/20 p-3; + font-size: calc(11px * var(--zone-font-scale, 1)); } /* Images */ .chat-markdown [data-streamdown="image-wrapper"] { - @apply my-4 overflow-visible rounded-none border-0 bg-transparent shadow-none; + @apply overflow-visible rounded-none border-0 bg-transparent shadow-none; + margin-block: var(--chat-md-block-gap); } .chat-markdown [data-streamdown="image"] { @apply block max-h-[34rem] max-w-full rounded-xl border border-border/60 bg-muted/10 object-contain; } - /* Horizontal rule */ + /* Horizontal rule — soft section break */ .chat-markdown [data-streamdown="horizontal-rule"] { - @apply my-5 border-border/45; + margin-block: 1.25em; + border: 0; + border-top: 1px solid hsl(var(--border) / 0.5); } /* First/last child margin reset */ @@ -968,9 +1170,9 @@ @apply rounded border border-border/50 bg-muted/60 px-1 py-0.5 font-mono text-[calc(11px*var(--zone-font-scale,1))] text-foreground/90; } - /* Smooth entrance for chat bubbles */ + /* Smooth entrance for chat bubbles — start from visible scale (never 0) */ .chat-bubble-enter { - animation: chatBubbleIn 0.3s cubic-bezier(0.16, 1, 0.3, 1); + animation: chatBubbleIn var(--duration-ui) var(--ease-out); } /* @@ -988,7 +1190,7 @@ @keyframes chatBubbleIn { from { opacity: 0; - transform: translateY(6px) scale(0.98); + transform: translateY(4px) scale(0.98); } to { opacity: 1; @@ -996,44 +1198,287 @@ } } - /* Jump-to-bottom affordance: mounted only while scroll-follow is detached, - * so it only needs an entrance animation. Centering lives here because the - * animation transform would override a Tailwind translate utility. - * Frosted-glass treatment follows the .hub-frost-hero idiom (translucent - * theme tint + blur/saturate, white-tint override in dark). */ + /* Floating chat header — translucent material; overlays transcript scroll */ + .chat-header { + background: hsl(var(--background) / 0.72); + -webkit-backdrop-filter: blur(20px) saturate(180%); + backdrop-filter: blur(20px) saturate(180%); + } + .chat-header::after { + content: ""; + position: absolute; + inset-inline: 0; + bottom: 0; + height: 1px; + pointer-events: none; + background: linear-gradient( + 90deg, + transparent 0%, + hsl(var(--border) / 0.55) 18%, + hsl(var(--border) / 0.55) 82%, + transparent 100% + ); + opacity: 0.7; + } + + /* Scroll content clears the overlay header; messages still pass under it. */ + .chat-transcript-scroll-pad { + padding-top: calc(var(--chat-header-height) + 0.5rem); + } + + /* Overlay host: header captures input; siblings can sit under it. */ + .chat-header-overlay { + position: absolute; + inset-inline: 0; + top: 0; + z-index: 20; + } + + /* Sidebar structural material + soft edge (scroll-edge, not a hard divider) */ + .chat-history-sidebar { + background: hsl(var(--sidebar-bg)); + border-right-color: hsl(var(--sidebar-separator) / 0.9); + } + .chat-history-sidebar-inner { + position: relative; + } + .chat-sidebar-section-label { + font-size: calc(11px * var(--zone-font-scale, 1)); + font-weight: 600; + letter-spacing: 0.02em; + line-height: 1.25; + color: hsl(var(--muted-foreground) / 0.92); + } + .chat-sidebar-row-active { + background: hsl(var(--foreground) / 0.08); + } + .dark .chat-sidebar-row-active { + background: hsl(var(--foreground) / 0.1); + } + + /* + * Sidebar list touch-down (Apple Response): highlight on pointer-down of the + * row's select control, not on release. No scale — list rows are high-frequency + * and should only change fill. `:has(button:active)` paints the whole rounded + * row while the primary select button is pressed. + */ + .chat-sidebar-row { + transition: + background-color 80ms var(--ease-out), + color 80ms var(--ease-out); + } + + .chat-sidebar-row:not(.chat-sidebar-row-active):not([data-selected="true"]):has( + > button:active, + [data-sidebar-row-select]:active + ) { + background-color: hsl(var(--foreground) / 0.08); + color: hsl(var(--foreground)); + } + + .chat-sidebar-row.chat-sidebar-row-active:has(> button:active, [data-sidebar-row-select]:active), + .chat-sidebar-row[data-selected="true"]:has(> button:active, [data-sidebar-row-select]:active) { + background-color: hsl(var(--foreground) / 0.14); + } + + .chat-sidebar-row[data-tone="danger"]:has(> button:active, [data-sidebar-row-select]:active) { + background-color: hsl(var(--destructive) / 0.12); + color: hsl(var(--destructive)); + } + + .chat-sidebar-row[data-tone="muted"]:has(> button:active, [data-sidebar-row-select]:active) { + background-color: hsl(var(--foreground) / 0.05); + } + + @media (prefers-reduced-motion: reduce) { + .chat-sidebar-row { + transition: none; + } + } + + /* Suggestion cards — light material + press feedback */ + .chat-suggest-card { + border: 1px solid hsl(var(--border) / 0.55); + background: hsl(var(--foreground) / 0.03); + box-shadow: 0 1px 0 hsl(0 0% 100% / 0.5) inset; + transition: + background-color var(--duration-ui) var(--ease-out), + border-color var(--duration-ui) var(--ease-out), + transform var(--duration-press) var(--ease-out), + box-shadow var(--duration-ui) var(--ease-out); + } + .chat-suggest-card:hover { + background: hsl(var(--foreground) / 0.055); + border-color: hsl(var(--border) / 0.8); + } + .chat-suggest-card:active { + transform: scale(var(--press-scale)); + } + .dark .chat-suggest-card { + border-color: hsl(0 0% 100% / 0.08); + background: hsl(0 0% 100% / 0.04); + box-shadow: 0 1px 0 hsl(0 0% 100% / 0.06) inset; + } + .dark .chat-suggest-card:hover { + background: hsl(0 0% 100% / 0.07); + border-color: hsl(0 0% 100% / 0.12); + } + + /* Composer toolbar icon buttons — instant press response */ + .composer-toolbar-action { + transition: + background-color var(--duration-ui) var(--ease-out), + color var(--duration-ui) var(--ease-out), + transform var(--duration-press) var(--ease-out); + } + .composer-toolbar-action:active:not(:disabled) { + transform: scale(var(--press-scale)); + } + + /* + * Jump-to-bottom — same glass material family as composer / header. + * Centering lives here: entrance animation transform would override a + * Tailwind translate utility. Press uses scale (occasional affordance). + */ .chat-jump-to-bottom { transform: translateX(-50%); - animation: chatJumpToBottomIn 0.18s cubic-bezier(0.16, 1, 0.3, 1); - border: 1px solid hsl(var(--border) / 0.55); - background: hsl(var(--background) / 0.45); - -webkit-backdrop-filter: blur(18px) saturate(180%); - backdrop-filter: blur(18px) saturate(180%); + animation: chatJumpToBottomIn 180ms var(--ease-out); + border: 1px solid hsl(var(--border) / 0.5); + background: hsl(var(--background) / 0.55); + -webkit-backdrop-filter: blur(20px) saturate(180%); + backdrop-filter: blur(20px) saturate(180%); box-shadow: - 0 1px 0 hsl(0 0% 100% / 0.45) inset, - 0 8px 24px -14px hsl(0 0% 0% / 0.35); + 0 1px 0 hsl(0 0% 100% / 0.5) inset, + 0 10px 28px -14px hsl(0 0% 0% / 0.32); + color: hsl(var(--muted-foreground)); + transition: + background-color 150ms var(--ease-out), + color 150ms var(--ease-out), + box-shadow 150ms var(--ease-out), + transform 100ms var(--ease-out); } .chat-jump-to-bottom:hover { - background: hsl(var(--background) / 0.65); + background: hsl(var(--background) / 0.72); + color: hsl(var(--foreground)); + } + .chat-jump-to-bottom:active { + transform: translateX(-50%) scale(var(--press-scale)); } .dark .chat-jump-to-bottom { border-color: hsl(0 0% 100% / 0.12); - background: hsl(0 0% 100% / 0.06); + background: hsl(0 0% 100% / 0.07); box-shadow: 0 1px 0 hsl(0 0% 100% / 0.08) inset, - 0 8px 24px -14px hsl(0 0% 0% / 0.6); + 0 10px 28px -14px hsl(0 0% 0% / 0.65); } .dark .chat-jump-to-bottom:hover { - background: hsl(0 0% 100% / 0.11); + background: hsl(0 0% 100% / 0.12); } @keyframes chatJumpToBottomIn { from { opacity: 0; - transform: translateX(-50%) translateY(6px); + transform: translateX(-50%) translateY(6px) scale(0.96); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0) scale(1); + } + } + + /* + * Floor nav panel — floating material aligned with model-selector / composer. + * Origin top-right (rail sits on the right edge); enter/exit under 200ms. + */ + .floor-nav-panel { + border: 1px solid hsl(var(--border) / 0.5); + background: hsl(var(--background) / 0.72); + -webkit-backdrop-filter: blur(20px) saturate(180%); + backdrop-filter: blur(20px) saturate(180%); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.5) inset, + 0 16px 40px -18px hsl(15 23% 10% / 0.32); + transform-origin: top right; + animation: floorNavPanelIn 180ms var(--ease-out); + } + .dark .floor-nav-panel { + border-color: hsl(0 0% 100% / 0.1); + background: hsl(0 0% 100% / 0.07); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.06) inset, + 0 16px 40px -18px hsl(0 0% 0% / 0.65); + } + + @keyframes floorNavPanelIn { + from { + opacity: 0; + transform: translateY(-4px) scale(0.96); + filter: blur(2px); } to { opacity: 1; - transform: translateX(-50%) translateY(0); + transform: translateY(0) scale(1); + filter: blur(0); + } + } + + .floor-nav-row { + transition: background-color 80ms var(--ease-out); + } + .floor-nav-row:has(button:active) { + background-color: hsl(var(--foreground) / 0.09); + } + .floor-nav-row[data-floor-active]:has(button:active) { + background-color: hsl(var(--foreground) / 0.12); + } + + .floor-nav-pin { + transition: + color 120ms var(--ease-out), + opacity 120ms var(--ease-out), + transform 100ms var(--ease-out), + background-color 120ms var(--ease-out); + } + .floor-nav-pin:active { + transform: scale(var(--press-scale)); + } + + .floor-nav-marker { + transition: + width 150ms var(--ease-out), + background-color 150ms var(--ease-out), + opacity 150ms var(--ease-out); + } + .floor-nav-marker:active { + opacity: 1; + background-color: hsl(var(--foreground) / 0.7); + } + + @media (prefers-reduced-motion: reduce) { + .chat-jump-to-bottom { + animation: none; + transition: background-color 120ms ease, color 120ms ease; + } + .chat-jump-to-bottom:active { + transform: translateX(-50%); + } + .floor-nav-panel { + animation: none; + } + .floor-nav-pin:active { + transform: none; + } + .floor-nav-marker { + transition: background-color 120ms ease; + } + } + + @media (prefers-reduced-transparency: reduce) { + .chat-jump-to-bottom, + .floor-nav-panel { + -webkit-backdrop-filter: none; + backdrop-filter: none; + background: hsl(var(--background)); } } @@ -1042,44 +1487,46 @@ * rises, then the suggestion cards cascade. Steady-state motion stays * transform/opacity-only: a slow float on the logo plus a halo breathe. */ .chat-hero-logo-enter { - animation: chatHeroLogoIn 0.55s cubic-bezier(0.34, 1.56, 0.64, 1) backwards; + /* Critically damped feel — no spring overshoot on non-gesture UI */ + animation: chatHeroLogoIn 0.45s var(--ease-out) backwards; will-change: transform, opacity; } /* Float and breathe share one 6s clock, delayed past the entrance so the - * pop settles before the idle motion takes over. */ + * pop settles before the idle motion takes over. Amplitude stays subtle. */ .chat-hero-logo-float { - animation: chatHeroFloat 6s ease-in-out 1.2s infinite; + animation: chatHeroFloat 7s ease-in-out 1s infinite; } .chat-hero-halo-breathe { - animation: chatHeroHaloBreathe 6s ease-in-out 1.2s infinite; + animation: chatHeroHaloBreathe 7s ease-in-out 1s infinite; } .chat-hero-title-enter { - animation: chatHeroRiseIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.12s backwards; + animation: chatHeroRiseIn 0.48s var(--ease-out) 0.08s backwards; will-change: transform, opacity; + letter-spacing: -0.02em; } .chat-hero-line-enter { - animation: chatHeroRiseIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.2s backwards; + animation: chatHeroRiseIn 0.48s var(--ease-out) 0.14s backwards; } .chat-hero-cta-enter { - animation: chatHeroRiseIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.28s backwards; + animation: chatHeroRiseIn 0.48s var(--ease-out) 0.2s backwards; } /* Suggestion cards: --chat-hero-delay is staggered per card from JSX. * `backwards` (not `both`) so hover styles win once the entrance ends. */ .chat-hero-card-enter { - animation: chatHeroCardIn 0.55s cubic-bezier(0.16, 1, 0.3, 1) var(--chat-hero-delay, 0.26s) - backwards; + animation: chatHeroCardIn 0.42s var(--ease-out) var(--chat-hero-delay, 0.22s) backwards; } @keyframes chatHeroLogoIn { from { opacity: 0; - transform: translateY(10px) scale(0.72); + /* Never animate from scale(0) — start from a visible shape */ + transform: translateY(8px) scale(0.92); } to { opacity: 1; @@ -1093,7 +1540,7 @@ transform: translateY(0); } 50% { - transform: translateY(-5px); + transform: translateY(-3px); } } @@ -1103,14 +1550,14 @@ opacity: 1; } 50% { - opacity: 0.6; + opacity: 0.7; } } @keyframes chatHeroRiseIn { from { opacity: 0; - transform: translateY(12px); + transform: translateY(8px); } to { opacity: 1; @@ -1121,7 +1568,7 @@ @keyframes chatHeroCardIn { from { opacity: 0; - transform: translateY(14px) scale(0.96); + transform: translateY(10px) scale(0.97); } to { opacity: 1; @@ -1427,6 +1874,11 @@ animation: none; } + .chat-suggest-card:active, + .composer-toolbar-action:active:not(:disabled) { + transform: none; + } + .sidebar-context-menu, .sidebar-context-menu [role="menuitem"] { animation: none !important; @@ -1438,6 +1890,12 @@ transition: none !important; } + .chat-header { + -webkit-backdrop-filter: none; + backdrop-filter: none; + background: hsl(var(--background)); + } + .ssh-auth-panel, .ssh-collapsible, .settings-modal-overlay, @@ -1448,6 +1906,26 @@ } } + /* Accessibility: solid surfaces when translucency hurts legibility */ + @media (prefers-reduced-transparency: reduce) { + .chat-header, + .project-tools-panel-header { + -webkit-backdrop-filter: none; + backdrop-filter: none; + background: hsl(var(--background)); + } + + .chat-jump-to-bottom { + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; + background: hsl(var(--background)) !important; + } + + .project-tools-panel { + background: hsl(var(--sidebar-bg)); + } + } + /* Mention composer placeholder */ .mention-composer.is-empty::before { content: attr(data-placeholder); @@ -1646,45 +2124,76 @@ transform: translateY(5px) scale(0.975); } - /* Chat header model selector: grow out of the trigger with a soft slide. - * Base UI drives the popup lifecycle, so enter/exit are keyed off - * data-starting-style / data-ending-style and the transition delays - * unmount (data-state rules never match Base UI popups). */ + /* + * Chat header model selector — origin-aware popover (Apple spatial consistency). + * Grows from the trigger via Base UI `--transform-origin`; enter/exit share a + * path; exit is faster than enter (asymmetric timing). Materializes with a + * light blur so the surface reads as glass arriving, not a flat opacity fade. + * Lifecycle: data-starting-style / data-ending-style (not data-state). + */ .model-selector-dropdown { - /* Slide from the anchor side; flipped placement overrides per data-side. */ - --model-menu-slide-y: -6px; - transform-origin: var(--transform-origin); + --model-menu-slide-y: -8px; + --model-menu-slide-x: 0px; + /* Fallback when --transform-origin is unset: grow from top-start (align=start). */ + transform-origin: var(--transform-origin, 16px 0); transition: - opacity 0.18s cubic-bezier(0.16, 1, 0.3, 1), - transform 0.18s cubic-bezier(0.16, 1, 0.3, 1); + opacity 200ms var(--ease-out), + transform 200ms var(--ease-out), + filter 200ms var(--ease-out); } .model-selector-dropdown[data-side="top"] { - --model-menu-slide-y: 6px; + --model-menu-slide-y: 8px; + } + + .model-selector-dropdown[data-side="left"], + .model-selector-dropdown[data-side="inline-start"] { + --model-menu-slide-x: 6px; + --model-menu-slide-y: 0px; + } + + .model-selector-dropdown[data-side="right"], + .model-selector-dropdown[data-side="inline-end"] { + --model-menu-slide-x: -6px; + --model-menu-slide-y: 0px; } .model-selector-dropdown[data-starting-style], .model-selector-dropdown[data-ending-style] { opacity: 0; - transform: translateY(var(--model-menu-slide-y)) scale(0.96); + /* Never scale(0) — start from a visible shape near the trigger */ + filter: blur(2px); + transform: translate(var(--model-menu-slide-x), var(--model-menu-slide-y)) scale(0.96); } + /* Exit: same path, snappier (~half enter) so dismiss feels responsive */ .model-selector-dropdown[data-ending-style] { - transition-duration: 0.12s; - transition-timing-function: cubic-bezier(0.4, 0, 1, 1); - transform: translateY(calc(var(--model-menu-slide-y) / 2)) scale(0.98); + transition-duration: 110ms; + transition-timing-function: var(--ease-out); + filter: blur(1px); + transform: translate( + calc(var(--model-menu-slide-x) * 0.45), + calc(var(--model-menu-slide-y) * 0.45) + ) + scale(0.98); + } + + /* Trigger: open state reads as “menu is attached here” */ + .model-selector-trigger[data-popup-open], + .model-selector-trigger[aria-expanded="true"] { + background-color: hsl(var(--muted) / 0.7); } - /* `backwards` (not `both`): a filled forwards animation would keep holding - * transform after it ends and suppress the item highlight translate. */ + /* Item cascade on open only — short stagger, never blocks interaction */ .model-selector-dropdown[data-open] .model-selector-item { - animation: modelSelectorItemIn 0.18s cubic-bezier(0.16, 1, 0.3, 1) backwards; + /* `backwards` (not `both`): don't hold transform after settle (kills hover). */ + animation: modelSelectorItemIn 160ms var(--ease-out) backwards; } @keyframes modelSelectorItemIn { from { opacity: 0; - transform: translateY(calc(var(--model-menu-slide-y) / 2)); + transform: translateY(calc(var(--model-menu-slide-y) * 0.35 + 3px)); } to { opacity: 1; @@ -1692,13 +2201,37 @@ } } + /* List rows: press highlight (no scale — high frequency while menu is open) */ + .model-selector-item { + transition: background-color 100ms var(--ease-out); + } + .model-selector-item:active { + background-color: hsl(var(--foreground) / 0.09); + } + + .model-selector-item .model-selector-check { + animation: modelSelectorCheckIn 140ms var(--ease-out) both; + } + + @keyframes modelSelectorCheckIn { + from { + opacity: 0; + transform: scale(0.85); + } + to { + opacity: 1; + transform: scale(1); + } + } + @media (prefers-reduced-motion: reduce) { .composer-dropdown, .composer-dropdown [role="menuitemcheckbox"], .composer-dropdown [role="menuitem"], .composer-branch-dropdown, .model-selector-dropdown, - .model-selector-item { + .model-selector-item, + .model-selector-item .model-selector-check { animation: none !important; } @@ -1710,6 +2243,20 @@ .model-selector-trigger svg { transition: none !important; } + + .model-selector-dropdown[data-starting-style], + .model-selector-dropdown[data-ending-style] { + filter: none; + transform: none; + opacity: 0; + } + } + + @media (prefers-reduced-transparency: reduce) { + .model-selector-dropdown[data-starting-style], + .model-selector-dropdown[data-ending-style] { + filter: none; + } } /* Context checkpoint animations */ @@ -2839,15 +3386,149 @@ body > div:not(#root) [data-streamdown="mermaid"]:has([style*="cursor:grabbing"] } /* - * Right-dock header metrics — the dock markup is byte-mirrored with the - * gateway WebUI and therefore carries the web-sized utilities (h-11 header, - * h-8 tab row, no vertical padding). The desktop GUI restores its own - * metrics here. MUST stay outside @layer so these rules beat the Tailwind - * utility classes baked into the mirrored markup (unlayered styles win over - * the utilities layer in Tailwind v4). + * Composer glass material — MUST stay outside @layer so utilities cannot + * override fill/blur. Light mode: high-opacity white on pure canvas reads solid; + * use cool translucent fill + strong blur so glass remains legible. */ +.composer-glass-card { + border-width: 1px; + border-style: solid; + border-color: hsl(0 0% 0% / 0.1); + background: hsl(240 12% 97% / 0.48); + -webkit-backdrop-filter: blur(40px) saturate(190%); + backdrop-filter: blur(40px) saturate(190%); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.85) inset, + 0 12px 40px -14px hsl(220 25% 10% / 0.18), + 0 2px 8px -2px hsl(220 25% 10% / 0.08); + transition: + background-color 180ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)), + border-color 180ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)), + box-shadow 180ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)); +} +.composer-glass-card:focus-within { + border-color: hsl(0 0% 0% / 0.14); + background: hsl(240 14% 98% / 0.58); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.9) inset, + 0 16px 48px -14px hsl(220 25% 10% / 0.22), + 0 4px 12px -4px hsl(220 25% 10% / 0.1); +} +.dark .composer-glass-card { + border-color: hsl(0 0% 100% / 0.12); + background: hsl(0 0% 100% / 0.08); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.1) inset, + 0 12px 40px -14px hsl(0 0% 0% / 0.72), + 0 2px 6px -2px hsl(0 0% 0% / 0.42); +} +.dark .composer-glass-card:focus-within { + border-color: hsl(0 0% 100% / 0.16); + background: hsl(0 0% 100% / 0.11); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.12) inset, + 0 16px 48px -14px hsl(0 0% 0% / 0.78), + 0 4px 12px -4px hsl(0 0% 0% / 0.5); +} + +.composer-queue-glass { + border-width: 1px; + border-style: solid; + border-color: hsl(0 0% 0% / 0.09); + background: hsl(240 12% 97% / 0.48); + -webkit-backdrop-filter: blur(36px) saturate(180%); + backdrop-filter: blur(36px) saturate(180%); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.8) inset, + 0 8px 24px -18px hsl(220 25% 10% / 0.2); +} +.dark .composer-queue-glass { + border-color: hsl(0 0% 100% / 0.1); + background: hsl(0 0% 100% / 0.06); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.08) inset, + 0 8px 24px -18px hsl(0 0% 0% / 0.72); +} + +.composer-queue-item-glass { + border-width: 1px; + border-style: solid; + border-color: hsl(0 0% 0% / 0.06); + background: hsl(0 0% 100% / 0.42); + -webkit-backdrop-filter: blur(20px) saturate(160%); + backdrop-filter: blur(20px) saturate(160%); + box-shadow: inset 0 1px 0 hsl(0 0% 100% / 0.7); +} +.dark .composer-queue-item-glass { + border-color: hsl(0 0% 100% / 0.06); + background: hsl(0 0% 100% / 0.04); + box-shadow: inset 0 1px 0 hsl(0 0% 100% / 0.05); +} + +.composer-bottom-fade { + background: linear-gradient( + to top, + hsl(var(--background) / 0.88) 0%, + hsl(var(--background) / 0.35) 55%, + transparent 100% + ); +} + +@media (prefers-reduced-transparency: reduce) { + .composer-glass-card, + .composer-queue-glass, + .composer-queue-item-glass { + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; + background: hsl(var(--background)) !important; + } + .composer-bottom-fade { + background: hsl(var(--background)); + } +} + +/* + * Right-dock chrome — materials aligned with chat header/sidebar. + * Metrics override the mirrored WebUI utilities (h-11 header, h-8 tabs). + * MUST stay outside @layer so unlayered styles beat Tailwind utilities. + */ +.project-tools-panel { + background: hsl(var(--sidebar-bg)); + border-color: hsl(var(--sidebar-separator) / 0.85); + transition: + opacity var(--duration-ui) var(--ease-out), + transform var(--duration-ui) var(--ease-out), + width var(--duration-ui) var(--ease-out); +} +.project-tools-panel-inner { + background: hsl(var(--sidebar-bg)); + transition: + opacity var(--duration-ui) var(--ease-out), + transform var(--duration-ui) var(--ease-out); +} .project-tools-panel-header { + position: relative; height: 3.25rem; + border-bottom: none; + background: hsl(var(--background) / 0.55); + -webkit-backdrop-filter: blur(18px) saturate(170%); + backdrop-filter: blur(18px) saturate(170%); +} +.project-tools-panel-header::after { + content: ""; + position: absolute; + inset-inline: 0; + bottom: 0; + height: 1px; + pointer-events: none; + background: linear-gradient( + 90deg, + transparent 0%, + hsl(var(--border) / 0.5) 12%, + hsl(var(--border) / 0.5) 88%, + transparent 100% + ); + opacity: 0.75; } .project-tools-panel-tabs { height: 2.5rem; @@ -2860,6 +3541,13 @@ body > div:not(#root) [data-streamdown="mermaid"]:has([style*="cursor:grabbing"] display: none; } +@media (prefers-reduced-motion: reduce) { + .project-tools-panel, + .project-tools-panel-inner { + transition: none; + } +} + /* Composer chip spacing — the gap after a chip is pure presentation; no * spacer character lives in the DOM (serialization re-inserts the word * boundary between a chip token and adjacent content). Outside @layer so From d145b318e2c5fe40d73c1b7f56cd82ab4296f5c3 Mon Sep 17 00:00:00 2001 From: BingZi-233 Date: Fri, 7 Aug 2026 23:57:19 +0800 Subject: [PATCH 02/14] feat(gui): add Apple-style press feedback on buttons Scale buttons to 0.97 on active with property-specific transitions, and skip the scale for link variants and reduced-motion. --- crates/agent-gui/src/components/ui/button.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/agent-gui/src/components/ui/button.tsx b/crates/agent-gui/src/components/ui/button.tsx index 6dccde47c..bb20c2f62 100644 --- a/crates/agent-gui/src/components/ui/button.tsx +++ b/crates/agent-gui/src/components/ui/button.tsx @@ -5,7 +5,9 @@ import * as React from "react"; import { cn } from "../../lib/shared/utils"; 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-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + // Press feedback on pointer-down (Apple Response): scale only — never layout. + // motion-reduce drops the transform so keyboard/high-frequency use stays crisp. + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,background-color,border-color,opacity,box-shadow,transform] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 active:scale-[0.97] motion-reduce:active:scale-100", { variants: { variant: { @@ -14,7 +16,7 @@ const buttonVariants = cva( destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", + link: "text-primary underline-offset-4 hover:underline active:scale-100", }, size: { default: "h-9 px-4 py-2", From d718536d6b617a53977b1ae7a5d29086b7b3715a Mon Sep 17 00:00:00 2001 From: BingZi-233 Date: Fri, 7 Aug 2026 23:57:19 +0800 Subject: [PATCH 03/14] feat(chat): polish empty-state greeting and suggestion cards Use material suggestion cards, stronger CTA, and tighter entrance timing aligned with the Apple empty-state treatment. --- .../src/pages/chat/transcript/ChatEmptyState.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/agent-gui/src/pages/chat/transcript/ChatEmptyState.tsx b/crates/agent-gui/src/pages/chat/transcript/ChatEmptyState.tsx index 34f95c626..e9f45e3c1 100644 --- a/crates/agent-gui/src/pages/chat/transcript/ChatEmptyState.tsx +++ b/crates/agent-gui/src/pages/chat/transcript/ChatEmptyState.tsx @@ -114,35 +114,35 @@ export function ChatEmptyState({ ) : null} ) : ( <> -
+
{t(GREETING_KEYS[period])},{t("chat.greetingSubtitle")}
{onSuggestionSelect ? ( -
+
{SUGGESTION_CARDS.map((card, index) => ( From b6bcede34950e92225d201895c5f67bdb29d0f1f Mon Sep 17 00:00:00 2001 From: BingZi-233 Date: Fri, 7 Aug 2026 23:57:19 +0800 Subject: [PATCH 04/14] feat(chat): refine user bubbles and message action press Round user bubbles closer to Messages geometry and add subtle press feedback on copy/retry/branch/edit controls. --- .../agent-gui/src/pages/chat/transcript/RowActions.tsx | 10 +++++----- .../src/pages/chat/transcript/UserMessageRow.tsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/agent-gui/src/pages/chat/transcript/RowActions.tsx b/crates/agent-gui/src/pages/chat/transcript/RowActions.tsx index 82bacc575..3bcc11d36 100644 --- a/crates/agent-gui/src/pages/chat/transcript/RowActions.tsx +++ b/crates/agent-gui/src/pages/chat/transcript/RowActions.tsx @@ -50,7 +50,7 @@ export function AssistantRowFooter(props: AssistantRowFooterProps) { > )}
-
+
@@ -1943,7 +1950,7 @@ export const ChatHistorySidebar = memo(function ChatHistorySidebar(props: ChatHi ref={recentHeaderRef} className={cn( "grid grid-cols-[minmax(0,1fr)_auto] items-center gap-2 px-2 pb-2", - showProjects ? "border-t border-border/35 pt-0.5" : "pt-3", + showProjects ? "border-t border-[hsl(var(--sidebar-separator)/0.7)] pt-1" : "pt-3", )} > {selectionMode ? ( @@ -1964,13 +1971,13 @@ export const ChatHistorySidebar = memo(function ChatHistorySidebar(props: ChatHi @@ -2130,16 +2137,16 @@ export const ChatHistorySidebar = memo(function ChatHistorySidebar(props: ChatHi
-
+
{appUpdate?.showUpdateButton ? ( From 475115366ccdf0499b61d33743353afeaebb69a1 Mon Sep 17 00:00:00 2001 From: BingZi-233 Date: Fri, 7 Aug 2026 23:57:19 +0800 Subject: [PATCH 06/14] feat(chat): polish model picker origin animation and trigger Grow the model menu from the trigger with asymmetric enter/exit, clearer open-state chrome, and staggered item entrance. --- .../src/pages/chat/components/ChatHeader.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/agent-gui/src/pages/chat/components/ChatHeader.tsx b/crates/agent-gui/src/pages/chat/components/ChatHeader.tsx index 8c5f41cf4..d777e8c42 100644 --- a/crates/agent-gui/src/pages/chat/components/ChatHeader.tsx +++ b/crates/agent-gui/src/pages/chat/components/ChatHeader.tsx @@ -143,7 +143,7 @@ export const ChatHeader = memo(function ChatHeader(props: {
@@ -167,8 +167,8 @@ export const ChatHeader = memo(function ChatHeader(props: { variant="ghost" disabled={!hasModels} className={cn( - "model-selector-trigger h-8 max-w-[min(20rem,calc(100vw-8.5rem))] -translate-y-px justify-between gap-1.5 overflow-hidden rounded-lg px-2.5 py-1 cursor-pointer text-xs font-normal text-foreground transition-all duration-200 ease-out hover:bg-muted/60 dark:text-white", - isModelPickerOpen && "bg-muted/60", + "model-selector-trigger h-8 max-w-[min(20rem,calc(100vw-8.5rem))] -translate-y-px justify-between gap-1.5 overflow-hidden rounded-lg px-2.5 py-1 cursor-pointer text-xs font-normal text-foreground transition-[background-color,color,transform,box-shadow] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] hover:bg-muted/60 dark:text-white", + isModelPickerOpen && "bg-muted/70 shadow-[inset_0_0_0_1px_hsl(var(--border)/0.55)]", )} /> } @@ -181,7 +181,7 @@ export const ChatHeader = memo(function ChatHeader(props: { @@ -190,14 +190,14 @@ export const ChatHeader = memo(function ChatHeader(props: { {(() => { const isAgent = isAgentExecutionMode(settings.system.executionMode); @@ -362,7 +362,8 @@ export const ChatHeader = memo(function ChatHeader(props: { {expanded ? group.opts.map((option) => { const isSelected = option.value === selectedValue; - const itemAnimationDelay = `${Math.min(animationIndex, 5) * 0.025}s`; + // Cap stagger so long lists don't feel slow (30–40ms steps, max ~5). + const itemAnimationDelay = `${Math.min(animationIndex, 5) * 0.035}s`; animationIndex += 1; return ( ); From 3498ccbfd5d34289e0349f92ed7b9cf844c4f250 Mon Sep 17 00:00:00 2001 From: BingZi-233 Date: Fri, 7 Aug 2026 23:57:19 +0800 Subject: [PATCH 07/14] feat(chat): float header over scrolling transcript Overlay the chat header on the transcript so content scrolls underneath, with top inset padding and jump-to-bottom material alignment. --- crates/agent-gui/src/pages/ChatPage.tsx | 79 ++++++++++--------- .../pages/chat/transcript/ChatTranscript.tsx | 6 +- 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/crates/agent-gui/src/pages/ChatPage.tsx b/crates/agent-gui/src/pages/ChatPage.tsx index 2297404af..b4db1e050 100644 --- a/crates/agent-gui/src/pages/ChatPage.tsx +++ b/crates/agent-gui/src/pages/ChatPage.tsx @@ -1932,7 +1932,43 @@ export function ChatPage(props: ChatPageProps) { /> ) : ( <> -
+ {/* Transcript fills the column; header overlays so content scrolls under. */} + + + + +
@@ -1970,7 +2006,7 @@ export function ChatPage(props: ChatPageProps) { ? "Collapse project tools panel" : (terminalDisabledMessage ?? "Expand project tools panel") } - className={`relative h-8 w-8 rounded-lg text-muted-foreground transition-[background-color,color,transform] duration-150 hover:text-foreground active:scale-95 ${ + className={`relative h-8 w-8 rounded-lg text-muted-foreground transition-[background-color,color,transform] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] hover:text-foreground active:scale-[0.97] motion-reduce:active:scale-100 ${ rightDockOpen ? "bg-muted text-foreground" : "" }`} > @@ -1987,44 +2023,11 @@ export function ChatPage(props: ChatPageProps) { } /> - +
+ +
- - - - -
+
{showNoModelsState || showStartChatState ? ( -
+
{/* Keyed per conversation so the hero entrance replays when switching between empty conversations, not just on mount. */} scrollFollowHandle.jumpToBottom()} - className="chat-jump-to-bottom absolute left-1/2 z-10 flex h-8 w-8 items-center justify-center rounded-full text-muted-foreground transition-colors hover:text-foreground" + className="chat-jump-to-bottom absolute left-1/2 z-10 flex h-8 w-8 items-center justify-center rounded-full motion-reduce:active:scale-100" style={{ bottom: Math.ceil(bottomReservePx) + 16 }} > From c47274b756b398f197f182baad487bc31a8bcf03 Mon Sep 17 00:00:00 2001 From: BingZi-233 Date: Fri, 7 Aug 2026 23:57:19 +0800 Subject: [PATCH 08/14] fix(chat): restore light-mode composer frosted glass Lower light fill opacity, strengthen blur, fade the bottom pad, and move glass styles out of utility layers so they are not overridden. --- .../pages/chat/components/ChatComposerBar.tsx | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/crates/agent-gui/src/pages/chat/components/ChatComposerBar.tsx b/crates/agent-gui/src/pages/chat/components/ChatComposerBar.tsx index 37f15b211..87b3bf874 100644 --- a/crates/agent-gui/src/pages/chat/components/ChatComposerBar.tsx +++ b/crates/agent-gui/src/pages/chat/components/ChatComposerBar.tsx @@ -162,8 +162,9 @@ const DEFAULT_QUEUE_SCROLLBAR_STATE: QueueScrollbarState = { thumbTop: 0, }; -const COMPOSER_EXPAND_ANIMATION_MS = 280; -const COMPOSER_EXPAND_EASING = "cubic-bezier(0.32, 0.72, 0.22, 1)"; +const COMPOSER_EXPAND_ANIMATION_MS = 300; +/* Apple sheet curve — critically readable settle without bounce */ +const COMPOSER_EXPAND_EASING = "cubic-bezier(0.32, 0.72, 0, 1)"; function prefersReducedMotion() { return ( @@ -569,13 +570,14 @@ export const ChatComposerBar = memo(function ChatComposerBar(props: { className={cn( "pointer-events-none absolute inset-x-0 bottom-0 z-20 flex justify-center px-4 pb-4", // 展开态从头部下沿一路铺到底部,把整个聊天区让给输入框。 - isComposerExpanded && "top-14", + // Align expanded top with overlay header height (CSS var, not fixed top-14). + isComposerExpanded && "top-[var(--chat-header-height)]", )} >
-
+
    {index > 0 ? ( @@ -741,23 +743,23 @@ export const ChatComposerBar = memo(function ChatComposerBar(props: { : undefined } className={cn( - // 过渡只针对 focus-within 的配色/阴影;不能用 transition-all—— - // 展开态切换 flex-grow 时会被一并动画,导致卡片先跳顶再长满的闪动。 + // Material (fill/blur/border/shadow) lives in .composer-glass-card CSS — + // light mode needs lower opacity + cool tint so glass reads on white. // 常驻 flex-col:FLIP 动画把卡片钳在中间高度时,flex-1 的编辑器 // 区吸收多余空间,工具栏才能始终贴住卡片底边。 - "composer-glass-card relative z-10 flex flex-col overflow-hidden rounded-[24px] border border-black/[0.055] bg-white/70 shadow-[0_12px_40px_-14px_rgba(15,23,42,0.22),0_2px_6px_-2px_rgba(15,23,42,0.08),inset_0_1px_0_rgba(255,255,255,0.74)] backdrop-blur-2xl backdrop-saturate-[165%] transition-[background-color,border-color,box-shadow] focus-within:border-black/[0.075] focus-within:bg-white/74 focus-within:shadow-[0_16px_46px_-14px_rgba(15,23,42,0.26),0_4px_12px_-4px_rgba(15,23,42,0.10),inset_0_1px_0_rgba(255,255,255,0.78)] dark:border-white/[0.10] dark:bg-white/[0.06] dark:shadow-[0_12px_40px_-14px_rgba(0,0,0,0.72),0_2px_6px_-2px_rgba(0,0,0,0.42),inset_0_1px_0_rgba(255,255,255,0.08)] dark:focus-within:border-white/[0.15] dark:focus-within:bg-white/[0.08]", + "composer-glass-card relative z-10 flex flex-col overflow-hidden rounded-[24px]", isComposerExpanded && "min-h-0 flex-1", )} > {/* macOS material rim-light */}
    - {/* subtle inner gloss gradient */} + {/* subtle inner gloss — slightly stronger in light so glass still reads over white */}
    {pendingUploadedFiles.length > 0 ? ( @@ -786,7 +788,7 @@ export const ChatComposerBar = memo(function ChatComposerBar(props: { title={toggleComposerExpandTooltip} aria-label={toggleComposerExpandTooltip} aria-expanded={isComposerExpanded} - className="absolute right-3 top-2 z-20 inline-flex h-8 w-8 items-center justify-center rounded-full text-muted-foreground/70 outline-hidden transition-[background-color,color,scale] hover:bg-muted/60 hover:text-foreground active:scale-90 focus-visible:bg-muted/60" + className="absolute right-3 top-2 z-20 inline-flex h-8 w-8 items-center justify-center rounded-full text-muted-foreground/70 outline-hidden transition-[background-color,color,transform] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] hover:bg-muted/60 hover:text-foreground active:scale-[0.97] focus-visible:bg-muted/60 motion-reduce:active:scale-100" > {isComposerExpanded ? ( @@ -837,7 +839,7 @@ export const ChatComposerBar = memo(function ChatComposerBar(props: { : t("chat.upload.selectFiles") } className={cn( - "composer-toolbar-action relative inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full outline-hidden transition-colors hover:bg-muted/60 focus-visible:bg-muted/60", + "composer-toolbar-action relative inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full outline-hidden hover:bg-muted/60 focus-visible:bg-muted/60 motion-reduce:active:scale-100", "disabled:pointer-events-none disabled:opacity-40", pendingUploadedFiles.length > 0 ? "text-sky-600 hover:text-sky-700 dark:text-sky-300 dark:hover:text-sky-200" @@ -875,7 +877,7 @@ export const ChatComposerBar = memo(function ChatComposerBar(props: { : t("chat.runtime.webSearchOff") } className={cn( - "composer-toolbar-action inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full outline-hidden transition-colors hover:bg-muted/60 focus-visible:bg-muted/60", + "composer-toolbar-action inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full outline-hidden hover:bg-muted/60 focus-visible:bg-muted/60 motion-reduce:active:scale-100", "disabled:pointer-events-none disabled:opacity-40", chatRuntimeControls.nativeWebSearchEnabled ? "text-emerald-600 hover:text-emerald-700 dark:text-emerald-300 dark:hover:text-emerald-200" @@ -907,7 +909,7 @@ export const ChatComposerBar = memo(function ChatComposerBar(props: { : t("chat.runtime.thinkingOff") } className={cn( - "composer-toolbar-action inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full outline-hidden transition-colors hover:bg-muted/60 focus-visible:bg-muted/60", + "composer-toolbar-action inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full outline-hidden hover:bg-muted/60 focus-visible:bg-muted/60 motion-reduce:active:scale-100", "disabled:pointer-events-none disabled:opacity-40", chatRuntimeControls.thinkingEnabled && thinkingSupported ? "text-amber-600 hover:text-amber-700 dark:text-amber-300 dark:hover:text-amber-200" @@ -1019,12 +1021,12 @@ export const ChatComposerBar = memo(function ChatComposerBar(props: { : undefined } className={cn( - "h-8 w-8 shrink-0 rounded-full border-0 p-0 shadow-none transition-all", + "h-8 w-8 shrink-0 rounded-full border-0 p-0 shadow-none transition-[background-color,color,opacity,transform,filter] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)]", canQueueDraftWhileSending - ? "hover:brightness-105 active:scale-95" + ? "hover:brightness-105 active:scale-[0.97]" : isSending - ? "hover:opacity-90 active:scale-95" - : "disabled:opacity-100 [&:not(:disabled)]:bg-foreground [&:not(:disabled)]:text-background [&:not(:disabled)]:hover:bg-foreground/85 [&:not(:disabled)]:active:scale-95 disabled:bg-muted/60 disabled:text-muted-foreground", + ? "hover:opacity-90 active:scale-[0.97]" + : "disabled:opacity-100 [&:not(:disabled)]:bg-foreground [&:not(:disabled)]:text-background [&:not(:disabled)]:hover:bg-foreground/90 [&:not(:disabled)]:active:scale-[0.97] disabled:bg-muted/60 disabled:text-muted-foreground", )} > {canQueueDraftWhileSending ? ( From b4b69c52b6b7b14621514dd41b6712b171d9f358 Mon Sep 17 00:00:00 2001 From: BingZi-233 Date: Fri, 7 Aug 2026 23:57:19 +0800 Subject: [PATCH 09/14] feat(chat): align right dock materials with chat chrome Use structural sidebar tones, translucent tab chrome, and material starter cards so the dock matches the rest of the shell. --- .../src/components/project-tools/RightDockLauncher.tsx | 2 +- .../src/components/project-tools/RightDockPanel.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/agent-gui/src/components/project-tools/RightDockLauncher.tsx b/crates/agent-gui/src/components/project-tools/RightDockLauncher.tsx index f2b9520e1..2bd23ebaa 100644 --- a/crates/agent-gui/src/components/project-tools/RightDockLauncher.tsx +++ b/crates/agent-gui/src/components/project-tools/RightDockLauncher.tsx @@ -190,7 +190,7 @@ export function RightDockChooser(props: RightDockChooserProps) { onClick={tool.onClick} disabled={tool.disabled} title={tool.titleAttr} - className="group flex items-center gap-3 rounded-lg border border-border/60 bg-background px-3.5 py-3 text-left text-sm text-foreground transition-all hover:border-border hover:bg-muted/60 hover:shadow-sm disabled:pointer-events-none disabled:opacity-50" + className="group flex items-center gap-3 rounded-2xl border border-border/50 bg-background/70 px-3.5 py-3 text-left text-sm text-foreground shadow-[0_1px_0_rgba(255,255,255,0.45)_inset] backdrop-blur-md transition-[background-color,border-color,transform,box-shadow] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] hover:border-border hover:bg-background/90 hover:shadow-sm active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50 motion-reduce:active:scale-100 dark:border-white/[0.08] dark:bg-white/[0.04] dark:shadow-[0_1px_0_rgba(255,255,255,0.06)_inset]" >
    {tool.icon} diff --git a/crates/agent-gui/src/components/project-tools/RightDockPanel.tsx b/crates/agent-gui/src/components/project-tools/RightDockPanel.tsx index 1d8fbecc2..b7b15eb22 100644 --- a/crates/agent-gui/src/components/project-tools/RightDockPanel.tsx +++ b/crates/agent-gui/src/components/project-tools/RightDockPanel.tsx @@ -742,9 +742,9 @@ export const RightDockPanel = memo(function RightDockPanel(props: RightDockPanel data-state={isOpen ? "open" : "closed"} data-project-tools-resizing={isResizing ? "true" : undefined} className={cn( - "project-tools-panel zone-font-scale fixed inset-x-0 bottom-0 z-40 flex h-[min(72vh,34rem)] min-h-0 w-full shrink-0 flex-col overflow-hidden bg-background shadow-2xl transition-[opacity,transform] duration-200 ease-out motion-reduce:transition-none md:relative md:inset-auto md:z-10 md:h-full md:overflow-visible md:shadow-none", + "project-tools-panel zone-font-scale fixed inset-x-0 bottom-0 z-40 flex h-[min(72vh,34rem)] min-h-0 w-full shrink-0 flex-col overflow-hidden shadow-[0_-12px_40px_-18px_rgba(15,23,42,0.28)] motion-reduce:transition-none md:relative md:inset-auto md:z-10 md:h-full md:overflow-visible md:shadow-none", isOpen - ? "pointer-events-auto translate-y-0 border-t border-border opacity-100 md:w-[var(--project-tools-panel-width)] md:translate-x-0 md:border-l md:border-t-0" + ? "pointer-events-auto translate-y-0 border-t opacity-100 md:w-[var(--project-tools-panel-width)] md:translate-x-0 md:border-l md:border-t-0" : "pointer-events-none translate-y-full border-t border-transparent opacity-0 md:translate-x-3 md:translate-y-0 md:border-l-0 md:border-t-0", effectiveWidthCollapsed ? "md:w-0" : "md:w-[var(--project-tools-panel-width)]", )} @@ -752,7 +752,7 @@ export const RightDockPanel = memo(function RightDockPanel(props: RightDockPanel >
    -
    +
    Date: Fri, 7 Aug 2026 23:57:19 +0800 Subject: [PATCH 10/14] feat(chat): polish floor nav panel and marker interactions Apply glass material to the expanded panel, touch-down row highlight, and snappier pin/marker feedback. --- .../pages/chat/transcript/FloorNavRail.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/agent-gui/src/pages/chat/transcript/FloorNavRail.tsx b/crates/agent-gui/src/pages/chat/transcript/FloorNavRail.tsx index e3f160a0b..75b012aec 100644 --- a/crates/agent-gui/src/pages/chat/transcript/FloorNavRail.tsx +++ b/crates/agent-gui/src/pages/chat/transcript/FloorNavRail.tsx @@ -253,15 +253,15 @@ export function FloorNavRail(props: { // 收藏区的副本不带定位锚点,展开自动居中永远对准主列表里的当前行。 data-floor-active={(isActive && !isPinnedCopy) || undefined} className={cn( - "group/floor flex items-center gap-1 rounded-lg pr-1 transition-colors", - isActive ? "bg-foreground/[0.06]" : "hover:bg-foreground/[0.04]", + "floor-nav-row group/floor flex items-center gap-1 rounded-[10px] pr-1", + isActive ? "bg-foreground/[0.07]" : "hover:bg-foreground/[0.045]", )} > ) : null}
    -
    +
    {icon}
    -

    +

    {title}

    {subtitle ? ( -

    +

    {subtitle}

    ) : null} @@ -97,31 +99,61 @@ export function GlassPanel(props: { className?: string; }) { const { children, tone = "default", active = false, className } = props; - const toneClass = (() => { - switch (tone) { - case "muted": - return "border-border/40 bg-muted/40"; - case "error": - return "border-destructive/30 bg-destructive/5"; - case "amber": - case "violet": - case "neutral": - return active - ? "border-border/55 bg-background/80 shadow-[0_1px_0_rgba(255,255,255,0.6)_inset,0_6px_22px_-14px_rgba(15,23,42,0.18)] dark:border-white/[0.09] dark:bg-white/[0.06] dark:shadow-[0_1px_0_rgba(255,255,255,0.06)_inset,0_6px_22px_-14px_rgba(0,0,0,0.55)]" - : "border-border/40 bg-background/60"; - default: - return "border-border/40 bg-background/60"; - } - })(); return (
    {children}
    ); } + +/** Status strip under HubHeader (Skills enabled / MCP ready). */ +export function HubStatusBanner(props: { + ready?: boolean; + children: ReactNode; + className?: string; +}) { + const { ready = false, children, className } = props; + return ( +
    + {children} +
    + ); +} + +/** Segmented tab track (Installed / Store / Import). */ +export function HubSegmentedTrack(props: { children: ReactNode; className?: string }) { + return
    {props.children}
    ; +} + +export function HubSegmentButton( + props: ButtonHTMLAttributes & { + active?: boolean; + icon?: ReactNode; + count?: number | null; + }, +) { + const { active, icon, count, className, children, type = "button", ...rest } = props; + return ( + + ); +} diff --git a/crates/agent-gui/src/index.css b/crates/agent-gui/src/index.css index 80c9bc1e7..c9f0c9630 100644 --- a/crates/agent-gui/src/index.css +++ b/crates/agent-gui/src/index.css @@ -2481,7 +2481,7 @@ @keyframes hubPageIn { from { opacity: 0; - transform: translateY(8px); + transform: translateY(6px); } to { opacity: 1; @@ -2492,7 +2492,7 @@ @keyframes hubPanelIn { from { opacity: 0; - transform: translateY(14px) scale(0.985); + transform: translateY(10px) scale(0.98); } to { opacity: 1; @@ -2503,7 +2503,7 @@ @keyframes hubChipIn { from { opacity: 0; - transform: translateY(6px) scale(0.94); + transform: translateY(5px) scale(0.97); } to { opacity: 1; @@ -2536,28 +2536,29 @@ } .hub-page-enter { - animation: hubPageIn 260ms cubic-bezier(0.16, 1, 0.3, 1) both; + animation: hubPageIn 220ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)) both; } .hub-page-enter .hub-header { - animation: hubPanelIn 360ms cubic-bezier(0.16, 1, 0.3, 1) both; + animation: hubPanelIn 280ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)) both; } .hub-page-enter .hub-content-stage { - animation: hubPanelIn 420ms cubic-bezier(0.16, 1, 0.3, 1) 70ms both; + animation: hubPanelIn 320ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)) 50ms both; } .hub-panel-enter, .hub-empty-state-enter { - animation: hubPanelIn 380ms cubic-bezier(0.16, 1, 0.3, 1) both; + animation: hubPanelIn 280ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)) both; } .hub-panel-enter-delayed { - animation-delay: 110ms; + animation-delay: 80ms; } + /* Critically damped — no spring overshoot on high-frequency chips */ .hub-chip-enter { - animation: hubChipIn 320ms cubic-bezier(0.34, 1.56, 0.64, 1) both; + animation: hubChipIn 240ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)) both; } .hub-chip-enter:nth-child(1) { animation-delay: 0ms; @@ -2859,32 +2860,32 @@ } .skill-card-enter { - animation: skillCardIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; + animation: skillCardIn 0.28s var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)) both; } .skill-card-enter:nth-child(1) { animation-delay: 0ms; } .skill-card-enter:nth-child(2) { - animation-delay: 50ms; + animation-delay: 35ms; } .skill-card-enter:nth-child(3) { - animation-delay: 100ms; + animation-delay: 70ms; } .skill-card-enter:nth-child(4) { - animation-delay: 150ms; + animation-delay: 105ms; } .skill-card-enter:nth-child(5) { - animation-delay: 200ms; + animation-delay: 140ms; } .skill-card-enter:nth-child(6) { - animation-delay: 250ms; + animation-delay: 175ms; } .skill-card-enter:nth-child(n + 7) { - animation-delay: 300ms; + animation-delay: 200ms; } .skill-check-enter { - animation: skillCardCheckIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) both; + animation: skillCardCheckIn 0.2s var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)) both; } .skills-scan-dot { @@ -3630,6 +3631,248 @@ body > div:not(#root) [data-streamdown="mermaid"]:has([style*="cursor:grabbing"] } } +/* + * Skills / MCP Hub materials — unlayered so they beat utility glass strings + * on cards and banners. Light mode uses cooler translucent fills (same lesson + * as composer glass: high white opacity on white canvas reads solid). + */ +.hub-header-icon { + border: 1px solid hsl(0 0% 0% / 0.08); + background: hsl(240 12% 98% / 0.72); + -webkit-backdrop-filter: blur(20px) saturate(170%); + backdrop-filter: blur(20px) saturate(170%); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.85) inset, + 0 4px 14px -10px hsl(220 25% 10% / 0.12); +} +.dark .hub-header-icon { + border-color: hsl(0 0% 100% / 0.1); + background: hsl(0 0% 100% / 0.06); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.06) inset, + 0 4px 14px -10px hsl(0 0% 0% / 0.45); +} + +.hub-glass-panel { + border-radius: 1rem; + border: 1px solid hsl(0 0% 0% / 0.08); + background: hsl(240 12% 97% / 0.55); + padding: 0.875rem 1rem; + -webkit-backdrop-filter: blur(24px) saturate(175%); + backdrop-filter: blur(24px) saturate(175%); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.75) inset, + 0 8px 24px -16px hsl(220 25% 10% / 0.14); +} +.hub-glass-panel[data-tone="muted"] { + background: hsl(240 10% 96% / 0.5); + border-color: hsl(0 0% 0% / 0.06); +} +.hub-glass-panel[data-tone="error"] { + background: hsl(0 72% 96% / 0.7); + border-color: hsl(0 72% 51% / 0.28); +} +.hub-glass-panel[data-active] { + border-color: hsl(0 0% 0% / 0.12); + background: hsl(240 14% 98% / 0.7); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.85) inset, + 0 10px 28px -14px hsl(220 25% 10% / 0.18); +} +.dark .hub-glass-panel { + border-color: hsl(0 0% 100% / 0.09); + background: hsl(0 0% 100% / 0.055); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.06) inset, + 0 8px 24px -16px hsl(0 0% 0% / 0.55); +} +.dark .hub-glass-panel[data-tone="muted"] { + background: hsl(0 0% 100% / 0.035); + border-color: hsl(0 0% 100% / 0.07); +} +.dark .hub-glass-panel[data-tone="error"] { + background: hsl(0 55% 18% / 0.35); + border-color: hsl(0 62% 45% / 0.35); +} +.dark .hub-glass-panel[data-active] { + border-color: hsl(0 0% 100% / 0.12); + background: hsl(0 0% 100% / 0.08); +} + +.hub-status-banner { + position: relative; + overflow: hidden; + border-radius: 1rem; + border: 1px solid hsl(0 0% 0% / 0.08); + background: hsl(240 12% 97% / 0.52); + -webkit-backdrop-filter: blur(28px) saturate(180%); + backdrop-filter: blur(28px) saturate(180%); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.8) inset, + 0 10px 28px -16px hsl(220 25% 10% / 0.14); +} +.hub-status-banner[data-ready] { + border-color: hsl(0 0% 0% / 0.1); + background: hsl(240 14% 98% / 0.62); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.88) inset, + 0 12px 32px -16px hsl(220 25% 10% / 0.16); +} +.dark .hub-status-banner { + border-color: hsl(0 0% 100% / 0.09); + background: hsl(0 0% 100% / 0.05); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.06) inset, + 0 10px 28px -16px hsl(0 0% 0% / 0.55); +} +.dark .hub-status-banner[data-ready] { + border-color: hsl(0 0% 100% / 0.11); + background: hsl(0 0% 100% / 0.07); +} + +.hub-segmented-track { + display: inline-flex; + flex-shrink: 0; + border-radius: 1rem; + border: 1px solid hsl(0 0% 0% / 0.08); + background: hsl(240 12% 97% / 0.5); + padding: 0.25rem; + -webkit-backdrop-filter: blur(20px) saturate(170%); + backdrop-filter: blur(20px) saturate(170%); + box-shadow: 0 1px 0 hsl(0 0% 100% / 0.75) inset; +} +.dark .hub-segmented-track { + border-color: hsl(0 0% 100% / 0.08); + background: hsl(0 0% 100% / 0.04); + box-shadow: 0 1px 0 hsl(0 0% 100% / 0.05) inset; +} + +.hub-segment-button { + position: relative; + display: inline-flex; + height: 2.25rem; + align-items: center; + justify-content: center; + gap: 0.5rem; + border-radius: 0.75rem; + padding-inline: 1rem; + font-size: 12.5px; + font-weight: 500; + letter-spacing: -0.01em; + color: hsl(var(--muted-foreground)); + transition: + background-color 150ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)), + color 150ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)), + box-shadow 150ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)), + transform 100ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)); +} +.hub-segment-button:hover { + background: hsl(var(--background) / 0.65); + color: hsl(var(--foreground)); +} +.hub-segment-button:active { + transform: scale(0.97); +} +.hub-segment-button[data-active] { + background: hsl(240 14% 99% / 0.92); + color: hsl(var(--foreground)); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.9) inset, + 0 4px 12px -8px hsl(220 25% 10% / 0.16); + outline: 1px solid hsl(0 0% 0% / 0.08); +} +.dark .hub-segment-button[data-active] { + background: hsl(0 0% 100% / 0.09); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.08) inset, + 0 4px 12px -8px hsl(0 0% 0% / 0.5); + outline-color: hsl(0 0% 100% / 0.1); +} +.hub-segment-count { + display: inline-flex; + height: 1rem; + min-width: 1rem; + align-items: center; + justify-content: center; + border-radius: 999px; + padding-inline: 0.25rem; + font-size: 10px; + font-weight: 600; + font-variant-numeric: tabular-nums; + background: hsl(var(--muted) / 0.7); + color: hsl(var(--muted-foreground)); +} +.hub-segment-count[data-active] { + background: hsl(var(--foreground) / 0.08); + color: hsl(var(--foreground) / 0.85); +} + +/* Skill / store cards — material + press; avoid transition: all */ +.hub-skill-card { + border-radius: 1rem; + border: 1px solid hsl(0 0% 0% / 0.08); + background: hsl(240 12% 98% / 0.72); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.8) inset, + 0 6px 20px -14px hsl(220 25% 10% / 0.14); + -webkit-backdrop-filter: blur(18px) saturate(160%); + backdrop-filter: blur(18px) saturate(160%); + transition: + transform 160ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)), + background-color 160ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)), + border-color 160ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)), + box-shadow 160ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)); +} +.hub-skill-card:hover { + border-color: hsl(0 0% 0% / 0.12); + background: hsl(240 14% 99% / 0.85); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.9) inset, + 0 10px 24px -12px hsl(220 25% 10% / 0.16); + transform: translateY(-1px); +} +.hub-skill-card:active { + transform: scale(0.985); +} +.hub-skill-card.hub-skill-card--static:hover, +.hub-skill-card.hub-skill-card--static:active { + transform: none; +} +.dark .hub-skill-card { + border-color: hsl(0 0% 100% / 0.09); + background: hsl(0 0% 100% / 0.055); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.05) inset, + 0 6px 20px -14px hsl(0 0% 0% / 0.5); +} +.dark .hub-skill-card:hover { + border-color: hsl(0 0% 100% / 0.13); + background: hsl(0 0% 100% / 0.08); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.07) inset, + 0 10px 24px -12px hsl(0 0% 0% / 0.55); +} + +@media (prefers-reduced-motion: reduce) { + .hub-segment-button:active, + .hub-skill-card:hover, + .hub-skill-card:active { + transform: none; + } +} + +@media (prefers-reduced-transparency: reduce) { + .hub-header-icon, + .hub-glass-panel, + .hub-status-banner, + .hub-segmented-track, + .hub-skill-card { + -webkit-backdrop-filter: none; + backdrop-filter: none; + background: hsl(var(--background)); + } +} + /* Composer chip spacing — the gap after a chip is pure presentation; no * spacer character lives in the DOM (serialization re-inserts the word * boundary between a chip token and adjacent content). Outside @layer so diff --git a/crates/agent-gui/src/pages/mcp-hub/McpHubPage.tsx b/crates/agent-gui/src/pages/mcp-hub/McpHubPage.tsx index 82301550e..619eed913 100644 --- a/crates/agent-gui/src/pages/mcp-hub/McpHubPage.tsx +++ b/crates/agent-gui/src/pages/mcp-hub/McpHubPage.tsx @@ -1,5 +1,11 @@ import { useState } from "react"; -import { HubBackdrop, HubHeader } from "../../components/hub/HubChrome"; +import { + HubBackdrop, + HubHeader, + HubSegmentButton, + HubSegmentedTrack, + HubStatusBanner, +} from "../../components/hub/HubChrome"; import { Cable, Cloud, Download, Plug, Plus, Server, Sparkles } from "../../components/icons"; import { Button } from "../../components/ui/button"; import { useLocale } from "../../i18n"; @@ -71,22 +77,13 @@ export function McpHubPage(props: McpHubPageProps) {
    {/* Status banner */} -
    +
    @@ -96,13 +93,13 @@ export function McpHubPage(props: McpHubPageProps) {
    -
    +
    {ready ? t("mcpHub.statusReady") : t("mcpHub.statusEmpty")}
    {ready ? ( 0 ? "bg-foreground/[0.06] text-foreground/85 ring-1 ring-border/50" : "bg-background/60 text-muted-foreground ring-1 ring-border/40", @@ -116,7 +113,7 @@ export function McpHubPage(props: McpHubPageProps) { ) : null}
    {statusHint ? ( -
    +
    {statusHint}
    ) : null} @@ -134,11 +131,11 @@ export function McpHubPage(props: McpHubPageProps) { {t("mcpHub.add")}
    -
    + {/* Tab bar */}
    -
    + {[ { value: "installed" as const, @@ -160,40 +157,22 @@ export function McpHubPage(props: McpHubPageProps) { }, ].map((item) => { const Icon = item.icon; - const active = view === item.value; return ( - + {item.label} + ); })} -
    + {view === "store" ? ( -
    +
    {t("mcpHub.storeSubtitle")}
    diff --git a/crates/agent-gui/src/pages/skills-hub/SkillsHubPage.tsx b/crates/agent-gui/src/pages/skills-hub/SkillsHubPage.tsx index f0575a575..10ba5234b 100644 --- a/crates/agent-gui/src/pages/skills-hub/SkillsHubPage.tsx +++ b/crates/agent-gui/src/pages/skills-hub/SkillsHubPage.tsx @@ -10,7 +10,14 @@ import { useState, } from "react"; import { createPortal } from "react-dom"; -import { GlassPanel, HubBackdrop, HubHeader } from "../../components/hub/HubChrome"; +import { + GlassPanel, + HubBackdrop, + HubHeader, + HubSegmentButton, + HubSegmentedTrack, + HubStatusBanner, +} from "../../components/hub/HubChrome"; import { Activity, AlertTriangle, @@ -1126,9 +1133,9 @@ const InstalledSkillCard = memo(function InstalledSkillCard(props: InstalledSkil onOpenPreview(skill); }} className={cn( - "hub-skill-card skill-card-enter group flex h-full min-h-[13rem] w-full cursor-pointer flex-col rounded-2xl border border-border/50 bg-background/75 p-3.5 text-left shadow-[0_1px_0_rgba(255,255,255,0.55)_inset,0_4px_18px_-12px_rgba(15,23,42,0.16)] transition-all hover:-translate-y-0.5 hover:border-border/60 hover:bg-background/85 hover:shadow-[0_4px_16px_-10px_rgba(15,23,42,0.18)] dark:border-white/[0.08] dark:bg-white/[0.05] dark:shadow-[0_1px_0_rgba(255,255,255,0.05)_inset,0_4px_18px_-12px_rgba(0,0,0,0.5)] dark:hover:border-white/[0.12] dark:hover:bg-white/[0.07] dark:hover:shadow-[0_4px_16px_-10px_rgba(0,0,0,0.55)] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-foreground/15", + "hub-skill-card skill-card-enter group flex h-full min-h-[13rem] w-full cursor-pointer flex-col p-3.5 text-left focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-foreground/15", "[content-visibility:auto] [contain-intrinsic-size:auto_13rem]", - bulkMode ? "cursor-default hover:translate-y-0" : null, + bulkMode ? "hub-skill-card--static cursor-default" : null, )} > {card} @@ -1165,14 +1172,15 @@ const InstalledSkillCard = memo(function InstalledSkillCard(props: InstalledSkil onOpenPreview(skill); }} className={cn( - "hub-skill-card skill-card-enter group relative flex h-full min-h-[13rem] w-full flex-col rounded-2xl border p-3.5 text-left transition-all", + "hub-skill-card skill-card-enter group relative flex h-full min-h-[13rem] w-full flex-col p-3.5 text-left", "cursor-pointer focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-foreground/15", "[content-visibility:auto] [contain-intrinsic-size:auto_13rem]", // Enabled style always visible; bulk selection overlays primary ring. checked ? "border-emerald-500/35 bg-emerald-50/90 shadow-[0_1px_0_rgba(255,255,255,0.55)_inset,0_4px_14px_-12px_rgba(16,185,129,0.28)] dark:border-emerald-400/30 dark:bg-emerald-500/12 dark:shadow-[0_1px_0_rgba(255,255,255,0.05)_inset,0_4px_14px_-12px_rgba(16,185,129,0.22)]" - : "border-border/40 bg-muted/45 text-muted-foreground shadow-none hover:-translate-y-0.5 hover:border-border/55 hover:bg-muted/55 dark:border-white/[0.06] dark:bg-white/[0.025] dark:hover:border-white/[0.10] dark:hover:bg-white/[0.04]", + : "border-border/40 bg-muted/45 text-muted-foreground shadow-none", bulkSelected ? "ring-2 ring-primary/50 ring-offset-1 ring-offset-background" : null, + bulkMode ? "hub-skill-card--static" : null, )} > {card} @@ -2360,22 +2368,13 @@ export function SkillsHubPage(props: SkillsHubPageProps) {
    {/* Status pill row */} -
    +
    @@ -2385,7 +2384,7 @@ export function SkillsHubPage(props: SkillsHubPageProps) {
    -
    +
    {skillsEnabled ? t("settings.skillsHubEnabled") : t("settings.skillsHubDisabled")} @@ -2393,7 +2392,7 @@ export function SkillsHubPage(props: SkillsHubPageProps) { {selectableSkills.length > 0 && ( 0 ? "bg-foreground/[0.06] text-foreground/85 ring-1 ring-border/50" : "bg-background/60 text-muted-foreground ring-1 ring-border/40", @@ -2409,7 +2408,7 @@ export function SkillsHubPage(props: SkillsHubPageProps) { )}
    {skillsStatusHint ? ( -
    +
    {skillsStatusHint}
    ) : null} @@ -2474,10 +2473,10 @@ export function SkillsHubPage(props: SkillsHubPageProps) {
    -
    +
    -
    + {[ { value: "installed" as const, @@ -2499,37 +2498,20 @@ export function SkillsHubPage(props: SkillsHubPageProps) { }, ].map((item) => { const Icon = item.icon; - const active = view === item.value; return ( - + {item.label} + ); })} -
    + {!lockedByChatMode ? (
    From 8d679faa2edc9aa4f11bdf961f9342f3c5372548 Mon Sep 17 00:00:00 2001 From: BingZi-233 Date: Sat, 8 Aug 2026 00:54:31 +0800 Subject: [PATCH 13/14] feat(settings): align settings chrome with Apple materials Restyle the settings sidebar and section cards to match chat/hub hierarchy, use neutral nav selection with press feedback, and soften section enter motion with critically damped easing. --- crates/agent-gui/src/index.css | 147 +++++++++++++++++- crates/agent-gui/src/pages/SettingsPage.tsx | 54 +++---- .../src/pages/settings/SystemSettingsForm.tsx | 14 +- .../agent-gui/src/pages/settings/shared.tsx | 10 +- 4 files changed, 175 insertions(+), 50 deletions(-) diff --git a/crates/agent-gui/src/index.css b/crates/agent-gui/src/index.css index c9f0c9630..ddfb0d515 100644 --- a/crates/agent-gui/src/index.css +++ b/crates/agent-gui/src/index.css @@ -1618,9 +1618,9 @@ } } - /* Settings page section transitions */ + /* Settings page section transitions — critically damped ease-out */ .settings-section-enter { - animation: settingsSectionIn 0.28s cubic-bezier(0.16, 1, 0.3, 1); + animation: settingsSectionIn 0.24s var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)); /* * No `will-change` here: this class sits on the settings scroll container * itself, and a permanent layer promotion on a scroller makes Android @@ -1630,7 +1630,7 @@ } .settings-section-title-enter { - animation: settingsSectionTitleIn 0.22s cubic-bezier(0.16, 1, 0.3, 1); + animation: settingsSectionTitleIn 0.2s var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)); } /* Provider dialog: panel switches (sections are keyed by panel id, so the @@ -1740,7 +1740,7 @@ @keyframes settingsSectionIn { from { opacity: 0; - transform: translateY(14px) scale(0.985); + transform: translateY(8px) scale(0.99); } to { opacity: 1; @@ -1751,7 +1751,7 @@ @keyframes settingsSectionTitleIn { from { opacity: 0; - transform: translateY(8px); + transform: translateY(4px); } to { opacity: 1; @@ -1762,7 +1762,7 @@ /* Settings system-tools tab views: soft enter when switching between the * built-in and custom tabs (the view wrapper is keyed by tab). */ .settings-tools-view-enter { - animation: settingsToolsViewIn 0.22s cubic-bezier(0.16, 1, 0.3, 1); + animation: settingsToolsViewIn 0.2s var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)); will-change: transform, opacity; } @@ -3044,6 +3044,141 @@ } } +/* + * Settings page chrome — Apple-like sidebar + content hierarchy. + * Unlayered so materials beat utility borders/backgrounds. + */ +.settings-sidebar { + background: hsl(var(--sidebar-bg)); + border-right: 1px solid hsl(var(--sidebar-separator) / 0.85); +} +.settings-sidebar-header { + border-bottom: 1px solid hsl(var(--sidebar-separator) / 0.7); +} +.settings-sidebar-footer { + border-top: 1px solid hsl(var(--sidebar-separator) / 0.7); +} +.settings-title-icon { + color: hsl(var(--foreground) / 0.8); + background: hsl(240 12% 97% / 0.7); + border: 1px solid hsl(0 0% 0% / 0.08); + box-shadow: 0 1px 0 hsl(0 0% 100% / 0.75) inset; +} +.dark .settings-title-icon { + background: hsl(0 0% 100% / 0.06); + border-color: hsl(0 0% 100% / 0.1); + box-shadow: 0 1px 0 hsl(0 0% 100% / 0.06) inset; +} + +.settings-back-button { + display: flex; + width: 100%; + align-items: center; + gap: 0.5rem; + border-radius: 10px; + padding: 0.5rem 0.625rem; + font-size: 13px; + letter-spacing: -0.01em; + color: hsl(var(--muted-foreground)); + transition: + background-color 120ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)), + color 120ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)), + transform 100ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)); +} +.settings-back-button:hover { + background: hsl(var(--foreground) / 0.06); + color: hsl(var(--foreground)); +} +.settings-back-button:active { + transform: scale(0.98); +} + +.settings-nav-group-label { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; + line-height: 1.25; + color: hsl(var(--muted-foreground) / 0.75); + text-transform: uppercase; +} + +.settings-nav-item { + color: hsl(var(--foreground) / 0.78); + transition: + background-color 80ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)), + color 80ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)); +} +.settings-nav-item:hover { + background: hsl(var(--foreground) / 0.05); + color: hsl(var(--foreground)); +} +.settings-nav-item:active { + background: hsl(var(--foreground) / 0.09); +} +.settings-nav-item[data-active] { + background: hsl(var(--foreground) / 0.08); + color: hsl(var(--foreground)); + font-weight: 600; +} +.settings-nav-item-icon { + color: inherit; + background: hsl(var(--foreground) / 0.05); + transition: background-color 120ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)); +} +.settings-nav-item[data-active] .settings-nav-item-icon { + background: hsl(var(--foreground) / 0.1); +} +.settings-nav-item:hover .settings-nav-item-icon { + background: hsl(var(--foreground) / 0.08); +} + +.settings-main-header { + border-bottom: 1px solid hsl(var(--border) / 0.55); + background: hsl(var(--background) / 0.72); + -webkit-backdrop-filter: blur(18px) saturate(170%); + backdrop-filter: blur(18px) saturate(170%); +} +.dark .settings-main-header { + border-bottom-color: hsl(0 0% 100% / 0.08); + background: hsl(var(--background) / 0.65); +} + +.settings-card { + border-radius: 1rem; + border: 1px solid hsl(0 0% 0% / 0.08); + background: hsl(240 12% 98% / 0.78); + padding: 1rem; + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.7) inset, + 0 6px 18px -14px hsl(220 25% 10% / 0.1); +} +.dark .settings-card { + border-color: hsl(0 0% 100% / 0.09); + background: hsl(0 0% 100% / 0.04); + box-shadow: + 0 1px 0 hsl(0 0% 100% / 0.05) inset, + 0 6px 18px -14px hsl(0 0% 0% / 0.45); +} + +.settings-switch { + transition: background-color 160ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)); +} + +@media (prefers-reduced-motion: reduce) { + .settings-back-button:active { + transform: none; + } +} + +@media (prefers-reduced-transparency: reduce) { + .settings-main-header, + .settings-card { + -webkit-backdrop-filter: none; + backdrop-filter: none; + background: hsl(var(--background)); + } +} + /* * SSH settings — responsive helpers. Unlayered so these win over the * Tailwind flex/align/gap/padding utilities on the same elements diff --git a/crates/agent-gui/src/pages/SettingsPage.tsx b/crates/agent-gui/src/pages/SettingsPage.tsx index 05422f7cf..e753a1495 100644 --- a/crates/agent-gui/src/pages/SettingsPage.tsx +++ b/crates/agent-gui/src/pages/SettingsPage.tsx @@ -66,19 +66,10 @@ function NavItem({ icon, label, active, onClick }: NavItemProps) { -
    -
    - +
    +
    +
    - {t("settings.title")} + + {t("settings.title")} +
    -