Match Canvas sidebar to Code with a lavender surface
This commit is contained in:
1 parent
d6c72bf8e5
commit
f58bb5c9cd
6 files changed
+304
-161
No files matched your search
@@ -0,0 +1,78 @@
|
|||||||
|
# Task: Deliver Canvas sidebar parity on the accepted Code base
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
- Task ID: 20261014-canvas-sidebar-style-9b2f8c14
|
||||||
|
- Mode: Feature
|
||||||
|
- Branch: codex/20261014-canvas-sidebar-style-9b2f8c14-canvas-sidebar-style
|
||||||
|
- Worktree: /Users/chillishark/Makelore 麦洛/.codex-worktrees/makelore/20261014-canvas-sidebar-style-9b2f8c14
|
||||||
|
- Base commit: d6c72bf8e566711e75e15129e305161f9c4975d9
|
||||||
|
- Owner: codex-root
|
||||||
|
- Status: Ready for Integration
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Match the desktop Canvas left rail to the accepted Code rail: common surface treatment, navigation, content inset, creation row, project rows, icon badges, menu and account footer. Only the rail background changes to pale lavender #f5f2fa. Preserve title-only My Projects presentation from the completed sidebar simplification task.
|
||||||
|
|
||||||
|
## Intent And Constraints
|
||||||
|
|
||||||
|
- User explicitly requests visual parity with Code and permits choosing the background color. Preserve Canvas fixed 256px width, desktop visibility, compact Sheet, project identities and recent-first sorting, all cloud APIs/auth authority, loading/retry states and typed-name deletion. Do not apply Code main-content layout or introduce Code-only navigation. The human coordination instruction in chat 01a12eba-e98b-76a3-8fd5-206f2820d623 authorizes completion reporting and assigns unified merge, push, app restart and cleanup to that chat; this task only prepares and commits source.
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
- Uses committed Code sidebar/simplification product f980827 through ordinary merge d6c72bf (the branches diverged only in prior Canvas acceptance records on this task side), retaining its full settings, gallery and work-entry chain. Canvas now shares the existing scoped Code sidebar CSS and row/menu recipes, with #f5f2fa as its sidebar/footer background. The creation row, My Projects heading, title-only project list, tone badges, actions and bottom gallery/account entries are aligned. Canvas keeps a 40px return-only desktop navigation row using existing fullscreen observation and ModuleSwitcher, preserving the compact layout, fixed width and project business methods. README synchronized.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Required planning/ownership documents checked; no blocking peer semantic conflict. Standard typecheck, scoped ESLint, git diff --check and full build:vite passed. Existing Canvas/sidebar/navigation/community unit tests: 42 passed across 5 files. Isolated Electron Code chat/settings chrome regression passed (1 test, 5.6s). Independent source review passed. Final isolated Electron Canvas V2 acceptance passed (1 test, 6.4s): 256px rail, lavender surface, aligned 40/96/136px vertical positions, row/badge/icon/action sizes, title-only list and adjacent gallery/account footer. Existing desktop/compact split, drafts, direct edits, production/preview, keyboard layout and canonical workspace interactions also passed. Inspected desktop split and compact current-work screenshots. No real Alice data or live runtime was changed. Logs and screenshots retained at /Users/chillishark/Makelore 麦洛/.trial-runtime/canvas-sidebar-parity-20261014-5d8a7c2e; these are test evidence, not a runtime activation manifest.
|
||||||
|
|
||||||
|
## Follow-ups
|
||||||
|
|
||||||
|
- Source handoff: repository /Users/chillishark/Makelore 麦洛/makelore; branch/worktree in Identity; final committed HEAD will be reported directly to the authorized coordinator.
|
||||||
|
- Predecessor staging task 20261014-canvas-sidebar-parity-5d8a7c2e produced the reviewed patch and test evidence. It was reduced to its own preparatory record eb33147c3308f9b6b31da220c8164e8055b0ea55 with no foreign source-record changes, officially completed and retired. Its branch and commits remain preserved.
|
||||||
|
- Source handoff: repository /Users/chillishark/Makelore 麦洛/makelore; branch/worktree in Identity; final committed HEAD will be reported to coordinator.
|
||||||
|
- Coordinating chat 01a12eba-e98b-76a3-8fd5-206f2820d623 owns combined activation, main merge/push and cleanup. Report final committed source SHA there; preserve this clean source worktree until it is integrated and officially retired. No actual trial/current.json/Main restart or real business write was performed.
|
||||||
|
|
||||||
|
## Promotion Candidates
|
||||||
|
|
||||||
|
- None recorded.
|
||||||
|
|
||||||
|
## Project Context Loaded
|
||||||
|
|
||||||
|
Task context:
|
||||||
|
- Task ID: 20261014-canvas-sidebar-style-9b2f8c14
|
||||||
|
- Mode: Feature
|
||||||
|
- Branch: codex/20261014-canvas-sidebar-style-9b2f8c14-canvas-sidebar-style
|
||||||
|
- Worktree: /Users/chillishark/Makelore 麦洛/.codex-worktrees/makelore/20261014-canvas-sidebar-style-9b2f8c14
|
||||||
|
- Base commit: d6c72bf8e566711e75e15129e305161f9c4975d9
|
||||||
|
- Other active local tasks: official status confirms this task/worktree/branch/base; 70 peer task records and their Scope, Intent And Constraints and Promotion Candidates sections inspected read-only. Inventory at /tmp/makelore-canvas-sidebar-style-peer-scopes.json.
|
||||||
|
- Overlap assessment: sidebar-simplify-a6d32f90 overlaps Canvas labels and Sidebar/CSS; its accepted no-brand, title-only intent agrees with this request. Its product commit f980827 is ready and already incorporated in the committed d6c72bf baseline for this isolated delivery task. It includes the shared Code settings rail and gallery footer. Other integration/runtime tasks own unified deployment; no semantic conflict. The previously unknown unrelated placeholder scope remains outside this task; all 70 records were readable in the refreshed inventory.
|
||||||
|
|
||||||
|
Read:
|
||||||
|
- AGENTS.md, README.md
|
||||||
|
- .project-docs/05-agent-entry/read-before-planning.md, memory-index.md, planning-gate.md
|
||||||
|
- .project-docs/00-brief/project-positioning.md
|
||||||
|
- .project-docs/30-worklog/current-state.md
|
||||||
|
- .project-docs/20-architecture/system-overview.md, business-rules.md
|
||||||
|
- success-criteria, community-collaboration and decision/evidence/reflection/commitment indexes selected through the entry documents; ADR007 Canvas cloud authority
|
||||||
|
- Own task record and official peer task Scope/Intent/Promotion sections
|
||||||
|
- design and maintain-project-docs skills; required design search performed, existing Code design reference takes precedence over generic results
|
||||||
|
|
||||||
|
Relevant understanding:
|
||||||
|
- Project goal: Chinese light-mode creative workspaces for children, with stable Main-owned authentication and network access.
|
||||||
|
- Current integrated focus: Canvas V2 is on main350; newer sidebar and work-entry improvements are ready in peer histories and runtime overlays.
|
||||||
|
- Active scope: visual parity for the desktop Canvas rail only.
|
||||||
|
- Active constraints: cloud-only Canvas, sole Living Form/server reducer, no new IPC/API/model/provider routes, no unrelated canonical-doc edits or dirty primary checkout adoption.
|
||||||
|
- Decisions: preserve simplified no-brand Code rail and title-only Canvas list; fixed Canvas geometry and compact fallback remain.
|
||||||
|
- Evidence/reflections: runtime source metadata and in-memory auth can differ; one coordinator must build and activate the combined result.
|
||||||
|
- Files/modules likely involved: Sidebar, ImageWorkspaceSidebar, TitleBar/MainLayout navigation and the shared classroom sidebar CSS; README and relevant existing tests.
|
||||||
|
- Unknowns/stale docs/conflicts: predecessor source base350 predates completed peer Code rail chain; that ready committed source is now part of this task baseline before source changes. The predecessor feature drift check classified inherited peer records as foreign writes; use this explicitly based delivery worktree so only this task record and the five Canvas implementation/verification files differ from its recorded base. Actual trial overlay metadata can advance concurrently, and live acceptance belongs to the coordinator. Primary teacher-integration checkout has unrelated dirty documentation and remains untouched.
|
||||||
|
|
||||||
|
Gate result:
|
||||||
|
- Passed
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
- Apply the already independently reviewed Canvas patch on the committed shared Code rail/simplification baseline, reuse its existing sidebar CSS with a Canvas-specific background variable, and use the same class recipes for Canvas creation/projects/menus.
|
||||||
|
- Keep a return-only 40px navigation row inside the desktop Canvas rail, align its native window safe inset with Code, and retain fullscreen handling without changing the shared TitleBar, compact content or sidebar collapse.
|
||||||
|
- Run existing Canvas/sidebar/navigation behavior tests, standard typecheck/scoped lint/full build, and a focused isolated Electron visual check using fixtures. Record independent review and hand off a committed Ready task to the authorized coordinator without altering the running trial.
|
||||||
@@ -24,7 +24,7 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智
|
|||||||
|
|
||||||
Agents 侧栏的「渠道」按每个微信一张卡展示备注、当前智能体与使用状态;一个微信同时只绑定一个智能体,多个微信可以使用同一智能体。常用操作是“更换智能体”,在同一个选择窗口中确认;暂停自动回复和解除微信绑定放在“更多操作”。暂停停止当前任务并保留登录、绑定和历史;更换保留登录与原启停状态;解除微信绑定是完整操作,停止任务、清除目标并退出微信登录,历史保留,再使用需扫码。掉线显示重新扫码入口,暂停显示继续回复,未知或未同步状态显示正在确认,不宣称可用。添加流程为选择已开始使用的智能体、扫码、自动完成绑定;只有这次用户发起的绑定在扫码确认、目标正确、启用成功且最终状态回读可用后才显示完成,不因页面打开、旧记录或验证码提交自动启用。无可用智能体时说明创建和“开始使用”的前提,加载失败不冒充空态,已有微信仍可管理或解绑。Main 持有凭据与原操作恢复;解除需要配套 Yuxi 用户级 `/channel-accounts/{id}/unbind`,旧后端不会通过改按钮文案或组合未确认操作假装完成。变更后以最新读取状态为准,未完成操作保留原身份供显式恢复;失败在当前窗口可见,结果未知不自动重放。服务端显示微信退出仍在处理中时,禁止新的连接、启停和绑定变更,保留刷新与原操作恢复;管理写入前再次读取当前状态。客户端已知目标智能体停用时明确显示不可用,并禁止恢复自动回复,仍可更换目标或安全暂停、解绑。旧请求无法安全恢复、账号关系不一致或目标不可用时提示先核对,保留原记录并避免盲目重试;账号关系或目标智能体问题可显式刷新状态后再次确认原操作,刷新本身不重放操作;“不再提醒”只移除本机提醒,不取消服务端仍在处理的操作。最新 Yuxi 的退出保护条件和持久恢复由服务端负责,发布需配套 Core/Adapter 与 Yuxi business=13、knowledge=2 数据库版本。渠道页不展示微信聊天或活动,既有历史、费用、内容隔离与应用 API 保持原边界。实际微信身份及重复扫码识别仍由云端负责,不从备注推测。
|
Agents 侧栏的「渠道」按每个微信一张卡展示备注、当前智能体与使用状态;一个微信同时只绑定一个智能体,多个微信可以使用同一智能体。常用操作是“更换智能体”,在同一个选择窗口中确认;暂停自动回复和解除微信绑定放在“更多操作”。暂停停止当前任务并保留登录、绑定和历史;更换保留登录与原启停状态;解除微信绑定是完整操作,停止任务、清除目标并退出微信登录,历史保留,再使用需扫码。掉线显示重新扫码入口,暂停显示继续回复,未知或未同步状态显示正在确认,不宣称可用。添加流程为选择已开始使用的智能体、扫码、自动完成绑定;只有这次用户发起的绑定在扫码确认、目标正确、启用成功且最终状态回读可用后才显示完成,不因页面打开、旧记录或验证码提交自动启用。无可用智能体时说明创建和“开始使用”的前提,加载失败不冒充空态,已有微信仍可管理或解绑。Main 持有凭据与原操作恢复;解除需要配套 Yuxi 用户级 `/channel-accounts/{id}/unbind`,旧后端不会通过改按钮文案或组合未确认操作假装完成。变更后以最新读取状态为准,未完成操作保留原身份供显式恢复;失败在当前窗口可见,结果未知不自动重放。服务端显示微信退出仍在处理中时,禁止新的连接、启停和绑定变更,保留刷新与原操作恢复;管理写入前再次读取当前状态。客户端已知目标智能体停用时明确显示不可用,并禁止恢复自动回复,仍可更换目标或安全暂停、解绑。旧请求无法安全恢复、账号关系不一致或目标不可用时提示先核对,保留原记录并避免盲目重试;账号关系或目标智能体问题可显式刷新状态后再次确认原操作,刷新本身不重放操作;“不再提醒”只移除本机提醒,不取消服务端仍在处理的操作。最新 Yuxi 的退出保护条件和持久恢复由服务端负责,发布需配套 Core/Adapter 与 Yuxi business=13、knowledge=2 数据库版本。渠道页不展示微信聊天或活动,既有历史、费用、内容隔离与应用 API 保持原边界。实际微信身份及重复扫码识别仍由云端负责,不从备注推测。
|
||||||
|
|
||||||
- `Makelore Canvas|AI 绘画`:每个设计项目(Workspace)维护一份从创建起就存在的 Living Form。左侧项目栏沿用 Agents 的图标徽章与文字入口,分组标题为“我的项目”,桌面项目列表仅显示名称,隐藏名称下的设计版本与更新时间;新建、切换和项目操作菜单集中管理 Workspace。工作区统一项目标题、设计版本与制作状态,创作对话和作品区支持拖动分栏、键盘调宽及单栏展开;对话以 Markdown 呈现,底部输入器保留草稿、发送状态与 Enter / Shift+Enter 快捷键。右侧通过“当前作品 / 历史作品”切换,在大图或视频预览旁编辑唯一制作方案、确认费用和制作;历史作品可回到工作区查看,切换面板保留未保存的制作参数。紧凑窗口以“创作对话 / 作品与方案”切换面板,通过项目抽屉管理 Workspace。图片支持适应窗口、原始尺寸及 Esc 关闭,视频支持预览和保存到电脑。参考图从本地上传后以 `@图片N` 绑定,具体用法只写在创作提示词中。
|
- `Makelore Canvas|AI 绘画`:每个设计项目(Workspace)维护一份从创建起就存在的 Living Form。左侧工作栏与 Code 共用间距、图标徽章、项目行、操作菜单和底部广场/账户样式,仅工作栏底色采用淡紫色(`#f5f2fa`);顶部返回箭头与 Code 对齐,分组标题为“我的项目”,桌面项目列表仅显示名称,隐藏名称下的设计版本与更新时间;新建、切换和项目操作菜单集中管理 Workspace。工作区统一项目标题、设计版本与制作状态,创作对话和作品区支持拖动分栏、键盘调宽及单栏展开;对话以 Markdown 呈现,底部输入器保留草稿、发送状态与 Enter / Shift+Enter 快捷键。右侧通过“当前作品 / 历史作品”切换,在大图或视频预览旁编辑唯一制作方案、确认费用和制作;历史作品可回到工作区查看,切换面板保留未保存的制作参数。紧凑窗口以“创作对话 / 作品与方案”切换面板,通过项目抽屉管理 Workspace。图片支持适应窗口、原始尺寸及 Esc 关闭,视频支持预览和保存到电脑。参考图从本地上传后以 `@图片N` 绑定,具体用法只写在创作提示词中。
|
||||||
- `Makelore Robot|AI 机器`:管理机器人智能体、设备激活绑定、智能体配置与设备分配;机器人工作台的智能体位于 Robot 全局侧栏,选中后在内容区先查看绑定设备、再查看基础设置,当前智能体通过 URL 参数保持可分享选择;绑定设备时默认先选择“引导配网”或“已有激活码”。在 Windows 与 macOS 的引导路径中,Makelore 可在弹窗内扫描并连接附近开放的 `Xiaozhi-*` 配网热点,失败时仍可通过系统 Wi-Fi 手动连接;后续继续复用机器人现有热点配网页面,不修改固件,也不由 Makelore 接收 Wi-Fi 密码。
|
- `Makelore Robot|AI 机器`:管理机器人智能体、设备激活绑定、智能体配置与设备分配;机器人工作台的智能体位于 Robot 全局侧栏,选中后在内容区先查看绑定设备、再查看基础设置,当前智能体通过 URL 参数保持可分享选择;绑定设备时默认先选择“引导配网”或“已有激活码”。在 Windows 与 macOS 的引导路径中,Makelore 可在弹窗内扫描并连接附近开放的 `Xiaozhi-*` 配网热点,失败时仍可通过系统 Wi-Fi 手动连接;后续继续复用机器人现有热点配网页面,不修改固件,也不由 Makelore 接收 Wi-Fi 密码。
|
||||||
|
|
||||||
应用启动默认进入 AI 模块入口选择页。入口页可在未登录状态浏览;未登录用户点击已开通模块时进入客户端原生登录页,可使用账号密码或手机号短信验证码登录。密码登录可选“记住密码”:正式安装包仅由 Electron Main 使用系统受保护凭据存储加密保存和回填账号密码,不写入 Renderer 持久状态,未打包开发版或系统安全存储不可用时禁用该选项。登录请求由 Renderer 经 Host API 交给 Electron Main,再由 Main 调用 Works Square;成功后回到入口选择页。已登录时,Electron Main 会从 Works Square `/api/auth/me` 读取当前账号,只向 Renderer 投影用户名、账号/租户/部门标识、权限名列表与四个模块布尔开关,不透传上游资料或凭据。工作区门禁同时要求有效 Token 和完整用户身份;旧状态缺失身份时会先尝试从 Main 恢复,仍无法确认则清除残留会话并返回登录页。被管理员关闭的模块会在入口页置灰且无法点击,直接访问其工作区路径也会返回入口页。旧服务端未返回策略或缺少单项字段时默认开放;这个客户端门禁不替代服务端 API 授权。
|
应用启动默认进入 AI 模块入口选择页。入口页可在未登录状态浏览;未登录用户点击已开通模块时进入客户端原生登录页,可使用账号密码或手机号短信验证码登录。密码登录可选“记住密码”:正式安装包仅由 Electron Main 使用系统受保护凭据存储加密保存和回填账号密码,不写入 Renderer 持久状态,未打包开发版或系统安全存储不可用时禁用该选项。登录请求由 Renderer 经 Host API 交给 Electron Main,再由 Main 调用 Works Square;成功后回到入口选择页。已登录时,Electron Main 会从 Works Square `/api/auth/me` 读取当前账号,只向 Renderer 投影用户名、账号/租户/部门标识、权限名列表与四个模块布尔开关,不透传上游资料或凭据。工作区门禁同时要求有效 Token 和完整用户身份;旧状态缺失身份时会先尝试从 Main 恢复,仍无法确认则清除残留会话并返回登录页。被管理员关闭的模块会在入口页置灰且无法点击,直接访问其工作区路径也会返回入口页。旧服务端未返回策略或缺少单项字段时默认开放;这个客户端门禁不替代服务端 API 授权。
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { IMAGE_WORKSPACE_CREATE_PROJECT_EVENT } from '@/lib/image-workspace';
|
import { IMAGE_WORKSPACE_CREATE_PROJECT_EVENT } from '@/lib/image-workspace';
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { useImageWorkspaceStore } from '@/stores/image-workspace';
|
import { useImageWorkspaceStore } from '@/stores/image-workspace';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import type { DesignWorkspaceSummary } from '../../../shared/image-workspace';
|
import type { DesignWorkspaceSummary } from '../../../shared/image-workspace';
|
||||||
@@ -138,7 +137,7 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid="sidebar-image-workspace" className="flex min-h-0 flex-1 flex-col px-2 pb-3 pt-3 font-sans">
|
<div data-testid="sidebar-image-workspace" className="flex min-h-0 flex-1 flex-col font-sans">
|
||||||
{sidebarCollapsed ? (
|
{sidebarCollapsed ? (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -153,137 +152,131 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
|
|||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
data-testid="sidebar-create-project"
|
||||||
className="motion-press flex min-h-10 w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-2 text-left text-sm font-semibold text-foreground transition-colors hover:bg-surface-tertiary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30"
|
aria-label="新建设计项目"
|
||||||
onClick={openCreateDialog}
|
className="motion-press flex min-h-12 w-full cursor-pointer items-center gap-2 rounded-lg border-0 bg-transparent px-2 py-2 text-left text-sm font-semibold text-foreground shadow-none transition-colors duration-100 hover:bg-surface-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35"
|
||||||
>
|
onClick={openCreateDialog}
|
||||||
<span aria-hidden="true" className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl border border-brand/15 bg-brand-soft text-brand">
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<span aria-hidden="true" className="classroom-create-project-icon">
|
||||||
</span>
|
<Plus strokeWidth={1.8} />
|
||||||
新建设计项目
|
</span>
|
||||||
</button>
|
新建项目
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
<div className="mb-1 mt-5 flex min-h-10 items-center justify-between px-2">
|
<div className="classroom-projects flex min-h-0 flex-1 flex-col">
|
||||||
<span className="text-xs font-semibold text-muted-foreground">我的项目</span>
|
<div className="classroom-projects-heading flex min-h-8 shrink-0 items-center justify-between text-xs font-semibold">
|
||||||
{(status === 'error' || status === 'unavailable') && (
|
<span className="text-xs font-semibold text-muted-foreground">我的项目</span>
|
||||||
<button
|
{(status === 'error' || status === 'unavailable') && (
|
||||||
type="button"
|
<button
|
||||||
aria-label="重新加载设计项目"
|
type="button"
|
||||||
className="flex h-10 w-10 cursor-pointer items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-surface-subtle hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30"
|
aria-label="重新加载设计项目"
|
||||||
onClick={() => void load()}
|
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-surface-subtle hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30"
|
||||||
>
|
onClick={() => void load()}
|
||||||
<RefreshCw className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 space-y-1 overflow-y-auto pr-1">
|
|
||||||
{status === 'loading' && (
|
|
||||||
<div className="space-y-2 px-1" aria-label="正在加载设计项目">
|
|
||||||
{[0, 1, 2].map((item) => (
|
|
||||||
<div key={item} className="h-12 animate-pulse rounded-xl bg-surface-subtle" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(status === 'error' || status === 'unavailable') && (
|
|
||||||
<div
|
|
||||||
data-testid="sidebar-image-workspace-unavailable"
|
|
||||||
className="rounded-xl bg-surface-subtle px-3 py-3 text-xs leading-5 text-muted-foreground"
|
|
||||||
>
|
|
||||||
{workspaceError ?? 'AI 设计暂时不可用'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{status === 'ready' && sortedProjects.length === 0 && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={openCreateDialog}
|
|
||||||
className="w-full rounded-xl border border-dashed border-border px-3 py-5 text-left transition-colors hover:border-brand/35 hover:bg-brand/[0.03]"
|
|
||||||
>
|
|
||||||
<span className="block text-sm font-medium text-foreground">从一个设计方向开始</span>
|
|
||||||
<span className="mt-1 block text-xs leading-5 text-muted-foreground">
|
|
||||||
对话和表单会一起完善同一份设计。
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{sortedProjects.map((project) => {
|
|
||||||
const active = project.workspaceId === activeWorkspaceId;
|
|
||||||
const pending = project.workspaceId === activeWorkspaceId && !workspace;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={project.workspaceId}
|
|
||||||
className={cn(
|
|
||||||
'group/project relative rounded-lg transition-colors',
|
|
||||||
active ? 'bg-brand-soft' : 'hover:bg-surface-tertiary',
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<button
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
type="button"
|
</button>
|
||||||
aria-current={active ? 'page' : undefined}
|
)}
|
||||||
className="flex min-h-10 w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-12 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30"
|
</div>
|
||||||
onClick={() => chooseProject(project)}
|
|
||||||
>
|
<div className="mt-1 min-h-0 flex-1 space-y-1.5 overflow-y-auto px-1 py-1">
|
||||||
<span aria-hidden="true" className={cn(
|
{status === 'loading' && (
|
||||||
'flex h-8 w-8 shrink-0 items-center justify-center rounded-xl border',
|
<div className="space-y-2 px-1" aria-label="正在加载设计项目">
|
||||||
active ? 'border-brand/20 bg-background/80 text-brand' : 'border-border/70 bg-background text-muted-foreground',
|
{[0, 1, 2].map((item) => (
|
||||||
)}>
|
<div key={item} className="h-12 animate-pulse rounded-xl bg-surface-subtle" />
|
||||||
{pending
|
))}
|
||||||
? <Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
: <FolderKanban className="h-4 w-4" />}
|
|
||||||
</span>
|
|
||||||
<span className="min-w-0 flex-1">
|
|
||||||
<span className="block truncate text-sm font-medium text-foreground">
|
|
||||||
{project.title}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<div className="absolute right-1 top-1/2 -translate-y-1/2">
|
|
||||||
<DropdownMenu.Root>
|
|
||||||
<DropdownMenu.Trigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-label={`项目操作:${project.title}`}
|
|
||||||
title="项目操作"
|
|
||||||
className="flex h-10 w-10 cursor-pointer items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-background/80 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30 data-[state=open]:bg-background/80 data-[state=open]:text-foreground"
|
|
||||||
>
|
|
||||||
{deletingWorkspaceId === project.workspaceId
|
|
||||||
? <Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
: <MoreHorizontal className="h-4 w-4" />}
|
|
||||||
</button>
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
<DropdownMenu.Portal>
|
|
||||||
<DropdownMenu.Content align="end" sideOffset={4} collisionPadding={12} className="z-50 min-w-40 rounded-xl border border-border/80 bg-background p-1 text-sm shadow-float">
|
|
||||||
<DropdownMenu.Item
|
|
||||||
className="flex min-h-10 cursor-pointer items-center gap-2 rounded-lg px-3 text-foreground outline-none data-[highlighted]:bg-surface-subtle"
|
|
||||||
onSelect={() => openRenameDialog(project)}
|
|
||||||
>
|
|
||||||
<Pencil aria-hidden="true" className="h-4 w-4 text-muted-foreground" />
|
|
||||||
重命名
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
disabled={Boolean(deletingWorkspaceId)}
|
|
||||||
className="flex min-h-10 cursor-pointer items-center gap-2 rounded-lg px-3 text-destructive outline-none data-[highlighted]:bg-destructive/[0.05] data-[disabled]:opacity-50"
|
|
||||||
onSelect={() => {
|
|
||||||
setDeleteTarget(project);
|
|
||||||
setDeleteConfirmation('');
|
|
||||||
setDeleteError(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash2 aria-hidden="true" className="h-4 w-4" />
|
|
||||||
删除项目
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Portal>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
})}
|
|
||||||
|
{(status === 'error' || status === 'unavailable') && (
|
||||||
|
<div
|
||||||
|
data-testid="sidebar-image-workspace-unavailable"
|
||||||
|
className="rounded-xl bg-surface-subtle px-3 py-3 text-xs leading-5 text-muted-foreground"
|
||||||
|
>
|
||||||
|
{workspaceError ?? 'AI 设计暂时不可用'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === 'ready' && sortedProjects.length === 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={openCreateDialog}
|
||||||
|
className="w-full rounded-xl border border-dashed border-border px-3 py-5 text-left transition-colors hover:border-brand/35 hover:bg-brand/[0.03]"
|
||||||
|
>
|
||||||
|
<span className="block text-sm font-medium text-foreground">从一个设计方向开始</span>
|
||||||
|
<span className="mt-1 block text-xs leading-5 text-muted-foreground">
|
||||||
|
对话和表单会一起完善同一份设计。
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sortedProjects.map((project) => {
|
||||||
|
const active = project.workspaceId === activeWorkspaceId;
|
||||||
|
const pending = project.workspaceId === activeWorkspaceId && !workspace;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={project.workspaceId}
|
||||||
|
className="group/project relative"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid={`sidebar-image-project-${project.workspaceId}`}
|
||||||
|
title={project.title}
|
||||||
|
aria-current={active ? 'page' : undefined}
|
||||||
|
className="classroom-project-card motion-press flex min-h-12 w-full cursor-pointer items-center gap-2 rounded-lg bg-transparent px-2 py-2 pr-10 text-left transition-colors duration-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35 focus-visible:ring-offset-1"
|
||||||
|
onClick={() => chooseProject(project)}
|
||||||
|
>
|
||||||
|
<span aria-hidden="true" className="classroom-project-icon" data-tone={['orange', 'green', 'blue'][Array.from(project.workspaceId).reduce((sum, char) => sum + char.charCodeAt(0), 0) % 3]}>
|
||||||
|
{pending
|
||||||
|
? <Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
: <FolderKanban strokeWidth={1.8} />}
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 flex-1 truncate text-sm font-semibold">{project.title}</span>
|
||||||
|
</button>
|
||||||
|
<div className="absolute right-1 top-1/2 -translate-y-1/2">
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={`项目操作:${project.title}`}
|
||||||
|
title="项目操作"
|
||||||
|
className="classroom-project-actions flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg text-muted-foreground hover:bg-surface-tertiary hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35 data-[state=open]:bg-surface-tertiary"
|
||||||
|
>
|
||||||
|
{deletingWorkspaceId === project.workspaceId
|
||||||
|
? <Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
: <MoreHorizontal className="h-4 w-4" />}
|
||||||
|
</button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.Content align="end" sideOffset={4} collisionPadding={12} className="z-[400] min-w-40 rounded-lg border bg-popover p-1 text-sm shadow-md">
|
||||||
|
<DropdownMenu.Item
|
||||||
|
className="flex cursor-pointer items-center rounded p-2 text-foreground outline-none data-[highlighted]:bg-accent"
|
||||||
|
onSelect={() => openRenameDialog(project)}
|
||||||
|
>
|
||||||
|
<Pencil aria-hidden="true" className="mr-2 h-3.5 w-3.5" />
|
||||||
|
重命名
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
disabled={Boolean(deletingWorkspaceId)}
|
||||||
|
className="flex cursor-pointer items-center rounded p-2 text-destructive outline-none data-[highlighted]:bg-accent data-[disabled]:opacity-50"
|
||||||
|
onSelect={() => {
|
||||||
|
setDeleteTarget(project);
|
||||||
|
setDeleteConfirmation('');
|
||||||
|
setDeleteError(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 aria-hidden="true" className="mr-2 h-3.5 w-3.5" />
|
||||||
|
删除项目
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import { PointWallet } from '@/components/account/PointWallet';
|
|||||||
import { AccountMenuTrigger } from '@/components/account/AccountMenuTrigger';
|
import { AccountMenuTrigger } from '@/components/account/AccountMenuTrigger';
|
||||||
import { canLeaveCurrentPage, getSettingsReturnState } from '@/lib/settings-navigation';
|
import { canLeaveCurrentPage, getSettingsReturnState } from '@/lib/settings-navigation';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { observeNativeFullscreen } from '@/lib/window-fullscreen';
|
||||||
import { useCurrentUserProfile } from '@/hooks/use-current-user-profile';
|
import { useCurrentUserProfile } from '@/hooks/use-current-user-profile';
|
||||||
import type { SidebarPeekSource } from './sidebar-peek';
|
import type { SidebarPeekSource } from './sidebar-peek';
|
||||||
import { ModuleSwitcher } from './ModuleSwitcher';
|
import { ModuleSwitcher } from './ModuleSwitcher';
|
||||||
@@ -86,6 +87,20 @@ type SidebarProps = {
|
|||||||
onSidebarPeekChange?: (open: boolean, source: SidebarPeekSource) => void;
|
onSidebarPeekChange?: (open: boolean, source: SidebarPeekSource) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function CanvasSidebarNavigation() {
|
||||||
|
const nativeTrafficLights = window.electron?.platform === 'darwin';
|
||||||
|
const [nativeFullscreen, setNativeFullscreen] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (nativeTrafficLights) return observeNativeFullscreen(setNativeFullscreen);
|
||||||
|
}, [nativeTrafficLights]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-testid="canvas-sidebar-navigation" className={cn('no-drag relative z-[400] flex h-10 items-center pr-3', nativeTrafficLights && !nativeFullscreen ? 'pl-[88px]' : 'pl-3')}>
|
||||||
|
<ModuleSwitcher sidebarCollapsed={false} toolbar iconOnly />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Sidebar({
|
export function Sidebar({
|
||||||
workspaceLayout = false,
|
workspaceLayout = false,
|
||||||
hideOnCompact = false,
|
hideOnCompact = false,
|
||||||
@@ -408,6 +423,7 @@ export function Sidebar({
|
|||||||
id={workspaceLayout ? 'classroom-sidebar' : undefined}
|
id={workspaceLayout ? 'classroom-sidebar' : undefined}
|
||||||
data-testid="sidebar"
|
data-testid="sidebar"
|
||||||
data-classroom={workspaceLayout}
|
data-classroom={workspaceLayout}
|
||||||
|
data-workspace-sidebar={isPaintingModule ? 'canvas' : undefined}
|
||||||
aria-hidden={sidebarCollapsed}
|
aria-hidden={sidebarCollapsed}
|
||||||
onPointerEnter={() => onSidebarPeekChange?.(true, 'sidebar')}
|
onPointerEnter={() => onSidebarPeekChange?.(true, 'sidebar')}
|
||||||
onPointerLeave={() => onSidebarPeekChange?.(false, 'sidebar')}
|
onPointerLeave={() => onSidebarPeekChange?.(false, 'sidebar')}
|
||||||
@@ -432,8 +448,8 @@ export function Sidebar({
|
|||||||
{activeModule !== 'cloud_agents' && <div
|
{activeModule !== 'cloud_agents' && <div
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-transparent px-3 text-foreground',
|
'bg-transparent px-3 text-foreground',
|
||||||
workspaceLayout && 'classroom-sidebar-header',
|
(workspaceLayout || isPaintingModule) && 'classroom-sidebar-header',
|
||||||
isPaintingModule && !sidebarCollapsed ? 'pb-2 pt-10' : 'py-2',
|
isPaintingModule ? 'h-10 shrink-0' : 'py-2',
|
||||||
sidebarCollapsed && 'px-2',
|
sidebarCollapsed && 'px-2',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -441,7 +457,7 @@ export function Sidebar({
|
|||||||
{workspaceLayout && !['darwin', 'win32'].includes(window.electron?.platform ?? '') && <div className="no-drag flex h-10 min-w-0 items-center gap-1" data-testid="classroom-sidebar-navigation">
|
{workspaceLayout && !['darwin', 'win32'].includes(window.electron?.platform ?? '') && <div className="no-drag flex h-10 min-w-0 items-center gap-1" data-testid="classroom-sidebar-navigation">
|
||||||
<ModuleSwitcher sidebarCollapsed={sidebarCollapsed} toolbar iconOnly />
|
<ModuleSwitcher sidebarCollapsed={sidebarCollapsed} toolbar iconOnly />
|
||||||
</div>}
|
</div>}
|
||||||
{!workspaceLayout && <ModuleSwitcher sidebarCollapsed={sidebarCollapsed} compact={!sidebarCollapsed} />}
|
{isPaintingModule ? <CanvasSidebarNavigation /> : !workspaceLayout && <ModuleSwitcher sidebarCollapsed={sidebarCollapsed} compact={!sidebarCollapsed} />}
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
@@ -874,7 +890,7 @@ export function Sidebar({
|
|||||||
onOpenChange={setProfileDialogOpen}
|
onOpenChange={setProfileDialogOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{workspaceLayout && isProgrammingModule && (
|
{((workspaceLayout && isProgrammingModule) || isPaintingModule) && (
|
||||||
<div className="classroom-community-footer">
|
<div className="classroom-community-footer">
|
||||||
<CommunityLink variant="sidebar" collapsed={sidebarCollapsed} />
|
<CommunityLink variant="sidebar" collapsed={sidebarCollapsed} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
/* Shared Code navigation skin; chat content remains scoped to its workspace. */
|
/* Shared Code/Canvas navigation skin; workspace content stays independently scoped. */
|
||||||
[data-testid="main-layout"][data-classroom="true"], [data-testid="sidebar"][data-classroom="true"], .classroom-workspace {
|
[data-testid="main-layout"][data-classroom="true"], [data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]), .classroom-workspace {
|
||||||
--background: 0 0% 100%; --foreground: 214 34% 23%;
|
--background: 0 0% 100%; --foreground: 214 34% 23%;
|
||||||
--muted-foreground: 214 16% 44%; --border: 213 31% 90%;
|
--muted-foreground: 214 16% 44%; --border: 213 31% 90%;
|
||||||
}
|
}
|
||||||
[data-testid="main-layout"][data-code-sidebar="true"] [data-testid="titlebar-sidebar-surface"] { background: #f3f7fb; border-color: #dde5ee; box-shadow: none; backdrop-filter: none; }
|
[data-testid="main-layout"][data-code-sidebar="true"] [data-testid="titlebar-sidebar-surface"] { background: #f3f7fb; border-color: #dde5ee; box-shadow: none; backdrop-filter: none; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] { background: #f3f7fb; border-color: #dde5ee; box-shadow: none; backdrop-filter: none; color: #26384d; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) { background: var(--workspace-sidebar-background, #f3f7fb); border-color: #dde5ee; box-shadow: none; backdrop-filter: none; color: #26384d; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-sidebar-header { padding-top: 0; padding-bottom: 0; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-sidebar-header { padding-top: 0; padding-bottom: 0; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] [data-testid="classroom-sidebar-navigation"] { align-items: center; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) [data-testid="classroom-sidebar-navigation"] { align-items: center; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-brand { height: 46px; gap: 10px; padding: 0 2px; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-brand { height: 46px; gap: 10px; padding: 0 2px; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-community-footer { flex-shrink: 0; padding: 0 12px; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-community-footer { flex-shrink: 0; padding: 0 12px; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-community-footer + .classroom-account-footer { padding-top: 0; border-top: 0; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-community-footer + .classroom-account-footer { padding-top: 0; border-top: 0; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-community-footer .community-link[data-variant="sidebar"] { min-height: 48px; margin-top: 0; gap: 8px; padding: 8px; border: 0; border-radius: 8px; background: transparent; box-shadow: none; color: #26384d; font-size: 14px; font-weight: 600; line-height: 20px; white-space: normal; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-community-footer .community-link[data-variant="sidebar"] { min-height: 48px; margin-top: 0; gap: 8px; padding: 8px; border: 0; border-radius: 8px; background: transparent; box-shadow: none; color: #26384d; font-size: 14px; font-weight: 600; line-height: 20px; white-space: normal; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-community-footer .community-link[data-variant="sidebar"]:hover:not(:disabled) { background: #eef3e7; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-community-footer .community-link[data-variant="sidebar"]:hover:not(:disabled) { background: #eef3e7; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .community-link-icon { display: grid; place-items: center; flex-shrink: 0; width: 32px; height: 32px; border: 1px solid #dce5d0; border-radius: 12px; background: #f0f5e8; color: #526b46; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .community-link-icon { display: grid; place-items: center; flex-shrink: 0; width: 32px; height: 32px; border: 1px solid #dce5d0; border-radius: 12px; background: #f0f5e8; color: #526b46; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .community-link-icon svg { width: 20px; height: 20px; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .community-link-icon svg { width: 20px; height: 20px; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .community-link-arrow { margin-left: auto; color: #64758a; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .community-link-arrow { margin-left: auto; color: #64758a; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-brand:hover { background: #eaf1f8; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-brand:hover { background: #eaf1f8; }
|
||||||
.classroom-brand-mark { display: grid; place-items: center; width: 40px; height: 42px; flex-shrink: 0; border: 1px solid #f1d1aa; border-radius: 12px; background: #fff0dd; box-shadow: 0 3px 0 #ecdbc5; }
|
.classroom-brand-mark { display: grid; place-items: center; width: 40px; height: 42px; flex-shrink: 0; border: 1px solid #f1d1aa; border-radius: 12px; background: #fff0dd; box-shadow: 0 3px 0 #ecdbc5; }
|
||||||
.classroom-brand-caption { display: block; margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; color: #64758a; }
|
.classroom-brand-caption { display: block; margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; color: #64758a; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-sidebar-content { padding: 0 12px 14px; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-sidebar-content { padding: 0 12px 14px; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] [data-testid="sidebar-create-project"] { min-height: 48px; gap: 8px; padding: 8px; border: 0; border-radius: 8px; background: transparent; color: #26384d; box-shadow: none; font-size: 14px; font-weight: 600; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) [data-testid="sidebar-create-project"] { min-height: 48px; gap: 8px; padding: 8px; border: 0; border-radius: 8px; background: transparent; color: #26384d; box-shadow: none; font-size: 14px; font-weight: 600; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] [data-testid="sidebar-create-project"] svg { color: #c44b23; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) [data-testid="sidebar-create-project"] svg { color: #c44b23; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] [data-testid="sidebar-create-project"]:hover { background: #fff0e6; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) [data-testid="sidebar-create-project"]:hover { background: #fff0e6; }
|
||||||
.classroom-create-project-icon { display: grid; place-items: center; flex-shrink: 0; width: 32px; height: 32px; border: 1px solid #f0d9c7; border-radius: 12px; background: #ffead9; }
|
.classroom-create-project-icon { display: grid; place-items: center; flex-shrink: 0; width: 32px; height: 32px; border: 1px solid #f0d9c7; border-radius: 12px; background: #ffead9; }
|
||||||
.classroom-create-project-icon svg { width: 20px; height: 20px; }
|
.classroom-create-project-icon svg { width: 20px; height: 20px; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-projects { margin-top: 8px; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-projects { margin-top: 8px; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-projects-heading { padding: 6px 3px; color: #64758a; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-projects-heading { padding: 6px 3px; color: #64758a; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-projects-heading > svg { display: none; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-projects-heading > svg { display: none; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-project-card { min-height: 48px; gap: 8px; padding: 8px 40px 8px 8px; border: 0; border-radius: 8px; box-shadow: none; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-project-card { min-height: 48px; gap: 8px; padding: 8px 40px 8px 8px; border: 0; border-radius: 8px; box-shadow: none; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-project-card:hover { background: #eaf1f8; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-project-card:hover { background: #eaf1f8; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-project-card[aria-current="page"] { background: hsl(var(--brand-soft)); box-shadow: none; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-project-card[aria-current="page"] { background: hsl(var(--brand-soft)); box-shadow: none; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-project-card > span:last-child { font-size: 14px; font-weight: 600; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-project-card > span:last-child { font-size: 14px; font-weight: 600; }
|
||||||
.classroom-project-icon { display: grid; place-items: center; flex-shrink: 0; width: 32px; height: 32px; border: 1px solid #f0d9c7; border-radius: 12px; background: #ffead9; color: #a85026; }
|
.classroom-project-icon { display: grid; place-items: center; flex-shrink: 0; width: 32px; height: 32px; border: 1px solid #f0d9c7; border-radius: 12px; background: #ffead9; color: #a85026; }
|
||||||
.classroom-project-icon svg { width: 20px; height: 20px; }
|
.classroom-project-icon svg { width: 20px; height: 20px; }
|
||||||
.classroom-project-icon[data-tone="green"] { border-color: #cfe3c6; background: #e1f1d9; color: #507d39; }
|
.classroom-project-icon[data-tone="green"] { border-color: #cfe3c6; background: #e1f1d9; color: #507d39; }
|
||||||
@@ -44,7 +44,10 @@
|
|||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-conversation-row[data-selected="true"] { background: #e0ecfa; border-color: #cedef0; color: #304f75; }
|
[data-testid="sidebar"][data-classroom="true"] .classroom-conversation-row[data-selected="true"] { background: #e0ecfa; border-color: #cedef0; color: #304f75; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-conversation-row > button:first-child { min-height: 35px; gap: 7px; }
|
[data-testid="sidebar"][data-classroom="true"] .classroom-conversation-row > button:first-child { min-height: 35px; gap: 7px; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-conversation-row > button:first-child > svg { color: #7891af; }
|
[data-testid="sidebar"][data-classroom="true"] .classroom-conversation-row > button:first-child > svg { color: #7891af; }
|
||||||
[data-testid="sidebar"][data-classroom="true"] .classroom-account-footer { border-top: 1px solid #dde5ee; background: #edf3f9; }
|
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-account-footer { border-top: 1px solid #dde5ee; background: var(--workspace-sidebar-background, #edf3f9); }
|
||||||
|
[data-testid="sidebar"][data-workspace-sidebar="canvas"] { --workspace-sidebar-background: #f5f2fa; }
|
||||||
|
[data-testid="sidebar"][data-workspace-sidebar="canvas"] .classroom-sidebar-header { padding: 0; }
|
||||||
|
[data-testid="sidebar"][data-workspace-sidebar="canvas"] .classroom-sidebar-content { display: flex; flex-direction: column; overflow: hidden; }
|
||||||
.classroom-workspace [data-testid="coding-conversation-header"] { border-bottom-color: #e8edf3; background: #fff; color: #304968; }
|
.classroom-workspace [data-testid="coding-conversation-header"] { border-bottom-color: #e8edf3; background: #fff; color: #304968; }
|
||||||
.classroom-tabs { display: flex; flex-shrink: 0; height: 46px; gap: 9px; padding: 6px 22px; border-bottom: 1px solid #e8edf3; }
|
.classroom-tabs { display: flex; flex-shrink: 0; height: 46px; gap: 9px; padding: 6px 22px; border-bottom: 1px solid #e8edf3; }
|
||||||
.classroom-workspace-toolbar { display: flex; flex-wrap: wrap; flex-shrink: 0; align-items: center; gap: 6px 12px; min-height: 46px; padding: 6px 22px; border-bottom: 1px solid #e8edf3; }
|
.classroom-workspace-toolbar { display: flex; flex-wrap: wrap; flex-shrink: 0; align-items: center; gap: 6px 12px; min-height: 46px; padding: 6px 22px; border-bottom: 1px solid #e8edf3; }
|
||||||
|
|||||||
@@ -473,6 +473,59 @@ test.describe('AI Design V2 workspace', () => {
|
|||||||
await expect(page.getByText('目标与用途')).toHaveCount(0);
|
await expect(page.getByText('目标与用途')).toHaveCount(0);
|
||||||
await expect(page.locator('[data-testid^="sidebar-image-conversation-"]')).toHaveCount(0);
|
await expect(page.locator('[data-testid^="sidebar-image-conversation-"]')).toHaveCount(0);
|
||||||
|
|
||||||
|
// Canvas shares Code's navigation recipe, with a lavender surface and its own top return row.
|
||||||
|
const sidebar = page.getByTestId('sidebar');
|
||||||
|
const createProject = sidebar.getByTestId('sidebar-create-project');
|
||||||
|
const project = sidebar.getByTestId('sidebar-image-project-workspace-1');
|
||||||
|
const projectsHeading = sidebar.locator('.classroom-projects-heading');
|
||||||
|
const moduleReturn = sidebar.getByTestId('sidebar-module-switcher-trigger');
|
||||||
|
const community = sidebar.getByRole('button', { name: '作品广场', exact: true });
|
||||||
|
const account = sidebar.getByTestId('sidebar-member-menu-trigger');
|
||||||
|
await expect(sidebar).toHaveAttribute('aria-hidden', 'false');
|
||||||
|
await expect(sidebar).toHaveCSS('background-color', 'rgb(245, 242, 250)');
|
||||||
|
await expect(createProject).toHaveText('新建项目');
|
||||||
|
await expect(projectsHeading).toHaveText('我的项目');
|
||||||
|
await expect(project).toHaveText('咖啡机新品主视觉');
|
||||||
|
await expect(project).toHaveAttribute('aria-current', 'page');
|
||||||
|
await expect(project.getByText('咖啡机新品主视觉', { exact: true })).toHaveCSS('font-size', '14px');
|
||||||
|
await expect(project.getByText('咖啡机新品主视觉', { exact: true })).toHaveCSS('font-weight', '600');
|
||||||
|
await expect(moduleReturn).toHaveText('');
|
||||||
|
await expect(moduleReturn).toHaveAttribute('aria-label', '返回首页');
|
||||||
|
await expect(moduleReturn).toHaveCSS('width', '32px');
|
||||||
|
await expect(moduleReturn).toHaveCSS('height', '32px');
|
||||||
|
for (const badge of [createProject.locator('.classroom-create-project-icon'), project.locator('.classroom-project-icon')]) {
|
||||||
|
await expect(badge).toHaveCSS('width', '32px');
|
||||||
|
await expect(badge).toHaveCSS('height', '32px');
|
||||||
|
await expect(badge.locator('svg')).toHaveCSS('width', '20px');
|
||||||
|
await expect(badge.locator('svg')).toHaveCSS('height', '20px');
|
||||||
|
}
|
||||||
|
const actions = sidebar.getByRole('button', { name: '项目操作:咖啡机新品主视觉', exact: true });
|
||||||
|
await expect(actions).toHaveCSS('width', '32px');
|
||||||
|
await expect(actions).toHaveCSS('height', '32px');
|
||||||
|
const [sidebarBox, createBox, headingBox, projectBox, returnBox, communityBox, accountBox] = await Promise.all(
|
||||||
|
[sidebar, createProject, projectsHeading, project, moduleReturn, community, account].map((locator) => locator.boundingBox()),
|
||||||
|
);
|
||||||
|
expect(sidebarBox!.width).toBe(256);
|
||||||
|
expect(createBox!.height).toBe(48);
|
||||||
|
expect(projectBox!.height).toBe(48);
|
||||||
|
expect(createBox!.y - sidebarBox!.y).toBe(40);
|
||||||
|
expect(headingBox!.y - sidebarBox!.y).toBe(96);
|
||||||
|
expect(projectBox!.y - sidebarBox!.y).toBe(136);
|
||||||
|
expect(returnBox!.y - sidebarBox!.y).toBe(4);
|
||||||
|
expect(communityBox!.x).toBe(createBox!.x);
|
||||||
|
expect(communityBox!.height).toBe(createBox!.height);
|
||||||
|
expect(communityBox!.y).toBeGreaterThan(projectBox!.y + projectBox!.height);
|
||||||
|
expect(accountBox!.y - communityBox!.y - communityBox!.height).toBeCloseTo(0, 0);
|
||||||
|
expect(sidebarBox!.y + sidebarBox!.height - accountBox!.y - accountBox!.height).toBeLessThanOrEqual(14);
|
||||||
|
const navigationStyles = await sidebar.evaluate((element) => {
|
||||||
|
const recipe = (selector: string) => {
|
||||||
|
const style = getComputedStyle(element.querySelector(selector)!);
|
||||||
|
return [style.borderRadius, style.borderWidth, style.boxShadow, style.padding, style.fontSize, style.fontWeight, style.lineHeight];
|
||||||
|
};
|
||||||
|
return { create: recipe('[data-testid="sidebar-create-project"]'), community: recipe('.community-link') };
|
||||||
|
});
|
||||||
|
expect(navigationStyles.community).toEqual(navigationStyles.create);
|
||||||
|
|
||||||
const splitBounds = (await split.boundingBox())!;
|
const splitBounds = (await split.boundingBox())!;
|
||||||
const conversationBounds = (await conversationPane.boundingBox())!;
|
const conversationBounds = (await conversationPane.boundingBox())!;
|
||||||
const worksBounds = (await worksPane.boundingBox())!;
|
const worksBounds = (await worksPane.boundingBox())!;
|
||||||
|
|||||||
Reference in new issue
Block a user