Merge Canvas sidebar parity with the approved lavender surface
This commit is contained in:
commit
30a698718e
5 files changed
+226
-161
No files matched your search
@@ -24,7 +24,7 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智
|
||||
|
||||
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 密码。
|
||||
|
||||
应用启动默认进入 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';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { IMAGE_WORKSPACE_CREATE_PROJECT_EVENT } from '@/lib/image-workspace';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useImageWorkspaceStore } from '@/stores/image-workspace';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import type { DesignWorkspaceSummary } from '../../../shared/image-workspace';
|
||||
@@ -138,7 +137,7 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
|
||||
};
|
||||
|
||||
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 ? (
|
||||
<Button
|
||||
type="button"
|
||||
@@ -153,137 +152,131 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
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>
|
||||
新建设计项目
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="sidebar-create-project"
|
||||
aria-label="新建设计项目"
|
||||
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="classroom-create-project-icon">
|
||||
<Plus strokeWidth={1.8} />
|
||||
</span>
|
||||
新建项目
|
||||
</button>
|
||||
|
||||
<div className="mb-1 mt-5 flex min-h-10 items-center justify-between px-2">
|
||||
<span className="text-xs font-semibold text-muted-foreground">我的项目</span>
|
||||
{(status === 'error' || status === 'unavailable') && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="重新加载设计项目"
|
||||
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"
|
||||
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',
|
||||
)}
|
||||
<div className="classroom-projects flex min-h-0 flex-1 flex-col">
|
||||
<div className="classroom-projects-heading flex min-h-8 shrink-0 items-center justify-between text-xs font-semibold">
|
||||
<span className="text-xs font-semibold text-muted-foreground">我的项目</span>
|
||||
{(status === 'error' || status === 'unavailable') && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="重新加载设计项目"
|
||||
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()}
|
||||
>
|
||||
<button
|
||||
type="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"
|
||||
onClick={() => chooseProject(project)}
|
||||
>
|
||||
<span aria-hidden="true" className={cn(
|
||||
'flex h-8 w-8 shrink-0 items-center justify-center rounded-xl border',
|
||||
active ? 'border-brand/20 bg-background/80 text-brand' : 'border-border/70 bg-background text-muted-foreground',
|
||||
)}>
|
||||
{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>
|
||||
<RefreshCw className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 min-h-0 flex-1 space-y-1.5 overflow-y-auto px-1 py-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="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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -41,6 +41,7 @@ import { PointWallet } from '@/components/account/PointWallet';
|
||||
import { AccountMenuTrigger } from '@/components/account/AccountMenuTrigger';
|
||||
import { canLeaveCurrentPage, getSettingsReturnState } from '@/lib/settings-navigation';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { observeNativeFullscreen } from '@/lib/window-fullscreen';
|
||||
import { useCurrentUserProfile } from '@/hooks/use-current-user-profile';
|
||||
import type { SidebarPeekSource } from './sidebar-peek';
|
||||
import { ModuleSwitcher } from './ModuleSwitcher';
|
||||
@@ -86,6 +87,20 @@ type SidebarProps = {
|
||||
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({
|
||||
workspaceLayout = false,
|
||||
hideOnCompact = false,
|
||||
@@ -408,6 +423,7 @@ export function Sidebar({
|
||||
id={workspaceLayout ? 'classroom-sidebar' : undefined}
|
||||
data-testid="sidebar"
|
||||
data-classroom={workspaceLayout}
|
||||
data-workspace-sidebar={isPaintingModule ? 'canvas' : undefined}
|
||||
aria-hidden={sidebarCollapsed}
|
||||
onPointerEnter={() => onSidebarPeekChange?.(true, 'sidebar')}
|
||||
onPointerLeave={() => onSidebarPeekChange?.(false, 'sidebar')}
|
||||
@@ -432,8 +448,8 @@ export function Sidebar({
|
||||
{activeModule !== 'cloud_agents' && <div
|
||||
className={cn(
|
||||
'bg-transparent px-3 text-foreground',
|
||||
workspaceLayout && 'classroom-sidebar-header',
|
||||
isPaintingModule && !sidebarCollapsed ? 'pb-2 pt-10' : 'py-2',
|
||||
(workspaceLayout || isPaintingModule) && 'classroom-sidebar-header',
|
||||
isPaintingModule ? 'h-10 shrink-0' : 'py-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">
|
||||
<ModuleSwitcher sidebarCollapsed={sidebarCollapsed} toolbar iconOnly />
|
||||
</div>}
|
||||
{!workspaceLayout && <ModuleSwitcher sidebarCollapsed={sidebarCollapsed} compact={!sidebarCollapsed} />}
|
||||
{isPaintingModule ? <CanvasSidebarNavigation /> : !workspaceLayout && <ModuleSwitcher sidebarCollapsed={sidebarCollapsed} compact={!sidebarCollapsed} />}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
@@ -874,7 +890,7 @@ export function Sidebar({
|
||||
onOpenChange={setProfileDialogOpen}
|
||||
/>
|
||||
|
||||
{workspaceLayout && isProgrammingModule && (
|
||||
{((workspaceLayout && isProgrammingModule) || isPaintingModule) && (
|
||||
<div className="classroom-community-footer">
|
||||
<CommunityLink variant="sidebar" collapsed={sidebarCollapsed} />
|
||||
</div>
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
/* Shared Code navigation skin; chat content remains scoped to its workspace. */
|
||||
[data-testid="main-layout"][data-classroom="true"], [data-testid="sidebar"][data-classroom="true"], .classroom-workspace {
|
||||
/* Shared Code/Canvas navigation skin; workspace content stays independently scoped. */
|
||||
[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%;
|
||||
--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="sidebar"][data-classroom="true"] { 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"][data-classroom="true"] [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"][data-classroom="true"] .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"][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"][data-classroom="true"] .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"][data-classroom="true"] .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"][data-classroom="true"] .classroom-brand:hover { background: #eaf1f8; }
|
||||
[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"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-sidebar-header { padding-top: 0; padding-bottom: 0; }
|
||||
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) [data-testid="classroom-sidebar-navigation"] { align-items: center; }
|
||||
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-brand { height: 46px; gap: 10px; padding: 0 2px; }
|
||||
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-community-footer { flex-shrink: 0; padding: 0 12px; }
|
||||
[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"]: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"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-community-footer .community-link[data-variant="sidebar"]:hover:not(:disabled) { background: #eef3e7; }
|
||||
[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"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .community-link-icon svg { width: 20px; height: 20px; }
|
||||
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .community-link-arrow { margin-left: auto; color: #64758a; }
|
||||
[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-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"][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"][data-classroom="true"] [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"]) .classroom-sidebar-content { padding: 0 12px 14px; }
|
||||
[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"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) [data-testid="sidebar-create-project"] svg { color: #c44b23; }
|
||||
[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 svg { width: 20px; height: 20px; }
|
||||
[data-testid="sidebar"][data-classroom="true"] .classroom-projects { margin-top: 8px; }
|
||||
[data-testid="sidebar"][data-classroom="true"] .classroom-projects-heading { padding: 6px 3px; color: #64758a; }
|
||||
[data-testid="sidebar"][data-classroom="true"] .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"][data-classroom="true"] .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"][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-projects { margin-top: 8px; }
|
||||
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-projects-heading { padding: 6px 3px; color: #64758a; }
|
||||
[data-testid="sidebar"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-projects-heading > svg { display: 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"]:is([data-classroom="true"], [data-workspace-sidebar="canvas"]) .classroom-project-card:hover { background: #eaf1f8; }
|
||||
[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"]: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 svg { width: 20px; height: 20px; }
|
||||
.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 > 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-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-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; }
|
||||
|
||||
@@ -473,6 +473,59 @@ test.describe('AI Design V2 workspace', () => {
|
||||
await expect(page.getByText('目标与用途')).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 conversationBounds = (await conversationPane.boundingBox())!;
|
||||
const worksBounds = (await worksPane.boundingBox())!;
|
||||
|
||||
Reference in new issue
Block a user