From 96ae3db1df642ed01603d256489c36c7968960d7 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Mon, 14 Sep 2026 21:41:26 +0800 Subject: [PATCH] =?UTF-8?q?AGC=20=E8=B5=84=E6=BA=90=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E5=8F=B0=E4=B8=89=E5=A4=84=E4=BA=A4=E4=BA=92=E6=94=B6=E5=8F=A3?= =?UTF-8?q?=EF=BC=9A=E4=BE=A7=E6=A0=8F=E5=85=B3=E9=97=AD=E5=85=A5=E5=8F=A3?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E3=80=81=E5=88=A0=E9=99=A4=E6=8A=98=E5=8F=A0?= =?UTF-8?q?=E6=8A=8A=E6=89=8B=E3=80=81=E6=92=AD=E6=94=BE=E5=B7=A6=E5=AF=B9?= =?UTF-8?q?=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 「生成任务」侧栏头部右上角由返回箭头(ChevronLeft / aria-label=收起生成任务)改为关闭 X,并与底部那枚重复的 X 统一为同一个 aria-label=关闭生成任务 - 删除「生成任务」侧栏底部 footer(关闭按钮 + border-top 分割线):关闭入口只留头部一枚 - 删除「生成任务」侧栏折叠态的左侧贴边竖排把手(handle 及其标签 / 在途角标 / 进场动画 / 悬停位移):折叠后组件不再渲染任何常驻入口 - 清理上述删除留下的悬空样式:focus-visible 分组选择器、480px 窄屏媒体查询与 prefers-reduced-motion 块里的把手分支 - 工具条「生成任务」入口移出 `mode === 'resources'` 分支:两个页签下都常驻。侧栏本体本来就是无条件渲染的浮层,运行态可见;入口只留资源页签时,切到运行关掉侧栏就再也打不开(此前靠左侧贴边把手兼作运行态入口) - 工具条把「播放」移到「资源管理 / 运行」右侧并左对齐:新增 .game-workbench-view-tabs 作为左侧控件组,播放按钮由 absolute + left:50% + translateX(-50%) 的居中悬浮改为 static;相应删除 max-width:1000px 里的居中兜底规则 - 播放按钮并进按钮基础外观与 focus-visible 规则列表:新位置不再命中 .game-workbench-tabs / .game-workbench-view-actions 的选择器,漏掉会掉成零圆角、零内边距、无边框、默认字号的裸按钮(颜色规则仍生效,肉眼容易漏) - 播放按钮保持两个页签下都常驻(回到改动前的可见性):运行视图空态与预览失败态要靠它重跑 - 同步更新断言:侧栏关闭入口唯一且 footer 不存在、折叠态不渲染任何按钮或把手、样式表不残留被删类名、播放按钮声明为左对齐静态定位且仍在基础外观/焦点环规则内 - 生成任务用例补回机器证据:折叠后工具条按钮的 data-resource-generation-task-count 归零、重开后头部「在途生成任务 0」徽标跟上收口结果、运行页签下侧栏仍能重开(先用 aria-selected 钉住真的切了页签);用例夹具新增 runnable 开关把首个原型标成完成 - 同步文档现状:PRD 顶部播放按钮与任务侧栏进度面两条、AGC 栏目画布底部工具栏入口矩阵的侧栏形态、decision-log 的进度面形态决策 验证:tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit 通过;vitest appSurface 全量 440 条通过、生成任务侧栏 8 条与侧栏样式 9 条通过;check-encoding 通过;prettier --list-different 未新增命中(project-development/index.tsx 属基线既有非格式化文件);变异验证:还原播放按钮基础外观规则、还原折叠把手与 footer、还原折叠态早返回、把「生成任务」入口退回 mode 分支,对应断言均变红 --- ...rceCanvasAssetGenerationTasksPanelView.tsx | 43 +------ ...ourceCanvasAssetGenerationTasksSidebar.css | 85 +------------- apps/ai-game-creator-shell/src/styles.css | 27 +++-- .../src/view/project-development/index.tsx | 109 ++++++++++-------- .../appSurface/project-development.suite.ts | 79 +++++++++++-- ...ceCanvasAssetGenerationTasksPanel.test.tsx | 48 ++++---- ...asAssetGenerationTasksSidebarStyle.test.ts | 32 ++--- ...AI游戏创作】项目开发工作台PRD-2026-07-20.md | 4 +- .../shared-memory/decision-log.md | 2 +- ...C】栏目画布底部工具栏入口矩阵-2026-09-13.md | 2 +- 10 files changed, 188 insertions(+), 243 deletions(-) diff --git a/apps/ai-game-creator-shell/src/features/resource-canvas/ResourceCanvasAssetGenerationTasksPanelView.tsx b/apps/ai-game-creator-shell/src/features/resource-canvas/ResourceCanvasAssetGenerationTasksPanelView.tsx index 494fa43c8..07ded6984 100644 --- a/apps/ai-game-creator-shell/src/features/resource-canvas/ResourceCanvasAssetGenerationTasksPanelView.tsx +++ b/apps/ai-game-creator-shell/src/features/resource-canvas/ResourceCanvasAssetGenerationTasksPanelView.tsx @@ -1,6 +1,6 @@ import './resourceCanvasAssetGenerationTasksSidebar.css'; -import { ChevronLeft, ChevronRight, ListChecks, X } from 'lucide-react'; +import { ListChecks, X } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { @@ -130,31 +130,10 @@ export function ResourceCanvasAssetGenerationTasksPanelView({ return () => clearInterval(timer); }, [hasLiveTask]); + // 折叠态不再在画布左侧留贴边把手:开合口只剩工具条上那一枚「生成任务」按钮 + // (带在途计数),收起时侧栏完全让出画布。 if (!open) { - return ( - - ); + return null; } return ( @@ -178,10 +157,10 @@ export function ResourceCanvasAssetGenerationTasksPanelView({
)}
- ); } diff --git a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationTasksSidebar.css b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationTasksSidebar.css index 89d8d9ce9..284c6b482 100644 --- a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationTasksSidebar.css +++ b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationTasksSidebar.css @@ -94,7 +94,6 @@ } .game-resource-generation-tasks-sidebar-icon-button:focus-visible, -.game-resource-generation-tasks-handle:focus-visible, .game-resource-generation-task-locate:focus-visible { outline: 2px solid var(--platform-accent); outline-offset: 2px; @@ -326,78 +325,7 @@ color: var(--platform-text-strong); } -.game-resource-generation-tasks-sidebar-footer { - display: flex; - align-items: center; - justify-content: flex-end; - border-top: 1px solid var(--platform-line-soft); - padding: 0.4rem 0.6rem; -} - -/* 折叠态:贴边竖向把手 + 在途数量角标。 */ -.game-resource-generation-tasks-handle { - position: fixed; - top: 50%; - left: 0; - z-index: 40; - display: flex; - flex-direction: column; - align-items: center; - gap: 0.3rem; - border: 1px solid var(--platform-subpanel-border); - border-left: 0; - border-radius: 0 0.75rem 0.75rem 0; - background: var(--platform-subpanel-fill); - color: var(--platform-text-strong); - padding: 0.6rem 0.3rem; - box-shadow: var(--platform-panel-shadow); - transform: translateY(-50%); - transition: - background 120ms ease, - box-shadow 120ms ease, - transform 120ms ease; - animation: game-resource-generation-tasks-handle-enter 160ms ease-out; -} - -.game-resource-generation-tasks-handle:hover { - background: var(--platform-nav-item-hover-fill); - box-shadow: var(--platform-desktop-hover-shadow); - transform: translateY(-50%) translateX(0.1rem); -} - -@keyframes game-resource-generation-tasks-handle-enter { - from { - opacity: 0; - transform: translateY(-50%) translateX(-0.5rem); - } - - to { - opacity: 1; - transform: translateY(-50%) translateX(0); - } -} - -.game-resource-generation-tasks-handle-label { - font-size: 0.7rem; - font-weight: 850; - writing-mode: vertical-rl; -} - -.game-resource-generation-tasks-handle-badge { - display: inline-flex; - min-width: 1.1rem; - height: 1.1rem; - align-items: center; - justify-content: center; - border: 1px solid var(--platform-accent); - border-radius: 999px; - color: var(--platform-accent); - font-size: 0.68rem; - font-variant-numeric: tabular-nums; - font-weight: 850; -} - -/* 窄屏(含 360px):侧栏占满可用宽度,把手只占一条窄边,不挡画布操作。 */ +/* 窄屏(含 360px):侧栏占满可用宽度,不挡画布操作。 */ @media (max-width: 480px) { .game-resource-generation-tasks-sidebar { top: 3.5rem; @@ -406,21 +334,15 @@ left: 0.5rem; width: auto; } - - .game-resource-generation-tasks-handle { - padding: 0.5rem 0.2rem; - } } /* 降低动效偏好:进场动画、悬停位移与呼吸全部关掉。 */ @media (prefers-reduced-motion: reduce) { - .game-resource-generation-tasks-sidebar, - .game-resource-generation-tasks-handle { + .game-resource-generation-tasks-sidebar { animation: none; } .game-resource-generation-task-card, - .game-resource-generation-tasks-handle, .game-resource-generation-tasks-sidebar-icon-button, .game-resource-generation-task-badge::before, .game-resource-generation-task-locate { @@ -428,8 +350,7 @@ animation: none; } - .game-resource-generation-task-card:hover, - .game-resource-generation-tasks-handle:hover { + .game-resource-generation-task-card:hover { transform: none; } } diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index af05c40b7..defe6cf17 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -5521,6 +5521,15 @@ iframe.preview-frame { background: transparent; } +/* 左侧控件组:「资源管理 / 运行」分段 + 紧贴其后的「播放」。 + * 整组在工具条里左对齐,取代播放按钮原先的居中悬浮(absolute + translateX(-50%))。 */ +.game-workbench-view-tabs { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 7px; +} + .game-workbench-tabs, .game-workbench-view-actions { display: flex; @@ -5557,7 +5566,9 @@ iframe.preview-frame { border-color: transparent; } +/* 左侧控件组里的按钮(目前只有「播放」)与分段控件、右侧动作区共用同一套基础外观。 */ .game-workbench-tabs button, +.game-workbench-view-tabs button, .game-workbench-view-actions button { display: inline-flex; align-items: center; @@ -5576,6 +5587,7 @@ iframe.preview-frame { } .game-workbench-tabs button:focus-visible, +.game-workbench-view-tabs button:focus-visible, .game-workbench-view-actions button:focus-visible, .game-workbench-approval-trigger:focus-visible, .game-agent-dock-more:focus-visible { @@ -5607,16 +5619,16 @@ iframe.preview-frame { color: var(--platform-button-secondary-text); } -.game-workbench-view-actions .game-workbench-play-button { - position: absolute; - left: 50%; +/* 「播放」与「资源管理 / 运行」同组,外观仍按主按钮走;禁用时退回次级按钮。 */ +.game-workbench-view-tabs .game-workbench-play-button { + position: static; border-color: var(--platform-button-primary-border); background: var(--platform-button-primary-fill); color: var(--platform-button-primary-text); - transform: translateX(-50%); + transform: none; } -.game-workbench-view-actions .game-workbench-play-button:disabled { +.game-workbench-view-tabs .game-workbench-play-button:disabled { border-color: var(--platform-surface-border); background: var(--platform-button-secondary-fill); color: var(--platform-text-muted); @@ -9129,11 +9141,6 @@ iframe.preview-frame { .game-workbench-view-actions { justify-content: flex-end; } - - .game-workbench-view-actions .game-workbench-play-button { - position: static; - transform: none; - } } @media (max-width: 760px) { diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index 658a8b40a..be85a1063 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -7468,35 +7468,40 @@ export default function ProjectDevelopmentView({ } >
-
- - -
-
+ {/* + 「资源管理 / 运行」与「播放」是同一组左侧控件:播放紧贴模式切换之后、整组左对齐, + 不再居中悬浮。**两个页签下都常驻**——运行视图空态与预览失败态都要靠它重跑 + (空态文案就是「点击顶部播放按钮后将在这里直接运行游戏」)。 + */} +
+
+ + +
+
+
{mode === 'run' && embeddedPreviewUrl ? ( - {/* - 「生成任务」入口:常驻可见,开合画布上的任务侧栏;有在途任务时带上数量, - 用户关掉侧栏后一眼就能看出还有几条在跑。 - */} - {pendingResourceEdits.length > 0 || pendingResourceEditsLoadState === 'failed' ? (
diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index 41f54cc3d..20bbefbac 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -5803,10 +5803,22 @@ export function registerProjectWorkbenchFoundationTests() { /\.game-workbench-toolbar\s*\{[^}]*position:\s*relative[^}]*padding:\s*8px 12px[^}]*background:\s*transparent/s, ); expect(styles).toMatch( - /\.game-workbench-view-actions \.game-workbench-play-button\s*\{[^}]*position:\s*absolute[^}]*left:\s*50%[^}]*transform:\s*translateX\(-50%\)/s, + /\.game-workbench-view-tabs\s*\{[^}]*display:\s*flex[^}]*align-items:\s*center/s, + ); + // 播放按钮跟着「资源管理 / 运行」左对齐,不再居中悬浮。 + expect(styles).toMatch( + /\.game-workbench-view-tabs \.game-workbench-play-button\s*\{[^}]*position:\s*static[^}]*transform:\s*none/s, + ); + expect(styles).not.toMatch( + /\.game-workbench-play-button\s*\{[^}]*position:\s*absolute/s, + ); + // 新位置必须并进按钮基础外观与焦点环的规则列表:不然播放按钮会掉成零圆角、零内边距、 + // 无边框、默认字号的裸按钮,而颜色规则看起来仍然生效。 + expect(styles).toMatch( + /\.game-workbench-tabs button,\s*\.game-workbench-view-tabs button,\s*\.game-workbench-view-actions button\s*\{[^}]*border-radius:\s*999px/s, ); expect(styles).toMatch( - /@media \(max-width: 1000px\)[\s\S]*?\.game-workbench-view-actions \.game-workbench-play-button\s*\{[^}]*position:\s*static[^}]*transform:\s*none/s, + /\.game-workbench-view-tabs button:focus-visible,\s*\.game-workbench-view-actions button:focus-visible/s, ); expect(styles).toMatch( /@media \(min-width: 761px\)[\s\S]*?\.game-project-workbench\s*\{[^}]*grid-template-rows:\s*minmax\(0, 1fr\) auto[^}]*height:\s*100dvh/, @@ -11257,11 +11269,22 @@ export function registerProjectAgentStatusTests() { assetName: string; }) => Promise; listRecords?: (started: Map>) => unknown; + /** 把首个原型标成已完成,让「运行」页签可切(`runAvailable` 为真)。 */ + runnable?: boolean; }) { const manifest = createGameCreationAppManifest( input.projectId, '生成提交面板测试', ); + if (input.runnable) { + const codePrototype = manifest.tasks.find( + (task) => task.id === 'code-prototype', + ); + if (!codePrototype) { + throw new Error('missing code-prototype seed task'); + } + codePrototype.status = 'completed'; + } manifest.assets = [ { id: 'submit-art-spec', @@ -11446,6 +11469,8 @@ export function registerProjectAgentStatusTests() { const panel = await renderAssetGenerationSubmitView({ projectId: 'workbench-sidebar-collapsed', projectPath: '/tmp/workbench-sidebar-collapsed', + // 运行页签要真的能切,末尾那条「运行态也能重开侧栏」才不是在资源态自证。 + runnable: true, startLocalAsset: async ({ taskId }) => ({ taskId, projectId: 'workbench-sidebar-collapsed', @@ -11485,24 +11510,58 @@ export function registerProjectAgentStatusTests() { // 折叠侧栏:折叠只影响这个视图,任务仍在后台推进。 fireEvent.click( - within(sidebar).getByRole('button', { name: '收起生成任务' }), + within(sidebar).getByRole('button', { name: '关闭生成任务' }), ); expect(screen.queryByRole('region', { name: '生成任务' })).toBeNull(); - const handle = screen.getByRole('button', { name: '展开生成任务' }); - expect(handle.dataset.resourceGenerationTaskCount).toBe('1'); - // 收口后折叠把手上的在途计数跟着归零 —— 折叠期间进度照常更新。 + // 画布上不留折叠把手:开合口只剩工具条那一枚「生成任务」按钮。 + expect( + document.querySelector('.game-resource-generation-tasks-handle'), + ).toBeNull(); + expect( + screen.getByRole('button', { name: /^生成任务(?: · \d+)?$/ }), + ).not.toBeNull(); + // 收口后工具条按钮上的在途计数跟着归零 —— 折叠期间进度照常更新; + // 工具条按钮的 aria-label 恒为「生成任务」,所以计数只能查 data 属性,不能查可访问名。 await waitFor( () => expect( - screen.getByRole('button', { name: '展开生成任务' }).dataset - .resourceGenerationTaskCount, + ( + screen.getByRole('button', { + name: /^生成任务(?: · \d+)?$/, + }) as HTMLElement + ).dataset.resourceGenerationTaskCount, ).toBe('0'), { timeout: 15_000 }, ); - fireEvent.click(screen.getByRole('button', { name: '展开生成任务' })); + fireEvent.click(screen.getByRole('button', { name: '生成任务' })); const reopened = await screen.findByRole('region', { name: '生成任务' }); - expect(within(reopened).getByText('生成已完成。')).not.toBeNull(); + // 重开后条目内容与在途计数都跟上了收口结果。 + await waitFor( + () => + expect( + within(reopened).getByLabelText('在途生成任务 0').textContent, + ).toBe('0'), + { timeout: 15_000 }, + ); + expect(within(reopened).getByText('待提交设计图')).not.toBeNull(); + + // 入口在「运行」页签下同样常驻:侧栏本体在运行态可见,入口若只在资源页签就没法再打开。 + fireEvent.click( + within(reopened).getByRole('button', { name: '关闭生成任务' }), + ); + expect(screen.queryByRole('region', { name: '生成任务' })).toBeNull(); + fireEvent.click(screen.getByRole('tab', { name: '运行' })); + // 先钉住真的切到了运行页签,否则下面那条断言只是在资源态自证。 + expect( + screen.getByRole('tab', { name: '运行' }).getAttribute('aria-selected'), + ).toBe('true'); + fireEvent.click( + screen.getByRole('button', { name: /^生成任务(?: · \d+)?$/ }), + ); + expect( + await screen.findByRole('region', { name: '生成任务' }), + ).not.toBeNull(); }, 20_000); it('routes the audio column entries to the existing audio generation chain', async () => { diff --git a/apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationTasksPanel.test.tsx b/apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationTasksPanel.test.tsx index 20cf0cf70..03feb7d3b 100644 --- a/apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationTasksPanel.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationTasksPanel.test.tsx @@ -114,6 +114,9 @@ describe('「生成任务」侧栏', () => { expect(within(activeSection).getByText('正在处理。')).not.toBeNull(); expect(within(activeSection).getByText('生成中')).not.toBeNull(); + // 头部标题里的在途计数:四条任务里两条未终态,徽标必须跟着走。 + expect(screen.getByLabelText('在途生成任务 2').textContent).toBe('2'); + const doneSection = screen.getByRole('region', { name: '已完成' }); expect(within(doneSection).getByText('2')).not.toBeNull(); expect(within(doneSection).getByText('生成已完成。')).not.toBeNull(); @@ -123,13 +126,18 @@ describe('「生成任务」侧栏', () => { ).toBeGreaterThan(0); }); - test('展开状态下收起与关闭都走同一个折叠开关', async () => { + test('收起只有一个入口:头部那枚关闭按钮,收起即整块让出画布', async () => { const user = userEvent.setup(); const { onToggleOpen } = renderSidebar([task({ taskId: 't1' })]); - await user.click(screen.getByRole('button', { name: '收起生成任务' })); - expect(onToggleOpen).toHaveBeenCalledTimes(1); + // 底部那枚重复的「关闭」连同它的分割线已经删掉:展开态只有一个关闭按钮。 + expect( + screen.getAllByRole('button', { name: '关闭生成任务' }), + ).toHaveLength(1); + expect( + document.querySelector('.game-resource-generation-tasks-sidebar-footer'), + ).toBeNull(); await user.click(screen.getByRole('button', { name: '关闭生成任务' })); - expect(onToggleOpen).toHaveBeenCalledTimes(2); + expect(onToggleOpen).toHaveBeenCalledTimes(1); }); test('侧栏高度有界、列表自己滚动,「已完成」条数封顶', () => { @@ -169,30 +177,16 @@ describe('「生成任务」侧栏', () => { ).not.toBeNull(); }); - test('折叠时只留贴边把手,把手上的在途计数跟着任务走(0 / 2)', async () => { - const user = userEvent.setup(); - const collapsed = renderSidebar([], { open: false }); - const handle = screen.getByRole('button', { name: '展开生成任务' }); - expect(handle.getAttribute('aria-expanded')).toBe('false'); - expect(handle.dataset.resourceGenerationTaskCount).toBe('0'); + test('折叠后不再渲染,画布上不留任何常驻入口或把手', async () => { + renderSidebar([], { open: false }); expect(screen.queryByRole('region', { name: '生成任务' })).toBeNull(); - await user.click(handle); - expect(collapsed.onToggleOpen).toHaveBeenCalledTimes(1); - cleanup(); - - const running = task({ - taskId: 't-running', - dispatched: true, - status: 'running', - phaseDetail: '正在生成。', - }); - const queued = task({ taskId: 't-queued' }); - const { container } = renderSidebar([running, queued], { open: false }); - const withTwo = container.querySelector( - 'button[data-resource-generation-task-count]', - ) as HTMLElement; - expect(withTwo.dataset.resourceGenerationTaskCount).toBe('2'); - expect(screen.getByLabelText('在途生成任务 2').textContent).toBe('2'); + expect(screen.queryByRole('button')).toBeNull(); + expect( + document.querySelector('.game-resource-generation-tasks-handle'), + ).toBeNull(); + expect( + document.querySelector('.game-resource-generation-tasks-sidebar'), + ).toBeNull(); }); test('只有已完成且拿到资源 id 的任务能定位到素材卡', async () => { diff --git a/apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationTasksSidebarStyle.test.ts b/apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationTasksSidebarStyle.test.ts index 4a0048426..d23bc114a 100644 --- a/apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationTasksSidebarStyle.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationTasksSidebarStyle.test.ts @@ -122,7 +122,7 @@ describe('「生成任务」侧栏样式', () => { expect(declaration(locate, 'text-decoration')).toContain('underline'); }); - test('展开 / 折叠都有过渡,折叠把手是竖向标签 + 在途角标', () => { + test('进场动画留在侧栏上,删除的折叠把手不再有样式残留', () => { const sidebar = resolved(['.game-resource-generation-tasks-sidebar']); expect(declaration(sidebar, 'animation')).toContain( 'game-resource-generation-tasks-enter', @@ -130,26 +130,17 @@ describe('「生成任务」侧栏样式', () => { expect(declaration(sidebar, 'border-radius')).toBe('0.75rem'); expect(declaration(sidebar, 'backdrop-filter')).toBe('blur(10px)'); - const handle = resolved([ + // 折叠把手与底部关闭条都已删除:对应类名不得再出现在样式表里。 + for (const removed of [ '.game-resource-generation-tasks-handle', - '.game-resource-generation-tasks-handle:hover', - ]); - expect(declaration(handle, 'transition')).toContain('transform'); - expect(declaration(handle, 'transform')).toBe( - 'translateY(-50%) translateX(0.1rem)', - ); - - const handleLabel = resolved([ '.game-resource-generation-tasks-handle-label', - ]); - expect(declaration(handleLabel, 'writing-mode')).toBe('vertical-rl'); - - const handleBadge = resolved([ '.game-resource-generation-tasks-handle-badge', - ]); - expect(declaration(handleBadge, 'font-variant-numeric')).toBe( - 'tabular-nums', - ); + '.game-resource-generation-tasks-sidebar-footer', + ]) { + expect(rules.some((rule) => rule.selectors.includes(removed))).toBe( + false, + ); + } }); test('列表滚动条是细样式,滚动区域有界', () => { @@ -194,9 +185,6 @@ describe('「生成任务」侧栏样式', () => { expect( motionless('.game-resource-generation-tasks-sidebar', 'animation'), ).toBe(true); - expect( - motionless('.game-resource-generation-tasks-handle', 'animation'), - ).toBe(true); expect( motionless('.game-resource-generation-task-card', 'transition'), ).toBe(true); @@ -205,7 +193,7 @@ describe('「生成任务」侧栏样式', () => { test('焦点环可见(键盘可见焦点用平台 token)', () => { const focusRule = rules.find((rule) => rule.selectors.includes( - '.game-resource-generation-tasks-handle:focus-visible', + '.game-resource-generation-tasks-sidebar-icon-button:focus-visible', ), ); expect(focusRule?.declarations.get('outline')).toBe( diff --git a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md index c1e86a1c5..bf734224b 100644 --- a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md +++ b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md @@ -121,7 +121,7 @@ - 资源总览的“资源依赖 / 资源类型”视图切换使用连通的分段按钮组,相邻选项共享边界并保持唯一选中语义。每个分段都必须有清晰的键盘焦点指示,焦点环不得被分段容器的圆角或 `overflow` 裁切。 - 右侧 Supervisor 对话中,用户消息使用右对齐、最大宽度受限的主题暖色气泡,assistant 消息保持左对齐;消息换行不得产生水平溢出,执行过程卡继续占满消息区可用宽度。消息列表必须约束在右侧对话列内并独立滚动,不得覆盖中央资源或运行视图;提交按钮必须保留随状态变化的可访问名称。气泡正文在 light / dark 平台主题下均须满足 WCAG AA 普通文本 `4.5:1` 对比度。 - 客户端正式产品仍只按最小 `1280×720` 横屏合同交付,并保留 `1280×800` 默认窗口与既有基线验收;更窄浏览器样式只负责不崩溃和开发兼容,不改成移动端创作工作台。 -- 工作台顶部播放按钮在桌面视窗中水平居中;资源管理视窗触发播放时直接切换到运行视窗并启动本地预览,不再弹出 `game.run_local` 二次确认。 +- 工作台顶部播放按钮紧贴「资源管理 / 运行」模式切换之后、整组左对齐;不再在桌面视窗中水平居中悬浮;两个页签下都常驻(运行视图空态与预览失败态要靠它重跑)。资源管理视窗触发播放时直接切换到运行视窗并启动本地预览,不再弹出 `game.run_local` 二次确认。 - 创建模式素材画布的“素材名称”是用户可编辑的正式输出名称;“资源用途”是 manifest subtype,不向普通用户开放自由文本。新增资源默认“普通游戏美术”,可从普通游戏美术、统一视觉规范、游戏界面原型、核心美术图集四项中选择。图片精修继承源名称和用途,不显示创建模式保存设置;候选图片只从选中图片的“设为最终图”提交。精修顶栏只保留返回、导入、定位当前最终图、撤销和重做,删除进入图片上下文工具栏,通用 AI 生成只保留给创建模式。图片输出统一使用 PNG;创建模式工具动作与保存设置分层展示,“保存到项目”在 `1280×800` 和窄容器中都必须完整可见。 - 首页创作输入区与“最近项目”之间不展示共享项目状态文本,“最近项目”标题下也不追加解释性副标题;默认、成功、进行中或失败状态均不得在该位置形成文字行,项目管理页继续保留自己的状态反馈。 @@ -155,7 +155,7 @@ - 位置与层级:画布左下角(`left: 14px; bottom: 14px; z-index: 40`)。右下角是既有的缩放 / 撤销 Dock(`right: 14px; bottom: 14px`),左下角是画布上唯一两者都不占的稳定空位。工具栏是管理区 `.game-resource-book-manager` 的**直接子节点**、与画本场景并列,不进带 `scale()` 的场景层;二级菜单与「入口不可用原因」都贴着工具栏上沿弹出,不做内嵌内容。 - 外壳用共享 chrome(`packages/image-canvas-react` 的 `CanvasToolbar / CanvasToolbarGroup / CanvasChromeButton`),样式落在 AGC 的 `resourceCanvasChrome.css`;共享包只承接通用表现,不含业务规则。 - 接线:图片类入口走本地 IPC `start_local_project_asset_generation`(`kind` ∈ `image / character / spec / icon-spec / ui-prototype / art-spritesheet`;**提交即返回任务记录**,生成由 Rust 后台任务跑完写回项目,进度用 `list_local_project_asset_generations` 读回项目内账本 `.agent/runtime/asset-generation-tasks/tasks.json`);音频入口复用既有的无源生成链路 `derive_local_project_resource`(`generationMode: 'create'`、`editKind` = `sound-effect` / `background-music`);「上传」复用 `upload_local_asset`。生成 / 上传成功后一律用「配对读 `(revision, manifest)`」交给 `onManifestChange`,走既有 manifest 刷新与资源投影链路,不重算依赖图、不另写布局。 -- 本地排队与进度可见:AGC 本地 durable 输出槽已按**精确动作指纹**分槽(不同 prompt / 素材名各自独立成槽,具备并行能力),但本批前端仍按「同一时刻只派发一条」排队——真并行派发需要并发收口设计(配对读 + manifest CAS + 聚焦意图互不覆盖),留待下一批;所以第一条未终态时第二条提交停在**前端本地队列**里(不调用提交 IPC,显示本地排队的「排队中。」),前一条终态后自动补发;任务状态与阶段文案(后端 `phaseDetail`)由任务账本提供,前端不拼阶段、不做百分比。进度面是**画布上常驻的可折叠任务侧栏**(形态对齐网页端美术画布的任务侧栏):折叠时留贴边把手并在把手上显示在途数量,展开是两个分栏「排队/生成中」与「已完成」(各带条数,「已完成」封顶 20 条 + 列表滚动 + 高度有界);每项显示状态徽标 / 阶段文案 / 已耗时 / 素材名,可「定位到素材」。侧栏非模态(不铺全屏遮罩、不做焦点陷阱、不参与模态遮挡判据),位置在画布左侧标题栏之下、**覆盖式**(不 reflow 挤窄画布视口),提交受理后自动展开。定位动作**每次点击都终局化**:能定位就定位并选中;素材在别的栏目先切栏目;不在投影里给「素材已不在项目里 / 已登记但尚未同步」的结论;3 秒内有界兜底,不允许提示条永久停在「正在定位生成的素材…」。 +- 本地排队与进度可见:AGC 本地 durable 输出槽已按**精确动作指纹**分槽(不同 prompt / 素材名各自独立成槽,具备并行能力),但本批前端仍按「同一时刻只派发一条」排队——真并行派发需要并发收口设计(配对读 + manifest CAS + 聚焦意图互不覆盖),留待下一批;所以第一条未终态时第二条提交停在**前端本地队列**里(不调用提交 IPC,显示本地排队的「排队中。」),前一条终态后自动补发;任务状态与阶段文案(后端 `phaseDetail`)由任务账本提供,前端不拼阶段、不做百分比。进度面是**画布上常驻的可折叠任务侧栏**(形态对齐网页端美术画布的任务侧栏):展开是两个分栏「排队/生成中」与「已完成」(各带条数,「已完成」封顶 20 条 + 列表滚动 + 高度有界),关闭入口只保留头部那一枚 ×(底部重复的关闭按钮与其分割线已删除)、折叠即整块让出画布且不留贴边把手,开合只走工具条上常驻的「生成任务 · N」按钮(两个页签下都在);每项显示状态徽标 / 阶段文案 / 已耗时 / 素材名,可「定位到素材」。侧栏非模态(不铺全屏遮罩、不做焦点陷阱、不参与模态遮挡判据),位置在画布左侧标题栏之下、**覆盖式**(不 reflow 挤窄画布视口),提交受理后自动展开。定位动作**每次点击都终局化**:能定位就定位并选中;素材在别的栏目先切栏目;不在投影里给「素材已不在项目里 / 已登记但尚未同步」的结论;3 秒内有界兜底,不允许提示条永久停在「正在定位生成的素材…」。 - 面板形态:独立浮层(`ThemedModal`),**不在当前面板下面追加内容**;面板内不写功能说明或规则解释文案。**点「生成」即同步关闭面板**(不等 IPC、不等排队、不等生成),面板里**不出现**「排队中。」「正在生成。」「提交中…」这类阶段文案——阶段文案的唯一去处是任务侧栏与工具栏提示条。**只有「点击瞬间就失败」**(校验不过、权限拒绝、start IPC 立即报错)才自动重开面板并带回草稿与原因;**受理之后才失败**只在侧栏把该任务收口为失败 + 原因,不重开面板。关闭 ≠ 取消请求(请求挂在任务与账本上,不挂在面板生命周期上)。 - 参数口径:比例 / 尺寸选项来自网页端美术画布的纯模型(`src/components/image-editor/ImageCanvasGenerationModel.ts`),并按本地 IPC 白名单收窄(本地通道明确拒绝 `4:3`);默认档 `1:1 · 1K`,生成 UI 设计图沿用网页端 UI 设计面板的默认 `16:9 · 1K`。本地 IPC 没有 `model` 入参,因此面板**不渲染模型选择器**(渲染一个改不了请求的控件就是假控件)。 - 规范类和固定档:生成规范下的图标规范 / 角色规范 / 自定义规范都是固定档——只读展示当前规格,不给点了不生效的比例控件。本地 IPC 只有一条规范通道(`spec` 在 Rust 侧收口到 `icon-spec`),**没有 `specType` 入参**,因此角色规范与自定义规范共用该通道,靠素材名与提示词区分。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index faea37b4a..26ee7d423 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -7,7 +7,7 @@ - 背景(验收人在真机上连报三条):① 提交按钮上渲染了后端 `phaseDetail`,「生成图片」的主按钮变成写着「排队中。」的状态胶囊;② 提交后提交面板不关、一直占着屏幕等生成,用户原话「不要显示排队中,点生成直接把窗口藏起来啊,你留个窗口意义何在」;③ 任务进度面是一个工具条按钮 + 非模态浮层,跟网页端美术画布的任务侧栏不是一个形态,用户原话「你把美术画布的照抄过来都不会吗」;④ 「定位到素材」点了没反应,提示条永久停在「正在定位生成的素材…」。 - 决策(提交面板):**点「生成」即同步关闭面板**——不等 IPC、不等排队、不等生成;面板内**不出现**任何阶段文案(主按钮文案恒为动作名)。**只有「点击瞬间就失败」**(后端校验 / 权限拒绝 / start IPC 立即报错)才自动重开面板并带回草稿与原因;**受理之后才失败**只在任务侧栏把该任务收口为失败 + 原因,不重开面板。关闭 ≠ 取消(请求挂在任务与项目内账本上,不挂在面板生命周期上)。 -- 决策(进度面形态):改为**常驻画布的可折叠任务侧栏**(对齐网页端 `ImageCanvasTaskSidebarView`)——折叠时留贴边把手并在把手上显示在途数量;展开是两个分栏「排队/生成中」与「已完成」(各带条数,「已完成」封顶 20 条 + 提示「仅显示最近 N 条」);每项显示状态徽标 / 后端阶段文案 / 已耗时(前端 1s 计时)/ 素材名 + 「定位到素材」;侧栏非模态(不铺遮罩、不做焦点陷阱、不进模态遮挡判据),工具栏入口按钮常驻并显示 `生成任务 · N`,提交受理后自动展开。**原先的非模态浮层形态已删除,不留平行入口。** +- 决策(进度面形态):改为**常驻画布的可折叠任务侧栏**(对齐网页端 `ImageCanvasTaskSidebarView`)——展开是两个分栏「排队/生成中」与「已完成」(各带条数,「已完成」封顶 20 条 + 提示「仅显示最近 N 条」),关闭入口只保留头部那一枚 ×(底部重复的关闭按钮与其 border-top 分割线已删除);折叠即整块让出画布、**不留贴边把手**;每项显示状态徽标 / 后端阶段文案 / 已耗时(前端 1s 计时)/ 素材名 + 「定位到素材」;侧栏非模态(不铺遮罩、不做焦点陷阱、不进模态遮挡判据),工具栏入口按钮是**唯一**开合口、常驻并显示 `生成任务 · N`,提交受理后自动展开。**原先的非模态浮层形态已删除,不留平行入口。** - 决策(侧栏位置):挂在画布**左侧、标题栏之下、工具栏之上**,宽 300px、**覆盖式**(不 reflow 挤窄画布视口)。理由:右侧已被「智能创作」对话面板占用、顶部是栏目标题栏、底部是栏目工具栏;覆盖式不触碰画布视口数学与资源卡排布,收起即完全让出画布。若产品要求「画布被挤窄」的 flex 兄弟列形态(网页端是那种),需要改 `game-resource-book-manager` 那段布局并单独排期。 - 决策(定位终局化):根因是聚焦 effect 的依赖全是画布自身状态,**手动点定位不改其中任何一项** → effect 不重跑、`pendingResourceFocusRef` 无人消费、提示条永久停在中转文案。修法:新增聚焦请求序号并加入 effect 依赖;handler 重写为「能定位就定位并选中;素材在别的栏目先切栏目;不在投影里给『素材已不在项目里 / 已登记但尚未同步』的结论;挂 intent 后推进序号 + **3 秒有界兜底**;intent 被判 invalid 时也给『定位请求已失效』」,并修掉「已聚焦过」提前返回分支不清提示的同类问题(自动落卡那条链同源)。 - 影响范围:`src/features/resource-canvas/{ResourceCanvasAssetGenerationPanelView.tsx,ResourceCanvasGenerationPanelView.tsx,ResourceCanvasAssetGenerationTasksPanelView.tsx,resourceCanvasAssetGenerationTaskModel.ts,resourceCanvasAssetGenerationQueue.ts}`、`src/view/project-development/index.tsx`,测试 `tests/{resourceCanvasAssetGenerationBackgroundClose.test.tsx,resourceCanvasAssetGenerationTasksPanel.test.tsx,resourceCanvasBottomToolbar.test.tsx,appSurface/project-development.suite.ts}`。**未改** IPC 形状与 Rust 生成通道、未改本地排队语义(仍单条在途)、未改 `packages/**`。 diff --git a/docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md b/docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md index a7672914f..71a965c78 100644 --- a/docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md +++ b/docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md @@ -50,7 +50,7 @@ - **本地排队**:AGC 本地 durable 输出槽按**精确动作指纹**分槽(`run_id = slot-`,材料为 prompt / outputPath / 比例 / 尺寸 / assetKind / assetLabel / replaceExisting / requireSlices;不同 prompt 或素材名各自独立成槽,不再按 `outputPath` 共用;旧 `{outputPath,requireSlices}` 槽账本由同一精确动作懒迁移)。因此后端已具备并行能力,但**本批前端仍按「同一时刻只派发一条」排队**——真并行派发需要并发收口设计(配对读 + manifest CAS + 聚焦意图互不覆盖),留待下一批。所以第二条提交停在**前端本地队列**里(不调用提交 IPC,阶段显示本地排队的「排队中。」),第一条终态后由同一条循环自动补发;判据是「存在 `dispatched && 未终态` 的任务时不派发下一条」。 - **阶段文案只来自后端,且只出现在任务侧栏**:派发之后由任务侧栏渲染后端账本给的 `phaseDetail`(「排队中。」/「正在生成。」/「生成已完成。」/失败原因),提交面板已关闭、不渲染任何阶段文案;前端不拼阶段、不做百分比。 -- **「生成任务」是常驻画布的可折叠侧栏**(形态对齐网页端美术画布的任务侧栏):折叠时留贴边把手并在把手上显示在途数量;展开是两个分栏「排队/生成中」与「已完成」(各带条数,「已完成」封顶 20 条 + 列表滚动 + 高度有界);位置在画布左侧标题栏之下、**覆盖式**(不 reflow 挤窄画布视口)。侧栏非模态(不铺全屏遮罩、不做焦点陷阱、**不参与** `isResourceCanvasFloatingPanelOpen` / `resourceCanvasHostGenerationPanelOpen` 的遮挡判据),提交受理后自动展开;工具栏入口按钮常驻并显示 `生成任务 · N`。每项的「定位到素材」**每次点击都终局化**(含跨栏目先切栏目、不在投影里给结论、3 秒有界兜底),不允许提示条永久停在「正在定位生成的素材…」。 +- **「生成任务」是常驻画布的可折叠侧栏**(形态对齐网页端美术画布的任务侧栏):展开是两个分栏「排队/生成中」与「已完成」(各带条数,「已完成」封顶 20 条 + 列表滚动 + 高度有界),关闭入口只保留头部那一枚 ×(底部重复的关闭按钮与其分割线已删除);折叠即整块让出画布、**不留贴边把手**;位置在画布左侧标题栏之下、**覆盖式**(不 reflow 挤窄画布视口)。侧栏非模态(不铺全屏遮罩、不做焦点陷阱、**不参与** `isResourceCanvasFloatingPanelOpen` / `resourceCanvasHostGenerationPanelOpen` 的遮挡判据),提交受理后自动展开;工具栏入口按钮是**唯一**开合口、**两个页签(资源管理 / 运行)下都常驻**,并显示 `生成任务 · N`。每项的「定位到素材」**每次点击都终局化**(含跨栏目先切栏目、不在投影里给结论、3 秒有界兜底),不允许提示条永久停在「正在定位生成的素材…」。 ## 5. 参数口径与复用程度