fix(canvas): compact workspace toolbar

This commit is contained in:
inman
2026-09-07 15:53:49 +08:00
parent 0619327fbe
commit e98603435d
4 changed files with 21 additions and 71 deletions

View File

@@ -5,7 +5,6 @@ import { DesignPlanHistory } from './DesignPlanHistory';
export function DesignHistoryRail({ workspace }: { workspace: DesignWorkspace | null }) {
const taskCount = workspace?.tasks.length ?? 0;
const resultCount = workspace?.assets.filter((asset) => asset.generationTaskId).length ?? 0;
const platform = window.electron?.platform;
return (
@@ -17,22 +16,14 @@ export function DesignHistoryRail({ workspace }: { workspace: DesignWorkspace |
<header
data-testid="image-workspace-history-header"
className={cn(
'flex h-16 shrink-0 items-center border-b border-border/70 px-4',
'flex h-12 shrink-0 items-center border-b border-border/70 px-4',
platform === 'darwin' && 'pr-[116px]',
platform === 'win32' && 'items-end pb-1.5 pr-[156px]',
platform === 'win32' && 'pr-[244px]',
)}
>
<div className="min-w-0">
<div className="flex items-center gap-2">
<Images className="h-4 w-4 shrink-0 text-brand" aria-hidden="true" />
<h2 className="truncate text-sm font-semibold text-foreground"></h2>
</div>
<p className={cn(
'mt-1 truncate text-[11px] tabular-nums text-muted-foreground',
platform === 'win32' && 'hidden',
)}>
{taskCount > 0 ? `${taskCount} 次制作 · ${resultCount} 个结果` : '当前设计项目的制作记录'}
</p>
<div className="flex min-w-0 items-center gap-2">
<Images className="h-4 w-4 shrink-0 text-brand" aria-hidden="true" />
<h2 className="truncate text-sm font-semibold text-foreground"></h2>
</div>
</header>

View File

@@ -1,6 +1,5 @@
import { useEffect, useState } from 'react';
import {
Cloud,
CloudOff,
PanelLeft,
Plus,
@@ -23,8 +22,6 @@ import { useImageWorkspaceStore } from '@/stores/image-workspace';
import { DesignConversationPane } from './DesignConversationPane';
import { DesignHistoryRail } from './DesignHistoryRail';
import { DesignWorksRail } from './DesignWorksRail';
import { hasActiveCreationPlan } from './plan-lifecycle';
import { projectYouthCreationCard } from './youth-form-projection';
function useDesktopLayout(): boolean {
const [desktop, setDesktop] = useState(() => (
@@ -49,7 +46,7 @@ function LoadingWorkspace() {
return (
<div className="flex h-full min-h-0 bg-background" aria-label="正在加载 AI 设计">
<div className="flex min-w-0 flex-1 flex-col">
<div className="h-16 animate-pulse border-b border-border/60 bg-surface-subtle/60" />
<div className="h-12 animate-pulse border-b border-border/60 bg-surface-subtle/60" />
<div className="min-h-0 flex-1 space-y-4 bg-surface-subtle/35 p-6">
<div className="h-16 animate-pulse rounded-2xl bg-surface-subtle" />
<div className="ml-auto h-20 w-4/5 animate-pulse rounded-2xl bg-brand/[0.07]" />
@@ -57,7 +54,7 @@ function LoadingWorkspace() {
</div>
</div>
<div className="hidden h-full w-[320px] shrink-0 flex-col border-l border-border/70 bg-background lg:flex xl:w-[340px]">
<div className="h-16 animate-pulse border-b border-border/60 bg-surface-subtle/45" />
<div className="h-12 animate-pulse border-b border-border/60 bg-surface-subtle/45" />
<div className="space-y-3 p-3">
{[0, 1, 2].map((item) => (
<div key={item} className="h-16 animate-pulse rounded-xl bg-surface-subtle" />
@@ -111,7 +108,6 @@ export function ImageCanvas() {
const status = useImageWorkspaceStore((state) => state.status);
const bootstrap = useImageWorkspaceStore((state) => state.bootstrap);
const workspace = useImageWorkspaceStore((state) => state.workspace);
const eventState = useImageWorkspaceStore((state) => state.eventState);
const error = useImageWorkspaceStore((state) => state.error);
const quoteBlockers = useImageWorkspaceStore((state) => state.quoteBlockers);
const load = useImageWorkspaceStore((state) => state.load);
@@ -150,23 +146,7 @@ export function ImageCanvas() {
if (!workspace && status !== 'ready') return <LoadingWorkspace />;
const streamHealthy = eventState === 'connected';
const generationAvailable = bootstrap?.capabilities.generation !== false;
const readiness = workspace
? workspace.form.activeQuotes.some((quote) => quote.status === 'offered')
? '制作方案已准备好,确认前仍可修改'
: hasActiveCreationPlan(workspace)
? '想法已整理清楚,可以检查制作方案'
: projectYouthCreationCard(workspace, { quoteBlockers }).readinessMessage
: '从一个新作品开始';
const updatedAt = workspace
? new Date(workspace.workspace.updatedAt).toLocaleString([], {
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
: null;
const platform = window.electron?.platform;
const openCreate = () => {
if (!desktopLayout) setWorksOpen(true);
@@ -179,7 +159,7 @@ export function ImageCanvas() {
<div data-testid="image-workspace" className="flex h-full min-h-0 overflow-hidden bg-background font-sans">
<div className="flex min-w-0 flex-1 flex-col">
<header data-testid="image-workspace-header" className={cn(
'flex h-16 shrink-0 items-center gap-4 border-b border-border/70 bg-background py-2',
'flex h-12 shrink-0 items-center gap-3 border-b border-border/70 bg-background',
platform === 'darwin'
? 'pl-4 pr-[116px] sm:pl-5'
: platform === 'win32'
@@ -193,36 +173,8 @@ export function ImageCanvas() {
{workspace?.workspace.title ?? 'Makelore Canvas'}
</h1>
</div>
<p className="mt-1 truncate text-xs text-muted-foreground">
{updatedAt ? `${readiness} · 更新于 ${updatedAt}` : readiness}
</p>
</div>
<div
className={cn(
'hidden items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs sm:flex',
streamHealthy ? 'bg-brand/[0.055] text-brand' : 'bg-surface-subtle text-muted-foreground',
)}
title={streamHealthy ? '已经连接' : '正在重新连接,你的内容不会丢失'}
>
{streamHealthy ? <Cloud className="h-3.5 w-3.5" /> : <CloudOff className="h-3.5 w-3.5" />}
{streamHealthy ? '实时同步' : '恢复连接'}
</div>
{workspace && (
<Button
type="button"
variant="ghost"
size="icon"
className="h-9 w-9 rounded-lg"
aria-label="刷新作品"
title="刷新"
onClick={() => void refreshWorkspace()}
>
<RefreshCw className="h-4 w-4" />
</Button>
)}
{!desktopLayout && (
<Button
type="button"

View File

@@ -356,6 +356,9 @@ test.describe('AI Design V2 workspace', () => {
await expect(page.getByTestId('image-workspace-history-rail')).toBeVisible();
await expect(page.getByTestId('image-workspace-works-rail')).toHaveCount(0);
await expect(page.getByRole('heading', { name: '和 AI 一起完善创作' })).toHaveCount(0);
await expect(page.getByText('当前设计项目的制作记录')).toHaveCount(0);
await expect(page.getByText('实时同步')).toHaveCount(0);
await expect(page.getByRole('button', { name: '刷新作品' })).toHaveCount(0);
await expect(page.getByRole('heading', { name: '制作方案' })).toBeVisible();
await expect(page.getByRole('textbox', { name: '创作提示词' })).toHaveValue(
'把便携咖啡机呈现为城市通勤中的精密工具',
@@ -404,8 +407,8 @@ test.describe('AI Design V2 workspace', () => {
expect(layout!.historyTopGap).toBeLessThanOrEqual(1);
expect(layout!.historyBottomGap).toBeLessThanOrEqual(1);
expect(layout!.historyHorizontalGap).toBeLessThanOrEqual(1);
expect(layout!.workspaceHeaderHeight).toBe(64);
expect(layout!.historyHeaderHeight).toBe(64);
expect(layout!.workspaceHeaderHeight).toBe(48);
expect(layout!.historyHeaderHeight).toBe(48);
expect(layout!.headerTopGap).toBeLessThanOrEqual(1);
expect(layout!.headerBottomGap).toBeLessThanOrEqual(1);
expect(layout!.conversationWidth).toBeGreaterThan(600);

View File

@@ -132,22 +132,26 @@ describe('AI Design Canvas page', () => {
const historyRail = screen.getByTestId('image-workspace-history-rail');
expect(historyRail).toHaveClass('h-full', 'border-l');
expect(screen.getByTestId('image-workspace-header')).toHaveClass('h-16');
expect(screen.getByTestId('image-workspace-history-header')).toHaveClass('h-16');
expect(screen.getByTestId('image-workspace-header')).toHaveClass('h-12');
expect(screen.getByTestId('image-workspace-history-header')).toHaveClass('h-12');
expect(within(historyRail).getByRole('heading', { name: '历史作品' })).toBeInTheDocument();
expect(within(historyRail).queryByText('当前设计项目的制作记录')).not.toBeInTheDocument();
expect(within(historyRail).getByText('还没有历史作品')).toBeInTheDocument();
expect(screen.queryByTestId('image-workspace-works-rail')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '项目' })).not.toBeInTheDocument();
expect(screen.queryByText('实时同步')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '刷新作品' })).not.toBeInTheDocument();
});
it('prioritizes an offered current plan over older completed-work status', () => {
it('keeps an offered current plan actionable without a second header status row', () => {
setViewport(true);
prepareWorkspace({
form: designFormFixture({ activeQuotes: [designQuoteFixture()] }),
});
render(<ImageCanvas />);
expect(screen.getByText(/制作方案已准备好,确认前仍可修改/)).toBeInTheDocument();
expect(screen.getByRole('button', { name: '确认并开始制作' })).toBeInTheDocument();
expect(screen.queryByText(/制作方案已准备好,确认前仍可修改/)).not.toBeInTheDocument();
expect(screen.getByTestId('image-workspace-history-rail')).toBeInTheDocument();
expect(screen.queryByTestId('image-workspace-works-rail')).not.toBeInTheDocument();
});