diff --git a/docs/superpowers/plans/2026-05-15-pc-style-polish-phase-2.md b/docs/superpowers/plans/2026-05-15-pc-style-polish-phase-2.md new file mode 100644 index 000000000..a0189c512 --- /dev/null +++ b/docs/superpowers/plans/2026-05-15-pc-style-polish-phase-2.md @@ -0,0 +1,138 @@ +# PC Style Polish Phase 2 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Tighten the remaining desktop settings chrome gaps and expand desktop UI review coverage with deeper workspace states. + +**Architecture:** Keep the phase-1 page structure intact, then layer a desktop-only settings header shell on top of the existing settings route while preserving the mobile header path. Extend `ui-preview` with isolated desktop review scenes for workspace editor and diff states so future visual review covers more than empty and launcher states. + +**Tech Stack:** React, TypeScript, Jotai, Vitest, Playwright `e2e-ui`, CSS tokens in `packages/web/src/styles` + +--- + +### Task 1: Add a dedicated desktop settings header shell + +**Files:** +- Modify: `packages/web/src/features/settings/components/settings-page.tsx` +- Modify: `packages/web/src/features/settings/components/settings-page.test.tsx` +- Modify: `packages/web/src/styles/components.css` +- Modify: `packages/web/src/styles/components.theme.test.ts` + +- [x] **Step 1: Write the failing tests** + +Add a desktop-only assertion in `settings-page.test.tsx` that checks: +- `.settings-header__desktop` renders on desktop +- `.settings-header .mobile-page-header` is absent on desktop +- the current section pill shows the active section label + +Add CSS assertions in `components.theme.test.ts` that check: +- `.settings-header__desktop` uses a centered max-width layout +- `.settings-header__section-pill` uses elevated panel styling +- mobile header assertions still target `.mobile-page-header` + +- [x] **Step 2: Run the focused tests and verify they fail** + +Run: `pnpm --filter @coder-studio/web exec vitest run src/features/settings/components/settings-page.test.tsx src/styles/components.theme.test.ts` + +Expected: +- settings page test fails because desktop header selectors do not exist +- theme test fails because new desktop header selectors are missing + +- [x] **Step 3: Implement the desktop settings header** + +In `settings-page.tsx`: +- render `MobilePageHeader` only for mobile +- add a desktop header block with back action, product/title copy, active section summary, and a section pill using the active section icon + +In `components.css`: +- keep existing mobile overrides intact +- add `.settings-header__desktop`, `.settings-header__copy`, `.settings-header__summary`, `.settings-header__section-pill`, and related desktop-only spacing/alignment rules + +- [x] **Step 4: Re-run the focused tests and verify green** + +Run: `pnpm --filter @coder-studio/web exec vitest run src/features/settings/components/settings-page.test.tsx src/styles/components.theme.test.ts` + +Expected: +- both test files pass with `0 failed` + +### Task 2: Expand desktop UI review coverage to editor and diff states + +**Files:** +- Modify: `packages/web/src/ui-preview/scenes/desktop-review-scenes.tsx` +- Modify: `packages/web/src/ui-preview/scene-metadata.ts` +- Modify: `packages/web/src/ui-preview/scene-metadata.test.ts` +- Modify: `packages/web/src/ui-preview/catalog.test.tsx` + +- [x] **Step 1: Write the failing tests** + +Add metadata assertions for two new scene ids: +- `workspace-editor-review` +- `workspace-diff-review` + +Add catalog render tests that check: +- editor review renders a desktop review card plus code editor chrome +- diff review renders a desktop review card plus git diff content + +- [x] **Step 2: Run the focused preview tests and verify they fail** + +Run: `pnpm --filter @coder-studio/web exec vitest run src/ui-preview/scene-metadata.test.ts src/ui-preview/catalog.test.tsx` + +Expected: +- metadata test fails because the new scene ids are not registered +- catalog test fails because the new scenes do not exist yet + +- [x] **Step 3: Implement the new desktop review scenes** + +In `desktop-review-scenes.tsx`: +- add helper state for a realistic code editor view +- add `workspace-editor-review` with a preloaded text editor surface +- add `workspace-diff-review` with a populated `GitDiffViewer` + +In `scene-metadata.ts`: +- register both scenes as desktop-only review entries with `.desktop-review-card` capture selectors + +- [x] **Step 4: Re-run the focused preview tests and verify green** + +Run: `pnpm --filter @coder-studio/web exec vitest run src/ui-preview/scene-metadata.test.ts src/ui-preview/catalog.test.tsx` + +Expected: +- both preview test files pass with `0 failed` + +### Task 3: Run regression verification for the phase-2 polish slice + +**Files:** +- No code changes required unless regressions surface + +- [x] **Step 1: Re-run targeted component and preview tests** + +Run: `pnpm --filter @coder-studio/web exec vitest run src/features/settings/components/settings-page.test.tsx src/styles/components.theme.test.ts src/ui-preview/scene-metadata.test.ts src/ui-preview/catalog.test.tsx src/features/workspace/index.test.tsx src/features/command-palette/components/command-palette.test.tsx` + +Expected: +- all selected tests pass + +- [x] **Step 2: Re-run targeted desktop captures** + +Run: `pnpm --dir e2e-ui exec playwright test --config playwright.config.ts --project desktop --workers 4 --grep '(settings-light-theme-review|settings-density-review|workspace-editor-review|workspace-diff-review|workspace-topbar-review|workspace-terminal-empty-review|desktop-overlay-review|desktop-statusbar-review) \\[desktop/'` + +Expected: +- desktop review capture matrix passes for the touched scenes + +- [x] **Step 3: Rebuild the report** + +Run: `pnpm --dir e2e-ui exec tsx report/build-report.ts` + +Expected: +- report command exits `0` + +## Verification Notes + +- `pnpm --filter @coder-studio/web exec vitest run src/features/settings/components/settings-page.test.tsx src/styles/components.theme.test.ts` + - Result: `Test Files 2 passed (2)`, `Tests 102 passed (102)` +- `pnpm --filter @coder-studio/web exec vitest run src/ui-preview/scene-metadata.test.ts src/ui-preview/catalog.test.tsx` + - Result: `Test Files 2 passed (2)`, `Tests 25 passed (25)` +- `pnpm --filter @coder-studio/web exec vitest run src/features/settings/components/settings-page.test.tsx src/styles/components.theme.test.ts src/ui-preview/scene-metadata.test.ts src/ui-preview/catalog.test.tsx src/features/workspace/index.test.tsx src/features/command-palette/components/command-palette.test.tsx` + - Result: `Test Files 6 passed (6)`, `Tests 149 passed (149)` +- `pnpm --dir e2e-ui exec playwright test --config playwright.config.ts --project desktop --workers 4 --grep '(settings-light-theme-review|settings-density-review|workspace-editor-review|workspace-diff-review|workspace-topbar-review|workspace-terminal-empty-review|desktop-overlay-review|desktop-statusbar-review) \\[desktop/'` + - Result: `120 passed (7.8m)` +- `pnpm --dir e2e-ui exec tsx report/build-report.ts` + - Result: exited `0` diff --git a/docs/superpowers/plans/2026-05-15-pc-style-polish-phase-3.md b/docs/superpowers/plans/2026-05-15-pc-style-polish-phase-3.md new file mode 100644 index 000000000..5359af9d3 --- /dev/null +++ b/docs/superpowers/plans/2026-05-15-pc-style-polish-phase-3.md @@ -0,0 +1,180 @@ +# PC Style Polish Phase 3 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Tighten the remaining desktop panel chrome in file tree, git panel, and command palette, then expand desktop review coverage for the most visible workspace states. + +**Architecture:** Keep the existing desktop workspace structure intact and polish the panel surfaces in place. Use CSS/token tweaks for the visual system, add focused tests for desktop-only selectors and theme rules, and reinforce the result with deterministic `ui-preview` scenes plus desktop capture coverage. + +**Tech Stack:** React, TypeScript, Jotai, Vitest, Playwright `e2e-ui`, CSS tokens in `packages/web/src/styles` + +--- + +### Task 1: Tighten the desktop file tree panel + +**Files:** +- Modify: `packages/web/src/features/workspace/views/shared/file-tree-panel.tsx` +- Modify: `packages/web/src/features/workspace/views/shared/file-tree-panel.test.tsx` +- Modify: `packages/web/src/styles/components.css` +- Modify: `packages/web/src/styles/components.theme.test.ts` + +- [ ] **Step 1: Write the failing test** + +Add a focused desktop assertion in `file-tree-panel.test.tsx` that checks: +- the desktop shell still renders `.file-tree-shell.file-tree-shell--desktop` +- the search bar and selected row stay in the same chrome family as the desktop panel +- the selected row keeps its selected class when the active path matches + +Add CSS assertions in `components.theme.test.ts` that check: +- `.file-tree-shell .file-tree-search` has the tighter desktop chrome used by the panel +- `.file-tree-shell .tree-item` keeps the desktop row density and hover transition +- `.file-tree-shell .tree-item.selected` uses the desktop active surface language + +- [ ] **Step 2: Run the focused test and verify it fails** + +Run: `pnpm --filter @coder-studio/web exec vitest run src/features/workspace/views/shared/file-tree-panel.test.tsx src/styles/components.theme.test.ts` + +Expected: +- the new desktop selector expectations fail until the panel chrome is tightened + +- [ ] **Step 3: Implement the desktop file tree polish** + +In `file-tree-panel.tsx`: +- keep the data flow unchanged +- keep using the existing `selected` class for the active row + +In `components.css`: +- tighten `.file-tree-shell .file-tree-search` +- tighten `.file-tree-shell .tree-item` +- tighten `.file-tree-shell .tree-item.selected` +- keep mobile rules intact under `.file-tree-shell--mobile` + +- [ ] **Step 4: Re-run the focused test and verify green** + +Run: `pnpm --filter @coder-studio/web exec vitest run src/features/workspace/views/shared/file-tree-panel.test.tsx src/styles/components.theme.test.ts` + +Expected: +- both test files pass with `0 failed` + +### Task 2: Tighten the desktop git panel and desktop command palette + +**Files:** +- Modify: `packages/web/src/features/workspace/views/shared/git-panel.tsx` +- Modify: `packages/web/src/features/workspace/views/shared/git-panel.test.tsx` +- Modify: `packages/web/src/features/command-palette/components/command-palette.tsx` +- Modify: `packages/web/src/features/command-palette/components/command-palette.test.tsx` +- Modify: `packages/web/src/styles/components.css` +- Modify: `packages/web/src/styles/components.theme.test.ts` + +- [ ] **Step 1: Write the failing tests** + +Add desktop assertions in `git-panel.test.tsx` that check: +- the desktop panel keeps `.git-panel.git-panel--desktop` +- `.git-commit-block`, `.git-panel-section`, `.git-worktree-row`, and `.git-history-row` all stay present in the desktop chrome +- at least one active change row still renders with the `.active` class + +Add desktop assertions in `command-palette.test.tsx` that check: +- the desktop overlay still renders `.command-palette-overlay` and `.command-palette` +- the header, search, hint, and list remain present as desktop chrome +- filtered items still render with `.command-palette-item-selected` when keyboard navigation moves selection + +Add CSS assertions in `components.theme.test.ts` that check: +- `.git-panel-scroll`, `.git-commit-block`, `.git-panel-section`, `.git-worktree-row`, and `.git-history-row` follow the tighter desktop density +- `.command-palette`, `.command-palette-header`, `.command-palette-search`, and `.command-palette-item` use the desktop tool surface language + +- [ ] **Step 2: Run the focused tests and verify they fail** + +Run: `pnpm --filter @coder-studio/web exec vitest run src/features/workspace/views/shared/git-panel.test.tsx src/features/command-palette/components/command-palette.test.tsx src/styles/components.theme.test.ts` + +Expected: +- the new desktop selector expectations fail until the git panel and command palette chrome are tightened + +- [ ] **Step 3: Implement the desktop git panel and command palette polish** + +In `git-panel.tsx` and `command-palette.tsx`: +- keep behavior unchanged +- keep mobile / desktop branching unchanged +- only adjust the desktop chrome classes and structure where needed for consistent spacing and hierarchy + +In `components.css`: +- tighten the desktop `.git-panel-*` rules for commit, section, worktree, change, and history rows +- tighten the desktop `.command-palette-*` rules for overlay, palette, header, search, hint, list, item, and shortcut chip +- keep mobile sheet rules unchanged + +- [ ] **Step 4: Re-run the focused tests and verify green** + +Run: `pnpm --filter @coder-studio/web exec vitest run src/features/workspace/views/shared/git-panel.test.tsx src/features/command-palette/components/command-palette.test.tsx src/styles/components.theme.test.ts` + +Expected: +- all selected tests pass with `0 failed` + +### Task 3: Expand desktop review scenes for workspace panel density + +**Files:** +- Modify: `packages/web/src/ui-preview/scenes/desktop-review-scenes.tsx` +- Modify: `packages/web/src/ui-preview/scene-metadata.ts` +- Modify: `packages/web/src/ui-preview/scene-metadata.test.ts` +- Modify: `packages/web/src/ui-preview/catalog.test.tsx` + +- [ ] **Step 1: Write the failing preview tests** + +Add metadata assertions that confirm the desktop review catalog still includes: +- `workspace-sidebar-files-review` +- `workspace-sidebar-git-review` +- `command-palette` + +Add catalog assertions that check: +- the file-tree review scene renders seeded file hierarchy chrome +- the git review scene renders seeded commit / worktree / history chrome +- the command palette scene renders the desktop overlay chrome and selected item state + +- [ ] **Step 2: Run the focused preview tests and verify they fail** + +Run: `pnpm --filter @coder-studio/web exec vitest run src/ui-preview/scene-metadata.test.ts src/ui-preview/catalog.test.tsx` + +Expected: +- the new desktop chrome expectations fail until the review scenes are updated to match the tightened panels + +- [ ] **Step 3: Update the review scenes and metadata** + +In `desktop-review-scenes.tsx`: +- keep the seeded data deterministic +- enrich the existing `workspace-sidebar-files-review` and `workspace-sidebar-git-review` scenes with denser desktop panel state so the new chrome rules are visible in screenshots +- keep `command-palette` as the canonical desktop palette review scene + +In `scene-metadata.ts`: +- keep the existing scene ids registered +- update descriptions only if the scene surface changed + +- [ ] **Step 4: Re-run the focused preview tests and verify green** + +Run: `pnpm --filter @coder-studio/web exec vitest run src/ui-preview/scene-metadata.test.ts src/ui-preview/catalog.test.tsx` + +Expected: +- both preview test files pass with `0 failed` + +### Task 4: Run regression verification for the phase-3 polish slice + +**Files:** +- No code changes required unless regressions surface + +- [ ] **Step 1: Re-run targeted component, style, and preview tests** + +Run: `pnpm --filter @coder-studio/web exec vitest run src/features/workspace/views/shared/file-tree-panel.test.tsx src/features/workspace/views/shared/git-panel.test.tsx src/features/command-palette/components/command-palette.test.tsx src/styles/base.theme.test.ts src/styles/components.theme.test.ts src/ui-preview/scene-metadata.test.ts src/ui-preview/catalog.test.tsx` + +Expected: +- all selected tests pass + +- [ ] **Step 2: Re-run targeted desktop captures** + +Run: `pnpm --dir e2e-ui exec playwright test --config playwright.config.ts --project desktop --workers 4 --grep '(workspace-sidebar-files-review|workspace-sidebar-git-review|command-palette|workspace-topbar-review|desktop-overlay-review|desktop-statusbar-review) \\[desktop/'` + +Expected: +- the desktop review capture matrix passes for the touched scenes + +- [ ] **Step 3: Rebuild the report** + +Run: `pnpm --dir e2e-ui exec tsx report/build-report.ts` + +Expected: +- report command exits `0` diff --git a/docs/superpowers/specs/2026-05-15-pc-style-polish-phase-3-design.md b/docs/superpowers/specs/2026-05-15-pc-style-polish-phase-3-design.md new file mode 100644 index 000000000..ad0eb92d7 --- /dev/null +++ b/docs/superpowers/specs/2026-05-15-pc-style-polish-phase-3-design.md @@ -0,0 +1,349 @@ +# PC 端样式精修 Phase 3 · 设计文档 + +> **版本:** 1.0 +> **日期:** 2026-05-15 +> **状态:** Draft(待评审) +> **作者:** Codex + +--- + +## 0. 文档说明 + +### 0.1 目标 + +在 phase 1 / phase 2 已完成桌面主壳、settings header、editor / diff review scenes 的基础上,再做一轮面向 PC 端的细部精修,重点处理仍然明显的样式割裂点,并把尚未闭环的桌面规范补完整。 + +本轮只做桌面端视觉与审图能力增强,不调整核心业务逻辑,不重做主题体系,也不改移动端主路径。 + +### 0.2 本轮范围 + +本轮聚焦四个互相关联但可以局部收敛的方向: + +- `workspace` 左侧 `file tree` +- `workspace` 左侧 `git panel` +- desktop `command palette` +- 全局 desktop chrome 与 review scenes + +这四项共同决定了用户在桌面主工作流里对“是否像一套产品”的第一感受,因此优先级高于继续扩展次要页面。 + +### 0.3 现有基础 + +当前代码已经具备可直接精修的前提: + +- `packages/web/src/features/workspace/views/shared/file-tree-panel.tsx` + - 结构已稳定,具备搜索、空态、树节点、创建/删除弹层,不需要先拆组件。 +- `packages/web/src/features/workspace/views/shared/git-panel.tsx` + - 已具备提交区、worktree 区、变更分组、history 区,问题主要是密度和视觉节奏,不是信息架构缺失。 +- `packages/web/src/features/command-palette/components/command-palette.tsx` + - desktop / mobile 已分流,desktop 现在更像基础弹层,还没有完全进入桌面产品语言。 +- `packages/web/src/ui-preview/scenes/desktop-review-scenes.tsx` + - 已有 topbar / sidebar / terminal / settings / editor / diff / overlay / statusbar review scene,可继续扩展审图矩阵。 + +换句话说,本轮适合做“收边”和“统一语言”,不适合做大规模重构。 + +--- + +## 1. 问题定义 + +### 1.1 File Tree 的割裂点 + +`file tree` 当前能用,但桌面感不够完整,主要表现为: + +- 搜索区、树节点区、空态区的密度关系偏散 +- 行高、缩进、hover、selected 态之间缺少统一节奏 +- 文件/目录层级主要靠缩进和图标区分,缺少更稳定的 active / hover / muted 语义 +- 与 phase 1 已经收紧过的 workspace shell 相比,panel 内部仍偏“局部组件样式” + +### 1.2 Git Panel 的割裂点 + +`git panel` 是当前桌面端最复杂的侧边栏之一,但其视觉语言还不够统一: + +- commit 区、worktree 区、changes 区、history 区像四块并排功能,而不是同一块 panel 内的连续层级 +- section header、count、row action、status tone 的强弱关系不稳定 +- 文件变化列表和 diff/editor chrome 的关系开始收敛,但 git panel 自身密度仍偏旧 +- 空态、错误态、工作树状态与变更列表没有形成一致的“辅助信息层” + +### 1.3 Command Palette 的割裂点 + +desktop `command palette` 现在具备功能完整性,但视觉上更接近基础 modal: + +- overlay、header、search、hint、result list 的层级过于平均 +- 宽度、输入区高度、结果项留白与快捷键标签缺少桌面产品的“工具面板感” +- 与 workspace 顶部 chrome、settings desktop header 相比,还没有形成同一套桌面工具语言 + +### 1.4 Desktop Chrome 仍有不一致 + +phase 1 / phase 2 已经补齐了不少 surface 语义,但以下问题还在: + +- topbar / sidebar / main stage / statusbar / overlay 的圆角、边框、阴影强度还存在局部差异 +- 某些容器仍然混用 `bg-surface`、`bg-panel`、`bg-elevated`,导致 light theme 下对比不够稳定 +- 审图 coverage 已覆盖主要壳层,但对“高密度 panel 内部状态”的回归保护仍偏弱 + +--- + +## 2. 设计目标与非目标 + +### 2.1 设计目标 + +- 让 `file tree`、`git panel`、`command palette` 进入统一的 desktop panel 语言 +- 补齐 panel 内部的间距、hover、selected、meta、empty state 规范 +- 把全局 desktop chrome 中最明显的 surface / border / radius 裂缝收紧 +- 为后续样式回归新增更有针对性的 desktop review scenes + +### 2.2 非目标 + +- 不改变 `file tree`、`git panel`、`command palette` 的业务能力与交互流程 +- 不重做 workspace 布局结构,不改 panel 宽高逻辑 +- 不新增主题 family,也不改移动端样式方向 +- 不在本轮拆分 `components.css` + +--- + +## 3. 方案选择 + +### 3.1 方案 A:只做 CSS 收边 + +只改 `components.css` 和少量 theme 测试,不增 preview scenes。 + +优点: + +- 实施快 +- 风险低 + +缺点: + +- 很难长期防止回归 +- 对 `command palette`、`git panel` 这类复杂面板,只改样式容易遗漏状态覆盖面 + +### 3.2 方案 B:CSS 精修 + review scenes 补齐 + +在局部 CSS 精修基础上,补桌面审图场景和对应测试。 + +优点: + +- 既解决当前观感问题,又补后续回归护栏 +- 可以把“桌面端规范”从口头标准变成稳定产物 + +缺点: + +- 改动面比纯 CSS 收边略大 +- 需要同步维护 metadata / catalog / capture + +### 3.3 方案 C:顺带做 panel 组件抽象 + +把三类 panel 的 header / body / empty state 再抽一层共享组件。 + +优点: + +- 长期潜力更大 + +缺点: + +- 本轮会从精修演变成结构重构 +- 风险与验证成本明显上升 + +### 3.4 结论 + +本轮采用 **方案 B**。 + +原因: + +- 当前最缺的是“桌面语言统一 + 审图护栏”,不是新的抽象层。 +- `file tree` / `git panel` / `command palette` 的结构已经可用,局部精修收益高。 +- 先通过 review scenes 把问题显性化,后续如果仍有重复结构,再决定是否抽象共享 panel chrome。 + +--- + +## 4. 详细设计 + +### 4.1 File Tree 精修 + +涉及文件: + +- `packages/web/src/features/workspace/views/shared/file-tree-panel.tsx` +- `packages/web/src/features/workspace/views/shared/file-tree-panel.test.tsx` +- `packages/web/src/styles/components.css` + +设计要点: + +- 搜索区作为 panel 顶部固定起始面,强化与树内容区的分层关系 +- 收紧 desktop 节点行高、左右 padding、图标列宽和层级缩进 +- 明确区分 `hover`、`selected`、`muted meta`、`empty/loading` 的视觉语义 +- 让空态、搜索无结果态和普通节点列表共享同一套 panel 内部留白基线 + +约束: + +- 不改变现有树结构、搜索逻辑、create/delete 行为 +- 不引入新的节点交互 + +### 4.2 Git Panel 精修 + +涉及文件: + +- `packages/web/src/features/workspace/views/shared/git-panel.tsx` +- `packages/web/src/features/workspace/views/shared/git-panel.test.tsx` +- `packages/web/src/styles/components.css` + +设计要点: + +- commit block 与 section block 统一成更明确的 desktop panel 节奏 +- 调整 section header、count、action link、change row 的信息权重 +- 收紧 status badge、row action、history row、worktree row 的密度 +- 强化 active diff row 与普通 hover row 的区别,避免只靠轻微底色变化 +- 让空态/错误态保留存在感,但退回辅助层,不抢 commit 与 change list 的注意力 + +约束: + +- 不改 git action 行为,不动 `useGitPanelActions` +- 不增加新的 git 操作入口 + +### 4.3 Command Palette 精修 + +涉及文件: + +- `packages/web/src/features/command-palette/components/command-palette.tsx` +- `packages/web/src/features/command-palette/components/command-palette.test.tsx` +- `packages/web/src/styles/components.css` +- `packages/web/src/styles/components.theme.test.ts` + +设计要点: + +- desktop palette 进一步向“桌面工具面板”靠拢,而不是通用弹窗 +- 调整 overlay 氛围、palette 宽度、header 间距、search 区高度和结果项密度 +- 强化结果项 active / hover / keyboard selection 的可辨识度 +- 统一 shortcut chip、meta 文案、empty state 与 desktop shell 语气 + +约束: + +- 不改变 keyboard navigation、command 过滤与 workspace launch 流程 +- mobile sheet 只做必要兼容,不作为本轮重点 + +### 4.4 Desktop Chrome 收边 + +涉及文件: + +- `packages/web/src/styles/components.css` +- `packages/web/src/styles/base.theme.test.ts` +- `packages/web/src/styles/components.theme.test.ts` + +设计要点: + +- 统一 topbar、sidebar panel、statusbar、overlay card 的 border / radius / shadow 语义 +- 减少 desktop 容器在 `bg-surface`、`bg-panel`、`bg-elevated` 之间的随意漂移 +- 仅在必要处补 desktop-specific token 或语义类,避免增加新的主题分叉 + +约束: + +- 不改 token 体系的大方向 +- 不重写 phase 1 / phase 2 已通过的结构规则 + +### 4.5 Review Scenes 补齐 + +涉及文件: + +- `packages/web/src/ui-preview/scenes/desktop-review-scenes.tsx` +- `packages/web/src/ui-preview/scene-metadata.ts` +- `packages/web/src/ui-preview/scene-metadata.test.ts` +- `packages/web/src/ui-preview/catalog.test.tsx` + +优先策略: + +- 优先强化已有 scene,而不是为了命名整洁新造一组近义 id +- 只有当现有 scene 无法承载目标状态时,才新增 dedicated desktop review scene + +本轮优先覆盖: + +- 已有 `workspace-sidebar-files-review` + - 聚焦搜索区、深层目录、selected row、empty/search state +- 已有 `workspace-sidebar-git-review` + - 聚焦 commit block、worktree 区、changes/history 区、active diff row +- 已有 `command-palette` + - 聚焦 desktop palette header、search、selected item、shortcut chips、empty state +- 如现有 scene 仍不足以承载高密度 workspace 状态,再新增 `workspace-split-density-review` + +要求: + +- scene 必须使用静态、可预测的 seed 数据 +- capture selector 必须稳定,避免把外层无关区域带入截图 +- catalog 测试需要验证关键 chrome 是否真实渲染,不只校验 scene id 存在 + +--- + +## 5. 验证策略 + +### 5.1 单元与主题测试 + +最小验证集合: + +- `src/features/workspace/views/shared/file-tree-panel.test.tsx` +- `src/features/workspace/views/shared/git-panel.test.tsx` +- `src/features/command-palette/components/command-palette.test.tsx` +- `src/styles/base.theme.test.ts` +- `src/styles/components.theme.test.ts` +- `src/ui-preview/scene-metadata.test.ts` +- `src/ui-preview/catalog.test.tsx` + +关注点: + +- desktop 专属 class / chrome 是否存在 +- 主题测试能否覆盖新增 desktop panel 规则 +- review scenes 是否被正确注册并渲染关键节点 + +### 5.2 定向 desktop capture + +需要补跑 desktop 定向 capture,至少覆盖: + +- `workspace-sidebar-files-review` +- `workspace-sidebar-git-review` +- `command-palette` +- 新增的 desktop review scenes +- `workspace-topbar-review` +- `desktop-overlay-review` +- `desktop-statusbar-review` + +验收标准: + +- 本轮修改涉及的 desktop scene capture 全部通过 +- 抽查至少 3 张关键截图,确认没有新的桌面端视觉裂缝 + +--- + +## 6. 实施顺序 + +建议按以下顺序执行: + +1. `file tree` +2. `git panel` +3. `command palette` +4. desktop chrome 收边 +5. review scenes 与回归验证 + +原因: + +- `file tree` 和 `git panel` 直接决定 workspace 侧边栏是否统一,应先形成 panel 基线 +- `command palette` 依赖同一套桌面工具语言,放在 panel 基线之后更容易收敛 +- review scenes 放在最后补齐,可以直接覆盖最终落地视觉 + +--- + +## 7. 风险与控制 + +- 风险 1:`components.css` 里已有多段历史规则,新增收边可能被后段覆盖 + - 控制:同步补主题测试和 scene capture,不只靠肉眼 +- 风险 2:`git panel` 和 `file tree` 状态多,容易只修默认态 + - 控制:scene seed 必须显式包含 selected / empty / history / search 等关键状态 +- 风险 3:desktop palette 与 overlay 变动后,mobile sheet 意外受影响 + - 控制:保持 desktop / mobile selector 分流,并保留现有 mobile 测试 + +--- + +## 8. 结论 + +本轮不做结构重写,而是围绕 `file tree`、`git panel`、`command palette` 和 desktop chrome 做一次可验证、可回归的精修。 + +核心原则只有两个: + +- 先把桌面端最常被看到的 panel 语言统一 +- 每修一块,就把它纳入 `ui-preview` 和 desktop capture 的护栏 + +如果这份设计确认无误,下一步进入 implementation plan,并按 TDD 顺序逐块落地。 diff --git a/docs/superpowers/specs/2026-05-17-mobile-settings-homepage-redesign-design.md b/docs/superpowers/specs/2026-05-17-mobile-settings-homepage-redesign-design.md new file mode 100644 index 000000000..2cd4d3ec2 --- /dev/null +++ b/docs/superpowers/specs/2026-05-17-mobile-settings-homepage-redesign-design.md @@ -0,0 +1,362 @@ +# 移动端设置首页重做 · 设计文档 + +> **版本:** 1.0 +> **日期:** 2026-05-17 +> **状态:** Draft(待评审) +> **作者:** Codex + +--- + +## 0. 文档说明 + +### 0.1 目标 + +重做移动端设置首页,修复当前首页混入旧版大圆角卡片语言、版心挤压和层级失衡的问题,让移动端首页重新回到与 PC 端一致的产品语气。 + +本轮重点不是扩展设置能力,而是把移动端首页从“带说明区的独立大卡片列表”改成“紧凑分组式设置目录页”。 + +### 0.2 本轮范围 + +本轮只覆盖移动端设置首页根页面,即用户进入 `/settings` 且尚未进入任何具体分区时的界面。 + +包含: + +- 首页信息重排 +- 首页 DOM 结构调整 +- 首页视觉语言重做 +- 首页回归测试与 UI preview 调整 + +不包含: + +- 任意设置详情页的结构重做 +- 设置项业务逻辑变更 +- 桌面端设置页结构改动 +- 主题 token 体系重构 + +### 0.3 现状与问题 + +当前移动端首页主要由以下结构组成: + +- `packages/web/src/features/settings/components/settings-page.tsx` + - `renderMobileRoot()` 由 `settings-mobile-root-hero + settings-mobile-list + settings-mobile-item` 组成 +- `packages/web/src/styles/components.css` + - 移动端首页使用独立大卡片条目、说明型 hero 和较重的圆角/渐变 + +当前问题集中在四点: + +1. 首页视觉语言与 PC 端设置页脱节 +2. 每个入口都是独立大卡片,移动端出现旧样式的“泡泡感” +3. 说明型 hero 占用首屏空间,但没有提供真正必要的信息 +4. 条目和页边距的组合让页面产生“左窄右挤”的压缩观感 + +--- + +## 1. 设计目标与非目标 + +### 1.1 设计目标 + +- 去掉移动端首页的旧版大圆角卡片感 +- 首页风格向用户确认的 `B` 方向收敛,即更接近原生设置目录页的阅读节奏 +- 同时保留当前产品的工具型克制语气,而不是复制系统设置样式 +- 通过更紧凑的分组和连续行结构,让移动端与 PC 端设置页共享同一套层级逻辑 +- 保持现有导航状态机和设置分区能力不变,降低改动风险 + +### 1.2 非目标 + +- 不在本轮引入新的设置分区 +- 不在首页增加展开、折叠、搜索、筛选等额外交互 +- 不处理详情页的大规模视觉统一 +- 不重构 `settings-page.tsx` 的整体导航模型 + +--- + +## 2. 方案选择 + +### 2.1 方案 A:工具面板式扁平入口列表 + +去掉 hero,把所有入口做成一组扁平行,强调工具页感。 + +优点: + +- 与桌面端语言最接近 +- 改造成本较低 + +缺点: + +- 更像压缩版 PC 面板,不够贴近移动端浏览节奏 +- 容易做得过于硬朗 + +### 2.2 方案 B:紧凑分组式目录页 + +使用移动端原生设置页式的分组目录结构,但控制圆角、边框和密度,使其仍属于当前产品语言。 + +优点: + +- 最符合用户选定方向 +- 可读性最好,能直接解决首页挤压和层级混乱 +- 便于后续将详情页逐步统一到同一语言 + +缺点: + +- 需要重新组织信息层级 +- 需要调整现有测试和 preview 捕获点 + +### 2.3 方案 C:混合式首页 + +保留一个高频入口区,再加分组目录列表。 + +优点: + +- 便于突出高频项 + +缺点: + +- 首页会出现双层规则,体系复杂度上升 +- 当前只有四个主分区,收益有限 + +### 2.4 结论 + +本轮采用 **方案 B:紧凑分组式目录页**。 + +原因: + +- 用户已明确选择 `B` +- 当前问题本质是首页被旧卡片样式劫持,而不是缺少视觉强调 +- 首页信息量不大,最适合用简洁分组目录承载,而不是继续堆入口卡片 + +--- + +## 3. 信息架构 + +### 3.1 首页层级 + +移动端设置首页保留三层结构: + +1. 系统已有移动端页头 +2. 分组标题 +3. 分组内连续入口行 + +移除内容: + +- `settings-mobile-root-hero` +- 首页说明型文案区域 +- 首页强调型首屏模块 + +### 3.2 分组与排序 + +首页分为两个分组: + +1. `工作区与运行` + - `通用` + - `Providers` + +2. `界面与交互` + - `外观` + - `快捷键` + +排序原则: + +- 先放功能配置,再放偏个性化和习惯类设置 +- 让用户进入设置后的第一屏更偏“操作/运行相关” + +### 3.3 文案策略 + +- 首页条目以主标签为主 +- 副文案从“默认展示所有项”调整为“仅在确有必要时显示” +- 本轮默认保留轻量提示,但允许只对信息量更高的项显示副文案 + +实现上不要求新增文案 key;优先复用现有 `getMobileSectionHintKey()` 返回的提示文案,并根据新布局决定是否对全部分区渲染提示 + +--- + +## 4. 视觉与交互设计 + +### 4.1 页面框架 + +- 保留现有 `MobilePageHeader` +- 内容区直接进入分组列表 +- 内容区左右 gutter 使用统一移动端页面基线,不再额外制造首页专属挤压 + +结果应表现为: + +- 页面外部留白稳定 +- 分组之间有呼吸感 +- 分组内部连续、紧凑 + +### 4.2 分组容器 + +每个分组使用单一连续容器承载,不再把条目渲染成彼此分离的大卡片。 + +容器规范: + +- 轻量底色 +- 1 层弱边框 +- 中小圆角,建议落在 `10px - 12px` +- 不使用明显浮起阴影 + +设计意图: + +- 保留可识别的“分组块” +- 去掉旧版独立卡片的鼓起感 +- 让移动端更接近 PC 的克制面板语言 + +### 4.3 入口行 + +每个入口为连续列表中的一行,而不是一张独立卡片。 + +行项规范: + +- 行高建议 `56px - 64px` +- 组内行与行之间使用分隔线 +- 行本体不单独描边、不单独做渐变卡面、不单独做阴影 +- 点击整行进入详情页 + +这会替代现有 `.settings-mobile-item` 的大卡片结构。 + +### 4.4 图标与箭头 + +- 图标保留,但缩到辅助层级 +- 图标底板建议 `32px - 36px` +- 图标底板使用中小圆角,不得回到大软胶囊样式 +- 右箭头保留为导航暗示,但视觉权重弱于标题 + +### 4.5 文本层级 + +- 标题为主,一行解决 +- 副文案弱化为辅助信息 +- 如果某项副文案对首页贡献低,可在实现中省略,以保持目录页紧凑度 + +### 4.6 交互状态 + +首页只承担目录跳转职责,不新增复杂交互。 + +状态规范: + +- `hover/press` 使用轻量底色或边框变化 +- 不使用浮起动画表达按压 +- 分组标题不参与交互 +- 进入详情页的现有导航行为保持不变 + +### 4.7 与 PC 对齐的规则 + +移动端首页需要与 PC 端共享以下语气: + +- 不使用说明型 hero +- 不使用旧版大圆角独立卡片 +- 不使用重渐变和过强的材质感 +- 保留清晰分组、克制边框和明确导航性 + +这不是把 PC 缩放到手机上,而是让两端在“信息层级和面板语气”上属于同一产品。 + +--- + +## 5. 实现设计 + +### 5.1 涉及文件 + +- `packages/web/src/features/settings/components/settings-page.tsx` +- `packages/web/src/styles/components.css` +- `packages/web/src/features/settings/components/settings-page.test.tsx` +- `packages/web/src/ui-preview/scenes/page-scenes.tsx` +- `packages/web/src/ui-preview/scene-metadata.ts` + +### 5.2 JSX 调整策略 + +保留当前状态机和跳转逻辑: + +- `navigationState` +- `shouldShowMobileRoot` +- `setNavigationState({ kind: "detail", section: id })` + +只重写移动端首页根结构: + +- 移除 `settings-mobile-root-hero` +- 将单一列表改为带分组的结构 +- 继续复用 `availableSections` / section metadata / icon semantic + +必要时新增轻量的首页分组映射,但不改变 `SettingsSection` 模型本身。 + +### 5.3 CSS 调整策略 + +移除或废弃旧移动端首页视觉规则: + +- `.settings-mobile-root-hero` +- `.settings-mobile-root-hero__eyebrow` +- `.settings-mobile-root-hero__body` +- 现有以独立大卡片为核心的 `.settings-mobile-item` 规则 + +新增规则目标: + +- 首页分组容器 +- 分组标题 +- 连续行列表 +- 行项图标、文本和箭头布局 +- 更克制的移动端圆角和边框密度 + +### 5.4 兼容性要求 + +- 不破坏现有移动端详情页展示 +- 不影响桌面端设置页 +- 不改变 section id、翻译 key 和跳转逻辑 +- 不依赖新的设计 token 才能完成本轮 + +--- + +## 6. 测试与回归保护 + +### 6.1 单元/组件测试 + +更新或补充 `settings-page.test.tsx`,覆盖以下行为: + +- 移动端首页仍渲染全部设置入口 +- 首页入口顺序与新分组设计一致 +- 点击入口后仍能进入对应详情页 +- 旧 hero 结构不再存在 +- 新分组结构存在 + +### 6.2 UI Preview + +保留 `settings-mobile-root` scene,用于移动端首页截图回归。 + +需要同步检查: + +- `page-scenes.tsx` 中 scene 是否仍能稳定渲染首页态 +- `scene-metadata.ts` 中 capture selector 是否仍指向稳定的首页主体 + +如果旧 selector 依赖 `.settings-mobile-list`,则应改为新首页根容器或更稳定的分组列表容器。 + +### 6.3 手工验证重点 + +- 中文环境下首页文案是否拥挤 +- 移动端首页左右 gutter 是否恢复正常 +- 分组外层和组内行项的圆角层级是否明显小于当前大卡片风格 +- 从首页进入详情页、再返回首页时状态是否正常 + +--- + +## 7. 风险与约束 + +### 7.1 主要风险 + +- DOM 结构变更可能导致现有测试选择器和 preview 捕获点失效 +- 如果副文案保留过多,首页仍可能显得松散 +- 如果分组容器和行项圆角没有拉开层级,仍会残留卡片感 + +### 7.2 风险控制 + +- 优先保持状态逻辑不变,只改首页结构与样式 +- 让“组容器圆角”和“行项自身无卡片化边界”形成明确区分 +- 用 scene capture 和组件测试双重保护首页回归 + +--- + +## 8. 验收标准 + +完成后应满足以下标准: + +- 移动端设置首页不再出现说明型 hero +- 首页入口不再是独立大圆角大卡片 +- 首页视觉密度明显收紧,消除“左窄右挤”的观感 +- 首页风格更接近原生设置目录页,但仍与 PC 端工具面板语言一致 +- 现有进入详情页和返回逻辑保持正常 +- 自动化测试和 UI preview 回归链路完成更新 diff --git a/docs/superpowers/specs/2026-05-17-mobile-terminal-fullscreen-redesign-design.md b/docs/superpowers/specs/2026-05-17-mobile-terminal-fullscreen-redesign-design.md new file mode 100644 index 000000000..b86ca2a9f --- /dev/null +++ b/docs/superpowers/specs/2026-05-17-mobile-terminal-fullscreen-redesign-design.md @@ -0,0 +1,427 @@ +# 移动端终端全屏页重设计 · 设计文档 + +> **版本:** 1.0 +> **日期:** 2026-05-17 +> **状态:** Draft(待评审) +> **作者:** Codex + +--- + +## 0. 文档说明 + +### 0.1 目标 + +重设计移动端终端全屏页,修复当前页面混入旧移动端大圆角 `sheet/card/pill` 语言的问题,让它重新回到与 PC 端终端/编辑器一致的工具化语气。 + +本轮重点不是新增终端能力,而是把当前页面从“移动端卡片式浮层页面”收敛为“专业工具画布页”。 + +### 0.2 范围 + +本轮只覆盖从移动端 workspace 中打开的终端全屏页。 + +包含: + +- 顶部 chrome 与终端工具栏的视觉重组 +- 终端主画布的容器与留白收敛 +- 移动端终端输入辅助条的视觉降噪 +- 底部状态带在终端页中的样式统一 +- 相关测试与 UI preview 调整 + +不包含: + +- 桌面端终端面板结构改动 +- 终端输入协议、快捷键语义、上传/粘贴业务逻辑改写 +- 主题 token 体系重构 +- 新增主题 family、私有配色体系或硬编码的页面专属主题 +- 文件页、详情页或其他移动端页面的同步重做 + +### 0.3 相关实现入口 + +当前页面由以下结构共同组成: + +- `packages/web/src/features/workspace/views/mobile/workspace-mobile-view.tsx` + - 负责移动端终端全屏 sheet 的装配 +- `packages/web/src/features/terminal-panel/views/shared/terminal-panel.tsx` + - 负责终端顶部工具栏、selector、会话操作与 xterm 内容区 +- `packages/web/src/features/terminal-panel/views/shared/xterm-host.tsx` + - 负责终端宿主与移动端输入辅助条装配 +- `packages/web/src/features/terminal-panel/mobile/mobile-terminal-input-bar.tsx` + - 负责移动端 Paste / Upload / Ctrl / Shift / 方向键等输入辅助控件 +- `packages/web/src/styles/components.css` + - 当前移动端终端页主要样式定义位置 + +本轮重点 class 区域: + +- `.mobile-terminal-sheet` +- `.mobile-sheet--terminal` +- `.bottom-terminal--mobile-fullscreen` +- `.mobile-terminal-input-bar` + +### 0.4 现状与问题 + +当前移动端终端页已经具备完整功能,但视觉归类是错位的。 + +现状特征: + +- `.mobile-terminal-sheet` 仍保留明显页面 padding 与渐变壳层 +- `.mobile-terminal-sheet .bottom-terminal` 使用较重的顶部大圆角容器 +- `.bottom-terminal--mobile-fullscreen .terminal-selector-btn` 与 `.panel-toolbar-btn` 仍偏胶囊 / 圆丸控件 +- `.mobile-terminal-input-bar` 中动作按钮和软键呈现为一排独立高存在感按钮 + +这导致三个问题: + +1. 终端页看起来像“移动端浮层卡片页面”,不像 workspace 工具页 +2. 顶部、正文、输入条、底部状态带之间都是各自成块,终端主舞台不够集中 +3. 页面虽然沿用了现有主题 token,但映射到了错误的视觉语义上,因此与 PC 端 `pc-style-polish` 调性断开 + +--- + +## 1. 设计目标与非目标 + +### 1.1 设计目标 + +- 让移动端终端页重新对齐 PC 端终端/编辑器气质 +- 保持终端内容区成为首要视觉焦点 +- 在不破坏现有主题体系的前提下,降低移动端的大圆角、卡片感和浮层感 +- 保留移动端终端的必要操作能力,但把控制区降为工具性存在 +- 让页面在浅色 / 深色主题下都继续依赖现有 token 正常工作 + +### 1.2 非目标 + +- 不把终端页改造成原生 app 风格目录页 +- 不把页面做成营销式或说明式布局 +- 不为了“更硬朗”而牺牲触控可用性 +- 不在本轮引入新的终端状态模型、快捷键布局配置或会话管理流程 + +--- + +## 2. 方案选择 + +### 2.1 候选方案 + +#### 方案 A:极致终端优先 + +最大限度压缩页面 chrome、会话信息与快捷键存在感,让终端输出区域尽可能接近满屏。 + +#### 方案 B:终端优先但更精致统一 + +保留终端优先,但增加一层轻量会话信息块和更完整的移动端结构层级。 + +### 2.2 最终选择 + +本轮采用 **方案 A:极致终端优先**。 + +选择原因: + +- 用户已明确选择 `A` +- 当前问题核心不是信息不够,而是终端页被旧移动端卡片语言劫持 +- 对齐 `pc-style-polish` 时,终端页最需要的是“专业工具页”语气,而不是补更多移动端产品装饰 + +--- + +## 3. 核心设计决策 + +### 3.1 视觉归类改正 + +本页后续归类为: + +- `workspace / tool / canvas` + +而不是: + +- `mobile / sheet / card / pill` + +这意味着本轮修的是 **主题 token 的映射方式**,不是重做主题本身。 + +### 3.2 主题约束 + +必须保留现有主题设定: + +- 继续使用现有颜色 token、文字层级、状态色、边框语义与明暗模式 +- 不新增页面私有主题 +- 不用硬编码颜色绕开主题体系 + +允许调整的是: + +- 终端页使用哪些现有 surface / border / text token +- 终端页各类 radius、padding、gap 的数值 +- 终端页内部各组件的视觉归类 + +### 3.3 顶部只读作一层工具 chrome + +终端页最终观感上只能有一层顶部工具带。 + +实现上即使仍复用 `MobileSheet` header 与 `TerminalPanel` toolbar 两段结构,也必须在视觉上合并成一个连续的薄工具带,避免出现“页头一层 + 终端卡片头一层”的双层堆叠感。 + +### 3.4 终端主画布优先于辅助控件 + +垂直空间优先级固定为: + +1. 终端内容与输入光标 +2. 输入辅助条 +3. 顶部 chrome 的留白与说明性内容 + +键盘弹起时,应该优先压缩辅助条密度和顶部空白,而不是继续牺牲终端正文高度。 + +--- + +## 4. 页面信息组织 + +### 4.1 顶部工具栏 + +顶部工具栏只保留一级动作: + +- 返回 +- 页面标题 `Terminal` +- 1 到 2 个高频动作,如新建终端、关闭当前页 + +会话切换如果保留,必须收成轻量入口,不允许再演化为独立会话信息卡块。 + +### 4.2 会话切换入口 + +多终端场景下,terminal selector 继续保留,但降为工具入口而不是信息中心: + +- 维持单行、可截断、mono 风格 +- 与 toolbar 同层,不独立抬起 +- 只在存在多个 terminal 时呈现 + +### 4.3 终端内容区 + +终端内容区是整个页面的主舞台。 + +要求: + +- 不再套用明显“卡片壳 + 大圆角 + 浮起感” +- 不引入额外的内容头部,如 `Live session` 之类次级标签 +- xterm 内容应尽可能直接接触页面主画布层 + +### 4.4 输入辅助条 + +输入辅助条保留现有功能: + +- `Paste` +- `Upload` +- `Ctrl` +- `Shift` +- `Esc` +- `Tab` +- 方向键 +- `Enter` + +但视觉语义从“按钮陈列区”调整为“输入辅助工具条”: + +- 操作按钮与软键应被看作同一条工具带中的不同分组 +- 可横向滚动,但不应看起来像一排独立的厚重 pill +- `Ctrl` / `Shift` 状态仍用现有主题状态色表达,不改变交互语义 + +### 4.5 底部状态带 + +终端页底部状态信息继续保留,但只承担状态职责: + +- branch +- session / 连接上下文 +- sync / workspace 状态 + +它必须表现为编辑器式状态带,而不是另一块可点击大卡片。 + +--- + +## 5. 视觉规范 + +### 5.1 圆角策略 + +本页圆角必须整体下调,并向 PC 端工具控件语义对齐。 + +规则: + +- 终端主壳层不再使用 `xl` 级大圆角,也不使用 full-pill +- 优先使用 PC 端已存在的小中圆角 token +- 工具按钮、selector、输入条软键统一收敛到小中圆角 +- 页面中不应再出现“一个工具页里同时混有大卡片圆角和极小控件圆角”的断裂 + +一句话约束: + +> 移动端终端页允许有轻圆角,但不允许再有旧样式的大软壳圆角。 + +### 5.2 分层方式 + +本页分层以: + +- surface 对比 +- 细边框 +- 文本层级 + +为主,不依赖: + +- 明显阴影 +- 大面积渐变壳层 +- 独立漂浮卡片 + +### 5.3 背景与表面 + +背景继续沿用当前主题体系中的 page / surface / terminal 语义,但需要做两点调整: + +- 页面外层背景减少装饰性渐变存在感 +- 终端内容层与 chrome 层之间主要靠轻微 surface 区分,而不是用一块圆角容器把终端包起来 + +### 5.4 顶部控件风格 + +顶部 selector、icon action、close/new 等控件应体现“轻工具控件”语气: + +- 更薄 +- 更紧凑 +- 更低对比 +- 不使用胶囊形态强化自己 + +### 5.5 输入辅助条风格 + +输入辅助条需要从“按钮列表”降噪为“连续工具条”: + +- 组内 keycap 风格允许保留 +- 但要降低边框厚感和 pill 感 +- `Paste` / `Upload` 不应像两个独立 CTA +- 常用软键应更像键盘辅助列,而不是功能卡片 + +### 5.6 字体与内容焦点 + +视觉焦点顺序固定为: + +1. monospace 终端内容 +2. 工具栏中的当前上下文入口 +3. 状态带与辅助控件 + +标题、说明和次级文案都应为辅助层,不能夺走终端内容的中心性。 + +--- + +## 6. 实现设计 + +### 6.1 DOM 与组件边界 + +本轮优先复用现有组件边界,不为了样式调整大改业务结构。 + +建议改动策略: + +- `workspace-mobile-view.tsx` + - 继续负责终端 sheet 的装配 + - 如有必要,只增加终端页专用 class 或 footer hook,不改导航状态机 +- `terminal-panel.tsx` + - 调整 `mobile-fullscreen` 模式下 toolbar 的层级与结构 + - 确保 selector 与 action 的位置关系符合单层工具栏语义 +- `xterm-host.tsx` + - 保持终端行为逻辑不变 + - 仅在确有必要时增加包裹结构或 class 以支撑新布局 +- `mobile-terminal-input-bar.tsx` + - 保持交互语义与键位映射不变 + - 允许做轻量结构调整,以支持更连续的工具条样式 +- `components.css` + - 作为本轮样式重做的主落点 + +### 6.2 顶部 chrome 合并策略 + +`MobileSheet` header 与 `TerminalPanel` toolbar 可以继续共存于实现层,但最终必须满足: + +- 用户不会感知到两个互相叠加的头部模块 +- 头部底边只有一层明确分隔 +- selector 与 action 看起来属于页面主工具栏,而不是终端内容区里又长出一排按钮 + +### 6.3 终端主画布策略 + +`.mobile-terminal-sheet` 与 `.bottom-terminal--mobile-fullscreen` 的主要职责应变为: + +- 提供满高布局 +- 控制最小必要的 surface 分层 +- 给 xterm 内容让出最大可用区域 + +不应再承担: + +- 大卡片容器表达 +- 装饰性背景表达 +- 二级标题区表达 + +### 6.4 输入辅助条策略 + +`.mobile-terminal-input-bar` 需要保留现有逻辑测试可依赖的交互语义,但视觉上要完成以下变化: + +- `actions` 与 `keys` 两组之间的断裂减弱 +- 各按钮厚度降低 +- 常见控件的形态从圆丸/胶囊收敛到更像工具键帽 +- 保持触控可用的最小命中面积,不把控件缩成难点按的小标签 + +### 6.5 底部状态带策略 + +终端页当前通过 `WorkspaceStatusBar` 作为 footer 注入。 + +本轮不改状态栏业务内容,但需要让它在 `.mobile-sheet--terminal` 场景下与终端页一起读作一体化工具页底栏: + +- 贴边 +- 扁平 +- 弱装饰 +- 信息优先 + +--- + +## 7. 测试与预览 + +### 7.1 需要同步的测试 + +- `packages/web/src/features/terminal-panel/__tests__/terminal-panel.test.tsx` +- `packages/web/src/features/terminal-panel/__tests__/xterm-host.test.tsx` +- `packages/web/src/features/terminal-panel/mobile/mobile-terminal-input-bar.test.tsx` +- `packages/web/src/styles/components.theme.test.ts` + +测试重点: + +- `mobile-fullscreen` 模式下的 toolbar / selector / actions 结构仍然正确 +- 移动端输入辅助条行为不回归 +- 主题样式仍通过现有 token 输出,不引入破坏性覆盖 + +### 7.2 需要同步的预览 + +- `packages/web/src/ui-preview/scenes/showcase-scenes.tsx` +- `packages/web/src/ui-preview/scene-metadata.ts` +- `packages/web/src/ui-preview/catalog.test.tsx` + +需要补齐或更新移动端终端全屏页的 preview 场景,确保后续能做视觉回归审查。 + +--- + +## 8. 验收标准 + +以下结果同时成立,才算本轮完成: + +- 移动端终端全屏页整体读作一张工具画布,而不是一组堆叠卡片 +- 顶部只表现为一层紧凑工具栏 +- 终端正文成为首要视觉焦点 +- 页面内不再出现旧移动端大圆角壳层与高存在感 pill 风格 +- 现有主题切换与状态色语义保持正常 +- Paste / Upload / Ctrl / Shift / 方向键等现有输入能力全部保留 +- 键盘弹起时,终端内容高度优先于装饰性留白 + +--- + +## 9. 风险与约束 + +### 9.1 双头部错觉 + +如果 `MobileSheet` header 与 `TerminalPanel` toolbar 只做局部修饰、不做整体收敛,最终仍可能看起来像双层头部。 + +### 9.2 过度收紧导致触控退化 + +如果只追求“更像 PC”,容易把移动端控件压得过小。实现时必须保持基本触控命中面积。 + +### 9.3 主题体系被页面私有样式绕开 + +如果实现中引入大量硬编码颜色或临时视觉补丁,会把本轮从“主题映射修正”变成“页面私有主题”,这是明确禁止的。 + +--- + +## 10. 结论 + +本轮设计的本质不是重新发明移动端终端,而是把它从错误的移动端卡片语言里拉回到正确的 workspace 工具语言里。 + +实现完成后,这个页面应该给人的感觉是: + +- 仍然是当前产品 +- 仍然遵循当前主题 +- 但终于和 PC 端终端属于同一套工具系统 diff --git a/e2e-ui/fixtures/scene-runner.ts b/e2e-ui/fixtures/scene-runner.ts index a3412d9eb..eca030a27 100644 --- a/e2e-ui/fixtures/scene-runner.ts +++ b/e2e-ui/fixtures/scene-runner.ts @@ -41,7 +41,7 @@ async function openSettingsSection( const index = sectionOrder[section]; if (device === "mobile") { - await page.locator(".settings-mobile-item").nth(index).click(); + await page.locator(".settings-mobile-root .settings-mobile-item").nth(index).click(); await page.locator(".settings-content--mobile, .settings-content").first().waitFor(); return; } diff --git a/packages/core/src/domain/supervisor.ts b/packages/core/src/domain/supervisor.ts index be43a4b9f..32849b569 100644 --- a/packages/core/src/domain/supervisor.ts +++ b/packages/core/src/domain/supervisor.ts @@ -22,8 +22,7 @@ export type CycleTrigger = "turn_completed" | "manual" | "scheduled"; export type SupervisorStopReason = | "objective_complete" | "max_supervision_count_reached" - | "supervisor_uncertain" - | "needs_user_input"; + | "supervisor_uncertain"; export type SupervisorPlanStepStatus = "pending" | "in_progress" | "done"; @@ -55,7 +54,7 @@ export interface SupervisorCycleTargetRecord { startedAt: number; completedAt: number; result: "continue" | "stop" | "error"; - stopReason?: "objective_complete" | "supervisor_uncertain" | "needs_user_input"; + stopReason?: "objective_complete" | "supervisor_uncertain"; reason?: string; guidance?: string; progressSummary?: string; diff --git a/packages/server/src/supervisor/evaluator.test.ts b/packages/server/src/supervisor/evaluator.test.ts index 30bfc2130..5db252be7 100644 --- a/packages/server/src/supervisor/evaluator.test.ts +++ b/packages/server/src/supervisor/evaluator.test.ts @@ -293,6 +293,21 @@ describe("SupervisorEvaluator", () => { }); }); + it("rejects needs_user_input as a stopReason", async () => { + const evaluator = makeEvaluator( + JSON.stringify({ + status: "stop", + stopReason: "needs_user_input", + reason: "Need user input", + }), + "claude" + ); + + await expect(evaluator.evaluate(makeSupervisor("claude"), makeContext())).rejects.toThrow( + "Supervisor stop result is missing a valid stopReason" + ); + }); + it("falls back to provider.defaultConfig when evaluator config is missing", async () => { const evaluator = new SupervisorEvaluator({ providerRegistry: [ @@ -434,8 +449,23 @@ describe("SupervisorEvaluator", () => { ).rejects.toThrow(); const prompt = (logger.warn.mock.calls[0]?.[0] as { prompt?: string } | undefined)?.prompt; - expect(prompt).toContain("You are supervising a target-scoped software task."); + expect(prompt).toContain("You are an autonomous supervisor for a target-scoped software task."); expect(prompt).toContain("Return JSON only."); + expect(prompt).toContain('Prefer "continue" whenever there is a reasonable next action.'); + expect(prompt).toContain( + "Do not ask the user to decide, clarify, or choose among implementation options." + ); + expect(prompt).toContain("Use the target memory as the current supervision state."); + expect(prompt).toContain("Identify which plan step is currently active."); + expect(prompt).toContain("If the active step is done, advance to the next useful step."); + expect(prompt).toContain( + "If the agent appears stuck or repeated the same action, give a different concrete next action." + ); + expect(prompt).toContain('Use "supervisor_uncertain" only as a last resort'); + expect(prompt).toContain('Guidance requirements for "continue":'); + expect(prompt).toContain( + "Be specific enough for the supervised agent to act without asking the user." + ); expect(prompt).toContain("Current objective:"); expect(prompt).toContain("Ship the fix"); expect(prompt).toContain("Current target memory:"); diff --git a/packages/server/src/supervisor/evaluator.ts b/packages/server/src/supervisor/evaluator.ts index 7183e93a6..8323db01f 100644 --- a/packages/server/src/supervisor/evaluator.ts +++ b/packages/server/src/supervisor/evaluator.ts @@ -30,10 +30,7 @@ const NOOP_LOGGER: FastifyBaseLogger = { export interface SupervisorEvaluationResult { status: "continue" | "stop"; - stopReason?: Extract< - SupervisorStopReason, - "objective_complete" | "supervisor_uncertain" | "needs_user_input" - >; + stopReason?: Extract; reason: string; guidance?: string; plan?: SupervisorPlanStep[]; @@ -135,9 +132,27 @@ export class SupervisorEvaluator { function buildPrompt(context: SupervisorEvaluationContext): string { const lines: string[] = [ - "You are supervising a target-scoped software task.", + "You are an autonomous supervisor for a target-scoped software task.", + "Your job is to keep the agent moving toward the objective until the objective is complete.", + "", "Return JSON only.", "", + "Decision policy:", + '- Prefer "continue" whenever there is a reasonable next action.', + "- Do not ask the user to decide, clarify, or choose among implementation options.", + "- When information is incomplete, choose a conservative next action based on the objective, target memory, latest user input, and terminal snapshot.", + "- Stop only when the objective is complete, or when continuing would likely push the agent in an unsafe or clearly unsupported direction.", + "", + "Stage decision policy:", + "- Use the target memory as the current supervision state.", + "- Base your decision on the objective, current plan, activeStepId, progressSummary, lastGuidance, stalledCount, latest user input, and terminal snapshot.", + "- Identify which plan step is currently active.", + "- Decide whether the active step is done, still in progress, blocked, or obsolete.", + "- If the active step is done, advance to the next useful step.", + "- If the active step is still in progress, give guidance that moves it forward.", + "- If the agent appears stuck or repeated the same action, give a different concrete next action.", + "- If the plan is obsolete, update only the affected steps unless a full replacement is necessary.", + "", "Allowed statuses:", '- "continue": more work is needed; include "reason" and "guidance".', '- "stop": supervision should stop; include "stopReason" and "reason".', @@ -145,10 +160,43 @@ function buildPrompt(context: SupervisorEvaluationContext): string { "Allowed stop reasons:", '- "objective_complete"', '- "supervisor_uncertain"', - '- "needs_user_input"', "", - "If planGenerated is false, bootstrap a plan with 3 to 7 milestone-sized steps.", - "If planGenerated is true, update progress incrementally; do not rewrite the full plan unless absolutely necessary.", + 'Use "objective_complete" only when the objective has been satisfied.', + 'Use "supervisor_uncertain" only as a last resort when no useful next action can be inferred and additional guidance would likely be misleading.', + "", + 'Guidance requirements for "continue":', + "- Give one concrete next action or a short ordered set of concrete actions.", + "- Focus on the highest-value step toward completing the objective.", + "- Be specific enough for the supervised agent to act without asking the user.", + "- Avoid generic reminders, encouragement, or restating the objective.", + "- If verification is needed, tell the agent exactly what to verify next.", + "- If implementation is needed, point to the likely area, behavior, or file/module based on available evidence.", + "", + "Planning policy:", + "- If planGenerated is false, include a plan with 3 to 7 milestone-sized steps.", + "- If planGenerated is true, update progress incrementally.", + "- Do not rewrite the full plan unless the existing plan is clearly wrong or obsolete.", + "- Use stepUpdates to mark completed or active steps when the terminal snapshot shows progress.", + "- Keep activeStepId aligned with the next useful step.", + "", + "Output schema:", + "For continue:", + "{", + ' "status": "continue",', + ' "reason": "brief explanation of why more work is needed",', + ' "guidance": "specific next action for the supervised agent",', + ' "plan": optional array of plan steps,', + ' "activeStepId": optional step id,', + ' "progressSummary": optional brief progress summary,', + ' "stepUpdates": optional array of { "id": string, "status": "pending" | "in_progress" | "done" }', + "}", + "", + "For stop:", + "{", + ' "status": "stop",', + ' "stopReason": "objective_complete" | "supervisor_uncertain",', + ' "reason": "brief explanation"', + "}", "", "Current objective:", context.objective, @@ -563,11 +611,7 @@ function parseSupervisorEvaluationResult( if (status === "stop") { const stopReason = record.stopReason; - if ( - stopReason !== "objective_complete" && - stopReason !== "supervisor_uncertain" && - stopReason !== "needs_user_input" - ) { + if (stopReason !== "objective_complete" && stopReason !== "supervisor_uncertain") { throw new Error("Supervisor stop result is missing a valid stopReason"); } diff --git a/packages/web/src/components/ui/select/index.test.tsx b/packages/web/src/components/ui/select/index.test.tsx index 89c214ce1..55f7a2077 100644 --- a/packages/web/src/components/ui/select/index.test.tsx +++ b/packages/web/src/components/ui/select/index.test.tsx @@ -202,6 +202,7 @@ describe("Select", () => { expect(trigger).toHaveClass("input", "mobile-select-trigger"); expect(trigger).toHaveAttribute("aria-describedby", "evaluator-helper"); expect(trigger).toHaveAttribute("aria-haspopup", "dialog"); + expect(trigger).not.toHaveAttribute("mobile"); fireEvent.click(trigger); expect(onOpen).toHaveBeenCalledTimes(1); diff --git a/packages/web/src/components/ui/select/index.tsx b/packages/web/src/components/ui/select/index.tsx index 931421308..0332a38b8 100644 --- a/packages/web/src/components/ui/select/index.tsx +++ b/packages/web/src/components/ui/select/index.tsx @@ -230,6 +230,7 @@ export function Select(props: SelectProps) { disabled, id, invalid, + mobile: _mobile, onClick, onOpen, options, diff --git a/packages/web/src/features/auth/index.test.tsx b/packages/web/src/features/auth/index.test.tsx index 0af2c038d..288fdfc32 100644 --- a/packages/web/src/features/auth/index.test.tsx +++ b/packages/web/src/features/auth/index.test.tsx @@ -38,8 +38,9 @@ describe("LoginPage", () => { expect(document.querySelector(".welcome-card")).toBeTruthy(); expect(document.querySelector(".auth-form")).toBeTruthy(); expect(document.querySelector(".auth-status-panel")).toBeTruthy(); - expect(container.querySelector(".auth-card-shell > .auth-status-panel")).toBeTruthy(); - expect(container.querySelector(".auth-card-shell > .auth-form")).toBeTruthy(); + expect(container.querySelector(".auth-card-shell__content")).toBeTruthy(); + expect(container.querySelector(".auth-card-shell__content > .auth-status-panel")).toBeTruthy(); + expect(container.querySelector(".auth-card-shell__content > .auth-form")).toBeTruthy(); expect(screen.getByRole("button")).toBeDisabled(); expect(screen.getAllByText("连接中").length).toBeGreaterThan(0); }); diff --git a/packages/web/src/features/auth/index.tsx b/packages/web/src/features/auth/index.tsx index 349015f5b..02651bd1c 100644 --- a/packages/web/src/features/auth/index.tsx +++ b/packages/web/src/features/auth/index.tsx @@ -161,40 +161,42 @@ export function LoginPage({ .filter(Boolean) .join(" ")} > - -
CODER STUDIO
-

{t("app.name")}

- - } - description={

{description}

} - /> -
-
{t("auth.status_title")}
-

{error ?? statusDetail}

-
-
- setPassword(e.target.value)} - aria-label={t("settings.auth.password")} - placeholder={t("settings.auth.password")} +
+ +
CODER STUDIO
+

{t("app.name")}

+
+ } + description={

{description}

} /> - -
+
+
{t("auth.status_title")}
+

{error ?? statusDetail}

+
+
+ setPassword(e.target.value)} + aria-label={t("settings.auth.password")} + placeholder={t("settings.auth.password")} + /> + +
+ ); diff --git a/packages/web/src/features/command-palette/components/command-palette.test.tsx b/packages/web/src/features/command-palette/components/command-palette.test.tsx index 7a2ab8840..d02bc0c9d 100644 --- a/packages/web/src/features/command-palette/components/command-palette.test.tsx +++ b/packages/web/src/features/command-palette/components/command-palette.test.tsx @@ -233,6 +233,43 @@ describe("CommandPalette", () => { expect(store.get(terminalPanelVisibleAtom)).toBe(true); }); + it("keeps the desktop palette overlay, chrome, and selected item styling visible", () => { + const store = createStore(); + store.set(localeAtom, "en"); + store.set(commandPaletteOpenAtom, true); + store.set(workspacesAtom, { + "ws-1": createWorkspace("ws-1", "/tmp/one"), + }); + store.set(workspaceOrderAtom, ["ws-1"]); + store.set(workspacesLoadStateAtom, "ready"); + + render( + + + + ); + + const palette = document.querySelector(".command-palette"); + const overlay = document.querySelector(".command-palette-overlay"); + const searchInput = screen.getByRole("textbox"); + + expect(overlay).toBeTruthy(); + expect(palette).toBeTruthy(); + expect(document.querySelector(".command-palette-header")).toBeTruthy(); + expect(document.querySelector(".command-palette-search")).toBeTruthy(); + expect(document.querySelector(".command-palette-hint")).toBeTruthy(); + expect(document.querySelector(".command-palette-list")).toBeTruthy(); + expect(palette).toContainElement(searchInput); + + fireEvent.change(searchInput, { + target: { value: "settings" }, + }); + + fireEvent.keyDown(palette!, { key: "ArrowDown" }); + + expect(document.querySelector(".command-palette-item-selected")).toBeTruthy(); + }); + it("closes the mobile palette before opening the workspace launcher", () => { viewportMocks.viewport = "mobile"; diff --git a/packages/web/src/features/command-palette/components/command-palette.tsx b/packages/web/src/features/command-palette/components/command-palette.tsx index 6396483c6..a6c8c35bd 100644 --- a/packages/web/src/features/command-palette/components/command-palette.tsx +++ b/packages/web/src/features/command-palette/components/command-palette.tsx @@ -257,7 +257,7 @@ export function CommandPalette() { return (
setIsOpen(false)}>
e.stopPropagation()} onKeyDown={handleKeyDown} > diff --git a/packages/web/src/features/not-found/index.test.tsx b/packages/web/src/features/not-found/index.test.tsx index abe232094..6b142ac6c 100644 --- a/packages/web/src/features/not-found/index.test.tsx +++ b/packages/web/src/features/not-found/index.test.tsx @@ -36,6 +36,8 @@ describe("NotFoundPage", () => { expect(document.querySelector(".welcome-container")).toBeTruthy(); expect(document.querySelector(".welcome-card")).toBeTruthy(); + expect(document.querySelector(".welcome-card__panel")).toBeTruthy(); + expect(document.querySelector(".welcome-card__panel .auth-status-panel")).toBeTruthy(); expect(screen.getByText("Requested path")).toBeInTheDocument(); expect(screen.getByText("/missing/path")).toBeInTheDocument(); expect(screen.getByRole("heading", { name: "Page not found" })).toBeInTheDocument(); diff --git a/packages/web/src/features/not-found/index.tsx b/packages/web/src/features/not-found/index.tsx index 0b70e2d72..032945655 100644 --- a/packages/web/src/features/not-found/index.tsx +++ b/packages/web/src/features/not-found/index.tsx @@ -18,28 +18,32 @@ export function NotFoundPage() { return (
- -
{t("not_found.kicker")}
-

{t("not_found.title")}

-
- } - description={ -
-

{t("not_found.description")}

-
- } - action={ - - } - /> -
-
{t("not_found.path_label")}
-

{location.pathname}

+
+ +
{t("not_found.kicker")}
+

{t("not_found.title")}

+
+ } + description={ +
+

{t("not_found.description")}

+
+ } + /> +
+
+ +
+
+
+
{t("not_found.path_label")}
+

{location.pathname}

+
diff --git a/packages/web/src/features/settings/components/settings-page.test.tsx b/packages/web/src/features/settings/components/settings-page.test.tsx index b8567aa78..8172a9917 100644 --- a/packages/web/src/features/settings/components/settings-page.test.tsx +++ b/packages/web/src/features/settings/components/settings-page.test.tsx @@ -207,6 +207,18 @@ describe("SettingsPage", () => { renderSettingsPage(store); expect(screen.getByText("v0.3.0")).toBeInTheDocument(); + expect(document.querySelector(".settings-footer__meta")).toBeTruthy(); + }); + + it("wraps desktop settings content in the shared content surface", async () => { + const store = createConnectedStore(vi.fn().mockResolvedValue({})); + + renderSettingsPage(store); + + await waitFor(() => { + expect(document.querySelector(".settings-content-surface")).toBeTruthy(); + }); + expect(document.querySelector(".settings-content-surface .settings-section")).toBeTruthy(); }); it("renders desktop and mobile settings entry icons through themed semantics", async () => { @@ -260,6 +272,40 @@ describe("SettingsPage", () => { ).toBeTruthy(); }); + it("renders the mobile settings homepage as grouped sections without the legacy hero", async () => { + viewportMocks.viewport = "mobile"; + const store = createConnectedStore(vi.fn().mockResolvedValue({})); + + renderSettingsPage(store); + + const mobileRoot = await screen.findByTestId("settings-mobile-root"); + const groupHeadings = within(mobileRoot).getAllByRole("heading", { level: 2 }); + + expect(groupHeadings).toHaveLength(2); + expect(within(mobileRoot).getByText("工作区与运行")).toBeInTheDocument(); + expect(within(mobileRoot).getByText("界面与交互")).toBeInTheDocument(); + expect(document.querySelector(".settings-mobile-root-hero")).toBeNull(); + + const buttons = within(mobileRoot).getAllByRole("button"); + const labels = buttons.map((button) => button.getAttribute("aria-label")).filter(Boolean); + + expect(labels).toEqual(expect.arrayContaining(["通用", "Agents", "外观", "快捷键"])); + expect(labels.indexOf("通用")).toBeLessThan(labels.indexOf("Agents")); + expect(labels.indexOf("Agents")).toBeLessThan(labels.indexOf("外观")); + expect(labels.indexOf("外观")).toBeLessThan(labels.indexOf("快捷键")); + }); + + it("localizes the new mobile settings homepage section headings", async () => { + viewportMocks.viewport = "mobile"; + window.localStorage.setItem("ui.locale", JSON.stringify("en")); + const store = createConnectedStore(vi.fn().mockResolvedValue({})); + + renderSettingsPage(store); + + expect(await screen.findByText("Workspace & Runtime")).toBeInTheDocument(); + expect(screen.getByText("Interface & Interaction")).toBeInTheDocument(); + }); + it("does not render default Agent Provider selection in general settings", async () => { const sendCommand = vi.fn().mockImplementation(async (op: string) => { if (op === "settings.get") { @@ -892,6 +938,30 @@ describe("SettingsPage", () => { expect(routerMocks.navigate).toHaveBeenCalledWith("/"); }); + it("renders a compact desktop header with only the settings title", async () => { + const sendCommand = vi.fn().mockResolvedValue({}); + const store = createConnectedStore(sendCommand); + + renderSettingsPage(store); + + const desktopHeader = document.querySelector(".settings-header__desktop") as HTMLElement | null; + const mobileHeader = document.querySelector( + ".settings-header .mobile-page-header" + ) as HTMLElement | null; + const headerCopy = document.querySelector(".settings-header__copy") as HTMLElement | null; + + expect(desktopHeader).not.toBeNull(); + expect(mobileHeader).toBeNull(); + expect(screen.getByRole("heading", { name: "设置" })).toBeInTheDocument(); + expect( + within(desktopHeader as HTMLElement).getByRole("button", { name: "返回" }) + ).toBeInTheDocument(); + expect(headerCopy).not.toBeNull(); + expect(within(headerCopy as HTMLElement).queryByText("Coder Studio")).toBeNull(); + expect(within(headerCopy as HTMLElement).queryByText("设置已自动保存")).toBeNull(); + expect(document.querySelector(".settings-header__section-pill")).toBeNull(); + }); + it("renders a mobile category list and returns from detail content to the settings root", async () => { viewportMocks.viewport = "mobile"; const sendCommand = vi.fn().mockImplementation(async (op: string, args: unknown) => { diff --git a/packages/web/src/features/settings/components/settings-page.tsx b/packages/web/src/features/settings/components/settings-page.tsx index d85d9851a..b8fbb09f0 100644 --- a/packages/web/src/features/settings/components/settings-page.tsx +++ b/packages/web/src/features/settings/components/settings-page.tsx @@ -22,7 +22,7 @@ import { resolveSupervisorRetryOnTimeout, } from "@coder-studio/core"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { Check, ChevronRight } from "lucide-react"; +import { Check, ChevronLeft, ChevronRight } from "lucide-react"; import { useEffect, useId, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import { localeAtom, themeAtom } from "../../../atoms/app-ui"; @@ -125,6 +125,59 @@ function loadProviderAdditionalArgs( ); } +function getMobileSectionHintKey(section: SettingsSection) { + switch (section) { + case "general": + return "settings.notifications_channel_hint"; + case "providers": + return "settings.provider.command_preview_hint"; + case "appearance": + return "settings.theme.hint"; + case "shortcuts": + return "settings.shortcuts.hint"; + } +} + +const MOBILE_SETTINGS_GROUPS = [ + { + titleKey: "settings.mobile_groups.workspace_runtime", + sections: ["general", "providers"], + }, + { + titleKey: "settings.mobile_groups.interface_interaction", + sections: ["appearance", "shortcuts"], + }, +] as const satisfies readonly { + titleKey: string; + sections: readonly SettingsSection[]; +}[]; + +function resolveMobileSettingsGroups( + availableSections: readonly { + id: SettingsSection; + labelKey: string; + iconSemantic: Parameters[0]["semantic"]; + }[] +) { + const sectionsById = new Map(availableSections.map((section) => [section.id, section])); + const groupedSectionIds = MOBILE_SETTINGS_GROUPS.flatMap((group) => group.sections); + + if (groupedSectionIds.length !== availableSections.length) { + throw new Error("Mobile settings groups are out of sync with available sections."); + } + + for (const sectionId of groupedSectionIds) { + if (!sectionsById.has(sectionId)) { + throw new Error(`Missing mobile settings section mapping for "${sectionId}".`); + } + } + + return MOBILE_SETTINGS_GROUPS.map((group) => ({ + titleKey: group.titleKey, + sections: group.sections.map((sectionId) => sectionsById.get(sectionId)!), + })); +} + /** * Settings Page * @@ -436,20 +489,35 @@ export function SettingsPage() { const renderMobileRoot = () => (
-
- {availableSections.map(({ id, labelKey, iconSemantic }) => ( - +
+ {resolveMobileSettingsGroups(availableSections).map((group) => ( +
+

{t(group.titleKey)}

+
+ {group.sections.map((section) => ( + + ))} +
+
))}
@@ -464,12 +532,28 @@ export function SettingsPage() { return (
- + {isMobile ? ( + + ) : ( +
+ +
+

{t("settings.title")}

+
+
+ )}
{shouldShowMobileRoot ? ( @@ -497,31 +581,35 @@ export function SettingsPage() {
- {settingsLoadError && ( - setSettingsRefreshKey((value) => value + 1)} - > - {t("action.refresh")} - - } - /> - )} - {renderContent()} +
+ {settingsLoadError && ( + setSettingsRefreshKey((value) => value + 1)} + > + {t("action.refresh")} + + } + /> + )} + {renderContent()} +
)}
- {t("settings.autosave_hint")} - v{serverInfo?.version ?? "0.0.0"} +
+ {t("settings.autosave_hint")} + v{serverInfo?.version ?? "0.0.0"} +
); diff --git a/packages/web/src/features/terminal-panel/__tests__/terminal-panel.test.tsx b/packages/web/src/features/terminal-panel/__tests__/terminal-panel.test.tsx index 783d1a636..e4908559c 100644 --- a/packages/web/src/features/terminal-panel/__tests__/terminal-panel.test.tsx +++ b/packages/web/src/features/terminal-panel/__tests__/terminal-panel.test.tsx @@ -2,6 +2,7 @@ import { Topics } from "@coder-studio/core"; import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { createStore, Provider } from "jotai"; +import { type ReactNode, useState } from "react"; import { MemoryRouter } from "react-router-dom"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { localeAtom } from "../../../atoms/app-ui"; @@ -108,6 +109,8 @@ describe("TerminalPanel", () => { ); expect(screen.getByText("No terminals")).toBeInTheDocument(); + expect(document.querySelector(".bottom-terminal-empty")).toBeTruthy(); + expect(document.querySelector(".bottom-terminal-empty-icon")).toBeTruthy(); expect(subscribe).toHaveBeenCalledWith([Topics.terminalsAll("ws-test")], expect.any(Function)); await act(async () => { @@ -600,7 +603,7 @@ describe("TerminalPanel", () => { expect(consoleSpy).not.toHaveBeenCalled(); }); - it("renders a lighter mobile fullscreen terminal toolbar when requested by the mobile sheet", async () => { + it("leaves the mobile fullscreen title slot blank when no terminal exists", async () => { const store = createStore(); const subscribe = vi.fn((topics: string[], handler: EventHandler) => { handlers.push(handler); @@ -627,14 +630,170 @@ describe("TerminalPanel", () => { store.set(bottomPanelHeightAtom, 240); setEnglishLocale(store); store.set(wsClientAtom, { subscribe, sendCommand } as never); - store.set(terminalMetaAtomFamily("term_1"), { - id: "term_1", - workspaceId: "ws-test", - kind: "shell", - alive: true, - title: "Workspace Shell", + render( + + +
+ +
+
+
+ ); + + expect( + document.querySelector(".bottom-terminal--mobile-fullscreen .terminal-toolbar") + ).toBeTruthy(); + expect( + document.querySelector(".bottom-terminal--mobile-fullscreen .terminal-toolbar-mobile-row") + ).toBeTruthy(); + expect( + document.querySelector( + ".bottom-terminal--mobile-fullscreen .terminal-toolbar-mobile-placeholder" + ) + ).toBeTruthy(); + expect(screen.queryByRole("button", { name: "Close Terminal" })).not.toBeInTheDocument(); + expect(screen.getAllByRole("button", { name: "New Terminal" }).length).toBeGreaterThan(0); + }); + + it("renders the mobile fullscreen terminal toolbar as a single compact chrome row", async () => { + const store = createStore(); + const subscribe = vi.fn((topics: string[], handler: EventHandler) => { + handlers.push(handler); + return () => { + handlers = handlers.filter((candidate) => candidate !== handler); + }; + }); + const sendCommand = vi.fn().mockImplementation((op: string) => { + if (op === "terminal.list") { + return Promise.resolve([ + { + id: "term_1", + workspaceId: "ws-test", + kind: "shell", + title: "Workspace Shell", + cwd: "/tmp/ws-test", + argv: ["/bin/bash"], + cols: 120, + rows: 30, + alive: true, + createdAt: 1, + }, + { + id: "term_2", + workspaceId: "ws-test", + kind: "shell", + title: "Workspace Shell 2", + cwd: "/tmp/ws-test", + argv: ["/bin/bash"], + cols: 120, + rows: 30, + alive: true, + createdAt: 2, + }, + ]); + } + + return Promise.resolve([]); + }); + + seedReadyWorkspaceState(store, { + "ws-test": { + id: "ws-test", + path: "/tmp/ws-test", + targetRuntime: "native", + openedAt: 1, + lastActiveAt: 1, + uiState: { + leftPanelWidth: 280, + bottomPanelHeight: 200, + focusMode: false, + }, + }, + }); + store.set(bottomPanelHeightAtom, 240); + setEnglishLocale(store); + store.set(wsClientAtom, { subscribe, sendCommand } as never); + + render( + + +
+ +
+
+
+ ); + + await waitFor(() => { + expect(screen.getByTestId("xterm-host")).toHaveTextContent("term_1"); + }); + + const toolbar = document.querySelector(".bottom-terminal--mobile-fullscreen .terminal-toolbar"); + const mobileRow = document.querySelector( + ".bottom-terminal--mobile-fullscreen .terminal-toolbar-mobile-row" + ); + const selector = document.querySelector( + ".bottom-terminal--mobile-fullscreen .terminal-selector" + ); + const actions = document.querySelector( + ".bottom-terminal--mobile-fullscreen .terminal-toolbar-actions" + ); + + expect(toolbar).toBeTruthy(); + expect(mobileRow).toBeTruthy(); + expect(toolbar?.querySelector(".terminal-toolbar-left")).toBeNull(); + expect(toolbar?.querySelector(".terminal-toolbar-right")).toBeNull(); + expect(selector).toBeTruthy(); + expect(actions).toBeTruthy(); + }); + + it("renders a static mobile fullscreen title when only one terminal exists", async () => { + const store = createStore(); + const subscribe = vi.fn((topics: string[], handler: EventHandler) => { + handlers.push(handler); + return () => { + handlers = handlers.filter((candidate) => candidate !== handler); + }; + }); + const sendCommand = vi.fn().mockImplementation((op: string) => { + if (op === "terminal.list") { + return Promise.resolve([ + { + id: "term_1", + workspaceId: "ws-test", + kind: "shell", + title: "Workspace Shell", + cwd: "/tmp/ws-test", + argv: ["/bin/bash"], + cols: 120, + rows: 30, + alive: true, + createdAt: 1, + }, + ]); + } + + return Promise.resolve([]); }); + seedReadyWorkspaceState(store, { + "ws-test": { + id: "ws-test", + path: "/tmp/ws-test", + targetRuntime: "native", + openedAt: 1, + lastActiveAt: 1, + uiState: { + leftPanelWidth: 280, + bottomPanelHeight: 200, + focusMode: false, + }, + }, + }); + store.set(bottomPanelHeightAtom, 240); + setEnglishLocale(store); + store.set(wsClientAtom, { subscribe, sendCommand } as never); + render( @@ -645,11 +804,136 @@ describe("TerminalPanel", () => { ); - expect(screen.queryByText("TERMINAL")).not.toBeInTheDocument(); - expect(screen.queryByText("Workspace Shell")).not.toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByTestId("xterm-host")).toHaveTextContent("term_1"); + }); + + expect(screen.getByText("Workspace Shell")).toBeInTheDocument(); + expect( + document.querySelector(".bottom-terminal--mobile-fullscreen .terminal-selector-btn--static") + ).toBeTruthy(); + expect( + document.querySelector( + ".bottom-terminal--mobile-fullscreen .terminal-toolbar-mobile-placeholder" + ) + ).toBeNull(); + expect(screen.getByRole("button", { name: "Close Terminal" })).toBeInTheDocument(); expect(screen.getAllByRole("button", { name: "New Terminal" }).length).toBeGreaterThan(0); }); + it("keeps mobile fullscreen terminal actions together in the toolbar when header actions are available", async () => { + const store = createStore(); + const subscribe = vi.fn((topics: string[], handler: EventHandler) => { + handlers.push(handler); + return () => { + handlers = handlers.filter((candidate) => candidate !== handler); + }; + }); + const sendCommand = vi.fn().mockImplementation((op: string) => { + if (op === "terminal.list") { + return Promise.resolve([ + { + id: "term_1", + workspaceId: "ws-test", + kind: "shell", + title: "Workspace Shell", + cwd: "/tmp/ws-test", + argv: ["/bin/bash"], + cols: 120, + rows: 30, + alive: true, + createdAt: 1, + }, + { + id: "term_2", + workspaceId: "ws-test", + kind: "shell", + title: "Workspace Shell 2", + cwd: "/tmp/ws-test", + argv: ["/bin/bash"], + cols: 120, + rows: 30, + alive: true, + createdAt: 2, + }, + ]); + } + + return Promise.resolve([]); + }); + + seedReadyWorkspaceState(store, { + "ws-test": { + id: "ws-test", + path: "/tmp/ws-test", + targetRuntime: "native", + openedAt: 1, + lastActiveAt: 1, + uiState: { + leftPanelWidth: 280, + bottomPanelHeight: 200, + focusMode: false, + }, + }, + }); + store.set(bottomPanelHeightAtom, 240); + setEnglishLocale(store); + store.set(wsClientAtom, { subscribe, sendCommand } as never); + + function HeaderActionHarness() { + const [headerActions, setHeaderActions] = useState(null); + + return ( + <> +
{headerActions}
+
+ +
+ + ); + } + + render( + + + + + + ); + + await waitFor(() => { + expect(screen.getByTestId("xterm-host")).toHaveTextContent("term_1"); + }); + + const headerActions = screen.getByTestId("terminal-header-actions"); + const toolbar = document.querySelector( + ".bottom-terminal--mobile-fullscreen .terminal-toolbar" + ) as HTMLElement | null; + + expect( + within(headerActions).queryByRole("button", { name: "New Terminal" }) + ).not.toBeInTheDocument(); + expect( + within(headerActions).queryByRole("button", { name: "Close Terminal" }) + ).not.toBeInTheDocument(); + expect(toolbar).toBeTruthy(); + expect( + within(toolbar as HTMLElement).getByRole("button", { name: "Close Terminal" }) + ).toBeInTheDocument(); + expect( + within(toolbar as HTMLElement).getByRole("button", { name: "New Terminal" }) + ).toBeInTheDocument(); + expect( + document.querySelector(".bottom-terminal--mobile-fullscreen .terminal-selector") + ).toBeTruthy(); + expect( + document.querySelectorAll(".bottom-terminal--mobile-fullscreen .terminal-toolbar-actions") + ).toHaveLength(2); + }); + it("keeps only one terminal switcher in mobile fullscreen mode", async () => { const store = createStore(); const subscribe = vi.fn((topics: string[], handler: EventHandler) => { diff --git a/packages/web/src/features/terminal-panel/views/shared/terminal-panel.tsx b/packages/web/src/features/terminal-panel/views/shared/terminal-panel.tsx index f1b3a4230..a58dcbf24 100644 --- a/packages/web/src/features/terminal-panel/views/shared/terminal-panel.tsx +++ b/packages/web/src/features/terminal-panel/views/shared/terminal-panel.tsx @@ -1,6 +1,6 @@ import { useStore } from "jotai"; import { ChevronDown, X } from "lucide-react"; -import { useEffect, useState } from "react"; +import { type ReactNode, useEffect, useState } from "react"; import { Button, EmptyState, @@ -23,6 +23,7 @@ import { XtermHost } from "./xterm-host"; interface TerminalPanelProps { chrome?: "default" | "mobile-fullscreen"; + onMobileHeaderActionsChange?: (actions: ReactNode | null) => void; } /** @@ -35,7 +36,10 @@ interface TerminalPanelProps { * - xterm.js rendering area * - Empty state when no terminals */ -export function TerminalPanel({ chrome = "default" }: TerminalPanelProps) { +export function TerminalPanel({ + chrome = "default", + onMobileHeaderActionsChange, +}: TerminalPanelProps) { const t = useTranslation(); const store = useStore(); const [desktopSelectorOpen, setDesktopSelectorOpen] = useState(false); @@ -76,126 +80,91 @@ export function TerminalPanel({ chrome = "default" }: TerminalPanelProps) { } }, [showSelector, terminalIds.length]); + useEffect(() => { + if (!onMobileHeaderActionsChange) { + return; + } + + return () => { + onMobileHeaderActionsChange(null); + }; + }, [onMobileHeaderActionsChange]); + + useEffect(() => { + if (!onMobileHeaderActionsChange) { + return; + } + + onMobileHeaderActionsChange(null); + }, [onMobileHeaderActionsChange]); + return (
-
- {isMobileFullscreen ? null : ( -
- {t("terminal.kicker")} - {activeTerminalMeta ? ( - {activeTerminalTitle} - ) : null} -
- )} -
- -
- {hasTerminals && ( - <> - {showSelector ? ( -
- {isMobileFullscreen ? ( - { + if (!selectorSheetOpen) { + return; } - contentClassName="terminal-selector-dropdown" - forceMode="desktop" - open={desktopSelectorOpen} - placement="bottom-end" - title={t("terminal.selector.title")} - onOpenChange={setDesktopSelectorOpen} - > - - - ) : ( - - )} - {isMobileFullscreen ? ( - selectorSheetOpen ? ( - { - return { - id: option.value, - label: option.label, - meta: - option.value === activeTerminalId - ? t("terminal.selector.current") - : t("terminal.selector.indexed", { index: index + 1 }), - }; - }), - }, - ]} - selectedId={activeTerminalId} - onSelect={handleSwitchTerminal} - onClose={() => setSelectorSheetOpen(false)} - /> - ) : null - ) : null} -
- ) : null} + event.preventDefault(); + setSelectorSheetOpen(false); + }} + onOpen={() => setSelectorSheetOpen(true)} + /> + ) : ( +
+ {activeTerminalTitle} +
+ )} + + {showSelector && selectorSheetOpen ? ( + ({ + id: option.value, + label: option.label, + meta: + option.value === activeTerminalId + ? t("terminal.selector.current") + : t("terminal.selector.indexed", { index: index + 1 }), + })), + }, + ]} + selectedId={activeTerminalId} + onSelect={handleSwitchTerminal} + onClose={() => setSelectorSheetOpen(false)} + /> + ) : null} +
+ ) : ( + + ) : ( + <> +
+
+ {t("terminal.kicker")} + {activeTerminalMeta ? ( + {activeTerminalTitle} + ) : null} +
+
+ +
+ {hasTerminals && ( + <> + {showSelector ? ( +
+ {terminalIds.length > 1 ? ( + + {terminalIds.map((id, index) => ( + { + handleSwitchTerminal(id); + setDesktopSelectorOpen(false); + }} + onClose={() => { + setDesktopSelectorOpen(false); + void handleCloseTerminal(id); + }} + /> + ))} + + } + contentClassName="terminal-selector-dropdown" + forceMode="desktop" + open={desktopSelectorOpen} + placement="bottom-end" + title={t("terminal.selector.title")} + onOpenChange={setDesktopSelectorOpen} + > + + + ) : ( + + )} +
+ ) : null} + +
+ + } + onClick={() => { + if (!activeTerminalId) { + return; + } + + setDesktopSelectorOpen(false); + setSelectorSheetOpen(false); + void handleCloseTerminal(activeTerminalId); + }} + size="sm" + /> + +
+ + )} + +
+ + } + onClick={handleCreateTerminal} + size="sm" + /> + +
+
+ + )}
diff --git a/packages/web/src/features/welcome/index.test.tsx b/packages/web/src/features/welcome/index.test.tsx index fa8b19f63..a75704ee0 100644 --- a/packages/web/src/features/welcome/index.test.tsx +++ b/packages/web/src/features/welcome/index.test.tsx @@ -96,6 +96,9 @@ describe("WelcomePage", () => { expect(screen.getByText("DEPLOY ONCE, CODE EVERYWHERE")).toBeInTheDocument(); expect(screen.getByRole("heading", { name: "Welcome to Coder Studio" })).toBeInTheDocument(); + expect(document.querySelector(".welcome-card__hero")).toBeTruthy(); + expect(document.querySelector(".welcome-card__actions")).toBeTruthy(); + expect(document.querySelector(".welcome-card__panel")).toBeTruthy(); const openWorkspaceButton = screen.getByRole("button", { name: "Open Workspace" }); const settingsButton = screen.getByRole("button", { name: "Settings" }); expect(document.querySelector(".welcome-divider")).toBeTruthy(); diff --git a/packages/web/src/features/welcome/index.tsx b/packages/web/src/features/welcome/index.tsx index 337bef6c2..9411c2dbe 100644 --- a/packages/web/src/features/welcome/index.tsx +++ b/packages/web/src/features/welcome/index.tsx @@ -70,40 +70,49 @@ export const WelcomePage: FC = () => { <>
- -
{t("welcome.kicker")}
-

{t("welcome.title")}

-
- } - description={

{t("welcome.description")}

} - action={ - - } - /> +
+ +
{t("welcome.kicker")}
+

{t("welcome.title")}

+
+ } + description={

{t("welcome.description")}

} + /> +
- +
+ + + +
-
+
+
-
- {features.map((f, i) => ( -
- -
-
{f.title}
-
{f.description}
+
+ {features.map((feature) => ( +
+ +
+
{feature.title}
+
{feature.description}
+
-
- ))} + ))} +
diff --git a/packages/web/src/features/workspace/index.test.tsx b/packages/web/src/features/workspace/index.test.tsx index be3faced5..e8d6eac6f 100644 --- a/packages/web/src/features/workspace/index.test.tsx +++ b/packages/web/src/features/workspace/index.test.tsx @@ -118,6 +118,9 @@ describe("WorkspacePage", () => { await screen.findByText("feature/refactor-ts"); + expect(document.querySelector(".workspace-page.workspace-page--desktop")).toBeTruthy(); + expect(document.querySelector(".workspace-main-stage")).toBeTruthy(); + expect(document.querySelector(".workspace-main-area > .workspace-main-stage")).toBeTruthy(); expect( document.querySelector(".workspace-status-bar .git-panel-status-strip__branch-text") ).toHaveTextContent("feature/refactor-ts"); diff --git a/packages/web/src/features/workspace/views/desktop/workspace-desktop-view.tsx b/packages/web/src/features/workspace/views/desktop/workspace-desktop-view.tsx index eb9a7bc52..1020ad01b 100644 --- a/packages/web/src/features/workspace/views/desktop/workspace-desktop-view.tsx +++ b/packages/web/src/features/workspace/views/desktop/workspace-desktop-view.tsx @@ -103,7 +103,7 @@ export const WorkspaceDesktopView: FC = () => { }; return ( -
+
@@ -193,15 +193,17 @@ export const WorkspaceDesktopView: FC = () => { )}
- {mainAreaMode === "diff" ? ( - - ) : mainAreaMode === "editor" ? ( - - ) : ( -
- -
- )} +
+ {mainAreaMode === "diff" ? ( + + ) : mainAreaMode === "editor" ? ( + + ) : ( +
+ +
+ )} +
{!focusMode && terminalPanelVisible && (
({ return `Switch to ${params?.name ?? ""}`; case "mobile.workspace_drawer.close_workspace": return `Close ${params?.name ?? ""}`; + case "worktree.current": + return "Current"; case "action.close": return "Close"; case "tooltip.new_workspace": diff --git a/packages/web/src/features/workspace/views/mobile/mobile-workspace-drawer.tsx b/packages/web/src/features/workspace/views/mobile/mobile-workspace-drawer.tsx index 21a51902a..6be734e70 100644 --- a/packages/web/src/features/workspace/views/mobile/mobile-workspace-drawer.tsx +++ b/packages/web/src/features/workspace/views/mobile/mobile-workspace-drawer.tsx @@ -99,7 +99,7 @@ export function MobileWorkspaceDrawer({ {isActive ? ( - Current + {t("worktree.current")} ) : null} diff --git a/packages/web/src/features/workspace/views/mobile/workspace-mobile-view.tsx b/packages/web/src/features/workspace/views/mobile/workspace-mobile-view.tsx index 3674d6478..0493163b5 100644 --- a/packages/web/src/features/workspace/views/mobile/workspace-mobile-view.tsx +++ b/packages/web/src/features/workspace/views/mobile/workspace-mobile-view.tsx @@ -1,12 +1,12 @@ import { useAtomValue, useSetAtom } from "jotai"; -import { type CSSProperties, useEffect, useRef, useState } from "react"; +import { type CSSProperties, type ReactNode, useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import { lastViewedTargetAtom, pendingFocusSessionAtom, visibleMobileSessionIdAtom, } from "../../../../atoms/app-ui"; -import { EmptyState, Sheet } from "../../../../components/ui"; +import { Sheet } from "../../../../components/ui"; import { useTranslation } from "../../../../lib/i18n"; import { SessionCard } from "../../../agent-panes/views/shared/session-card"; import { useCodeEditorActions } from "../../../code-editor/actions/use-code-editor-actions"; @@ -105,6 +105,7 @@ export function WorkspaceMobileView() { ); const [mobileFileCollapseVersion, setMobileFileCollapseVersion] = useState(0); const [workspaceLaunchOpen, setWorkspaceLaunchOpen] = useState(false); + const [mobileTerminalHeaderAction, setMobileTerminalHeaderAction] = useState(null); const keyboardInset = useVisualViewportInset(); const layoutMode = useMobileLayoutMode(); const motionMode = useMobileMotionMode(); @@ -279,7 +280,10 @@ export function WorkspaceMobileView() { title: t("label.terminal"), body: (
- +
), footer: activeWorkspaceId ? ( @@ -291,6 +295,7 @@ export function WorkspaceMobileView() { /> ) : null, kicker: null, + headerAction: mobileTerminalHeaderAction, fullscreen: true, bodyClassName: "mobile-sheet__body--flush mobile-sheet__body--fullscreen", contentClassName: "mobile-sheet--terminal", @@ -360,33 +365,24 @@ export function WorkspaceMobileView() { ) : (
-
- - {t("label.agent")} -

{t("mobile.empty.start_session")}

-
- } - description={ -
-

{t("mobile.empty.files_terminal_hint")}

-
- } - action={ -
- -
- } - /> +
+
+
+

{t("mobile.empty.start_session")}

+
+
+

{t("mobile.empty.files_terminal_hint")}

+
+
+ +
+
)} diff --git a/packages/web/src/features/workspace/views/shared/file-tree-panel.test.tsx b/packages/web/src/features/workspace/views/shared/file-tree-panel.test.tsx index e60c22401..bddbca7ba 100644 --- a/packages/web/src/features/workspace/views/shared/file-tree-panel.test.tsx +++ b/packages/web/src/features/workspace/views/shared/file-tree-panel.test.tsx @@ -64,6 +64,8 @@ describe("FileTreePanel", () => { ); + expect(document.querySelector(".file-tree-shell.file-tree-shell--desktop")).toBeTruthy(); + await waitFor(() => { expect(sendCommand).toHaveBeenCalledWith( "file.readTree", @@ -923,6 +925,44 @@ describe("FileTreePanel", () => { expect(emptyShell).not.toBeNull(); }); + it("marks desktop search chrome and active rows with the polished desktop selectors", () => { + const store = createStore(); + store.set(wsClientAtom, { sendCommand: vi.fn().mockResolvedValue({}) } as never); + store.set( + fileTreeAtomFamily("ws-test"), + new Map([ + [ + ".", + [ + { + path: "src/app.tsx", + name: "app.tsx", + kind: "file", + }, + ], + ], + ]) + ); + store.set(activeFilePathAtomFamily("ws-test"), "src/app.tsx"); + + render( + + + + ); + + const desktopSearch = document.querySelector( + ".file-tree-search.file-tree-search--desktop" + ) as HTMLElement | null; + const searchInput = screen.getByLabelText("action.search_files"); + const activeRow = screen.getByText("app.tsx").closest(".tree-item"); + + expect(desktopSearch).toBeTruthy(); + expect(desktopSearch).toContainElement(searchInput); + expect(activeRow).toHaveClass("selected"); + expect(activeRow?.querySelector(".tree-item-actions")).toBeTruthy(); + }); + it("uses shared tooltip behavior for the search result delete action", async () => { const sendCommand = vi.fn().mockImplementation(async (op: string, args: { query?: string }) => { if (op === "file.search") { diff --git a/packages/web/src/features/workspace/views/shared/file-tree-panel.tsx b/packages/web/src/features/workspace/views/shared/file-tree-panel.tsx index 42d86b8ed..17d1f8651 100644 --- a/packages/web/src/features/workspace/views/shared/file-tree-panel.tsx +++ b/packages/web/src/features/workspace/views/shared/file-tree-panel.tsx @@ -181,7 +181,10 @@ export const FileTreePanel: FC = ({ return ( <>
-
} - bodyClassName="mobile-sheet__body--flush" - contentClassName="mobile-sheet--worktree" - onBack={view === "list" ? undefined : () => setView("list")} - headerAction={ - view === "list" ? ( - - ) : undefined - } - onClose={onClose} - /> - ) : ( + if (isMobile) { + return ( + {body}
} + bodyClassName="mobile-sheet__body--flush" + contentClassName="mobile-sheet--worktree" + onBack={view === "list" ? undefined : () => setView("list")} + headerAction={ + view === "list" ? ( + + ) : undefined + } + onClose={onClose} + /> + ); + } + + if (desktopPreviewInline) { + return ( +
+ +
+ {title} +
+
+ {view === "list" ? ( + + ) : ( + + )} + +
+
+ +
{body}
+
+ ); + } + + return ( (view === "create" ? branchInputRef.current : null)} diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index 08946b130..110bb4905 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -489,6 +489,10 @@ "general": "General", "appearance": "Appearance", "providers": "Agents", + "mobile_groups": { + "workspace_runtime": "Workspace & Runtime", + "interface_interaction": "Interface & Interaction" + }, "autosave_hint": "Settings saved automatically", "load_failed": "Failed to load settings", "load_failed_unknown": "Server did not return settings data", @@ -598,6 +602,7 @@ }, "shortcuts": { "title": "Keyboard Shortcuts", + "hint": "Review and adjust common keyboard actions", "reset_all": "Reset All", "capture_hint": "Press new shortcut combination", "reset_hint": "Reset to default" @@ -676,8 +681,7 @@ "stop_reason": { "objective_complete": "Objective complete. Supervisor stopped automatically.", "max_supervision_count_reached": "Reached the configured max supervision count.", - "supervisor_uncertain": "Supervisor could not determine the next step and stopped automatic supervision.", - "needs_user_input": "Supervisor needs more user input before automatic supervision can continue." + "supervisor_uncertain": "Supervisor could not determine the next step and stopped automatic supervision." }, "target_memory": { "title": "Target memory", diff --git a/packages/web/src/locales/zh.json b/packages/web/src/locales/zh.json index ae38f9220..f28060e86 100644 --- a/packages/web/src/locales/zh.json +++ b/packages/web/src/locales/zh.json @@ -489,6 +489,10 @@ "general": "通用", "appearance": "外观", "providers": "Agents", + "mobile_groups": { + "workspace_runtime": "工作区与运行", + "interface_interaction": "界面与交互" + }, "autosave_hint": "设置已自动保存", "load_failed": "设置加载失败", "load_failed_unknown": "未收到服务端返回的设置数据", @@ -598,6 +602,7 @@ }, "shortcuts": { "title": "快捷键", + "hint": "查看并调整常用键盘操作", "reset_all": "重置全部", "capture_hint": "按下新的快捷键组合", "reset_hint": "重置为默认" @@ -676,8 +681,7 @@ "stop_reason": { "objective_complete": "目标已达成,Supervisor 已自动停止。", "max_supervision_count_reached": "已达到配置的最大监督次数。", - "supervisor_uncertain": "Supervisor 暂时无法判断下一步,已停止自动监督。", - "needs_user_input": "Supervisor 需要更多用户输入后才能继续自动监督。" + "supervisor_uncertain": "Supervisor 暂时无法判断下一步,已停止自动监督。" }, "target_memory": { "title": "目标记忆", diff --git a/packages/web/src/shells/mobile-shell/index.test.tsx b/packages/web/src/shells/mobile-shell/index.test.tsx index fb6aa513d..ffc1262dd 100644 --- a/packages/web/src/shells/mobile-shell/index.test.tsx +++ b/packages/web/src/shells/mobile-shell/index.test.tsx @@ -2789,7 +2789,16 @@ describe("MobileShell Phase 2 workspace", () => { expect(screen.getByTestId("mobile-agent-empty")).toBeInTheDocument(); }); expect(screen.queryByTestId("agent-panes-empty-mock")).not.toBeInTheDocument(); + expect(document.querySelector(".mobile-shell__empty-kicker")).toBeNull(); + expect(document.querySelector(".mobile-shell__empty-panel")).toBeNull(); + expect(document.querySelector(".mobile-shell__empty-content")).toHaveClass( + "mobile-shell__empty-content--flat" + ); + expect(document.querySelector(".mobile-shell__placeholder-pills")).toBeNull(); expect(screen.getByRole("button", { name: "Create Session" })).toBeInTheDocument(); + expect( + document.querySelector(".mobile-shell__bottom-stack .workspace-status-bar") + ).not.toBeNull(); await user.click(screen.getByRole("button", { name: "Create Session" })); @@ -2826,6 +2835,7 @@ describe("MobileShell Phase 2 workspace", () => { expect(screen.getByRole("button", { name: "打开终端面板" })).toBeInTheDocument(); expect(screen.getByText("为当前工作区启动一个新的 Agent 会话。")).toBeInTheDocument(); expect(screen.getByText("文件和终端可继续通过底部栏访问。")).toBeInTheDocument(); + expect(document.querySelector(".mobile-shell__empty-kicker")).toBeNull(); await user.click(screen.getByRole("button", { name: "切换工作区" })); diff --git a/packages/web/src/styles/base.css b/packages/web/src/styles/base.css index 09fc038b8..1e119dc92 100644 --- a/packages/web/src/styles/base.css +++ b/packages/web/src/styles/base.css @@ -51,7 +51,7 @@ h1 { font-size: var(--text-3xl); } h2 { - font-size: var(--2xl); + font-size: var(--text-2xl); } h3 { font-size: var(--text-xl); @@ -71,6 +71,47 @@ p { line-height: var(--leading-normal); } +.page-kicker, +.section-kicker { + font-size: var(--text-xs); + font-weight: var(--font-semibold); + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-tertiary); +} + +.page-title { + font-size: var(--text-3xl); + font-weight: var(--font-semibold); + line-height: var(--leading-tight); + color: var(--text-primary); +} + +.section-title { + font-size: var(--text-base); + font-weight: var(--font-semibold); + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-primary); +} + +.meta-text { + font-size: var(--text-sm); + color: var(--text-secondary); +} + +.hint-text { + font-size: var(--text-xs); + line-height: 1.45; + color: var(--text-muted); +} + +.mono-meta { + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--text-muted); +} + code, pre, kbd, @@ -393,6 +434,11 @@ textarea::placeholder { border-radius: var(--radius-md); } +.themed-icon--surface-info { + background: var(--icon-surface-info); + border-radius: var(--radius-md); +} + /* Spacing utilities (using 4px grid) */ .p-1 { padding: var(--sp-1); diff --git a/packages/web/src/styles/base.theme.test.ts b/packages/web/src/styles/base.theme.test.ts index 497d97448..01ff6c57d 100644 --- a/packages/web/src/styles/base.theme.test.ts +++ b/packages/web/src/styles/base.theme.test.ts @@ -3,8 +3,10 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; const stylesheet = readFileSync(`${process.cwd()}/src/styles/base.css`, "utf8"); +const tokensStylesheet = readFileSync(`${process.cwd()}/src/styles/tokens.css`, "utf8"); function getRuleBlock(selector: string) { + const blocks: string[] = []; const matcher = /([^{}]+)\{([^}]*)\}/g; let match: RegExpExecArray | null = null; const normalizedSelector = selector.replace(/\s+/g, " ").trim(); @@ -16,11 +18,15 @@ function getRuleBlock(selector: string) { .map((entry) => entry.replace(/\s+/g, " ").trim()); if (selectors.includes(normalizedSelector)) { - return match[2]; + blocks.push(match[2]); } } - throw new Error(`expected CSS rule for ${selector}`); + if (blocks.length === 0) { + throw new Error(`expected CSS rule for ${selector}`); + } + + return blocks.at(-1) ?? ""; } describe("base.css theme-sensitive shells", () => { @@ -54,3 +60,29 @@ describe("base.css theme-sensitive shells", () => { expect(themedSurface).toContain("background: var(--icon-surface-info)"); }); }); + +describe("base.css desktop typography foundation", () => { + it("defines the semantic and desktop layout tokens used by polished PC surfaces", () => { + expect(tokensStylesheet).toContain("--text-muted:"); + expect(tokensStylesheet).toContain("--bg-panel:"); + expect(tokensStylesheet).toContain("--bg-elevated:"); + expect(tokensStylesheet).toContain("--accent-red:"); + expect(tokensStylesheet).toContain("--desktop-topbar-height:"); + expect(tokensStylesheet).toContain("--desktop-statusbar-height:"); + expect(tokensStylesheet).toContain("--desktop-sidebar-header-height:"); + expect(tokensStylesheet).toContain("--desktop-panel-padding:"); + expect(tokensStylesheet).toContain("--desktop-panel-gap:"); + expect(tokensStylesheet).toContain("--desktop-content-max-width:"); + expect(tokensStylesheet).toContain("--desktop-modal-max-width-md:"); + expect(tokensStylesheet).toContain("--desktop-modal-max-width-lg:"); + }); + + it("maps desktop heading helpers to the corrected typography tokens", () => { + expect(getRuleBlock("h2")).toContain("font-size: var(--text-2xl)"); + expect(getRuleBlock(".page-kicker")).toContain("font-size: var(--text-xs)"); + expect(getRuleBlock(".page-title")).toContain("font-size: var(--text-3xl)"); + expect(getRuleBlock(".section-title")).toContain("font-size: var(--text-base)"); + expect(getRuleBlock(".hint-text")).toContain("color: var(--text-muted)"); + expect(getRuleBlock(".mono-meta")).toContain("font-family: var(--font-mono)"); + }); +}); diff --git a/packages/web/src/styles/components.css b/packages/web/src/styles/components.css index f9885e605..4b78fd525 100644 --- a/packages/web/src/styles/components.css +++ b/packages/web/src/styles/components.css @@ -19,8 +19,8 @@ .command-palette { width: 100%; - max-width: 660px; - background: var(--bg-surface); + max-width: var(--desktop-modal-max-width-md); + background: var(--bg-elevated); border: 1px solid var(--border); border-radius: var(--radius-xl); box-shadow: var(--shadow-xl); @@ -28,6 +28,10 @@ animation: slideInUp var(--duration-normal) var(--ease-out); } +.command-palette--desktop { + overflow: hidden; +} + .command-palette-header { display: flex; align-items: center; @@ -95,6 +99,12 @@ transition: background var(--duration-fast) var(--ease-out); } +.command-palette--desktop .command-palette-item { + align-items: center; + gap: var(--sp-3); + padding: var(--sp-3) var(--sp-4); +} + .command-palette-item:hover, .command-palette-item-selected { background: var(--bg-hover); @@ -106,6 +116,11 @@ gap: var(--sp-1); } +.command-palette--desktop .command-palette-item-content { + flex: 1; + min-width: 0; +} + .command-palette-item-label { font-size: var(--text-base); color: var(--text-primary); @@ -142,7 +157,12 @@ position: sticky; top: 0; z-index: 1; - background: var(--bg-surface); + padding-top: var(--sp-3); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-surface) 98%, transparent) 0%, + color-mix(in srgb, var(--bg-page) 96%, var(--bg-surface) 4%) 100% + ); } .command-palette-sheet__meta { @@ -151,7 +171,8 @@ justify-content: space-between; gap: var(--sp-3); padding: var(--sp-2) var(--sp-4); - border-bottom: 1px solid var(--border); + border-bottom: 1px solid color-mix(in srgb, var(--border) 82%, transparent); + background: color-mix(in srgb, var(--bg-surface) 66%, transparent); } .command-palette-sheet-shell { @@ -166,6 +187,19 @@ min-height: min(82dvh, 700px); } +@media (prefers-reduced-motion: reduce) { + .command-palette-overlay, + .command-palette, + .mobile-sheet-layer__backdrop, + .mobile-drawer-layer__backdrop, + .mobile-sheet, + .mobile-workspace-drawer { + animation: none !important; + opacity: 1 !important; + transform: none !important; + } +} + .mobile-select-sheet { display: flex; min-height: 0; @@ -679,11 +713,41 @@ } .settings-header { - padding: var(--sp-2) var(--sp-4); + padding: var(--sp-1) var(--sp-4); background: var(--bg-surface); border-bottom: 1px solid var(--border); } +.settings-header__desktop { + width: 100%; + margin: 0; + display: flex; + align-items: center; + justify-content: flex-start; + gap: var(--sp-3); +} + +.settings-header__back { + flex-shrink: 0; + min-height: 30px; + font-size: var(--text-xs); +} + +.settings-header__copy { + min-width: 0; + display: flex; + flex: 0 1 auto; + flex-direction: column; + justify-content: flex-start; + gap: 0; +} + +.settings-header__title { + margin: 0; + font-size: 18px; + line-height: 1.05; +} + .settings-body { display: flex; flex: 1; @@ -693,10 +757,10 @@ } .settings-sidebar { - width: 216px; - background: var(--bg-sidebar); + width: 240px; + background: var(--bg-panel); border-right: 1px solid var(--border); - padding: var(--sp-3); + padding: var(--sp-4); } .settings-nav { @@ -726,7 +790,8 @@ .settings-nav-item-active { background: var(--bg-active); color: var(--text-primary); - border-color: var(--border-focus); + border-color: color-mix(in srgb, var(--accent-blue) 42%, var(--border) 58%); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-blue) 18%, transparent); } .settings-nav-icon { @@ -744,9 +809,11 @@ } .settings-content { + display: flex; flex: 1; min-height: 0; - padding: var(--sp-5) var(--sp-6) var(--sp-6); + justify-content: center; + padding: var(--sp-6); overflow-y: auto; background: var(--bg-page); } @@ -754,6 +821,26 @@ .settings-content--fill-height { display: flex; flex-direction: column; + align-items: center; + justify-content: flex-start; +} + +.settings-content--fill-height > .settings-content-surface { + display: flex; + flex: 1; + min-height: 0; + flex-direction: column; +} + +.settings-content-surface { + width: min(100%, var(--desktop-content-max-width)); + min-width: 0; + min-height: 100%; + padding: var(--sp-6); + background: var(--bg-elevated); + border: 1px solid color-mix(in srgb, var(--border) 84%, transparent); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); } .settings-section { @@ -1132,14 +1219,24 @@ .settings-footer { display: flex; align-items: center; - justify-content: space-between; + justify-content: center; gap: var(--sp-3); min-height: 32px; - padding: var(--sp-2) var(--sp-4); + padding: var(--sp-3) var(--sp-6); background: var(--bg-surface); border-top: 1px solid var(--border); } +.settings-footer__meta { + width: min(100%, calc(240px + var(--desktop-content-max-width) + var(--sp-6))); + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--sp-3); + color: var(--text-muted); +} + .settings-autosave { font-size: var(--text-xs); color: var(--text-tertiary); @@ -1347,19 +1444,18 @@ } .xterm-host-shell--mobile-input { - gap: var(--sp-1); + gap: 0; } .mobile-terminal-input-bar { display: flex; align-items: center; - gap: 4px; + gap: 0; flex-shrink: 0; min-width: 0; - padding: 2px var(--sp-2) 2px; - border-top: 1px solid color-mix(in srgb, var(--border) 36%, transparent); - border-bottom: 1px solid color-mix(in srgb, var(--border) 72%, transparent); - background: color-mix(in srgb, var(--bg-page) 90%, var(--bg-surface) 10%); + padding: 4px var(--sp-2) 3px; + border-top: 1px solid color-mix(in srgb, var(--border) 68%, transparent); + background: color-mix(in srgb, var(--bg-page) 92%, var(--bg-terminal) 8%); } .mobile-terminal-input-bar__actions { @@ -1367,8 +1463,9 @@ flex: 0 0 auto; align-items: center; gap: 3px; - padding-right: 4px; - border-right: 1px solid color-mix(in srgb, var(--border) 50%, transparent); + margin-right: var(--sp-1); + padding-right: var(--sp-1); + border-right: 1px solid color-mix(in srgb, var(--border) 56%, transparent); } .mobile-terminal-input-bar__action { @@ -1378,12 +1475,12 @@ flex: 0 0 auto; min-height: 20px; padding: 0 6px; - border: 1px solid color-mix(in srgb, var(--border) 74%, transparent); - border-radius: 3px; - background: color-mix(in srgb, var(--bg-page) 70%, var(--bg-surface) 30%); + border: 1px solid color-mix(in srgb, var(--border) 70%, transparent); + border-radius: var(--radius-sm); + background: color-mix(in srgb, var(--bg-page) 78%, var(--bg-surface) 22%); color: var(--text-secondary); font-family: var(--font-mono); - font-size: 10px; + font-size: 9px; font-weight: var(--font-medium); line-height: 1; letter-spacing: 0.02em; @@ -1396,7 +1493,7 @@ min-width: 0; flex: 1 1 auto; justify-content: flex-start; - gap: 2px; + gap: 3px; overflow-x: auto; padding: 0; border: none; @@ -1418,12 +1515,12 @@ min-width: 0; min-height: 20px; padding: 0 5px; - border: 1px solid color-mix(in srgb, var(--border) 74%, transparent); - border-radius: 3px; - background: color-mix(in srgb, var(--bg-page) 74%, var(--bg-surface) 26%); + border: 1px solid color-mix(in srgb, var(--border) 68%, transparent); + border-radius: var(--radius-sm); + background: color-mix(in srgb, var(--bg-page) 80%, var(--bg-surface) 20%); color: var(--text-secondary); font-family: var(--font-mono); - font-size: 10px; + font-size: 9px; font-weight: var(--font-medium); line-height: 1; letter-spacing: 0.02em; @@ -1434,18 +1531,18 @@ .mobile-terminal-input-bar__shift, .mobile-terminal-input-bar__key[aria-label="Escape"], .mobile-terminal-input-bar__key[aria-label="Tab"] { - min-width: 34px; + min-width: 28px; } .mobile-terminal-input-bar__key[aria-label="Enter"] { - min-width: 40px; + min-width: 34px; } .mobile-terminal-input-bar__key[aria-label="Up arrow"], .mobile-terminal-input-bar__key[aria-label="Left arrow"], .mobile-terminal-input-bar__key[aria-label="Down arrow"], .mobile-terminal-input-bar__key[aria-label="Right arrow"] { - min-width: 24px; + min-width: 20px; padding: 0; } @@ -2146,23 +2243,53 @@ body.is-resizing-panels * { align-items: center; justify-content: center; min-height: 100vh; - background: var(--bg); + padding: var(--sp-10); + background: + radial-gradient( + circle at top, + color-mix(in srgb, var(--accent-green) 10%, transparent), + transparent 36% + ), + radial-gradient( + circle at bottom right, + color-mix(in srgb, var(--accent-blue) 12%, transparent), + transparent 32% + ), + var(--bg-page); } .welcome-card { - max-width: 560px; + width: min(100%, 760px); + max-width: 760px; text-align: center; - padding: var(--sp-12) var(--sp-10); + padding: var(--sp-10); display: flex; flex-direction: column; align-items: center; - gap: var(--sp-5); - background: var(--bg-surface); + gap: var(--sp-6); + background: var(--bg-elevated); border: 1px solid var(--border); border-radius: var(--radius-xl); box-shadow: var(--shadow-xl); } +.welcome-card__hero, +.welcome-card__actions, +.welcome-card__panel { + width: 100%; +} + +.welcome-card__actions { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--sp-2); +} + +.welcome-card__panel { + padding-top: var(--sp-2); +} + .welcome-kicker { font-size: var(--text-xs); text-transform: uppercase; @@ -2188,6 +2315,7 @@ body.is-resizing-panels * { .welcome-btn { display: flex; align-items: center; + justify-content: center; gap: var(--sp-3); padding: var(--sp-3) var(--sp-8); border-radius: var(--radius-md); @@ -2200,7 +2328,7 @@ body.is-resizing-panels * { font-family: var(--sans); transition: filter var(--duration-fast) var(--ease-out); min-height: var(--btn-height-lg); - margin-top: var(--sp-2); + width: min(100%, 320px); } .welcome-btn:hover { @@ -2210,8 +2338,8 @@ body.is-resizing-panels * { .welcome-link { display: flex; align-items: center; + justify-content: center; gap: var(--sp-2); - margin-top: var(--sp-1); font-size: var(--text-sm); color: var(--text-ter); background: none; @@ -2236,23 +2364,22 @@ body.is-resizing-panels * { .welcome-features { display: grid; grid-template-columns: repeat(3, 1fr); - gap: var(--sp-4); + gap: var(--desktop-panel-gap); width: 100%; - margin-top: var(--sp-4); } .welcome-feature { display: flex; - align-items: center; + align-items: flex-start; gap: var(--sp-3); - padding: var(--sp-4) var(--sp-3); + padding: var(--sp-4); border-radius: var(--radius-md); - background: var(--bg-surface); - min-height: 110px; - justify-content: center; + background: var(--bg-panel); + min-height: 148px; + justify-content: flex-start; flex-direction: column; border: 1px solid var(--border); - text-align: center; + text-align: left; } .welcome-feature-icon { @@ -2314,6 +2441,13 @@ body.is-resizing-panels * { box-shadow: var(--shadow-xl); } +.auth-card-shell__content { + width: 100%; + display: flex; + flex-direction: column; + gap: var(--sp-4); +} + .auth-card-shell .welcome-kicker, .auth-card-shell .welcome-title, .auth-card-shell .welcome-body { @@ -2581,7 +2715,7 @@ body.is-resizing-panels * { /* ========== TopBar ========== */ .app-topbar { - height: 36px; + min-height: var(--desktop-topbar-height); display: flex; align-items: center; padding: 0 8px; @@ -4245,10 +4379,10 @@ body.is-resizing-panels * { } .launch-modal { - width: 760px; - max-width: 90vw; + width: min(var(--desktop-modal-max-width-lg), 90vw); + max-width: min(var(--desktop-modal-max-width-lg), 90vw); max-height: 85vh; - background: var(--bg-surface); + background: var(--bg-elevated); border: 1px solid var(--border); border-radius: var(--radius-xl); box-shadow: var(--shadow-lg); @@ -6015,7 +6149,15 @@ textarea.input { linear-gradient(180deg, rgba(255, 255, 255, 0.015), transparent 18%), var(--bg-page); } -.workspace-main-area > .agent-panes { +.workspace-main-stage { + flex: 1; + min-height: 0; + min-width: 0; + display: flex; + flex-direction: column; +} + +.workspace-main-stage > .agent-panes { padding: 14px 14px 0; background: transparent; } @@ -6073,13 +6215,7 @@ textarea.input { border-radius: 6px; } -.workspace-main-area > .bottom-terminal { - background: transparent; - border-top: none; - padding: 0 14px 14px; -} - -.workspace-main-area > .bottom-terminal > .bottom-terminal { +.workspace-bottom-panel > .bottom-terminal { border: 1px solid color-mix(in srgb, var(--border-light) 64%, var(--accent-blue) 36%); border-radius: 14px; box-shadow: var(--shadow-lg); @@ -6685,7 +6821,7 @@ textarea.input { background: var(--bg-page); } -.workspace-main-area > .agent-panes { +.workspace-main-stage > .agent-panes { flex: 1; min-height: 0; padding: 0; @@ -6970,16 +7106,21 @@ textarea.input { flex-direction: column; flex-shrink: 0; min-height: 0; - overflow: hidden; - background: var(--bg-surface); - border-top: 1px solid var(--border); + overflow: visible; + padding: 0 14px 14px; + background: transparent; + border-top: none; } .workspace-bottom-panel > .bottom-terminal { flex: 1; min-height: 0; border-top: none; - background: var(--bg-surface); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-terminal) 96%, white 4%), + color-mix(in srgb, var(--bg-terminal) 88%, var(--bg-page) 12%) + ); } .terminal-toolbar { @@ -7365,6 +7506,8 @@ textarea.input { } .launch-modal { + width: min(var(--desktop-modal-max-width-lg), 90vw); + max-width: min(var(--desktop-modal-max-width-lg), 90vw); background: linear-gradient( 180deg, color-mix(in srgb, var(--bg-surface) 97%, white 3%), @@ -7375,6 +7518,7 @@ textarea.input { } .app-topbar { + min-height: var(--desktop-topbar-height); background: color-mix(in srgb, var(--bg-surface) 95%, var(--accent-blue) 5%); border-bottom-color: color-mix(in srgb, var(--border) 80%, var(--accent-blue) 20%); } @@ -7405,6 +7549,97 @@ textarea.input { box-shadow: var(--shadow-lg); } +.desktop-review-shell { + display: flex; + align-items: flex-start; + justify-content: center; + padding: var(--sp-8); +} + +.desktop-review-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--desktop-panel-gap); + align-items: start; + padding: var(--sp-8); +} + +.desktop-review-card { + position: relative; + min-height: 220px; + padding: var(--desktop-panel-padding); + border: 1px solid color-mix(in srgb, var(--border) 78%, transparent); + border-radius: var(--radius-xl); + background: var(--bg-panel); + box-shadow: var(--shadow-lg); + overflow: hidden; +} + +.desktop-review-card--topbar { + width: min(100%, 1080px); + min-height: 0; + padding: 0; +} + +.desktop-review-card--sidebar { + width: min(100%, 360px); + min-height: 520px; + padding: 0; +} + +.desktop-review-card--editor, +.desktop-review-card--diff { + width: min(100%, 1080px); + min-height: 520px; + padding: 0; +} + +.desktop-review-card--terminal { + width: min(100%, 880px); + min-height: 320px; + padding: 0; +} + +.desktop-review-card--statusbar { + width: min(100%, 1080px); + min-height: 0; + padding: 0; +} + +.desktop-review-card--worktree { + min-height: 540px; + padding: 0; +} + +.desktop-review-card .command-palette-overlay, +.desktop-review-card .launch-overlay { + position: static; + inset: auto; + min-height: 100%; + padding-top: 0; + background: transparent; + backdrop-filter: none; +} + +.desktop-review-card .command-palette, +.desktop-review-card .launch-modal { + margin: 0 auto; +} + +.desktop-review-embedded-worktree .modal-overlay { + position: static; + inset: auto; + display: block; + background: transparent; +} + +.desktop-review-embedded-worktree .modal-card.worktree-manager-surface { + width: 100%; + max-width: none; + min-height: 540px; + box-shadow: none; +} + .workspace-resolving-preview { background: linear-gradient( 180deg, @@ -7704,10 +7939,21 @@ textarea.input { } .config-card-body--fill-height { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; +} + +.config-card-body--fill-height > .monaco-host { flex: 1; min-height: 0; } +.config-card-body--fill-height > .config-card-actions { + flex: 0 0 auto; +} + /* Monaco Editor */ .config-card-body .monaco-host { min-height: 200px; @@ -7998,8 +8244,12 @@ textarea.input { gap: var(--sp-3); padding: calc(var(--mobile-safe-top) + var(--sp-1)) calc(var(--mobile-safe-right) + var(--sp-4)) var(--sp-1) calc(var(--mobile-safe-left) + var(--sp-4)); - background: var(--bg-surface); - border-bottom: 1px solid var(--border); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-surface) 98%, transparent) 0%, + color-mix(in srgb, var(--bg-page) 96%, var(--bg-surface) 4%) 100% + ); + border-bottom: 1px solid color-mix(in srgb, var(--border) 84%, transparent); } .mobile-topbar__workspace-button, @@ -8018,18 +8268,19 @@ textarea.input { gap: var(--sp-2); min-width: 0; width: 100%; - min-height: 32px; - height: 32px; - padding: 0 var(--sp-2); - border: 1px solid transparent; - border-radius: var(--radius-md); - background: transparent; - color: var(--text-primary); - } - + min-height: 36px; + height: 36px; + padding: 0 var(--sp-3); + border: none; + border-bottom: 1px solid color-mix(in srgb, var(--border) 62%, transparent); + border-radius: 0; + background: color-mix(in srgb, var(--bg-surface) 24%, transparent); + color: var(--text-primary); + } + .mobile-topbar__workspace-leading { flex-shrink: 0; - color: var(--text-tertiary); + color: color-mix(in srgb, var(--text-secondary) 88%, var(--accent-blue) 12%); } .mobile-topbar__workspace-name { @@ -8039,8 +8290,8 @@ textarea.input { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-size: var(--text-sm); - font-weight: var(--font-medium); + font-size: 13px; + font-weight: var(--font-semibold); line-height: 1.2; } @@ -8124,7 +8375,7 @@ textarea.input { position: relative; display: inline-flex; align-items: center; - gap: var(--sp-1); + gap: var(--sp-2); } .mobile-shell__recovery-strip { @@ -8163,14 +8414,14 @@ textarea.input { display: inline-flex; align-items: center; justify-content: center; - width: 32px; - min-width: 32px; - min-height: 32px; - height: 32px; + width: 36px; + min-width: 36px; + min-height: 36px; + height: 36px; padding: 0; - border: 1px solid transparent; - border-radius: var(--radius-md); - background: transparent; + border: 1px solid color-mix(in srgb, var(--border) 66%, transparent); + border-radius: 10px; + background: color-mix(in srgb, var(--bg-surface) 42%, transparent); color: var(--text-primary); transition: background-color var(--mobile-shell-motion-surface-duration) var(--ease-out), @@ -8190,7 +8441,7 @@ textarea.input { gap: var(--sp-3); padding: var(--sp-2) var(--sp-2) var(--sp-3); border: 1px solid color-mix(in srgb, var(--border) 88%, transparent); - border-radius: 12px; + border-radius: var(--radius-xl); background: linear-gradient( 180deg, color-mix(in srgb, var(--bg-surface) 98%, transparent) 0%, @@ -8257,7 +8508,7 @@ textarea.input { min-height: 44px; padding: 0 var(--sp-3); border: 1px solid color-mix(in srgb, var(--border) 86%, transparent); - border-radius: 14px; + border-radius: var(--radius-md); background: color-mix(in srgb, var(--bg-surface) 90%, transparent); color: var(--text-primary); text-align: left; @@ -8342,14 +8593,15 @@ textarea.input { flex-direction: column; min-height: 0; overflow: hidden; - padding: var(--sp-1) var(--mobile-safe-right) 0 var(--mobile-safe-left); + padding: 0; + border-top: 1px solid color-mix(in srgb, var(--border) 42%, transparent); } .mobile-shell__content { display: flex; flex-direction: column; flex: 1; - gap: var(--sp-2); + gap: 4px; min-height: 0; overflow: hidden; } @@ -8380,6 +8632,10 @@ textarea.input { min-width: 0; } + .mobile-shell__agent-stage .session-progress { + display: none; + } + .mobile-shell__agent-stage .xterm-host-shell, .mobile-shell__agent-stage .xterm-host { flex: 1 1 auto; @@ -8391,7 +8647,7 @@ textarea.input { flex-direction: column; align-items: stretch; justify-content: flex-start; - padding: clamp(56px, 14vh, 116px) var(--sp-4) var(--sp-3); + padding: clamp(34px, 9vh, 72px) var(--sp-4) var(--sp-3); border: none; background: transparent; box-shadow: none; @@ -8400,26 +8656,20 @@ textarea.input { .mobile-shell__empty-content { position: relative; display: flex; - width: min(100%, 344px); + width: min(100%, 320px); flex-direction: column; align-self: flex-start; - padding: var(--sp-2) 0 0 var(--sp-4); - border-top: none; + gap: 0; + padding: 0; + border: none; + border-radius: 0; background: transparent; box-shadow: none; + overflow: visible; } - .mobile-shell__empty-content::before { - content: ""; - position: absolute; - inset: 0 auto 0 0; - width: 1px; - background: linear-gradient( - 180deg, - color-mix(in srgb, var(--accent-blue) 18%, var(--border)) 0%, - color-mix(in srgb, var(--border) 58%, transparent) 34%, - transparent 100% - ); + .mobile-shell__empty-content--flat { + width: min(100%, 320px); } .mobile-shell__empty-state { @@ -8430,42 +8680,24 @@ textarea.input { min-height: 0; padding: 0; text-align: left; + display: flex; + flex-direction: column; } .mobile-shell__empty-heading { display: flex; width: 100%; flex-direction: column; - gap: var(--sp-2); - } - - .mobile-shell__empty-kicker { - display: inline-flex; - align-items: center; - gap: 6px; - font-size: 10px; - line-height: 1.1; - color: var(--text-tertiary); - font-family: var(--font-mono); - text-transform: uppercase; - letter-spacing: 0.08em; - } - - .mobile-shell__empty-kicker::before { - content: ""; - width: 6px; - height: 6px; - border: 1px solid color-mix(in srgb, var(--accent-blue) 38%, var(--border)); - border-radius: var(--radius-full); - background: color-mix(in srgb, var(--accent-blue) 22%, transparent); + gap: 0; } .mobile-shell__empty-title { margin: 0; color: var(--text-primary); - font-size: var(--text-base); + font-size: clamp(24px, 6.4vw, 32px); font-weight: var(--font-semibold); - line-height: 1.42; + line-height: 1.08; + letter-spacing: -0.035em; } .mobile-shell__placeholder-copy { @@ -8473,38 +8705,38 @@ textarea.input { flex-direction: column; align-items: flex-start; gap: var(--sp-2); - max-width: 31ch; - color: var(--text-tertiary); + max-width: 18ch; + color: var(--text-secondary); text-align: left; } .mobile-shell__placeholder-copy p { margin: 0; - font-size: var(--text-sm); - line-height: 1.55; + font-size: 14px; + line-height: 1.62; } .mobile-shell__empty-action-row { display: flex; width: 100%; justify-content: flex-start; - margin-top: var(--sp-1); + margin-top: 0; } .mobile-shell__empty-cta { display: inline-flex; align-items: center; justify-content: center; - min-height: 36px; + min-height: 38px; width: auto; - min-width: 132px; - padding: 0 var(--sp-3); - border: 1px solid color-mix(in srgb, var(--accent-blue) 12%, var(--border)); - border-radius: 8px; - background: color-mix(in srgb, var(--bg-surface) 56%, transparent); + min-width: 136px; + padding: 0 var(--sp-4); + border: 1px solid color-mix(in srgb, var(--accent-blue) 34%, var(--border)); + border-radius: 12px; + background: color-mix(in srgb, var(--accent-blue) 8%, var(--bg-surface)); color: var(--text-primary); - font-size: var(--text-xs); - font-weight: var(--font-medium); + font-size: 13px; + font-weight: var(--font-semibold); transition: background-color var(--mobile-shell-motion-surface-duration) var(--ease-out), border-color var(--mobile-shell-motion-surface-duration) var(--ease-out), @@ -8521,14 +8753,18 @@ textarea.input { flex-direction: column; gap: 0; padding: 0 0 var(--mobile-keyboard-inset) 0; - border-top: 1px solid var(--border); - background: var(--bg-surface); + border-top: 1px solid color-mix(in srgb, var(--border) 84%, transparent); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-surface) 98%, transparent) 0%, + color-mix(in srgb, var(--bg-page) 94%, var(--bg-surface) 6%) 100% + ); transition: padding-bottom var(--mobile-shell-motion-enter-duration) var(--ease-out); } .mobile-dock-shell { position: relative; - padding: 2px calc(var(--mobile-safe-right) + var(--sp-4)) 0 + padding: 3px calc(var(--mobile-safe-right) + var(--sp-4)) 0 calc(var(--mobile-safe-left) + var(--sp-4)); } @@ -8585,8 +8821,11 @@ textarea.input { } .mobile-shell__agent-stage .session-header { - padding: var(--sp-2) calc(var(--mobile-safe-right) + var(--sp-3)) var(--sp-2) + padding: 4px calc(var(--mobile-safe-right) + var(--sp-3)) 4px calc(var(--mobile-safe-left) + var(--sp-3)); + gap: var(--sp-2); + background: color-mix(in srgb, var(--bg-page) 96%, var(--bg-surface) 4%); + border-bottom: 1px solid color-mix(in srgb, var(--border) 74%, transparent); } .mobile-shell__agent-stage .session-header-left, @@ -8595,8 +8834,8 @@ textarea.input { min-width: 0; } - .mobile-shell__agent-stage .session-header { - gap: var(--sp-2); + .mobile-shell__agent-stage .session-header-left { + gap: 6px; } .mobile-shell__agent-stage .session-header-right { @@ -8607,7 +8846,7 @@ textarea.input { .mobile-shell__agent-stage .session-title-row { display: flex; align-items: center; - gap: var(--sp-2); + gap: 6px; min-width: 0; flex-wrap: nowrap; } @@ -8622,28 +8861,34 @@ textarea.input { .mobile-shell__agent-stage .session-provider-badge, .mobile-shell__agent-stage .session-state-badge { + height: 15px; + padding: 0 5px; + border-radius: 3px; + font-size: 10px; flex-shrink: 0; max-width: 100%; } .mobile-shell__agent-stage .mobile-supervisor-badge { - min-height: 32px; + min-height: 26px; max-width: 100%; - padding: 0 var(--sp-2); - border-radius: var(--radius-lg); + padding: 0 8px; + border-radius: 4px; + border-color: color-mix(in srgb, var(--border) 78%, var(--accent-blue) 22%); + background: color-mix(in srgb, var(--accent-blue) 6%, var(--bg-page) 94%); } .mobile-shell__agent-stage .mobile-supervisor-badge__label { max-width: 100%; - font-size: var(--text-xs); + font-size: 11px; } .mobile-dock { position: static; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 0; - padding: 0; + gap: var(--sp-2); + padding: 0 0 var(--sp-1); background: transparent; border-top: none; border-bottom: none; @@ -8655,11 +8900,12 @@ textarea.input { flex-direction: column; align-items: center; justify-content: center; - min-height: 34px; - gap: 1px; - padding: 3px var(--sp-1) 5px; + min-height: 30px; + gap: 4px; + padding: 2px var(--sp-2) 2px; border: none; - border-radius: 6px; + border-top: 1px solid transparent; + border-radius: 0; background: transparent; color: var(--text-tertiary); transition: @@ -8671,21 +8917,13 @@ textarea.input { } .mobile-dock__item--active { - border-color: transparent; + border-top-color: color-mix(in srgb, var(--accent-blue) 72%, transparent); background: transparent; color: var(--text-primary); } .mobile-dock__item--active::before { - content: ""; - position: absolute; - top: -1px; - left: 50%; - width: 14px; - height: 2px; - border-radius: var(--radius-full); - background: color-mix(in srgb, var(--accent-blue) 62%, transparent); - transform: translateX(-50%); + content: none; } .mobile-dock__icon { @@ -8696,8 +8934,8 @@ textarea.input { } .mobile-dock__icon svg { - width: 16px; - height: 16px; + width: 17px; + height: 17px; } .mobile-dock__label { @@ -8736,8 +8974,8 @@ textarea.input { calc(var(--sp-5) + var(--mobile-safe-bottom)) calc(var(--mobile-safe-left) + var(--sp-4)); border: 1px solid color-mix(in srgb, var(--border) 86%, transparent); border-bottom: none; - border-top-left-radius: 16px; - border-top-right-radius: 16px; + border-top-left-radius: var(--radius-xl); + border-top-right-radius: var(--radius-xl); background: var(--bg-surface); box-shadow: none; transform: translateY(18px); @@ -8781,8 +9019,8 @@ textarea.input { max-height: calc(100dvh - var(--sp-4)); border: 1px solid color-mix(in srgb, var(--border) 86%, transparent); border-bottom: none; - border-top-left-radius: 12px; - border-top-right-radius: 12px; + border-top-left-radius: var(--radius-xl); + border-top-right-radius: var(--radius-xl); box-shadow: none; } @@ -8923,7 +9161,7 @@ textarea.input { min-height: 0; flex-direction: column; padding: var(--sp-3); - border-radius: 16px; + border-radius: var(--radius-xl); background: color-mix(in srgb, var(--bg-surface) 92%, var(--bg-page)); box-shadow: inset 0 1px 0 color-mix(in srgb, var(--fg-white) 8%, transparent), @@ -8968,7 +9206,7 @@ textarea.input { width: 100%; padding: var(--sp-2); border: 1px solid color-mix(in srgb, var(--border) 86%, transparent); - border-radius: 16px; + border-radius: var(--radius-xl); background: color-mix(in srgb, var(--bg-surface) 96%, var(--bg-page)); box-shadow: inset 0 1px 0 color-mix(in srgb, var(--fg-white) 6%, transparent); } @@ -8981,7 +9219,7 @@ textarea.input { width: 100%; min-height: 44px; border: 1px solid color-mix(in srgb, var(--accent-blue) 52%, transparent); - border-radius: 12px; + border-radius: var(--radius-md); background: color-mix(in srgb, var(--accent-blue) 86%, var(--bg-surface)); font-size: var(--text-sm); box-shadow: none; @@ -9051,30 +9289,103 @@ textarea.input { .command-palette-sheet-layer { gap: 0; + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-surface) 96%, transparent) 0%, + var(--bg-page) 100% + ); } .command-palette-sheet__meta { - padding-inline: var(--sp-4); + padding: var(--sp-2) var(--sp-4) var(--sp-3); + } + + .command-palette-sheet { + gap: var(--sp-2); + } + + .command-palette-sheet .command-palette-search { + margin: 0 var(--sp-4); + padding: 0 var(--sp-3); + min-height: 46px; + border: 1px solid color-mix(in srgb, var(--border) 80%, transparent); + border-radius: var(--radius-xl); + background: color-mix(in srgb, var(--bg-surface) 88%, var(--bg-page)); + box-shadow: + inset 0 1px 0 color-mix(in srgb, var(--fg-white) 5%, transparent), + 0 12px 28px color-mix(in srgb, var(--bg-page) 20%, transparent); + } + + .command-palette-sheet .command-palette-search-icon { + color: color-mix(in srgb, var(--text-secondary) 86%, var(--accent-blue) 14%); + } + + .command-palette-sheet .command-palette-input { + font-size: 15px; + } + + .command-palette-sheet .command-palette-hint { + padding: 0; + border-bottom: none; + } + + .command-palette-sheet .command-palette-meta { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; } .command-palette-list { flex: 1; max-height: none; + padding: 0 var(--sp-3) var(--sp-4); } .command-palette-item { align-items: flex-start; gap: var(--sp-3); + padding: var(--sp-3); + border: 1px solid color-mix(in srgb, var(--border) 74%, transparent); + border-radius: var(--radius-xl); + background: color-mix(in srgb, var(--bg-surface) 74%, transparent); + box-shadow: inset 0 1px 0 color-mix(in srgb, var(--fg-white) 4%, transparent); + } + + .command-palette-item + .command-palette-item { + margin-top: var(--sp-2); + } + + .command-palette-item:hover, + .command-palette-item-selected { + background: color-mix(in srgb, var(--accent-blue) 10%, var(--bg-surface)); + border-color: color-mix(in srgb, var(--accent-blue) 30%, var(--border)); } .command-palette-item-content { flex: 1; min-width: 0; + gap: 3px; + } + + .command-palette-item-label { + font-size: 14px; + font-weight: var(--font-semibold); + } + + .command-palette-item-desc { + line-height: 1.5; } .command-palette-item-shortcut { flex-shrink: 0; margin-top: 2px; + padding: 4px 7px; + border: 1px solid color-mix(in srgb, var(--border) 72%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--bg-page) 76%, var(--bg-surface) 24%); + font-size: 10px; + letter-spacing: 0.06em; } .mobile-files-sheet, @@ -9087,11 +9398,17 @@ textarea.input { } .mobile-files-sheet { - gap: 0; + gap: var(--sp-3); + padding: var(--sp-3) var(--sp-3) 0; + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-surface) 96%, transparent) 0%, + var(--bg-page) 100% + ); } .mobile-files-sheet__branch-row { - padding: 6px var(--sp-3) 0; + padding: 0; flex-shrink: 0; } @@ -9102,16 +9419,26 @@ textarea.input { width: 100%; min-width: 0; margin-bottom: 0; - padding: 7px 10px; - border: 1px solid color-mix(in srgb, var(--border) 72%, transparent); - border-radius: 4px; - background: color-mix(in srgb, var(--bg-page) 76%, var(--bg-surface) 24%); + padding: 10px 12px; + border: 1px solid color-mix(in srgb, var(--border) 78%, transparent); + border-radius: 16px; + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-surface) 90%, transparent) 0%, + color-mix(in srgb, var(--bg-page) 92%, var(--bg-surface) 8%) 100% + ); color: var(--text-primary); - box-shadow: none; + box-shadow: + inset 0 1px 0 color-mix(in srgb, var(--fg-white) 6%, transparent), + 0 10px 28px color-mix(in srgb, var(--bg-page) 18%, transparent); } .mobile-files-sheet__branch:hover { - background: color-mix(in srgb, var(--bg-hover) 82%, var(--accent-blue) 18%); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--accent-blue) 8%, var(--bg-surface)) 0%, + color-mix(in srgb, var(--accent-blue) 12%, var(--bg-page)) 100% + ); } .mobile-files-sheet__branch:active { @@ -9133,7 +9460,7 @@ textarea.input { flex: 1; flex-direction: column; align-items: flex-start; - gap: 1px; + gap: 2px; } .mobile-files-sheet__branch-label { @@ -9149,8 +9476,8 @@ textarea.input { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-size: 11px; - line-height: 1.2; + font-size: 12px; + line-height: 1.25; font-weight: 600; } @@ -9174,7 +9501,7 @@ textarea.input { .mobile-files-sheet__detail-toolbar { display: flex; align-items: center; - padding: 0 var(--sp-3) var(--sp-2); + padding: 0 var(--sp-1) var(--sp-2); flex-shrink: 0; } @@ -9243,19 +9570,26 @@ textarea.input { min-height: 0; } + .mobile-terminal-sheet { + padding: 0; + gap: 0; + background: var(--bg-page); + } + .mobile-terminal-sheet .bottom-terminal { - border: none; - background: transparent; + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--border) 82%, transparent); + border-radius: 0; + background: color-mix(in srgb, var(--bg-terminal) 96%, var(--bg-page)); box-shadow: none; } .mobile-terminal-sheet .terminal-toolbar { - border-top-left-radius: 18px; - border-top-right-radius: 18px; + border-top-left-radius: 0; + border-top-right-radius: 0; } .mobile-sheet--files, - .mobile-sheet--terminal, .mobile-sheet--launch { background: linear-gradient( 180deg, @@ -9275,6 +9609,11 @@ textarea.input { line-height: 1.2; } + .mobile-sheet--terminal, + .mobile-sheet--terminal .mobile-sheet__footer { + background: var(--bg-page); + } + .mobile-sheet--fullscreen .page-header__kicker { overflow: hidden; max-width: 100%; @@ -9338,14 +9677,33 @@ textarea.input { padding: 0; } + .mobile-sheet--files .mobile-files-sheet__content, + .mobile-sheet--files .mobile-files-sheet__detail, + .mobile-sheet--terminal .mobile-terminal-sheet, + .mobile-sheet--terminal .bottom-terminal { + min-height: 0; + } + + .mobile-sheet--files .file-tree-shell--mobile, + .mobile-sheet--files .git-panel--mobile, + .mobile-sheet--files .workspace-git-editor, + .mobile-sheet--files .workspace-git-view { + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--border) 80%, transparent); + border-radius: var(--radius-xl) var(--radius-xl) 0 0; + background: color-mix(in srgb, var(--bg-surface) 90%, var(--bg-page)); + box-shadow: + inset 0 1px 0 color-mix(in srgb, var(--fg-white) 5%, transparent), + 0 14px 34px color-mix(in srgb, var(--bg-page) 18%, transparent); + } + .mobile-sheet--files .workspace-git-view { padding: 0; - background: transparent; } .mobile-sheet--files .workspace-git-editor { border: none; - border-radius: 0; + border-radius: var(--radius-xl) var(--radius-xl) 0 0; box-shadow: none; } @@ -9365,25 +9723,18 @@ textarea.input { .bottom-terminal--mobile-fullscreen .terminal-toolbar { min-height: 32px; - padding: 3px var(--sp-3) 1px; + padding: 0 var(--sp-2); border-radius: 0; - border-bottom-color: color-mix(in srgb, var(--border) 74%, transparent); - background: color-mix(in srgb, var(--bg-page) 95%, var(--bg-surface) 5%); - gap: var(--sp-1); - } - - .bottom-terminal--mobile-fullscreen .terminal-toolbar-left::before { - display: none; + border-bottom: 1px solid color-mix(in srgb, var(--border) 72%, transparent); + background: color-mix(in srgb, var(--bg-page) 94%, var(--bg-surface) 6%); + gap: 0; } - .bottom-terminal--mobile-fullscreen .terminal-toolbar-left { - flex: 0 0 auto; + .bottom-terminal--mobile-fullscreen .terminal-toolbar-mobile-row { + display: flex; + align-items: center; + width: 100%; min-width: 0; - } - - .bottom-terminal--mobile-fullscreen .terminal-toolbar-right { - width: auto; - justify-content: flex-end; gap: 4px; } @@ -9391,22 +9742,22 @@ textarea.input { min-width: 0; flex: 1; position: relative; - max-width: min(100%, 196px); + max-width: none; } .bottom-terminal--mobile-fullscreen .terminal-selector-btn { display: flex; width: 100%; - height: 22px; - min-height: 22px; + height: 24px; + min-height: 24px; min-width: 0; align-items: center; - gap: var(--sp-2); - padding: 0 6px; + gap: 4px; + padding: 0 7px; justify-content: space-between; - border-radius: 3px; - border-color: color-mix(in srgb, var(--border) 64%, transparent); - background: color-mix(in srgb, var(--bg-page) 74%, var(--bg-surface) 26%); + border-radius: var(--radius-sm); + border-color: color-mix(in srgb, var(--border) 66%, transparent); + background: color-mix(in srgb, var(--bg-page) 82%, var(--bg-surface) 18%); color: var(--text-secondary); font-family: var(--font-mono); font-size: var(--text-xs); @@ -9434,12 +9785,26 @@ textarea.input { transform-origin: center; } + .bottom-terminal--mobile-fullscreen .terminal-selector-btn--static { + cursor: default; + } + + .bottom-terminal--mobile-fullscreen .terminal-toolbar-mobile-placeholder { + flex: 1; + min-width: 0; + height: 24px; + border: none; + background: transparent; + } + .bottom-terminal--mobile-fullscreen .bottom-terminal-tabs { padding-inline: var(--sp-3); + border-bottom-color: color-mix(in srgb, var(--border) 74%, transparent); + background: color-mix(in srgb, var(--bg-page) 80%, var(--bg-surface) 20%); } .bottom-terminal--mobile-fullscreen .bottom-terminal-xterm { - padding: 0 var(--sp-1) var(--sp-1); + padding: 0 var(--sp-2) var(--sp-1); } .bottom-terminal--mobile-fullscreen .bottom-terminal-content { @@ -9451,11 +9816,11 @@ textarea.input { } .bottom-terminal--mobile-fullscreen .panel-toolbar-btn { - width: 22px; - height: 22px; - border-radius: 3px; - border: 1px solid color-mix(in srgb, var(--border) 46%, transparent); - background: color-mix(in srgb, var(--bg-page) 74%, var(--bg-surface) 26%); + width: 24px; + height: 24px; + border-radius: var(--radius-sm); + border: 1px solid color-mix(in srgb, var(--border) 54%, transparent); + background: color-mix(in srgb, var(--bg-page) 80%, var(--bg-surface) 20%); color: var(--text-tertiary); box-shadow: none; } @@ -9474,6 +9839,12 @@ textarea.input { .mobile-supervisor-sheet { gap: var(--sp-3); + padding: var(--sp-3) var(--sp-3) 0; + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-surface) 96%, transparent) 0%, + var(--bg-page) 100% + ); } .mobile-sheet__body--supervisor-detail { @@ -9496,6 +9867,13 @@ textarea.input { gap: var(--sp-4); padding: var(--sp-4); padding-bottom: var(--sp-5); + border: 1px solid color-mix(in srgb, var(--border) 82%, transparent); + border-radius: var(--radius-xl) var(--radius-xl) 0 0; + background: color-mix(in srgb, var(--bg-surface) 90%, var(--bg-page)); + box-shadow: + inset 0 1px 0 color-mix(in srgb, var(--fg-white) 5%, transparent), + 0 14px 34px color-mix(in srgb, var(--bg-page) 18%, transparent); + overflow: auto; } .mobile-supervisor-sheet__actions, @@ -9509,7 +9887,7 @@ textarea.input { width: 100%; padding: var(--sp-2); border: 1px solid color-mix(in srgb, var(--border) 86%, transparent); - border-radius: 16px; + border-radius: var(--radius-xl); background: color-mix(in srgb, var(--bg-surface) 96%, var(--bg-page)); box-shadow: inset 0 1px 0 color-mix(in srgb, var(--fg-white) 6%, transparent); } @@ -9537,6 +9915,14 @@ textarea.input { display: flex; align-items: flex-start; gap: var(--sp-3); + padding: var(--sp-3); + border: 1px solid color-mix(in srgb, var(--border) 78%, transparent); + border-radius: var(--radius-xl); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-surface) 90%, transparent) 0%, + color-mix(in srgb, var(--bg-page) 92%, var(--bg-surface) 8%) 100% + ); } .mobile-supervisor-sheet__detail-header h3, @@ -9558,6 +9944,14 @@ textarea.input { line-height: var(--leading-normal); } + .mobile-supervisor-sheet__detail-header .supervisor-dialog-header-icon { + flex-shrink: 0; + width: 36px; + height: 36px; + border-radius: var(--radius-md); + background: color-mix(in srgb, var(--icon-surface-subtle) 84%, var(--bg-surface)); + } + .mobile-supervisor-sheet__empty { display: flex; flex: 1; @@ -9584,11 +9978,15 @@ textarea.input { display: flex; flex-direction: column; gap: 0; - width: min(88vw, 376px); + width: min(88vw, 388px); padding: calc(var(--mobile-safe-top) + var(--sp-3)) calc(var(--mobile-safe-right) + var(--sp-3)) calc(var(--sp-3) + var(--mobile-safe-bottom)) calc(var(--mobile-safe-left) + var(--sp-3)); border-right: 1px solid color-mix(in srgb, var(--border) 82%, transparent); - background: color-mix(in srgb, var(--bg-page) 95%, var(--bg-surface) 5%); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-surface) 96%, transparent) 0%, + color-mix(in srgb, var(--bg-page) 94%, var(--bg-surface) 6%) 100% + ); box-shadow: none; transform: translateX(-12px); opacity: 0; @@ -9600,7 +9998,7 @@ textarea.input { align-items: center; justify-content: space-between; gap: var(--sp-3); - padding-bottom: 10px; + padding: 0 0 var(--sp-3); border-bottom: 1px solid color-mix(in srgb, var(--border) 82%, transparent); } @@ -9609,30 +10007,46 @@ textarea.input { min-width: 0; flex: 1; flex-direction: column; - gap: 0; + gap: 4px; } .mobile-workspace-drawer__kicker { - display: none; + display: inline-flex; + align-items: center; + gap: 6px; + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-tertiary); + } + + .mobile-workspace-drawer__kicker::before { + content: ""; + width: 6px; + height: 6px; + border-radius: 999px; + background: color-mix(in srgb, var(--accent-blue) 76%, transparent); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-blue) 14%, transparent); } .mobile-workspace-drawer__title { margin: 0; - font-size: 16px; + font-size: 18px; font-weight: var(--font-semibold); line-height: 1.2; - letter-spacing: 0.01em; + letter-spacing: -0.01em; color: var(--text-primary); } .mobile-workspace-drawer__dismiss { - width: 28px; - min-width: 28px; - height: 28px; - min-height: 28px; - border: 1px solid color-mix(in srgb, var(--border) 44%, transparent); - border-radius: 3px; - background: transparent; + width: 34px; + min-width: 34px; + height: 34px; + min-height: 34px; + border: 1px solid color-mix(in srgb, var(--border) 66%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--bg-surface) 78%, var(--bg-page)); color: var(--text-secondary); box-shadow: none; } @@ -9646,15 +10060,13 @@ textarea.input { display: flex; flex: 0 1 auto; flex-direction: column; - gap: 0; + gap: var(--sp-2); min-height: 0; max-height: 100%; overflow-y: auto; - border-top: 1px solid color-mix(in srgb, var(--border) 76%, transparent); - border-bottom: 1px solid color-mix(in srgb, var(--border) 76%, transparent); - border-right: none; - border-left: none; - border-radius: 0; + margin: var(--sp-3) 0; + padding-right: 2px; + border: none; background: transparent; box-shadow: none; } @@ -9662,12 +10074,18 @@ textarea.input { .mobile-workspace-drawer__item { display: flex; align-items: stretch; - gap: 0; - padding: 0; - border: none; - box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--border) 92%, transparent); - border-radius: 0; - background: transparent; + gap: var(--sp-2); + padding: var(--sp-2); + border: 1px solid color-mix(in srgb, var(--border) 78%, transparent); + border-radius: var(--radius-xl); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-surface) 88%, transparent) 0%, + color-mix(in srgb, var(--bg-page) 92%, var(--bg-surface) 8%) 100% + ); + box-shadow: + inset 0 1px 0 color-mix(in srgb, var(--fg-white) 5%, transparent), + 0 12px 30px color-mix(in srgb, var(--bg-page) 16%, transparent); text-align: left; transition: background-color var(--mobile-shell-motion-surface-duration) var(--ease-out), @@ -9677,22 +10095,18 @@ textarea.input { box-shadow var(--mobile-shell-motion-surface-duration) var(--ease-out); } - .mobile-workspace-drawer__item:last-child { - box-shadow: none; - } - .mobile-workspace-drawer__item--active { background: linear-gradient( - 90deg, - color-mix(in srgb, var(--accent-blue) 12%, transparent) 0, - color-mix(in srgb, var(--accent-blue) 4%, transparent) 46%, - transparent 100% + 180deg, + color-mix(in srgb, var(--accent-blue) 12%, var(--bg-surface)) 0%, + color-mix(in srgb, var(--accent-blue) 10%, var(--bg-page)) 100% ), color-mix(in srgb, var(--bg-panel) 92%, var(--surface-elevated)); box-shadow: - inset 0 1px 0 color-mix(in srgb, var(--accent-blue) 10%, transparent), - inset 0 -1px 0 color-mix(in srgb, var(--accent-blue) 12%, transparent); + inset 0 1px 0 color-mix(in srgb, var(--accent-blue) 12%, transparent), + 0 14px 32px color-mix(in srgb, var(--accent-blue) 8%, transparent); + border-color: color-mix(in srgb, var(--accent-blue) 28%, var(--border)); } .mobile-workspace-drawer__item-main { @@ -9702,9 +10116,9 @@ textarea.input { flex-direction: column; align-items: flex-start; justify-content: center; - gap: 1px; - padding: 10px 0; - padding-right: var(--sp-2); + gap: 3px; + padding: var(--sp-2); + padding-right: 0; text-align: left; } @@ -9720,15 +10134,15 @@ textarea.input { display: inline-flex; align-items: center; justify-content: center; - width: 28px; - min-width: 28px; - min-height: 28px; + width: 32px; + min-width: 32px; + min-height: 32px; margin: auto 0; align-self: auto; - border: 1px solid color-mix(in srgb, var(--border) 40%, transparent); - border-radius: 3px; + border: 1px solid color-mix(in srgb, var(--border) 56%, transparent); + border-radius: 999px; color: color-mix(in srgb, var(--color-error) 82%, var(--text-primary)); - background: transparent; + background: color-mix(in srgb, var(--bg-surface) 76%, transparent); transition: background-color var(--mobile-shell-motion-surface-duration) var(--ease-out), color var(--mobile-shell-motion-surface-duration) var(--ease-out); @@ -9742,7 +10156,7 @@ textarea.input { min-width: 0; flex: 1; color: var(--text-primary); - font-size: 12px; + font-size: 13px; font-weight: var(--font-semibold); line-height: 1.35; } @@ -9768,9 +10182,9 @@ textarea.input { overflow: hidden; color: var(--text-tertiary); font-family: var(--font-mono); - font-size: 9px; - line-height: 1.35; - letter-spacing: 0.03em; + font-size: 10px; + line-height: 1.45; + letter-spacing: 0.02em; text-overflow: ellipsis; white-space: nowrap; } @@ -9780,8 +10194,8 @@ textarea.input { grid-template-columns: minmax(0, 1fr); gap: 0; margin-top: auto; - padding-top: 0; - border-top: none; + padding-top: var(--sp-2); + border-top: 1px solid color-mix(in srgb, var(--border) 78%, transparent); } .mobile-workspace-drawer__footer-button { @@ -9790,13 +10204,11 @@ textarea.input { justify-content: flex-start; gap: var(--sp-2); width: 100%; - min-height: 42px; - padding: 10px 0; - border: none; - border-top: 1px solid color-mix(in srgb, var(--border) 76%, transparent); - border-bottom: 1px solid color-mix(in srgb, var(--border) 76%, transparent); - border-radius: 0; - background: transparent; + min-height: 46px; + padding: 0 var(--sp-3); + border: 1px solid color-mix(in srgb, var(--border) 78%, transparent); + border-radius: var(--radius-md); + background: color-mix(in srgb, var(--bg-surface) 80%, transparent); color: var(--text-primary); font-size: 12px; font-weight: var(--font-medium); @@ -9923,42 +10335,107 @@ textarea.input { align-items: stretch; justify-content: flex-start; min-height: 100dvh; - padding: calc(env(safe-area-inset-top, 0px) + var(--sp-6)) + padding: calc(env(safe-area-inset-top, 0px) + var(--sp-4)) calc(env(safe-area-inset-right, 0px) + var(--sp-4)) - calc(env(safe-area-inset-bottom, 0px) + var(--sp-6)) + calc(env(safe-area-inset-bottom, 0px) + var(--sp-5)) calc(env(safe-area-inset-left, 0px) + var(--sp-4)); + background: + radial-gradient( + circle at top center, + color-mix(in srgb, var(--accent-blue) 10%, transparent), + transparent 38% + ), + linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-page) 92%, var(--bg-surface) 8%) 0%, + var(--bg-page) 100% + ); } .welcome-card--mobile { width: 100%; max-width: none; - padding: var(--sp-8) var(--sp-5); - gap: var(--sp-4); + padding: var(--sp-7) var(--sp-5); + gap: var(--sp-5); + border-radius: var(--radius-xl); + } + + .welcome-card--mobile .welcome-card__actions { + align-items: stretch; + } + + .welcome-card--mobile .welcome-btn { + width: 100%; } .welcome-card--mobile .welcome-title { - font-size: var(--text-2xl); + font-size: clamp(30px, 8vw, 40px); + line-height: 1.05; + letter-spacing: -0.03em; } .welcome-card--mobile .welcome-body { max-width: none; - font-size: var(--text-base); + font-size: 16px; + line-height: 1.7; } .welcome-card--mobile .welcome-features { grid-template-columns: 1fr; + margin-top: var(--sp-2); + } + + .welcome-card--mobile .welcome-feature { + align-items: flex-start; + min-height: 0; + padding: var(--sp-4); + border-radius: var(--radius-xl); + text-align: left; + } + + .welcome-card--mobile .welcome-feature-text { + width: 100%; + } + + .welcome-card--mobile .welcome-btn { + width: 100%; + justify-content: center; + padding-inline: var(--sp-5); + border-radius: var(--radius-md); + } + + .welcome-card--mobile .welcome-link { + margin-top: 0; } .auth-screen--mobile { - padding: calc(env(safe-area-inset-top, 0px) + var(--sp-6)) + padding: calc(env(safe-area-inset-top, 0px) + var(--sp-4)) calc(env(safe-area-inset-right, 0px) + var(--sp-4)) - calc(env(safe-area-inset-bottom, 0px) + var(--sp-6)) + calc(env(safe-area-inset-bottom, 0px) + var(--sp-5)) calc(env(safe-area-inset-left, 0px) + var(--sp-4)); } .auth-card-shell--mobile { width: 100%; - padding: var(--sp-8) var(--sp-5); + padding: var(--sp-7) var(--sp-5); + gap: var(--sp-5); + border-radius: var(--radius-xl); + } + + .auth-card-shell--mobile .welcome-title { + font-size: clamp(28px, 7.8vw, 38px); + line-height: 1.06; + letter-spacing: -0.03em; + } + + .auth-card-shell--mobile .welcome-body { + font-size: 15px; + line-height: 1.65; + } + + .auth-card-shell--mobile .auth-status-panel { + padding: var(--sp-4); + border-radius: var(--radius-xl); } .settings-page--mobile { @@ -9995,6 +10472,10 @@ textarea.input { padding-bottom: var(--sp-5); } + .settings-content-surface { + width: 100%; + } + .settings-content--mobile-detail { flex: 1; min-height: 0; @@ -10002,8 +10483,8 @@ textarea.input { } .settings-page--mobile .settings-content--mobile-root { - padding-left: var(--sp-2); - padding-right: var(--sp-2); + padding-left: var(--sp-3); + padding-right: var(--sp-3); } .settings-pills { @@ -10032,39 +10513,107 @@ textarea.input { padding-bottom: var(--sp-4); } - .settings-mobile-list { + .settings-content--mobile .settings-content-surface { + min-height: auto; + padding: 0; + background: transparent; + border: none; + border-radius: 0; + box-shadow: none; + } + + .settings-mobile-root { display: flex; flex-direction: column; - gap: 0; - border-top: 1px solid var(--border); + gap: var(--sp-5); + padding: var(--sp-4) 0 var(--sp-4); + } + + .settings-mobile-group { + display: flex; + flex-direction: column; + gap: var(--sp-2); + } + + .settings-mobile-group__title { + margin: 0; + padding: 0 var(--sp-1); + color: var(--text-secondary); + font-size: 11px; + font-weight: var(--font-semibold); + letter-spacing: 0.08em; + text-transform: uppercase; + } + + .settings-mobile-group__list { + display: flex; + flex-direction: column; + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--border) 82%, transparent); + border-radius: 12px; + background: color-mix(in srgb, var(--bg-surface) 92%, var(--bg-page)); } .settings-mobile-item { display: flex; align-items: center; gap: var(--sp-3); - min-height: 48px; + min-height: 60px; padding: var(--sp-3) var(--sp-4); border: none; - border-bottom: 1px solid var(--border); border-radius: 0; background: transparent; text-align: left; } + .settings-mobile-item + .settings-mobile-item { + border-top: 1px solid color-mix(in srgb, var(--border) 72%, transparent); + } + + .settings-mobile-item__icon-shell { + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: var(--radius-xl); + background: color-mix(in srgb, var(--icon-surface-subtle) 82%, var(--bg-surface)); + border: 1px solid color-mix(in srgb, var(--border) 68%, transparent); + flex-shrink: 0; + } + .settings-mobile-item__icon { display: inline-flex; color: var(--icon-secondary); } - .settings-mobile-item__label { + .settings-mobile-item__copy { + display: flex; flex: 1; - font-size: var(--text-base); + min-width: 0; + flex-direction: column; + gap: 2px; + } + + .settings-mobile-item__label { + font-size: 15px; + font-weight: var(--font-semibold); color: var(--text-primary); } + .settings-mobile-item__hint { + display: -webkit-box; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + color: var(--text-tertiary); + font-size: 11px; + line-height: 1.45; + } + .settings-mobile-item__arrow { color: var(--text-tertiary); + flex-shrink: 0; } .settings-provider-mobile-entry { @@ -10121,6 +10670,12 @@ textarea.input { .settings-header { padding: calc(var(--mobile-safe-top) + var(--sp-1)) calc(var(--mobile-safe-right) + var(--sp-4)) var(--sp-1) calc(var(--mobile-safe-left) + var(--sp-4)); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-surface) 98%, transparent) 0%, + color-mix(in srgb, var(--bg-page) 95%, var(--bg-surface) 5%) 100% + ); + border-bottom: 1px solid color-mix(in srgb, var(--border) 82%, transparent); } .settings-page--mobile > .settings-header { @@ -10150,6 +10705,11 @@ textarea.input { min-height: 32px; padding: var(--sp-2) calc(var(--mobile-safe-right) + var(--sp-4)) calc(var(--mobile-safe-bottom) + var(--sp-4)) calc(var(--mobile-safe-left) + var(--sp-4)); + color: var(--text-tertiary); + } + + .settings-footer--mobile .settings-footer__meta { + width: 100%; } .settings-page--mobile > .settings-footer { @@ -10169,7 +10729,7 @@ textarea.input { } .mobile-shell--landscape-compact .mobile-shell__viewport { - padding-top: var(--sp-1); + padding-top: 0; padding-bottom: 0; } @@ -10365,10 +10925,10 @@ textarea.input { /* ========== Workspace File/Git Panel Redesign ========== */ .workspace-status-bar { display: flex; - min-height: 28px; + min-height: var(--desktop-statusbar-height); flex-shrink: 0; border-top: 1px solid color-mix(in srgb, var(--border) 78%, transparent); - background: color-mix(in srgb, var(--bg-surface) 94%, var(--bg-page)); + background: var(--bg-panel); } .workspace-status-bar--flush { @@ -10391,7 +10951,7 @@ textarea.input { min-height: 0; height: 100%; flex-direction: column; - background: color-mix(in srgb, var(--bg-surface) 94%, var(--bg-page)); + background: var(--bg-panel); } .workspace-sidebar-panel__header { @@ -10399,8 +10959,8 @@ textarea.input { align-items: flex-end; justify-content: space-between; gap: 12px; - min-height: 40px; - padding: 10px 12px 8px; + min-height: var(--desktop-sidebar-header-height); + padding: 10px var(--desktop-panel-padding) 8px; border-bottom: 1px solid color-mix(in srgb, var(--border) 78%, transparent); } @@ -10480,7 +11040,8 @@ textarea.input { flex-direction: column; } -.file-tree-shell .file-tree-search { +.file-tree-shell .file-tree-search, +.file-tree-shell .file-tree-search--desktop { display: flex; align-items: center; gap: 8px; @@ -10492,7 +11053,8 @@ textarea.input { background: color-mix(in srgb, var(--bg-surface) 90%, var(--bg-page)); } -.file-tree-shell .file-tree-search:focus-within { +.file-tree-shell .file-tree-search:focus-within, +.file-tree-shell .file-tree-search--desktop:focus-within { border-color: color-mix(in srgb, var(--accent-blue) 70%, transparent); } @@ -11090,9 +11652,18 @@ textarea.input { display: flex; align-items: center; flex-shrink: 0; - gap: 10px; - padding: 6px 12px 0; - border-bottom: 1px solid color-mix(in srgb, var(--border) 82%, transparent); + gap: var(--sp-3); + padding: var(--sp-3) var(--sp-3) var(--sp-2); + border: 1px solid color-mix(in srgb, var(--border) 78%, transparent); + border-radius: 18px; + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--bg-surface) 92%, transparent) 0%, + color-mix(in srgb, var(--bg-page) 92%, var(--bg-surface) 8%) 100% + ); + box-shadow: + inset 0 1px 0 color-mix(in srgb, var(--fg-white) 5%, transparent), + 0 10px 24px color-mix(in srgb, var(--bg-page) 16%, transparent); } .mobile-files-sheet__segment { @@ -11101,10 +11672,10 @@ textarea.input { align-items: center; justify-content: flex-start; gap: 6px; - min-height: 34px; - padding: 0 2px; + min-height: 36px; + padding: 0 4px; border: none; - border-radius: 0; + border-radius: 999px; background: transparent; color: var(--text-tertiary); font-size: 13px; @@ -11124,10 +11695,10 @@ textarea.input { .mobile-files-sheet__segment.active::after { content: ""; position: absolute; - right: 0; - bottom: -1px; - left: 0; - height: 1.5px; + right: 4px; + bottom: 2px; + left: 4px; + height: 2px; border-radius: 999px; background: color-mix(in srgb, var(--accent-blue) 90%, white 10%); } @@ -11135,22 +11706,22 @@ textarea.input { .mobile-files-sheet__tab-actions { display: inline-flex; align-items: center; - gap: 2px; + gap: 6px; flex-shrink: 0; margin-left: auto; - padding-bottom: 3px; + padding-bottom: 0; } .mobile-files-sheet__tab-action { display: inline-flex; align-items: center; justify-content: center; - width: 28px; - height: 28px; + width: 32px; + height: 32px; padding: 0; - border: none; - border-radius: 6px; - background: transparent; + border: 1px solid color-mix(in srgb, var(--border) 74%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--bg-surface) 74%, transparent); color: var(--text-tertiary); transition: background-color var(--duration-fast) var(--ease-out), @@ -11158,7 +11729,7 @@ textarea.input { } .mobile-files-sheet__tab-action:hover { - background: var(--bg-hover); + background: color-mix(in srgb, var(--bg-hover) 82%, var(--accent-blue) 10%); color: var(--text-primary); } @@ -11197,9 +11768,9 @@ textarea.input { } .git-panel--mobile .git-panel-scroll { - gap: 12px; - padding-inline: 12px; - padding-bottom: 18px; + gap: var(--sp-3); + padding: var(--sp-3); + padding-bottom: calc(var(--sp-4) + var(--mobile-safe-bottom)); } .git-panel--mobile .git-panel-section-toggle, @@ -11234,7 +11805,8 @@ textarea.input { } .git-panel--mobile .git-commit-primary { - min-height: 32px; + min-height: 36px; + border-radius: 999px; } .mobile-shell__bottom-stack .git-panel-status-strip { diff --git a/packages/web/src/styles/components.theme.test.ts b/packages/web/src/styles/components.theme.test.ts index 2a429b705..40e5b9f0a 100644 --- a/packages/web/src/styles/components.theme.test.ts +++ b/packages/web/src/styles/components.theme.test.ts @@ -233,22 +233,44 @@ describe("components.css theme-sensitive surfaces", () => { const activeTab = getLastRuleBlock(".topbar-tab.active"); const emptyCard = getLastRuleBlock(".workspace-empty-inner"); const resolvingCard = getLastRuleBlock(".workspace-resolving-card"); + const mainStage = getLastRuleBlock(".workspace-main-stage"); const sessionTerminal = getLastRuleBlock(".session-terminal"); - const bottomTerminalShell = getLastRuleBlock( - ".workspace-main-area > .bottom-terminal > .bottom-terminal" - ); + const agentPanes = getLastRuleBlock(".workspace-main-stage > .agent-panes"); + const bottomTerminalShell = getLastRuleBlock(".workspace-bottom-panel > .bottom-terminal"); expect(topbar).toContain("var(--bg-surface)"); expect(activeTab).toContain("var(--bg-active)"); expect(activeTab).not.toContain("rgba(45, 63, 79, 0.92)"); expect(emptyCard).toContain("var(--bg-surface)"); expect(resolvingCard).toContain("var(--bg-surface)"); + expect(mainStage).toContain("flex: 1"); + expect(mainStage).toContain("min-height: 0"); + expect(mainStage).toContain("min-width: 0"); + expect(mainStage).toContain("display: flex"); + expect(mainStage).toContain("flex-direction: column"); + expect(agentPanes).toContain("flex: 1"); + expect(agentPanes).toContain("min-height: 0"); expect(sessionTerminal).toContain("var(--bg-terminal)"); expect(sessionTerminal).not.toContain("rgba(11, 18, 24, 0.98)"); expect(bottomTerminalShell).toContain("var(--bg-terminal)"); expect(bottomTerminalShell).not.toContain("rgba(17, 24, 31, 0.96)"); }); + it("maps desktop chrome blocks to the dedicated desktop layout tokens", () => { + const topbar = getLastRuleBlock(".app-topbar"); + const statusBar = getLastRuleBlock(".workspace-status-bar"); + const sidebarHeader = getLastRuleBlock(".workspace-sidebar-panel__header"); + const commandPalette = getLastRuleBlock(".command-palette"); + const launchModal = getLastRuleBlock(".launch-modal"); + + expect(topbar).toContain("min-height: var(--desktop-topbar-height)"); + expect(statusBar).toContain("min-height: var(--desktop-statusbar-height)"); + expect(sidebarHeader).toContain("min-height: var(--desktop-sidebar-header-height)"); + expect(sidebarHeader).toContain("padding: 10px var(--desktop-panel-padding) 8px"); + expect(commandPalette).toContain("max-width: var(--desktop-modal-max-width-md)"); + expect(launchModal).toContain("max-width: min(var(--desktop-modal-max-width-lg), 90vw)"); + }); + it("keeps auth shells theme-aware instead of forcing dark gradients", () => { const authScreen = getLastRuleBlock(".auth-screen"); const authCard = getLastRuleBlock(".auth-card-shell"); @@ -399,10 +421,9 @@ describe("components.css theme-sensitive surfaces", () => { ".mobile-shell--landscape-compact .mobile-shell__viewport" ); - expect(viewport).toContain( - "padding: var(--sp-1) var(--mobile-safe-right) 0 var(--mobile-safe-left)" - ); - expect(viewport).not.toContain("var(--sp-3) var(--mobile-safe-left)"); + expect(viewport).toContain("padding: 0"); + expect(viewport).toContain("border-top:"); + expect(viewport).not.toContain("padding: 4px"); expect(compactViewport).toContain("padding-bottom: 0"); }); @@ -451,6 +472,7 @@ describe("components.css theme-sensitive surfaces", () => { it("uses a unified inline sheet treatment for mobile selectors and keeps topbar controls height-aligned", () => { const inlineSheet = getLastRuleBlock(".mobile-inline-sheet"); + const inlineSheetAction = getLastRuleBlock(".mobile-inline-sheet__action"); const inlineSelectSheet = getLastRuleBlock(".mobile-select-sheet--inline"); const workspaceButton = getLastGroupedRuleBlock( /\.mobile-topbar__workspace-button\s*\{([^}]*)\}/g @@ -459,14 +481,15 @@ describe("components.css theme-sensitive surfaces", () => { const iconButton = getLastRuleBlock(".mobile-topbar__icon-button"); expect(inlineSheet).toContain("position: absolute"); - expect(inlineSheet).toContain("border-radius: 12px"); + expect(inlineSheet).toContain("border-radius: var(--radius-xl)"); + expect(inlineSheetAction).toContain("border-radius: var(--radius-md)"); expect(inlineSelectSheet).toContain("flex: 1"); expect(inlineSelectSheet).toContain("flex-direction: column"); - expect(workspaceButton).toContain("height: 32px"); + expect(workspaceButton).toContain("height: 36px"); expect(workspaceButton).not.toContain("box-shadow:"); expect(sessionButton).toContain("min-height: 40px"); expect(sessionButton).not.toContain("box-shadow:"); - expect(iconButton).toContain("height: 32px"); + expect(iconButton).toContain("height: 36px"); expect(iconButton).not.toContain("box-shadow:"); }); @@ -501,17 +524,17 @@ describe("components.css theme-sensitive surfaces", () => { block.includes("display: flex") ); const activeDockItem = getRuleBlocksFrom(stylesheet, ".mobile-dock__item--active").find( - (block) => block.includes("background:") + (block) => block.includes("border-top-color:") ); expect(mobileDock).toContain("grid-template-columns: repeat(3, minmax(0, 1fr))"); - expect(mobileDock).toContain("gap: 0"); + expect(mobileDock).toContain("gap: var(--sp-2)"); expect(mobileDockItem).toBeTruthy(); - expect(mobileDockItem).toContain("border: none"); + expect(mobileDockItem).toContain("border-top: 1px solid transparent"); expect(mobileDockItem).toContain("background: transparent"); - expect(mobileDockItem).toContain("min-height: 34px"); + expect(mobileDockItem).toContain("min-height: 30px"); expect(activeDockItem).toBeTruthy(); - expect(activeDockItem).toContain("background: transparent"); + expect(activeDockItem).toContain("border-top-color:"); }); it("keeps mobile sheets closer to IDE panes than floating cards", () => { @@ -519,8 +542,8 @@ describe("components.css theme-sensitive surfaces", () => { const mobileSheetHandle = getLastRuleBlock(".mobile-sheet__handle"); const fullscreenBack = getLastRuleBlock(".mobile-sheet--fullscreen .page-header__back"); - expect(mobileSheet).toContain("border-top-left-radius: 12px"); - expect(mobileSheet).toContain("border-top-right-radius: 12px"); + expect(mobileSheet).toContain("border-top-left-radius: var(--radius-xl)"); + expect(mobileSheet).toContain("border-top-right-radius: var(--radius-xl)"); expect(mobileSheet).toContain("border: 1px solid"); expect(mobileSheet).not.toContain("box-shadow: var(--shadow-xl)"); expect(mobileSheetHandle).toContain("width: 32px"); @@ -530,9 +553,16 @@ describe("components.css theme-sensitive surfaces", () => { it("keeps settings navigation aligned with desktop editor chrome on both desktop and mobile", () => { const settingsPage = getLastRuleBlock(".settings-page"); const baseSettingsHeader = getRuleBlocksFrom(stylesheet, ".settings-header")[0]; + const desktopSettingsHeader = getLastRuleBlock(".settings-header__desktop"); + const desktopSettingsCopy = getLastRuleBlock(".settings-header__copy"); + const desktopSettingsTitle = getLastRuleBlock(".settings-header__title"); const settingsBody = getLastRuleBlock(".settings-body"); const settingsSidebar = getLastRuleBlock(".settings-sidebar"); const settingsContent = getLastRuleBlock(".settings-content"); + const settingsContentFillHeight = getLastRuleBlock(".settings-content--fill-height"); + const settingsContentFillHeightSurface = getLastRuleBlock( + ".settings-content--fill-height > .settings-content-surface" + ); const settingsNavItem = getLastRuleBlock(".settings-nav-item"); const settingsNavItemHover = getLastRuleBlock(".settings-nav-item:hover"); const settingsNavItemActive = getLastRuleBlock(".settings-nav-item-active"); @@ -542,7 +572,9 @@ describe("components.css theme-sensitive surfaces", () => { const mobileRootContent = getLastRuleBlock( ".settings-page--mobile .settings-content--mobile-root" ); - const mobileList = getLastRuleBlock(".settings-mobile-list"); + const mobileRoot = getLastRuleBlock(".settings-mobile-root"); + const mobileGroup = getLastRuleBlock(".settings-mobile-group"); + const mobileGroupList = getLastRuleBlock(".settings-mobile-group__list"); const mobileItem = getLastRuleBlock(".settings-mobile-item"); const mobileItemIcon = getLastRuleBlock(".settings-mobile-item__icon"); const mobileItemArrow = getLastRuleBlock(".settings-mobile-item__arrow"); @@ -552,32 +584,53 @@ describe("components.css theme-sensitive surfaces", () => { expect(settingsPage).toContain("background: var(--bg-page)"); expect(baseSettingsHeader).toContain("background: var(--bg-surface)"); expect(baseSettingsHeader).toContain("border-bottom: 1px solid var(--border)"); - expect(baseSettingsHeader).toContain("padding: var(--sp-2) var(--sp-4)"); + expect(baseSettingsHeader).toContain("padding: var(--sp-1) var(--sp-4)"); + expect(desktopSettingsHeader).toContain("width: 100%"); + expect(desktopSettingsHeader).toContain("margin: 0"); + expect(desktopSettingsHeader).toContain("display: flex"); + expect(desktopSettingsHeader).toContain("align-items: center"); + expect(desktopSettingsHeader).toContain("justify-content: flex-start"); + expect(desktopSettingsCopy).toContain("justify-content: flex-start"); + expect(desktopSettingsCopy).toContain("flex: 0 1 auto"); + expect(desktopSettingsCopy).toContain("gap: 0"); + expect(desktopSettingsTitle).toContain("font-size: 18px"); + expect(desktopSettingsTitle).toContain("line-height: 1.05"); expect(settingsBody).toContain("align-items: stretch"); expect(settingsBody).toContain("background: var(--bg-page)"); - expect(settingsSidebar).toContain("background: var(--bg-sidebar)"); - expect(settingsSidebar).toContain("padding: var(--sp-3)"); - expect(settingsSidebar).toContain("width: 216px"); - expect(settingsContent).toContain("padding: var(--sp-5) var(--sp-6) var(--sp-6)"); + expect(settingsSidebar).toContain("background: var(--bg-panel)"); + expect(settingsSidebar).toContain("padding: var(--sp-4)"); + expect(settingsSidebar).toContain("width: 240px"); + expect(settingsContent).toContain("display: flex"); + expect(settingsContent).toContain("justify-content: center"); + expect(settingsContent).toContain("padding: var(--sp-6)"); expect(settingsContent).toContain("background: var(--bg-page)"); + expect(settingsContentFillHeight).toContain("justify-content: flex-start"); + expect(settingsContentFillHeightSurface).toContain("display: flex"); + expect(settingsContentFillHeightSurface).toContain("flex-direction: column"); + expect(settingsContentFillHeightSurface).toContain("flex: 1"); + expect(settingsContentFillHeightSurface).toContain("min-height: 0"); expect(settingsNavItem).toContain("min-height: 40px"); expect(settingsNavItem).toContain("border: 1px solid transparent"); expect(settingsNavItem).toContain("border-radius: var(--radius-md)"); expect(settingsNavItemHover).toContain("background: var(--bg-hover)"); expect(settingsNavItemActive).toContain("background: var(--bg-active)"); - expect(settingsNavItemActive).toContain("border-color: var(--border-focus)"); - expect(settingsNavItemActive).not.toContain("var(--accent-blue)"); + expect(settingsNavItemActive).toContain("border-color: color-mix"); + expect(settingsNavItemActive).toContain("var(--accent-blue)"); expect(mobileContent).toContain("padding: 0"); - expect(mobileRootContent).toContain("padding-left: var(--sp-2)"); - expect(mobileRootContent).toContain("padding-right: var(--sp-2)"); - expect(mobileList).toContain("gap: 0"); - expect(mobileList).toContain("border-top: 1px solid var(--border)"); - expect(mobileItem).toContain("min-height: 48px"); + expect(mobileRootContent).toContain("padding-left: var(--sp-3)"); + expect(mobileRootContent).toContain("padding-right: var(--sp-3)"); + expect(mobileRoot).toContain("display: flex"); + expect(mobileRoot).toContain("flex-direction: column"); + expect(mobileRoot).toContain("gap: var(--sp-5)"); + expect(mobileRoot).toContain("padding: var(--sp-4) 0 var(--sp-4)"); + expect(mobileGroup).toContain("gap: var(--sp-2)"); + expect(mobileGroupList).toContain("border: 1px solid"); + expect(mobileGroupList).toContain("border-radius: 12px"); + expect(mobileGroupList).toContain("overflow: hidden"); + expect(mobileItem).toContain("min-height: 60px"); expect(mobileItem).toContain("padding: var(--sp-3) var(--sp-4)"); expect(mobileItem).toContain("border: none"); - expect(mobileItem).toContain("border-bottom: 1px solid var(--border)"); - expect(mobileItem).toContain("border-radius: 0"); - expect(mobileItem).toContain("background: transparent"); + expect(mobileItem).not.toContain("linear-gradient("); expect(mobileItemIcon).toContain("color: var(--icon-secondary)"); expect(mobileItemArrow).toContain("color: var(--text-tertiary)"); }); @@ -627,6 +680,14 @@ describe("components.css theme-sensitive surfaces", () => { expect(mobileSettingsFooter).toContain("min-height: 32px"); }); + it("keeps mobile container surfaces on shared radius tokens instead of bespoke rounded-card values", () => { + const settingsGroupList = getLastRuleBlock(".settings-mobile-group__list"); + const settingsItemIconShell = getLastRuleBlock(".settings-mobile-item__icon-shell"); + + expect(settingsGroupList).toContain("border-radius: 12px"); + expect(settingsItemIconShell).toContain("border-radius: var(--radius-xl)"); + }); + it("keeps the mobile workspace launch action docked in a compact editor-style footer rail", () => { const launchFooter = getLastRuleBlock(".mobile-sheet--launch .mobile-sheet__footer").replace( /\s+/g, @@ -641,10 +702,10 @@ describe("components.css theme-sensitive surfaces", () => { "padding: var(--sp-2) calc(var(--mobile-safe-right) + var(--sp-4)) calc(var(--mobile-safe-bottom) + var(--sp-4)) calc(var(--mobile-safe-left) + var(--sp-4))" ); expect(launchActionRail).toContain("padding: var(--sp-2)"); - expect(launchActionRail).toContain("border-radius: 16px"); + expect(launchActionRail).toContain("border-radius: var(--radius-xl)"); expect(launchActionRail).toContain("background: color-mix("); expect(launchActionButton).toContain("min-height: 44px"); - expect(launchActionButton).toContain("border-radius: 12px"); + expect(launchActionButton).toContain("border-radius: var(--radius-md)"); expect(launchActionButton).toContain("font-size: var(--text-sm)"); expect(launchActionButton).toContain("box-shadow: none"); }); @@ -676,7 +737,9 @@ describe("components.css theme-sensitive surfaces", () => { expect(supervisorFooterButton).toContain("min-height: 44px"); expect(supervisorFooterButton).toContain("box-shadow: none"); expect(getLastRuleBlock(".mobile-supervisor-sheet__footer")).toContain("padding: var(--sp-2)"); - expect(getLastRuleBlock(".mobile-supervisor-sheet__footer")).toContain("border-radius: 16px"); + expect(getLastRuleBlock(".mobile-supervisor-sheet__footer")).toContain( + "border-radius: var(--radius-xl)" + ); expect(getLastRuleBlock(".mobile-supervisor-sheet__footer")).toContain( "background: color-mix(" ); @@ -699,25 +762,33 @@ describe("components.css theme-sensitive surfaces", () => { ".mobile-shell__bottom-stack .git-panel-status-strip" ).replace(/\s+/g, " "); const emptyPane = getLastRuleBlock(".mobile-shell__empty-content"); - const emptyPaneBefore = getLastRuleBlock(".mobile-shell__empty-content::before"); const emptyState = getLastRuleBlock(".mobile-shell__empty-state"); + const emptyTitle = getLastRuleBlock(".mobile-shell__empty-title"); + const placeholderCopy = getLastRuleBlock(".mobile-shell__placeholder-copy"); const emptyCta = getLastRuleBlock(".mobile-shell__empty-cta"); + const topbarWorkspaceButton = getLastGroupedRuleBlock( + /\.mobile-topbar__workspace-button\s*\{([^}]*)\}/g + ); + const topbarIconButton = getLastRuleBlock(".mobile-topbar__icon-button"); expect(topbar).toContain( "padding: calc(var(--mobile-safe-top) + var(--sp-1)) calc(var(--mobile-safe-right) + var(--sp-4)) var(--sp-1) calc(var(--mobile-safe-left) + var(--sp-4))" ); - expect(emptyStage).toContain("padding: clamp(56px, 14vh, 116px) var(--sp-4) var(--sp-3)"); - expect(bottomStack).toContain("background: var(--bg-surface)"); - expect(bottomStack).toContain("border-top: 1px solid var(--border)"); + expect(topbarWorkspaceButton).toContain("border-bottom: 1px solid"); + expect(topbarWorkspaceButton).toContain("border-radius: 0"); + expect(topbarIconButton).toContain("border-radius: 10px"); + expect(emptyStage).toContain("padding: clamp(34px, 9vh, 72px) var(--sp-4) var(--sp-3)"); + expect(bottomStack).toContain("background: linear-gradient("); + expect(bottomStack).toContain("border-top: 1px solid color-mix("); expect(bottomStack).not.toContain("backdrop-filter"); expect(dockShell).toContain( - "padding: 2px calc(var(--mobile-safe-right) + var(--sp-4)) 0 calc(var(--mobile-safe-left) + var(--sp-4))" + "padding: 3px calc(var(--mobile-safe-right) + var(--sp-4)) 0 calc(var(--mobile-safe-left) + var(--sp-4))" ); - expect(dock).toContain("gap: 0"); + expect(dock).toContain("gap: var(--sp-2)"); expect(dock).toContain("border-bottom: none"); expect(dockItem).toBeTruthy(); - expect(dockItem).toContain("min-height: 34px"); - expect(dockItem).toContain("padding: 3px var(--sp-1) 5px"); + expect(dockItem).toContain("min-height: 30px"); + expect(dockItem).toContain("padding: 2px var(--sp-2) 2px"); expect(dockLabel).toContain("font-size: 10px"); expect(statusBar).toContain("padding-bottom: calc(var(--mobile-safe-bottom) + var(--sp-1))"); expect(statusBar).toContain("border-top: 1px solid"); @@ -727,22 +798,59 @@ describe("components.css theme-sensitive surfaces", () => { "padding: 0 calc(var(--mobile-safe-right) + var(--sp-4)) 0 calc(var(--mobile-safe-left) + var(--sp-4))" ); expect(emptyPane).toContain("position: relative"); - expect(emptyPane).toContain("width: min(100%, 344px)"); + expect(emptyPane).toContain("width: min(100%, 320px)"); expect(emptyPane).toContain("align-self: flex-start"); - expect(emptyPane).toContain("padding: var(--sp-2) 0 0 var(--sp-4)"); - expect(emptyPane).toContain("border-top: none"); + expect(emptyPane).toContain("padding: 0"); + expect(emptyPane).toContain("border: none"); expect(emptyPane).toContain("background: transparent"); - expect(emptyPaneBefore).toContain('content: ""'); - expect(emptyPaneBefore).toContain("position: absolute"); - expect(emptyPaneBefore).toContain("width: 1px"); - expect(emptyPaneBefore).toContain("background: linear-gradient("); expect(emptyState).toContain("align-items: flex-start"); expect(emptyState).toContain("width: 100%"); expect(emptyState).toContain("text-align: left"); - expect(emptyCta).toContain("min-height: 36px"); + expect(emptyState).toContain("gap: var(--sp-3)"); + expect(emptyTitle).toContain("font-size: clamp(24px, 6.4vw, 32px)"); + expect(placeholderCopy).toContain("gap: var(--sp-2)"); + expect(emptyCta).toContain("min-height: 38px"); expect(emptyCta).toContain("width: auto"); - expect(emptyCta).toContain("min-width: 132px"); - expect(emptyCta).toContain("border-radius: 8px"); + expect(emptyCta).toContain("min-width: 136px"); + expect(emptyCta).toContain("border-radius: 12px"); + }); + + it("keeps mobile container surfaces on shared radius tokens instead of bespoke rounded-card values", () => { + const dockItem = getRuleBlocksFrom(stylesheet, ".mobile-dock__item").find((block) => + block.includes("display: flex") + ); + const mobileTerminalSheet = getLastRuleBlock(".mobile-terminal-sheet"); + const mobileTerminal = getLastRuleBlock(".mobile-terminal-sheet .bottom-terminal"); + const mobileFilesSurface = getLastRuleBlock(".mobile-sheet--files .file-tree-shell--mobile"); + const supervisorRoot = getLastRuleBlock(".mobile-supervisor-sheet__root"); + const supervisorHeader = getLastRuleBlock(".mobile-supervisor-sheet__detail-header"); + const drawerItem = getLastRuleBlock(".mobile-workspace-drawer__item"); + const drawerFooterButton = getLastRuleBlock(".mobile-workspace-drawer__footer-button"); + const welcomeCard = getLastRuleBlock(".welcome-card--mobile"); + const welcomeFeature = getLastRuleBlock(".welcome-card--mobile .welcome-feature"); + const welcomeButton = getLastRuleBlock(".welcome-card--mobile .welcome-btn"); + const authCard = getLastRuleBlock(".auth-card-shell--mobile"); + const authStatusPanel = getLastRuleBlock(".auth-card-shell--mobile .auth-status-panel"); + const settingsItem = getLastRuleBlock(".settings-mobile-item"); + const settingsItemIconShell = getLastRuleBlock(".settings-mobile-item__icon-shell"); + + expect(dockItem).toBeTruthy(); + expect(dockItem).toContain("border-radius: 0"); + expect(mobileTerminalSheet).not.toContain("linear-gradient("); + expect(mobileTerminal).toContain("border-radius: 0"); + expect(mobileTerminal).not.toContain("var(--radius-xl) var(--radius-xl) 0 0"); + expect(mobileFilesSurface).toContain("border-radius: var(--radius-xl) var(--radius-xl) 0 0"); + expect(supervisorRoot).toContain("border-radius: var(--radius-xl) var(--radius-xl) 0 0"); + expect(supervisorHeader).toContain("border-radius: var(--radius-xl)"); + expect(drawerItem).toContain("border-radius: var(--radius-xl)"); + expect(drawerFooterButton).toContain("border-radius: var(--radius-md)"); + expect(welcomeCard).toContain("border-radius: var(--radius-xl)"); + expect(welcomeFeature).toContain("border-radius: var(--radius-xl)"); + expect(welcomeButton).toContain("border-radius: var(--radius-md)"); + expect(authCard).toContain("border-radius: var(--radius-xl)"); + expect(authStatusPanel).toContain("border-radius: var(--radius-xl)"); + expect(settingsItem).toContain("border-radius: 0"); + expect(settingsItemIconShell).toContain("border-radius: var(--radius-xl)"); }); it("keeps settings content groups and provider controls aligned with editor configuration panels", () => { @@ -820,7 +928,7 @@ describe("components.css theme-sensitive surfaces", () => { expect(settingsFooter).toContain("background: var(--bg-surface)"); expect(settingsFooter).toContain("border-top: 1px solid var(--border)"); expect(settingsFooter).toContain("min-height: 32px"); - expect(settingsFooter).toContain("padding: var(--sp-2) var(--sp-4)"); + expect(settingsFooter).toContain("padding: var(--sp-3) var(--sp-6)"); }); it("keeps shared appearance pills aligned with flat editor option toggles instead of rounded app chips", () => { @@ -984,6 +1092,13 @@ describe("components.css theme-sensitive surfaces", () => { const configCardBase = getRuleBlocksFrom(stylesheet, ".config-card")[0]; const configCardHeaderBase = getRuleBlocksFrom(stylesheet, ".config-card-header")[0]; const configCardBody = getLastRuleBlock(".config-card-body"); + const configCardBodyFillHeight = getLastRuleBlock(".config-card-body--fill-height"); + const configCardBodyFillHeightMonaco = getLastRuleBlock( + ".config-card-body--fill-height > .monaco-host" + ); + const configCardBodyFillHeightActions = getLastRuleBlock( + ".config-card-body--fill-height > .config-card-actions" + ); const configCardActionsBase = getRuleBlocksFrom(stylesheet, ".config-card-actions")[0]; const configCardMobile = getLastGroupedRuleBlockFrom( stylesheet, @@ -1010,6 +1125,11 @@ describe("components.css theme-sensitive surfaces", () => { expect(configCardHeaderBase).not.toContain("cursor: pointer"); expect(configHeaderActionsMobile).toContain("padding-left: var(--sp-4)"); expect(configCardBody).toContain("background: transparent"); + expect(configCardBodyFillHeight).toContain("display: flex"); + expect(configCardBodyFillHeight).toContain("flex-direction: column"); + expect(configCardBodyFillHeightMonaco).toContain("flex: 1"); + expect(configCardBodyFillHeightMonaco).toContain("min-height: 0"); + expect(configCardBodyFillHeightActions).toContain("flex: 0 0 auto"); expect(configCardActionsBase).toContain("border-top: 1px solid var(--border)"); expect(configCardActionsBase).toContain("background: transparent"); expect(configEmptyStateBase).toContain("align-items: flex-start"); @@ -1072,6 +1192,51 @@ describe("components.css theme-sensitive surfaces", () => { expect(desktopPanel).toContain("width: 100%"); }); + it("keeps the desktop file tree search and selected row aligned with the polished panel chrome", () => { + const search = getLastRuleBlock(".file-tree-shell .file-tree-search"); + const searchInput = getLastRuleBlock(".file-tree-shell .file-tree-search-input"); + const row = getLastRuleBlock(".file-tree-shell .tree-item"); + const rowSelected = getLastRuleBlock(".file-tree-shell .tree-item.selected"); + const rowActions = getLastRuleBlock(".file-tree-shell--desktop .tree-item-actions"); + + expect(search).toContain("margin: 8px 12px 10px"); + expect(search).toContain("border-radius: 8px"); + expect(search).toContain("background: color-mix("); + expect(searchInput).toContain("font-size: 13px"); + expect(row).toContain("min-height: 26px"); + expect(row).toContain("border-radius: 8px"); + expect(row).toContain("transition:"); + expect(rowSelected).toContain("border-left: 2px solid"); + expect(rowSelected).toContain("background: color-mix("); + expect(rowActions).toContain("opacity: 0"); + }); + + it("keeps the desktop git panel and command palette on tighter tool-surface chrome", () => { + const gitScroll = getLastRuleBlock(".git-panel-scroll"); + const gitCommitBlock = getLastRuleBlock(".git-commit-block"); + const gitSection = getLastRuleBlock(".git-panel-section"); + const gitWorktreeRow = getLastRuleBlock(".git-worktree-row"); + const gitHistoryRow = getLastRuleBlock(".git-history-row"); + const commandPalette = getLastRuleBlock(".command-palette"); + const commandPaletteDesktop = getLastRuleBlock(".command-palette--desktop"); + const commandPaletteHeader = getLastRuleBlock(".command-palette-header"); + const commandPaletteSearch = getLastRuleBlock(".command-palette-search"); + const commandPaletteItem = getLastRuleBlock(".command-palette--desktop .command-palette-item"); + + expect(gitScroll).toContain("gap: 14px"); + expect(gitCommitBlock).toContain("gap: 10px"); + expect(gitSection).toContain("gap: 8px"); + expect(gitWorktreeRow).toContain("min-height: 28px"); + expect(gitHistoryRow).toContain("min-height: 34px"); + expect(commandPalette).toContain("max-width: var(--desktop-modal-max-width-md)"); + expect(commandPaletteDesktop).toContain("overflow: hidden"); + expect(commandPaletteHeader).toContain("padding: var(--sp-3) var(--sp-4)"); + expect(commandPaletteSearch).toContain("padding: var(--sp-3) var(--sp-4)"); + expect(commandPaletteItem).toContain("align-items: center"); + expect(commandPaletteItem).toContain("gap: var(--sp-3)"); + expect(commandPaletteItem).toContain("padding: var(--sp-3) var(--sp-4)"); + }); + it("keeps session header badges on a single line by truncating the title first", () => { const titleRow = getLastRuleBlock(".mobile-shell__agent-stage .session-title-row"); const title = getLastRuleBlock(".mobile-shell__agent-stage .session-title"); @@ -1087,6 +1252,46 @@ describe("components.css theme-sensitive surfaces", () => { expect(badges).toContain("max-width: 100%"); }); + it("keeps the mobile active session chrome visually flat and aligned with the shell header", () => { + const viewport = getLastRuleBlock(".mobile-shell__viewport"); + const content = getLastRuleBlock(".mobile-shell__content"); + const landscapeViewport = getLastRuleBlock( + ".mobile-shell--landscape-compact .mobile-shell__viewport" + ); + const sessionCard = getLastRuleBlock(".mobile-shell__agent-stage > .session-card"); + const progress = getLastRuleBlock(".mobile-shell__agent-stage .session-progress"); + const header = getLastRuleBlock(".mobile-shell__agent-stage .session-header"); + const headerLeft = getLastRuleBlock(".mobile-shell__agent-stage .session-header-left"); + const titleRow = getLastRuleBlock(".mobile-shell__agent-stage .session-title-row"); + const badges = getLastGroupedRuleBlock( + /\.mobile-shell__agent-stage \.session-provider-badge,\s*\.mobile-shell__agent-stage \.session-state-badge\s*\{([^}]*)\}/g + ); + const supervisorBadge = getLastRuleBlock(".mobile-shell__agent-stage .mobile-supervisor-badge"); + const supervisorLabel = getLastRuleBlock( + ".mobile-shell__agent-stage .mobile-supervisor-badge__label" + ); + + expect(viewport).toContain("padding: 0"); + expect(viewport).toContain("border-top:"); + expect(viewport).not.toContain("padding: 4px"); + expect(landscapeViewport).toContain("padding-top: 0"); + expect(content).toContain("gap: 4px"); + expect(sessionCard).toContain("border-radius: 0"); + expect(sessionCard).toContain("box-shadow: none"); + expect(progress).toContain("display: none"); + expect(header).toContain("padding: 4px"); + expect(header).toContain("border-bottom:"); + expect(header).not.toContain("linear-gradient("); + expect(headerLeft).toContain("gap: 6px"); + expect(titleRow).toContain("gap: 6px"); + expect(badges).toContain("height: 15px"); + expect(badges).toContain("border-radius: 3px"); + expect(supervisorBadge).toContain("min-height: 26px"); + expect(supervisorBadge).toContain("border-radius: 4px"); + expect(supervisorBadge).not.toContain("border-radius: var(--radius-lg)"); + expect(supervisorLabel).toContain("font-size: 11px"); + }); + it("keeps supervisor entry icons and labels vertically centered", () => { const desktopButton = getLastRuleBlock(".supervisor-enable-btn"); const desktopButtonIcon = getLastRuleBlock(".supervisor-enable-btn > .themed-icon"); @@ -1120,8 +1325,16 @@ describe("components.css theme-sensitive surfaces", () => { const shellWithMobileInput = getLastRuleBlock(".xterm-host-shell--mobile-input"); const host = getLastRuleBlock(".xterm-host"); const keybar = getLastRuleBlock(".mobile-terminal-input-bar"); + const actions = getLastRuleBlock(".mobile-terminal-input-bar__actions"); + const actionButton = getLastRuleBlock(".mobile-terminal-input-bar__action"); const keys = getLastRuleBlock(".mobile-terminal-input-bar__keys"); const key = getLastRuleBlock(".mobile-terminal-input-bar__key"); + const ctrlKey = getLastRuleBlock(".mobile-terminal-input-bar__ctrl"); + const shiftKey = getLastRuleBlock(".mobile-terminal-input-bar__shift"); + const escapeKey = getLastRuleBlock('.mobile-terminal-input-bar__key[aria-label="Escape"]'); + const tabKey = getLastRuleBlock('.mobile-terminal-input-bar__key[aria-label="Tab"]'); + const enterKey = getLastRuleBlock('.mobile-terminal-input-bar__key[aria-label="Enter"]'); + const upArrowKey = getLastRuleBlock('.mobile-terminal-input-bar__key[aria-label="Up arrow"]'); const ctrlLocked = getLastRuleBlock( '.mobile-terminal-input-bar__ctrl[data-ctrl-mode="locked"]' ); @@ -1132,24 +1345,75 @@ describe("components.css theme-sensitive surfaces", () => { expect(shell).toContain("display: flex"); expect(shell).toContain("flex-direction: column"); expect(shell).toContain("min-height: 0"); - expect(shellWithMobileInput).toContain("gap: var(--sp-1)"); + expect(shellWithMobileInput).toContain("gap: 0"); expect(host).toContain("position: relative"); expect(host).toContain("flex: 1 1 auto"); expect(host).toContain("min-height: 0"); expect(keybar).toContain("flex-shrink: 0"); expect(keybar).not.toContain("position: absolute"); expect(keybar).toContain("min-width: 0"); - expect(keybar).toContain("padding:"); expect(keybar).toContain("border-top:"); + expect(keybar).not.toContain("border-bottom:"); + expect(actions).toContain("border-right:"); + expect(actions).toContain("gap: 3px"); + expect(actions).toContain("margin-right: var(--sp-1)"); + expect(actions).toContain("padding-right: var(--sp-1)"); + expect(actionButton).toContain("min-height: 20px"); + expect(actionButton).toContain("font-size: 9px"); expect(keys).toContain("display: flex"); expect(keys).toContain("overflow-x: auto"); + expect(keys).toContain("gap: 3px"); expect(key).toContain("min-height: 20px"); - expect(key).toContain("flex: 0 0 auto"); - expect(key).toContain("var(--bg-page)"); + expect(key).toContain("font-size: 9px"); + expect(key).toContain("border-radius: var(--radius-sm)"); + expect(key).not.toContain("border-radius: 999px"); + expect(ctrlKey).toContain("min-width: 28px"); + expect(shiftKey).toContain("min-width: 28px"); + expect(escapeKey).toContain("min-width: 28px"); + expect(tabKey).toContain("min-width: 28px"); + expect(enterKey).toContain("min-width: 34px"); + expect(upArrowKey).toContain("min-width: 20px"); expect(ctrlLocked).toContain("var(--accent-blue)"); expect(shiftArmed).toContain("var(--accent-blue)"); }); + it("keeps the mobile fullscreen terminal chrome on a single compact tool surface", () => { + const terminalSheet = getLastRuleBlock(".mobile-terminal-sheet"); + const mobileTerminal = getLastRuleBlock(".mobile-terminal-sheet .bottom-terminal"); + const toolbar = getLastRuleBlock(".bottom-terminal--mobile-fullscreen .terminal-toolbar"); + const mobileToolbarRow = getLastRuleBlock( + ".bottom-terminal--mobile-fullscreen .terminal-toolbar-mobile-row" + ); + const selector = getLastRuleBlock(".bottom-terminal--mobile-fullscreen .terminal-selector"); + const selectorButton = getLastRuleBlock( + ".bottom-terminal--mobile-fullscreen .terminal-selector-btn" + ); + const placeholder = getLastRuleBlock( + ".bottom-terminal--mobile-fullscreen .terminal-toolbar-mobile-placeholder" + ); + const panelButton = getLastRuleBlock(".bottom-terminal--mobile-fullscreen .panel-toolbar-btn"); + const xterm = getLastRuleBlock(".bottom-terminal--mobile-fullscreen .bottom-terminal-xterm"); + + expect(terminalSheet).toContain("padding: 0"); + expect(terminalSheet).not.toContain("linear-gradient("); + expect(mobileTerminal).toContain("border-radius: 0"); + expect(mobileTerminal).toContain("box-shadow: none"); + expect(toolbar).toContain("min-height: 32px"); + expect(toolbar).toContain("padding: 0 var(--sp-2)"); + expect(toolbar).not.toContain("background: linear-gradient("); + expect(mobileToolbarRow).toContain("display: flex"); + expect(mobileToolbarRow).toContain("align-items: center"); + expect(mobileToolbarRow).toContain("width: 100%"); + expect(selector).toContain("flex: 1"); + expect(selectorButton).toContain("border-radius: var(--radius-sm)"); + expect(selectorButton).not.toContain("border-radius: 999px"); + expect(placeholder).toContain("border: none"); + expect(placeholder).toContain("background: transparent"); + expect(panelButton).toContain("border-radius: var(--radius-sm)"); + expect(panelButton).not.toContain("border-radius: 999px"); + expect(xterm).toContain("padding: 0 var(--sp-2) var(--sp-1)"); + }); + it("keeps the supervisor timeout setting aligned as a label-left control-right row", () => { const inlineFieldBase = getRuleBlocksFrom(stylesheet, ".settings-config-field--inline")[0]; const inlineFieldMobile = getLastRuleBlock(".settings-config-field--inline"); diff --git a/packages/web/src/styles/tokens.css b/packages/web/src/styles/tokens.css index 23d701b20..36e5c54a6 100644 --- a/packages/web/src/styles/tokens.css +++ b/packages/web/src/styles/tokens.css @@ -94,6 +94,14 @@ /* Topbar */ --topbar-height: 36px; + --desktop-topbar-height: 40px; + --desktop-statusbar-height: 28px; + --desktop-sidebar-header-height: 40px; + --desktop-panel-padding: 12px; + --desktop-panel-gap: 12px; + --desktop-content-max-width: 880px; + --desktop-modal-max-width-md: 680px; + --desktop-modal-max-width-lg: 840px; /* Safe areas */ --mobile-safe-top: env(safe-area-inset-top, 0px); @@ -153,6 +161,7 @@ --text-primary: #e5edf3; --text-secondary: #9fb0bc; --text-tertiary: #728492; + --text-muted: var(--text-tertiary); --text-disabled: #4a5b6a; --text-inverse: #0a1014; @@ -161,6 +170,7 @@ --accent-green: #78d7b2; --accent-amber: #f1b86a; --accent-pink: #ff9eb0; + --accent-red: var(--color-error); --accent-purple: #c792ea; /* ========== Semantic Colors ========== */ @@ -168,6 +178,8 @@ --color-warning: #f1b86a; --color-error: #ff9eb0; --color-info: #6cb6ff; + --bg-panel: color-mix(in srgb, var(--bg-surface) 94%, var(--bg-page)); + --bg-elevated: color-mix(in srgb, var(--bg-surface) 98%, white 2%); /* ========== Icon Tokens ========== */ --icon-primary: var(--text-primary); @@ -229,6 +241,7 @@ --text-primary: #1f2328; --text-secondary: #557067; --text-tertiary: #8b949e; + --text-muted: var(--text-tertiary); --text-disabled: #6e7681; --text-inverse: #ffffff; @@ -237,6 +250,7 @@ --accent-green: #1a7f37; --accent-amber: #9a6700; --accent-pink: #cf222e; + --accent-red: var(--color-error); --accent-purple: #8250df; /* ========== Semantic Colors ========== */ @@ -244,6 +258,8 @@ --color-warning: #9a6700; --color-error: #cf222e; --color-info: #148a7a; + --bg-panel: color-mix(in srgb, var(--bg-surface) 92%, var(--bg-sidebar)); + --bg-elevated: color-mix(in srgb, var(--bg-surface) 96%, white 4%); /* ========== Icon Tokens ========== */ --icon-primary: var(--text-primary); @@ -305,6 +321,7 @@ --text-primary: #e6e6e6; --text-secondary: #a8b0ba; --text-tertiary: #7d8590; + --text-muted: var(--text-tertiary); --text-disabled: #5d6672; --text-inverse: #111317; @@ -313,6 +330,7 @@ --accent-green: #8ddb8c; --accent-amber: #dcb86a; --accent-pink: #ff7b72; + --accent-red: var(--color-error); --accent-purple: #c099ff; /* ========== Semantic Colors ========== */ @@ -320,6 +338,8 @@ --color-warning: #dcb86a; --color-error: #ff7b72; --color-info: #7aa2f7; + --bg-panel: color-mix(in srgb, var(--bg-surface) 94%, var(--bg-page)); + --bg-elevated: color-mix(in srgb, var(--bg-surface) 98%, white 2%); /* ========== Icon Tokens ========== */ --icon-primary: var(--text-primary); @@ -381,6 +401,7 @@ --text-primary: #1f2933; --text-secondary: #4d5b6a; --text-tertiary: #7b8794; + --text-muted: var(--text-tertiary); --text-disabled: #9aa5b1; --text-inverse: #ffffff; @@ -389,6 +410,7 @@ --accent-green: #15803d; --accent-amber: #a16207; --accent-pink: #d14343; + --accent-red: var(--color-error); --accent-purple: #7c3aed; /* ========== Semantic Colors ========== */ @@ -396,6 +418,8 @@ --color-warning: #a16207; --color-error: #d14343; --color-info: #315fdd; + --bg-panel: color-mix(in srgb, var(--bg-surface) 92%, var(--bg-sidebar)); + --bg-elevated: color-mix(in srgb, var(--bg-surface) 96%, white 4%); /* ========== Icon Tokens ========== */ --icon-primary: var(--text-primary); @@ -457,6 +481,7 @@ --text-primary: #d8dee9; --text-secondary: #b5c1d2; --text-tertiary: #7b88a1; + --text-muted: var(--text-tertiary); --text-disabled: #616e88; --text-inverse: #2e3440; @@ -465,6 +490,7 @@ --accent-green: #a3be8c; --accent-amber: #ebcb8b; --accent-pink: #bf616a; + --accent-red: var(--color-error); --accent-purple: #b48ead; /* ========== Semantic Colors ========== */ @@ -472,6 +498,8 @@ --color-warning: #ebcb8b; --color-error: #bf616a; --color-info: #88c0d0; + --bg-panel: color-mix(in srgb, var(--bg-surface) 94%, var(--bg-page)); + --bg-elevated: color-mix(in srgb, var(--bg-surface) 98%, white 2%); /* ========== Icon Tokens ========== */ --icon-primary: var(--text-primary); @@ -533,6 +561,7 @@ --text-primary: #2e3440; --text-secondary: #4d5a6f; --text-tertiary: #7b88a1; + --text-muted: var(--text-tertiary); --text-disabled: #93a0b7; --text-inverse: #ffffff; @@ -541,6 +570,7 @@ --accent-green: #4c7a5b; --accent-amber: #a77f2f; --accent-pink: #bf616a; + --accent-red: var(--color-error); --accent-purple: #8f5b9c; /* ========== Semantic Colors ========== */ @@ -548,6 +578,8 @@ --color-warning: #a77f2f; --color-error: #bf616a; --color-info: #5b7fa8; + --bg-panel: color-mix(in srgb, var(--bg-surface) 92%, var(--bg-sidebar)); + --bg-elevated: color-mix(in srgb, var(--bg-surface) 96%, white 4%); /* ========== Icon Tokens ========== */ --icon-primary: var(--text-primary); @@ -609,6 +641,7 @@ --text-primary: #ffffff; --text-secondary: #f0f0f0; --text-tertiary: #d0d0d0; + --text-muted: var(--text-tertiary); --text-disabled: #8f8f8f; --text-inverse: #000000; @@ -617,6 +650,7 @@ --accent-green: #00ff7f; --accent-amber: #ffff66; --accent-pink: #ff4d4d; + --accent-red: var(--color-error); --accent-purple: #ff7fff; /* ========== Semantic Colors ========== */ @@ -624,6 +658,8 @@ --color-warning: #ffff66; --color-error: #ff4d4d; --color-info: #66b3ff; + --bg-panel: color-mix(in srgb, var(--bg-surface) 92%, #101010); + --bg-elevated: color-mix(in srgb, var(--bg-surface) 98%, white 2%); /* ========== Icon Tokens ========== */ --icon-primary: #ffffff; @@ -685,6 +721,7 @@ --text-primary: #000000; --text-secondary: #111111; --text-tertiary: #333333; + --text-muted: var(--text-tertiary); --text-disabled: #5c5c5c; --text-inverse: #ffffff; @@ -693,6 +730,7 @@ --accent-green: #006b3c; --accent-amber: #7a5c00; --accent-pink: #b00020; + --accent-red: var(--color-error); --accent-purple: #7a00cc; /* ========== Semantic Colors ========== */ @@ -700,6 +738,8 @@ --color-warning: #7a5c00; --color-error: #b00020; --color-info: #0037da; + --bg-panel: color-mix(in srgb, var(--bg-surface) 92%, #f0f0f0); + --bg-elevated: color-mix(in srgb, var(--bg-surface) 96%, white 4%); /* ========== Icon Tokens ========== */ --icon-primary: #000000; diff --git a/packages/web/src/ui-preview/catalog.test.tsx b/packages/web/src/ui-preview/catalog.test.tsx index 740beef12..469a3736c 100644 --- a/packages/web/src/ui-preview/catalog.test.tsx +++ b/packages/web/src/ui-preview/catalog.test.tsx @@ -1,7 +1,9 @@ -import { fireEvent, render, screen } from "@testing-library/react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { Provider } from "jotai"; import { MemoryRouter, Route, Routes } from "react-router-dom"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { terminalOutputAtomFamily } from "../features/terminal-panel/atoms"; +import { getThemeById } from "../theme"; import { getUiPreviewScene, UI_PREVIEW_SCENES } from "./catalog"; import { buildUiPreviewStore } from "./preview-store"; @@ -24,17 +26,25 @@ function installMatchMedia(device: "desktop" | "mobile") { }); } -function renderScene(sceneId: string, device: "desktop" | "mobile" = "desktop") { +function renderScene( + sceneId: string, + device: "desktop" | "mobile" = "desktop", + theme: "mint-dark" | "mint-light" = "mint-dark" +) { const scene = getUiPreviewScene(sceneId); if (!scene) { throw new Error(`Missing scene ${sceneId}`); } installMatchMedia(device); - const context = { theme: "mint-dark" as const, locale: "en" as const, device }; + const context = { theme, locale: "en" as const, device }; const store = buildUiPreviewStore(scene.seed(context)); const router = scene.router(context); + document.documentElement.setAttribute("data-theme", getThemeById(theme).documentThemeAttr); + document.documentElement.setAttribute("lang", "en"); + document.body.dataset.uiPreviewDevice = device; + return render( @@ -56,6 +66,9 @@ describe("UI preview catalog", () => { afterEach(() => { window.matchMedia = originalMatchMedia; + document.documentElement.removeAttribute("data-theme"); + document.documentElement.removeAttribute("lang"); + delete document.body.dataset.uiPreviewDevice; }); it("registers unique first-batch page scene ids", () => { @@ -140,6 +153,58 @@ describe("UI preview catalog", () => { expect(document.querySelector(".launch-modal, .mobile-sheet--launch")).toBeTruthy(); }); + it("renders the mobile terminal showcase without the replay loading overlay", async () => { + renderScene("mobile-terminal-sheet", "mobile"); + + await waitFor(() => { + expect(screen.queryByText("Restoring terminal output...")).not.toBeInTheDocument(); + }); + expect(document.querySelector(".mobile-sheet--terminal")).toBeTruthy(); + expect(screen.getByRole("button", { name: "New Terminal" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Close Terminal" })).toBeInTheDocument(); + expect( + document.querySelector(".mobile-sheet--terminal .terminal-toolbar-mobile-row") + ).toBeTruthy(); + expect( + document.querySelector( + '.mobile-sheet--terminal .page-header__actions [aria-label="New Terminal"]' + ) + ).toBeNull(); + expect( + document.querySelector( + '.mobile-sheet--terminal .terminal-toolbar [aria-label="New Terminal"]' + ) + ).toBeTruthy(); + expect(document.querySelector(".mobile-sheet--terminal .workspace-status-bar")).toBeTruthy(); + expect(document.querySelector(".mobile-sheet--terminal .terminal-toolbar-left")).toBeNull(); + }); + + it("captures the mobile terminal showcase from the fullscreen terminal sheet root", () => { + const scene = getUiPreviewScene("mobile-terminal-sheet"); + expect(scene?.capture?.selector).toBe(".mobile-sheet--terminal"); + expect(scene?.devices).toEqual(["mobile"]); + }); + + it("keeps mobile terminal showcase history in replay state instead of preloading live output", () => { + const scene = getUiPreviewScene("mobile-terminal-sheet"); + if (!scene) { + throw new Error("Missing mobile-terminal-sheet scene"); + } + + const store = buildUiPreviewStore( + scene.seed({ + theme: "mint-dark", + locale: "en", + device: "mobile", + }) + ); + + expect(store.get(terminalOutputAtomFamily("term-preview-1"))).toEqual({ + chunks: [], + lastSeq: 0, + }); + }); + it("renders the shortcuts settings scene with the shortcuts list", async () => { renderScene("settings-shortcuts"); @@ -151,8 +216,11 @@ describe("UI preview catalog", () => { it("renders the mobile settings root scene as the section list", async () => { renderScene("settings-mobile-root", "mobile"); - expect(await screen.findByText("Settings")).toBeInTheDocument(); - expect(document.querySelector(".settings-mobile-list")).toBeTruthy(); + expect(await screen.findByRole("button", { name: /general/i })).toBeInTheDocument(); + expect(document.querySelector(".settings-mobile-root")).toBeTruthy(); + expect(document.querySelector(".settings-mobile-group")).toBeTruthy(); + expect(document.querySelector(".settings-mobile-group__list")).toBeTruthy(); + expect(document.querySelector(".settings-mobile-root-hero")).toBeNull(); }); it("renders the app loading shell scene without bootstrapping routes", async () => { @@ -198,4 +266,100 @@ describe("UI preview catalog", () => { expect(await screen.findByText("Workspace opened")).toBeInTheDocument(); expect(document.querySelectorAll(".toast").length).toBeGreaterThanOrEqual(4); }); + + it("renders the workspace topbar review scene", async () => { + renderScene("workspace-topbar-review"); + + expect(await screen.findByRole("tablist", { name: "Workspace tabs" })).toBeInTheDocument(); + expect(document.querySelector(".desktop-review-card--topbar .app-topbar")).toBeTruthy(); + }); + + it("renders the workspace sidebar files review scene", async () => { + renderScene("workspace-sidebar-files-review"); + + expect(await screen.findByText("packages")).toBeInTheDocument(); + expect(document.querySelector(".desktop-review-card--sidebar .file-tree-shell")).toBeTruthy(); + }); + + it("renders the workspace sidebar git review scene", async () => { + renderScene("workspace-sidebar-git-review"); + + expect(await screen.findByText(/changes|更改/i)).toBeInTheDocument(); + expect(document.querySelector(".desktop-review-card--sidebar .git-panel")).toBeTruthy(); + }); + + it("renders the workspace editor review scene", async () => { + renderScene("workspace-editor-review"); + + expect( + await screen.findByText("packages/web/src/features/settings/components/settings-page.tsx") + ).toBeInTheDocument(); + expect( + document.querySelector(".desktop-review-card--editor .workspace-git-editor") + ).toBeTruthy(); + expect(screen.getByText("const headerTitle = isMobile")).toBeInTheDocument(); + }); + + it("renders the workspace diff review scene", async () => { + renderScene("workspace-diff-review"); + + expect(await screen.findByText("packages/web/src/styles/components.css")).toBeInTheDocument(); + expect(document.querySelector(".desktop-review-card--diff .workspace-git-editor")).toBeTruthy(); + expect(screen.getByText(/\+\s+background: var\(--bg-elevated\);/)).toBeInTheDocument(); + }); + + it("renders the workspace terminal empty review scene", async () => { + renderScene("workspace-terminal-empty-review"); + + expect(await screen.findByText(/no terminal|暂无终端/i)).toBeInTheDocument(); + expect( + document.querySelector(".desktop-review-card--terminal .bottom-terminal-empty") + ).toBeTruthy(); + }); + + it("renders the settings density review scene", async () => { + renderScene("settings-density-review"); + + expect(await screen.findByRole("heading", { name: /settings|设置/i })).toBeInTheDocument(); + expect(document.querySelector(".settings-header__desktop")).toBeTruthy(); + expect(document.querySelector(".settings-sidebar")).toBeTruthy(); + }); + + it("renders the settings light theme review scene", async () => { + renderScene("settings-light-theme-review", "desktop", "mint-light"); + + expect(await screen.findByRole("heading", { name: /settings|设置/i })).toBeInTheDocument(); + expect(document.querySelector(".settings-page")).toBeTruthy(); + expect(document.querySelector(".settings-nav-item")).toBeTruthy(); + expect(document.documentElement).toHaveAttribute("data-theme", "mint-light"); + }); + + it("renders the desktop overlay review scene", async () => { + renderScene("desktop-overlay-review"); + + expect(await screen.findByText("Open Workspace")).toBeInTheDocument(); + expect(document.querySelector(".desktop-review-grid")).toBeTruthy(); + expect(document.querySelector(".desktop-review-card .command-palette")).toBeTruthy(); + expect(document.querySelector(".desktop-review-card .launch-modal")).toBeTruthy(); + expect( + document.querySelector(".desktop-review-card .desktop-review-embedded-worktree") + ).toBeTruthy(); + + const embeddedSurface = document.querySelector( + ".desktop-review-card--worktree .worktree-manager-surface" + ); + + expect(embeddedSurface).toBeTruthy(); + expect(embeddedSurface?.closest(".desktop-review-card--worktree")).toBeTruthy(); + expect(document.querySelector(".desktop-review-card--worktree .modal-overlay")).toBeNull(); + }); + + it("renders the desktop statusbar review scene", async () => { + renderScene("desktop-statusbar-review"); + + expect(await screen.findByText("main")).toBeInTheDocument(); + expect( + document.querySelector(".desktop-review-card--statusbar .workspace-status-bar") + ).toBeTruthy(); + }); }); diff --git a/packages/web/src/ui-preview/catalog.ts b/packages/web/src/ui-preview/catalog.ts index 125fe6534..533a6964c 100644 --- a/packages/web/src/ui-preview/catalog.ts +++ b/packages/web/src/ui-preview/catalog.ts @@ -13,6 +13,7 @@ import type { UiPreviewSeed, UiPreviewTheme, } from "./preview-store"; +import { createDesktopReviewScenes } from "./scenes/desktop-review-scenes"; import { createPageScenes } from "./scenes/page-scenes"; import { createShowcaseScenes } from "./scenes/showcase-scenes"; @@ -34,6 +35,7 @@ export interface UiPreviewSceneDefinition extends UiPreviewSceneMetadata { export const UI_PREVIEW_SCENES: UiPreviewSceneDefinition[] = [ ...createPageScenes(), ...createShowcaseScenes(), + ...createDesktopReviewScenes(), ]; export function getUiPreviewScene(id: string): UiPreviewSceneDefinition | null { diff --git a/packages/web/src/ui-preview/preview-store.ts b/packages/web/src/ui-preview/preview-store.ts index 4b0a0fbcd..1ea0298a9 100644 --- a/packages/web/src/ui-preview/preview-store.ts +++ b/packages/web/src/ui-preview/preview-store.ts @@ -32,7 +32,7 @@ import { supervisorDialogAtom, supervisorsAtom, } from "../features/supervisor/atoms"; -import { terminalMetaAtomFamily, terminalOutputAtomFamily } from "../features/terminal-panel/atoms"; +import { terminalMetaAtomFamily } from "../features/terminal-panel/atoms"; import { activeFilePathAtomFamily, branchQuickPickAtom, @@ -144,6 +144,26 @@ function ok(data: T) { return { ok: true as const, data }; } +function getTerminalPreviewBytes(seed: UiPreviewSeed, terminalId: string): Uint8Array { + const chunks = seed.terminalOutputById?.[terminalId] ?? []; + if (chunks.length === 0) { + return new Uint8Array(); + } + + const totalLength = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0); + const bytes = new Uint8Array(totalLength); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + return bytes; +} + +function getTerminalPreviewSeq(seed: UiPreviewSeed, terminalId: string): number { + return getTerminalPreviewBytes(seed, terminalId).byteLength; +} + function err(message: string) { return { ok: false as const, @@ -304,11 +324,41 @@ function createPreviewDispatcher(seed: UiPreviewSeed): DispatchCommand { } as unknown as T); } + if (op === "terminal.snapshot") { + const terminalId = (args as { terminalId?: string })?.terminalId ?? ""; + const bytes = getTerminalPreviewBytes(seed, terminalId); + return ok({ + status: "ok", + transport: "binary", + streamId: 1, + size: bytes.byteLength, + seq: getTerminalPreviewSeq(seed, terminalId), + rows: 28, + cols: 120, + source: "headless", + bytes, + } as unknown as T); + } + + if (op === "terminal.replay") { + const terminalId = (args as { terminalId?: string })?.terminalId ?? ""; + const lastSeq = (args as { lastSeq?: number })?.lastSeq ?? 0; + const fullBytes = getTerminalPreviewBytes(seed, terminalId); + const replayBytes = fullBytes.subarray(Math.max(0, lastSeq)); + return ok({ + status: "ok", + transport: "binary", + streamId: 1, + size: replayBytes.byteLength, + seq: fullBytes.byteLength, + bytes: replayBytes, + } as unknown as T); + } + if ( op === "terminal.close" || op === "terminal.resize" || op === "terminal.input" || - op === "terminal.replay" || op === "provider.runtimeStatus" || op === "provider.install.start" || op === "provider.install.get" || @@ -438,12 +488,5 @@ export function buildUiPreviewStore(seed: UiPreviewSeed): Store { store.set(terminalMetaAtomFamily(terminalId), meta); } - for (const [terminalId, chunks] of Object.entries(seed.terminalOutputById ?? {})) { - store.set(terminalOutputAtomFamily(terminalId), { - chunks, - lastSeq: chunks.length, - }); - } - return store; } diff --git a/packages/web/src/ui-preview/scene-metadata.test.ts b/packages/web/src/ui-preview/scene-metadata.test.ts index ec3a73116..8e940051f 100644 --- a/packages/web/src/ui-preview/scene-metadata.test.ts +++ b/packages/web/src/ui-preview/scene-metadata.test.ts @@ -17,6 +17,23 @@ describe("ui preview scene metadata", () => { ); }); + it("registers the desktop review scene ids", () => { + expect(UI_PREVIEW_SCENE_METADATA.map((scene) => scene.id)).toEqual( + expect.arrayContaining([ + "workspace-topbar-review", + "workspace-sidebar-files-review", + "workspace-sidebar-git-review", + "workspace-editor-review", + "workspace-diff-review", + "workspace-terminal-empty-review", + "settings-density-review", + "settings-light-theme-review", + "desktop-overlay-review", + "desktop-statusbar-review", + ]) + ); + }); + it("covers every built-in theme for route-backed workspace scenes", () => { expect( UI_PREVIEW_SCENE_METADATA.filter( @@ -30,4 +47,19 @@ describe("ui preview scene metadata", () => { it("enumerates concrete theme ids instead of dark/light buckets", () => { expect(source).not.toContain('themeIdsForKinds("dark", "light")'); }); + + it("limits the light-theme review scene to desktop light themes", () => { + const scene = UI_PREVIEW_SCENE_METADATA.find( + (entry) => entry.id === "settings-light-theme-review" + ); + + expect(scene?.devices).toEqual(["desktop"]); + expect(scene?.themes.every((theme) => theme.endsWith("-light"))).toBe(true); + }); + + it("captures the mobile settings homepage from the grouped root container", () => { + const scene = UI_PREVIEW_SCENE_METADATA.find((entry) => entry.id === "settings-mobile-root"); + + expect(scene?.capture?.selector).toBe(".settings-mobile-root"); + }); }); diff --git a/packages/web/src/ui-preview/scene-metadata.ts b/packages/web/src/ui-preview/scene-metadata.ts index 076cd6576..60297df44 100644 --- a/packages/web/src/ui-preview/scene-metadata.ts +++ b/packages/web/src/ui-preview/scene-metadata.ts @@ -34,6 +34,10 @@ function allThemeIds(): UiPreviewSceneTheme[] { return [...THEME_IDS] as UiPreviewSceneTheme[]; } +function lightThemeIds(): UiPreviewSceneTheme[] { + return THEME_IDS.filter((themeId) => themeId.endsWith("-light")) as UiPreviewSceneTheme[]; +} + export const UI_PREVIEW_SCENE_METADATA: UiPreviewSceneMetadata[] = [ { id: "welcome", @@ -99,7 +103,7 @@ export const UI_PREVIEW_SCENE_METADATA: UiPreviewSceneMetadata[] = [ devices: ["mobile"], themes: allThemeIds(), locales: ["zh", "en"], - capture: { selector: ".settings-mobile-list" }, + capture: { selector: ".settings-mobile-root" }, }, { id: "app-loading-shell", @@ -286,7 +290,7 @@ export const UI_PREVIEW_SCENE_METADATA: UiPreviewSceneMetadata[] = [ category: "sheet", source: "showcase", description: - "Mobile terminal fullscreen sheet using xterm placeholder chrome instead of live ws runtime.", + "Mobile terminal fullscreen sheet with compact tool chrome and status bar for screenshot comparison.", devices: ["mobile"], themes: allThemeIds(), locales: ["zh", "en"], @@ -314,6 +318,117 @@ export const UI_PREVIEW_SCENE_METADATA: UiPreviewSceneMetadata[] = [ locales: ["zh", "en"], capture: { selector: ".supervisor-dialog" }, }, + { + id: "workspace-topbar-review", + title: "Workspace / Topbar Review", + category: "page", + source: "showcase", + description: "Desktop topbar chrome review with seeded workspaces and quick actions.", + devices: ["desktop"], + themes: allThemeIds(), + locales: ["zh", "en"], + capture: { selector: ".desktop-review-card--topbar" }, + }, + { + id: "workspace-sidebar-files-review", + title: "Workspace / Sidebar Files Review", + category: "page", + source: "showcase", + description: "Desktop file tree sidebar review with seeded search and file hierarchy.", + devices: ["desktop"], + themes: allThemeIds(), + locales: ["zh", "en"], + capture: { selector: ".desktop-review-card--sidebar" }, + }, + { + id: "workspace-sidebar-git-review", + title: "Workspace / Sidebar Git Review", + category: "page", + source: "showcase", + description: "Desktop git sidebar review with seeded status, history, and worktrees.", + devices: ["desktop"], + themes: allThemeIds(), + locales: ["zh", "en"], + capture: { selector: ".desktop-review-card--sidebar" }, + }, + { + id: "workspace-editor-review", + title: "Workspace / Editor Review", + category: "page", + source: "showcase", + description: "Desktop editor review with code chrome, dirty state, and static content density.", + devices: ["desktop"], + themes: allThemeIds(), + locales: ["zh", "en"], + capture: { selector: ".desktop-review-card--editor" }, + }, + { + id: "workspace-diff-review", + title: "Workspace / Diff Review", + category: "page", + source: "showcase", + description: "Desktop diff review with populated git preview lines and editor-surface chrome.", + devices: ["desktop"], + themes: allThemeIds(), + locales: ["zh", "en"], + capture: { selector: ".desktop-review-card--diff" }, + }, + { + id: "workspace-terminal-empty-review", + title: "Workspace / Terminal Empty Review", + category: "empty", + source: "showcase", + description: "Desktop terminal empty state review for bottom panel density and hierarchy.", + devices: ["desktop"], + themes: allThemeIds(), + locales: ["zh", "en"], + capture: { selector: ".desktop-review-card--terminal" }, + }, + { + id: "settings-density-review", + title: "Settings / Density Review", + category: "page", + source: "showcase", + description: "Desktop settings page review for spacing, grouping, and information density.", + devices: ["desktop"], + themes: allThemeIds(), + locales: ["zh", "en"], + capture: { selector: ".settings-page" }, + }, + { + id: "settings-light-theme-review", + title: "Settings / Light Theme Review", + category: "page", + source: "showcase", + description: + "Desktop settings review constrained to light themes for surface hierarchy checks.", + devices: ["desktop"], + themes: lightThemeIds(), + locales: ["zh", "en"], + capture: { selector: ".settings-page" }, + }, + { + id: "desktop-overlay-review", + title: "Desktop / Overlay Review", + category: "modal", + source: "showcase", + description: "Desktop overlay comparison for command palette, launcher, and worktree manager.", + devices: ["desktop"], + themes: allThemeIds(), + locales: ["zh", "en"], + capture: { selector: ".desktop-review-grid" }, + }, + { + id: "desktop-statusbar-review", + title: "Desktop / Status Bar Review", + category: "page", + source: "showcase", + description: "Desktop status bar review with seeded branch and file-change state.", + devices: ["desktop"], + themes: allThemeIds(), + locales: ["zh", "en"], + capture: { selector: ".desktop-review-card--statusbar" }, + }, { id: "worktree-manager", title: "Worktree Manager", diff --git a/packages/web/src/ui-preview/scenes/desktop-review-scenes.tsx b/packages/web/src/ui-preview/scenes/desktop-review-scenes.tsx new file mode 100644 index 000000000..880faac3c --- /dev/null +++ b/packages/web/src/ui-preview/scenes/desktop-review-scenes.tsx @@ -0,0 +1,479 @@ +import type { + FileNode, + GitCommitSummary, + GitStatus, + Workspace, + WorktreeInfo, +} from "@coder-studio/core"; +import { CommandPalette } from "../../features/command-palette"; +import { SettingsPage } from "../../features/settings"; +import { TerminalPanel } from "../../features/terminal-panel"; +import { TopBar } from "../../features/topbar"; +import { FileTreePanel } from "../../features/workspace/views/shared/file-tree-panel"; +import { GitDiffViewer } from "../../features/workspace/views/shared/git-diff-viewer"; +import { GitPanel } from "../../features/workspace/views/shared/git-panel"; +import { WorkspaceLaunchModal } from "../../features/workspace/views/shared/workspace-launch-modal"; +import { WorkspaceStatusBar } from "../../features/workspace/views/shared/workspace-status-bar"; +import { WorktreeManagerSurface } from "../../features/workspace/views/shared/worktree-manager-surface"; +import type { UiPreviewSceneDefinition } from "../catalog"; +import { getUiPreviewSceneMetadata } from "../scene-metadata"; + +const workspace: Workspace = { + id: "ws-review", + name: "coder-studio", + path: "/home/spencer/workspace/coder-studio", + targetRuntime: "native", + openedAt: 1, + lastActiveAt: 1, + uiState: { + leftPanelWidth: 280, + bottomPanelHeight: 220, + focusMode: false, + activeSessionId: undefined, + paneLayout: { id: "root", type: "leaf" }, + }, +}; + +const gitStatus: GitStatus = { + branch: "main", + ahead: 1, + behind: 0, + staged: [{ path: "packages/web/src/styles/tokens.css", status: "modified" }], + modified: [ + { path: "packages/web/src/styles/components.css", status: "modified" }, + { path: "packages/web/src/ui-preview/scenes/desktop-review-scenes.tsx", status: "modified" }, + ], + untracked: [ + { path: "e2e-ui/output/screenshots/page/workspace-topbar-review.png", status: "untracked" }, + ], + deleted: [], +}; + +const gitHistory: GitCommitSummary[] = [ + { + sha: "8f5f7d54d9f8b7f08f483699f2f16c3c442f0a11", + shortSha: "8f5f7d5", + subject: "style: tighten workspace desktop chrome", + authorName: "Spencer", + authoredAt: 1715731200000, + }, + { + sha: "f1c44d42d2b8ef3c83bb8aeeb0898422bf94b31c", + shortSha: "f1c44d4", + subject: "test: add ui preview desktop review scenes", + authorName: "Spencer", + authoredAt: 1715644800000, + }, +]; + +const worktrees: WorktreeInfo[] = [ + { + name: "main", + path: "/home/spencer/workspace/coder-studio", + branch: "main", + commit: "8f5f7d5", + status: "clean", + }, + { + name: "feat-pc-style-polish", + path: "/home/spencer/workspace/coder-studio/.worktrees/feat-pc-style-polish", + branch: "feat/pc-style-polish", + commit: "f1c44d4", + status: "dirty", + }, +]; + +const worktreeStatusByPath: Record = { + [worktrees[1].path]: gitStatus, +}; + +const worktreeTreeByPath: Record = { + [worktrees[1].path]: [ + { name: "packages", path: "packages", kind: "dir" }, + { name: "docs", path: "docs", kind: "dir" }, + { name: "README.md", path: "README.md", kind: "file" }, + ], +}; + +const rootTree: FileNode[] = [ + { name: "packages", path: "packages", kind: "dir" }, + { name: "docs", path: "docs", kind: "dir" }, + { name: "README.md", path: "README.md", kind: "file" }, +]; + +const packagesTree: FileNode[] = [ + { name: "web", path: "packages/web", kind: "dir" }, + { name: "core", path: "packages/core", kind: "dir" }, +]; + +const webTree: FileNode[] = [ + { name: "src", path: "packages/web/src", kind: "dir" }, + { name: "package.json", path: "packages/web/package.json", kind: "file" }, +]; + +const srcTree: FileNode[] = [ + { name: "styles", path: "packages/web/src/styles", kind: "dir" }, + { name: "ui-preview", path: "packages/web/src/ui-preview", kind: "dir" }, + { name: "app.tsx", path: "packages/web/src/app.tsx", kind: "file" }, +]; + +const searchResults: FileNode[] = [ + { name: "components.css", path: "packages/web/src/styles/components.css", kind: "file" }, + { name: "tokens.css", path: "packages/web/src/styles/tokens.css", kind: "file" }, +]; + +const editorReviewLines = [ + "const headerTitle = isMobile", + ' ? t(shouldShowMobileRoot ? "settings.title" : activeSectionMeta.labelKey)', + ' : t("settings.title");', + "", + "return (", + '
', + '
', + '
', + " ...", +]; + +function scene( + id: string, + config: Pick +): UiPreviewSceneDefinition { + const metadata = getUiPreviewSceneMetadata(id); + if (!metadata) { + throw new Error(`Missing UI preview metadata for ${id}`); + } + + return { + ...metadata, + ...config, + }; +} + +function buildWorkspaceSeed(context: { + theme: string; + locale: "zh" | "en"; + device: "desktop" | "mobile"; +}) { + return { + ...context, + workspaces: [ + workspace, + { + ...workspace, + id: "ws-review-2", + name: "playground", + path: "/home/spencer/workspace/playground", + }, + ], + activeWorkspaceId: workspace.id, + fileTreeByWorkspaceId: { + [workspace.id]: new Map([ + [".", rootTree], + ["packages", packagesTree], + ["packages/web", webTree], + ["packages/web/src", srcTree], + ]), + }, + gitStateByWorkspaceId: { + [workspace.id]: gitStatus, + }, + gitBranchListByWorkspaceId: { + [workspace.id]: { + current: "main", + branches: [ + { name: "main", isCurrent: true, isRemote: false }, + { name: "feat/pc-style-polish", isCurrent: false, isRemote: false }, + { name: "origin/main", isCurrent: false, isRemote: true }, + ], + }, + }, + worktreeListByWorkspaceId: { + [workspace.id]: worktrees, + }, + commands: { + workspaceList: [workspace], + fileTreeByWorkspaceId: { + [workspace.id]: { + ".": rootTree, + packages: packagesTree, + "packages/web": webTree, + "packages/web/src": srcTree, + }, + }, + fileSearchByWorkspaceId: { + [workspace.id]: searchResults, + }, + gitStatusByWorkspaceId: { + [workspace.id]: gitStatus, + }, + gitBranchesByWorkspaceId: { + [workspace.id]: { + current: "main", + branches: [ + { name: "main", isCurrent: true, isRemote: false }, + { name: "feat/pc-style-polish", isCurrent: false, isRemote: false }, + { name: "origin/main", isCurrent: false, isRemote: true }, + ], + }, + }, + gitLogByWorkspaceId: { + [workspace.id]: { entries: gitHistory }, + }, + gitDiffByWorkspaceId: { + [workspace.id]: { + diff: "@@ preview diff\n- background: #11181f;\n+ background: var(--bg-elevated);", + }, + }, + gitShowByWorkspaceId: { + [workspace.id]: { diff: "@@ preview commit diff\n+ register desktop review scenes" }, + }, + worktreeListByWorkspaceId: { + [workspace.id]: worktrees, + }, + worktreeStatusByPath, + worktreeDiffByPath: { + [worktrees[1].path]: "@@ worktree diff\n+ packages/web/src/styles/components.css", + }, + worktreeTreeByPath, + terminalListByWorkspaceId: { + [workspace.id]: [], + }, + }, + }; +} + +function DesktopReviewShell({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return ( +
+ {children} +
+ ); +} + +function DesktopEditorReviewSurface() { + return ( +
+
+
+ + packages/web/src/features/settings/components/settings-page.tsx + * + +
+ + +
+
+
+
+ {editorReviewLines.map((line, index) => ( +
+ {index + 1} + {line || " "} +
+ ))} +
+
+
+
+ ); +} + +export function createDesktopReviewScenes(): UiPreviewSceneDefinition[] { + return [ + scene("workspace-topbar-review", { + router: () => ({ initialEntries: ["/workspace"], path: "/workspace" }), + seed: (context) => buildWorkspaceSeed(context), + render: () => ( + +
+ +
+
+ ), + }), + scene("workspace-sidebar-files-review", { + router: () => ({ initialEntries: ["/workspace"], path: "/workspace" }), + seed: (context) => buildWorkspaceSeed(context), + render: () => ( + +
+ +
+
+ ), + }), + scene("workspace-sidebar-git-review", { + router: () => ({ initialEntries: ["/workspace"], path: "/workspace" }), + seed: (context) => buildWorkspaceSeed(context), + render: () => ( + +
+ +
+
+ ), + }), + scene("workspace-editor-review", { + router: () => ({ initialEntries: ["/workspace"], path: "/workspace" }), + seed: (context) => buildWorkspaceSeed(context), + render: () => ( + +
+ +
+
+ ), + }), + scene("workspace-diff-review", { + router: () => ({ initialEntries: ["/workspace"], path: "/workspace" }), + seed: (context) => ({ + ...buildWorkspaceSeed(context), + gitDiffPreviewByWorkspaceId: { + [workspace.id]: { + path: "packages/web/src/styles/components.css", + diff: [ + "@@ workspace shell", + "- background: var(--bg-surface);", + "+ background: var(--bg-elevated);", + "+ border-radius: var(--radius-xl);", + ].join("\n"), + source: "file", + }, + }, + }), + render: () => ( + +
+ +
+
+ ), + }), + scene("workspace-terminal-empty-review", { + router: () => ({ initialEntries: ["/workspace"], path: "/workspace" }), + seed: (context) => buildWorkspaceSeed(context), + render: () => ( + +
+ +
+
+ ), + }), + scene("settings-density-review", { + router: () => ({ initialEntries: ["/settings"], path: "/settings" }), + seed: (context) => ({ + ...buildWorkspaceSeed(context), + commands: { + ...buildWorkspaceSeed(context).commands, + settingsGet: { + "notifications.enabled": true, + "notifications.soundEnabled": true, + "supervisor.evaluationTimeoutSec": 600, + "appearance.locale": context.locale, + "appearance.themeId": context.theme, + "appearance.terminalRenderer": "standard", + "providers.claude.additionalArgs": ["--verbose"], + "providers.codex.additionalArgs": ["--sandbox", "workspace-write"], + }, + settingsUpdate: {}, + settingsPreviewCommandByProviderId: { + claude: "claude --verbose", + codex: "codex --sandbox workspace-write", + }, + }, + }), + render: () => , + }), + scene("settings-light-theme-review", { + router: () => ({ initialEntries: ["/settings"], path: "/settings" }), + seed: (context) => ({ + ...buildWorkspaceSeed(context), + commands: { + ...buildWorkspaceSeed(context).commands, + settingsGet: { + "notifications.enabled": true, + "notifications.soundEnabled": true, + "supervisor.evaluationTimeoutSec": 600, + "appearance.locale": context.locale, + "appearance.themeId": context.theme, + "appearance.terminalRenderer": "standard", + "providers.claude.additionalArgs": ["--verbose"], + "providers.codex.additionalArgs": ["--sandbox", "workspace-write"], + }, + settingsUpdate: {}, + settingsPreviewCommandByProviderId: { + claude: "claude --verbose", + codex: "codex --sandbox workspace-write", + }, + }, + }), + render: () => , + }), + scene("desktop-overlay-review", { + router: () => ({ initialEntries: ["/workspace"], path: "/workspace" }), + seed: (context) => ({ + ...buildWorkspaceSeed(context), + commandPaletteOpen: true, + commands: { + ...buildWorkspaceSeed(context).commands, + workspaceBrowse: { + currentPath: "/home/spencer/workspace", + parentPath: "/home/spencer", + directories: [ + { name: "coder-studio", path: "/home/spencer/workspace/coder-studio", itemCount: 24 }, + { name: "playground", path: "/home/spencer/workspace/playground", itemCount: 6 }, + ], + }, + workspaceOpen: workspace, + }, + }), + render: () => ( +
+
+ +
+
+ {}} /> +
+
+
+ {}} + /> +
+
+
+ ), + }), + scene("desktop-statusbar-review", { + router: () => ({ initialEntries: ["/workspace"], path: "/workspace" }), + seed: (context) => buildWorkspaceSeed(context), + render: () => ( + +
+ +
+
+ ), + }), + ]; +} diff --git a/packages/web/src/ui-preview/scenes/showcase-scenes.tsx b/packages/web/src/ui-preview/scenes/showcase-scenes.tsx index 1fc756aa2..943471062 100644 --- a/packages/web/src/ui-preview/scenes/showcase-scenes.tsx +++ b/packages/web/src/ui-preview/scenes/showcase-scenes.tsx @@ -1,15 +1,18 @@ import type { FileNode, GitStatus, Supervisor, Workspace, WorktreeInfo } from "@coder-studio/core"; +import { type ReactNode, useState } from "react"; import { ConfirmDialog, EmptyState, Notice, Sheet, ThemedIcon } from "../../components/ui"; import { CommandPalette } from "../../features/command-palette"; import { ToastContainer } from "../../features/notifications"; import { MobileSupervisorBadge } from "../../features/supervisor/views/mobile/mobile-supervisor-badge"; import { MobileSupervisorSheet } from "../../features/supervisor/views/mobile/mobile-supervisor-sheet"; import { ObjectiveDialog } from "../../features/supervisor/views/shared/objective-dialog"; -import { XtermPlaceholder } from "../../features/terminal-panel/views/shared/xterm-placeholder"; +import { TerminalPanel } from "../../features/terminal-panel"; import { MobileDock } from "../../features/workspace/views/mobile/mobile-dock"; +import { MobileFilesSheet } from "../../features/workspace/views/mobile/mobile-files-sheet"; import { MobileWorkspaceDrawer } from "../../features/workspace/views/mobile/mobile-workspace-drawer"; import { BranchQuickPick } from "../../features/workspace/views/shared/branch-quick-pick"; import { WorkspaceLaunchModal } from "../../features/workspace/views/shared/workspace-launch-modal"; +import { WorkspaceStatusBar } from "../../features/workspace/views/shared/workspace-status-bar"; import { WorktreeManagerSurface } from "../../features/workspace/views/shared/worktree-manager-surface"; import type { UiPreviewSceneDefinition } from "../catalog"; import { getUiPreviewSceneMetadata } from "../scene-metadata"; @@ -93,6 +96,17 @@ const worktreeTree: FileNode[] = [ { name: "e2e-ui", path: "e2e-ui", kind: "dir" }, ]; +const fileTreeRoot: FileNode[] = [ + { name: "packages", path: "packages", kind: "dir" }, + { name: "README.md", path: "README.md", kind: "file" }, + { name: "pnpm-workspace.yaml", path: "pnpm-workspace.yaml", kind: "file" }, +]; + +const fileTreePackages: FileNode[] = [ + { name: "web", path: "packages/web", kind: "dir" }, + { name: "core", path: "packages/core", kind: "dir" }, +]; + function scene( id: string, config: Pick @@ -442,44 +456,170 @@ export function createShowcaseScenes(): UiPreviewSceneDefinition[] { }), scene("mobile-files-sheet", { router: () => ({ initialEntries: ["/workspace"], path: "/workspace" }), - seed: (context) => ({ ...context }), + seed: (context) => ({ + ...context, + workspaces: [workspace], + activeWorkspaceId: workspace.id, + commands: { + fileTreeByWorkspaceId: { + [workspace.id]: { + ".": fileTreeRoot, + packages: fileTreePackages, + }, + }, + gitStatusByWorkspaceId: { + [workspace.id]: { + branch: "feature/mobile-polish", + ahead: 2, + behind: 0, + staged: [{ path: "packages/web/src/styles/components.css", status: "modified" }], + modified: [ + { + path: "packages/web/src/features/settings/components/settings-page.tsx", + status: "modified", + }, + { + path: "packages/web/src/features/workspace/views/mobile/mobile-workspace-drawer.tsx", + status: "modified", + }, + ], + untracked: [{ path: "e2e-ui/output/mobile-review.png", status: "untracked" }], + deleted: [], + }, + }, + gitBranchesByWorkspaceId: { + [workspace.id]: { + current: "feature/mobile-polish", + branches: [ + { name: "feature/mobile-polish", isCurrent: true, isRemote: false }, + { name: "develop", isCurrent: false, isRemote: false }, + ], + }, + }, + terminalListByWorkspaceId: { + [workspace.id]: [], + }, + }, + }), render: () => ( {}} body={ -
-
-
-
packages/web/src/app.tsx
-
packages/web/src/ui-preview/app.tsx
-
-
-
+ } /> ), }), scene("mobile-terminal-sheet", { router: () => ({ initialEntries: ["/workspace"], path: "/workspace" }), - seed: (context) => ({ ...context }), - render: () => ( - {}} - body={ -
- -
- } - /> - ), + seed: (context) => ({ + ...context, + workspaces: [workspace], + activeWorkspaceId: workspace.id, + terminalMetaById: { + "term-preview-1": { + id: "term-preview-1", + workspaceId: workspace.id, + kind: "shell", + alive: true, + title: "Workspace Shell", + }, + "term-preview-2": { + id: "term-preview-2", + workspaceId: workspace.id, + kind: "shell", + alive: true, + title: "Preview Runner", + }, + }, + terminalOutputById: { + "term-preview-1": [new TextEncoder().encode("$ pnpm --filter @coder-studio/web test\n")], + "term-preview-2": [new TextEncoder().encode("$ playwright test --project=mobile\n")], + }, + commands: { + terminalListByWorkspaceId: { + [workspace.id]: [ + { + id: "term-preview-1", + workspaceId: workspace.id, + kind: "shell", + title: "Workspace Shell", + cwd: workspace.path, + argv: ["zsh"], + cols: 120, + rows: 28, + alive: true, + createdAt: 1, + }, + { + id: "term-preview-2", + workspaceId: workspace.id, + kind: "shell", + title: "Preview Runner", + cwd: workspace.path, + argv: ["zsh"], + cols: 120, + rows: 28, + alive: true, + createdAt: 2, + }, + ], + }, + }, + }), + render: () => { + const MobileTerminalPreviewSheet = () => { + const [headerAction, setHeaderAction] = useState(null); + + return ( + {}} + body={ +
+ +
+ } + footer={ + + } + /> + ); + }; + + return ; + }, }), scene("mobile-supervisor-sheet", { router: () => ({ initialEntries: ["/workspace"], path: "/workspace" }),