Align Canvas with Agents and give current works a resizable workspace
This commit is contained in:
1 parent
70166d22b3
commit
2ca6e3199d
19 files changed
+1548
-511
No files matched your search
@@ -1,7 +1,9 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import {
|
||||
FolderKanban,
|
||||
Loader2,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
@@ -71,6 +73,8 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
|
||||
|
||||
useEffect(() => {
|
||||
const openCreateDialog = () => {
|
||||
// The compact project Sheet owns this event while the desktop sidebar is hidden.
|
||||
if (typeof window.matchMedia === 'function' && !window.matchMedia('(min-width: 1024px)').matches) return;
|
||||
setProjectName('');
|
||||
setDialogError(null);
|
||||
setDialog({ mode: 'create', project: null });
|
||||
@@ -144,138 +148,159 @@ export function ImageWorkspaceSidebar({ sidebarCollapsed }: ImageWorkspaceSideba
|
||||
navigate('/image-canvas');
|
||||
};
|
||||
|
||||
if (sidebarCollapsed) {
|
||||
return (
|
||||
<div data-testid="sidebar-image-workspace" className="flex flex-col items-center gap-2 px-2 py-3">
|
||||
return (
|
||||
<div data-testid="sidebar-image-workspace" className="flex min-h-0 flex-1 flex-col px-2 pb-3 pt-3 font-sans">
|
||||
{sidebarCollapsed ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="新建设计项目"
|
||||
title="新建设计项目"
|
||||
className="h-10 w-10 self-center rounded-lg text-brand hover:bg-brand-soft"
|
||||
onClick={openCreateDialog}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="sidebar-image-workspace" className="flex min-h-0 flex-1 flex-col px-2 pb-3 pt-2 font-sans">
|
||||
<div className="space-y-1">
|
||||
<button
|
||||
type="button"
|
||||
className="motion-press flex min-h-9 w-full items-center gap-2 rounded-xl px-2.5 text-left text-sm font-medium text-foreground transition-colors hover:bg-surface-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30"
|
||||
onClick={openCreateDialog}
|
||||
>
|
||||
<Plus className="h-4 w-4 text-brand" />
|
||||
新建设计项目
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 mt-5 flex items-center justify-between px-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">设计项目</span>
|
||||
{(status === 'error' || status === 'unavailable') && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="重新加载设计项目"
|
||||
className="rounded-lg p-1 text-muted-foreground hover:bg-surface-subtle hover:text-foreground"
|
||||
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-xl transition-colors',
|
||||
active ? 'bg-brand/[0.08]' : 'hover:bg-surface-subtle',
|
||||
)}
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
|
||||
<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"
|
||||
className="flex min-h-12 w-full items-center gap-2.5 rounded-xl px-2.5 py-2 pr-16 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30"
|
||||
onClick={() => chooseProject(project)}
|
||||
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()}
|
||||
>
|
||||
{pending ? (
|
||||
<Loader2 className="h-4 w-4 shrink-0 animate-spin text-brand" />
|
||||
) : (
|
||||
<FolderKanban className={cn('h-4 w-4 shrink-0', active ? 'text-brand' : 'text-muted-foreground')} />
|
||||
)}
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-medium text-foreground">
|
||||
{project.title}
|
||||
</span>
|
||||
<span className="mt-0.5 block text-[11px] text-muted-foreground">
|
||||
设计版本 {project.specificationRevision} · {formatUpdatedAt(project.updatedAt)}
|
||||
</span>
|
||||
<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>
|
||||
<div className="absolute right-1.5 top-2 flex opacity-0 transition-opacity group-hover/project:opacity-100 focus-within:opacity-100">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`重命名 ${project.title}`}
|
||||
className="rounded-lg p-1.5 text-muted-foreground hover:bg-background hover:text-foreground"
|
||||
onClick={() => openRenameDialog(project)}
|
||||
)}
|
||||
|
||||
{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',
|
||||
)}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`删除 ${project.title}`}
|
||||
className="rounded-lg p-1.5 text-muted-foreground hover:bg-background hover:text-destructive"
|
||||
onClick={() => {
|
||||
setDeleteTarget(project);
|
||||
setDeleteConfirmation('');
|
||||
setDeleteError(null);
|
||||
}}
|
||||
>
|
||||
{deletingWorkspaceId === project.workspaceId
|
||||
? <Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
: <Trash2 className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<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 className="mt-0.5 block truncate text-xs text-muted-foreground">
|
||||
设计版本 {project.specificationRevision} · {formatUpdatedAt(project.updatedAt)}
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Dialog open={dialog !== null} onOpenChange={(open) => !open && !saving && setDialog(null)}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
|
||||
Reference in new issue
Block a user