修复项目工作台布局回归

恢复 UI Editor 顶栏正式钱包入口
修正资源分段按钮键盘焦点裁切
更新工作台布局合同与回归测试
同步项目工作台 PRD 验收约束
This commit is contained in:
2026-08-23 21:44:06 +08:00
parent e3efdbb72c
commit 7a7e60e2a2
6 changed files with 122 additions and 19 deletions
+14
View File
@@ -5059,6 +5059,10 @@ iframe.preview-frame {
box-shadow: 0 0 0 3px var(--platform-input-focus-ring);
}
.game-workbench-tabs.game-resource-sort-tabs button:focus-visible {
box-shadow: inset 0 0 0 3px var(--platform-input-focus-ring);
}
.game-workbench-tabs button.is-active,
.game-workbench-view-actions button.is-active {
border-color: var(--platform-button-primary-border);
@@ -6241,6 +6245,16 @@ iframe.preview-frame {
position: static;
}
.game-workbench-editor-actions {
display: flex;
align-items: center;
gap: 8px;
}
.game-workbench-editor-wallet .launcher-account-bar {
position: static;
}
.game-workbench-approval-trigger {
display: inline-flex;
align-items: center;
@@ -3039,6 +3039,7 @@ export default function ProjectDevelopmentView({
resourceId={uiEditorRoute.resourceId}
projectId={manifest.projectId}
resourceLabel={uiEditorRoute.resourceLabel}
walletEntry={walletEntry}
onBack={() => setUiEditorRoute(null)}
/>
</div>
@@ -1,5 +1,5 @@
import { ChevronLeft } from 'lucide-react';
import { useMemo, useState } from 'react';
import { type ReactNode, useMemo, useState } from 'react';
import { ThemedModal } from '../../components/modal/ThemedModal';
import {
@@ -27,6 +27,7 @@ export default function UiEditorPage({
resourceId,
projectId,
resourceLabel,
walletEntry,
onBack,
stateStore,
}: {
@@ -34,6 +35,7 @@ export default function UiEditorPage({
resourceId?: string;
projectId?: string;
resourceLabel?: string;
walletEntry?: ReactNode;
onBack?: () => void;
stateStore?: IUiDesignStateStore;
}) {
@@ -92,20 +94,25 @@ export default function UiEditorPage({
</button>
<strong className="text-sm">{resourceLabel ?? 'UI 设计'}</strong>
<button
type="button"
className="rounded-lg bg-orange-600 px-3 py-1.5 text-sm font-semibold text-white disabled:opacity-60"
disabled={
session.save.isSaving ||
session.save.isLoading ||
Boolean(session.save.loadError) ||
session.save.persistedRevision === null ||
session.save.isLocked
}
onClick={() => requestSave()}
>
{session.save.isSaving ? '保存中…' : '保存'}
</button>
<div className="game-workbench-editor-actions">
{walletEntry ? (
<div className="game-workbench-editor-wallet">{walletEntry}</div>
) : null}
<button
type="button"
className="rounded-lg bg-orange-600 px-3 py-1.5 text-sm font-semibold text-white disabled:opacity-60"
disabled={
session.save.isSaving ||
session.save.isLoading ||
Boolean(session.save.loadError) ||
session.save.persistedRevision === null ||
session.save.isLocked
}
onClick={() => requestSave()}
>
{session.save.isSaving ? '保存中…' : '保存'}
</button>
</div>
</header>
) : null}
{session.save.saveError ? (
@@ -3429,7 +3429,13 @@ export function registerProjectWorkbenchFoundationTests() {
);
expect(styles).toMatch(
/\.game-project-workbench\s*\{[^}]*width:\s*calc\(100vw - 72px\)[^}]*padding:\s*72px 8px 8px[^}]*overflow:\s*hidden/s,
/\.game-project-workbench\s*\{[^}]*width:\s*calc\(100vw - var\(--launcher-sidebar-width\)\)[^}]*padding:\s*0[^}]*overflow:\s*hidden/s,
);
expect(styles).toMatch(
/\.launcher-main:has\(\.game-project-workbench\)\s*\{[^}]*padding-bottom:\s*0/s,
);
expect(styles).toMatch(
/\.game-workbench-toolbar\s*\{[^}]*padding:\s*8px 12px[^}]*background:\s*transparent/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/,
@@ -3444,8 +3450,12 @@ export function registerProjectWorkbenchFoundationTests() {
/\.game-workbench-chat \.project-runtime-summary\s*\{[^}]*position:\s*sticky[^}]*top:\s*-10px/s,
);
expect(styles).toMatch(
/\.game-workbench-chat \.project-supervisor-composer\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\) 82px[^}]*z-index:\s*2[^}]*border-top:\s*1px solid var\(--platform-line-soft\)/s,
/\.game-workbench-chat \.project-supervisor-composer\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\) 82px[^}]*z-index:\s*2[^}]*padding-top:\s*8px[^}]*background:\s*transparent/s,
);
const composerRule = styles.match(
/\.game-workbench-chat \.project-supervisor-composer\s*\{([^}]*)\}/s,
);
expect(composerRule?.[1]).not.toContain('border-top:');
expect(styles).toMatch(
/\.game-workbench-chat \.project-supervisor-composer button\s*\{[^}]*min-height:\s*72px[^}]*white-space:\s*nowrap/s,
);
@@ -3482,6 +3492,41 @@ export function registerProjectWorkbenchFoundationTests() {
);
});
it('keeps the wallet entry available when the workbench opens the UI editor', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
const projectDevelopmentSource = readFileSync(
resolve(
process.cwd(),
'apps/ai-game-creator-shell/src/view/project-development/index.tsx',
),
'utf8',
);
expect(styles).toMatch(
/\.launcher-main:has\(\.game-project-workbench\)\s*>\s*\.launcher-account-bar\s*\{[^}]*display:\s*none/s,
);
expect(projectDevelopmentSource).toMatch(
/<UiEditorPage[\s\S]*?walletEntry=\{walletEntry\}[\s\S]*?\/>/,
);
expect(projectDevelopmentSource).toMatch(
/<div className="game-workbench-chat-wallet">\{walletEntry\}<\/div>/,
);
});
it('keeps resource sort tab keyboard focus inside the clipped segmented control', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
expect(styles).toMatch(
/\.game-workbench-tabs\.game-resource-sort-tabs button:focus-visible\s*\{[^}]*box-shadow:\s*inset 0 0 0 3px var\(--platform-input-focus-ring\)/s,
);
});
it('keeps workbench chat bubbles aligned without shrinking process cards', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
@@ -9200,7 +9245,8 @@ export function registerProjectSupervisorSurfaceTests() {
projectPath,
});
let directTurnUpdateHandler:
((event: { payload: Record<string, unknown> }) => void) | null = null;
| ((event: { payload: Record<string, unknown> }) => void)
| null = null;
const listen = vi.fn(
async (
eventName: string,
@@ -10157,7 +10203,8 @@ export function registerProjectAgentStatusTests() {
},
);
let runtimeUpdateHandler:
((event: { payload: Record<string, unknown> }) => void) | null = null;
| ((event: { payload: Record<string, unknown> }) => void)
| null = null;
const listen = vi.fn(
async (
eventName: string,
@@ -146,6 +146,34 @@ async function renderLoadedSession(state: State) {
}
describe('UiEditorPage', () => {
it('keeps the wallet entry in the resource editor header', async () => {
const stateStore: IUiDesignStateStore = {
load: vi.fn().mockResolvedValue(structuredClone(EMPTY_SNAPSHOT)),
save: vi.fn(),
};
render(
createElement(UiEditorPage, {
projectPath: '/tmp/ui-editor-wallet',
resourceId: 'ui-resource',
stateStore,
walletEntry: createElement(
'button',
{ type: 'button', 'aria-label': '打开账户资产' },
'泥点',
),
}),
);
const walletEntry = await screen.findByRole('button', {
name: '打开账户资产',
});
expect(walletEntry.closest('header')).not.toBeNull();
expect(
screen.getAllByRole('button', { name: '打开账户资产' }),
).toHaveLength(1);
});
it('cancels image preview scopes when the resource changes and on unmount', async () => {
vi.mocked(invoke)
.mockReset()
@@ -108,6 +108,9 @@
- 第一批共享 chrome 固定覆盖画布动作按钮、工具栏、工具分组和分隔符。按钮的默认、悬停、键盘焦点、选中、禁用和主次色语义由共享层表达;宿主只提供图标、文案、事件与业务禁用条件。
- 主站账号、钱包、服务端项目、云端素材库和生成面板仍留在网站宿主;客户端本地项目、manifest、Runtime、审批、草稿、生成与正式提交仍留在 Tauri 宿主。共享视觉组件不得读取这些业务事实。
- 客户端 UI 对齐采用“同视觉、同组件、保留四区布局”,不复制主站整页布局。资源总览、运行视图和 Supervisor 对话继续是 Game Agent 工作台独有语义。
- 工作台四区作为同一应用工作面直接衔接,外层不使用卡片圆角或留白间距。中央主视窗与右侧 Supervisor 使用各自的平台区域底色,并以稳定的竖向分隔线明确边界;不通过外层圆角容器、阴影或区块间缝重新包装四区。
- 右侧正式钱包入口属于客户端宿主 chrome,在普通项目工作台与 UI Editor 子路由中都必须持续可见、键盘可达且可操作,不得因进入素材编辑壳或隐藏全局 chrome 而丢失余额、充值和使用详情入口。
- 资源总览的“资源依赖 / 资源类型”视图切换使用连通的分段按钮组,相邻选项共享边界并保持唯一选中语义。每个分段都必须有清晰的键盘焦点指示,焦点环不得被分段容器的圆角或 `overflow` 裁切。
- 右侧 Supervisor 对话中,用户消息使用右对齐、最大宽度受限的主题暖色气泡,assistant 消息保持左对齐;消息换行不得产生水平溢出,执行过程卡继续占满消息区可用宽度。气泡正文在 light / dark 平台主题下均须满足 WCAG AA 普通文本 `4.5:1` 对比度。
- 客户端正式产品仍只按最小 `1280×720` 横屏合同交付,并保留 `1280×800` 默认窗口与既有基线验收;更窄浏览器样式只负责不崩溃和开发兼容,不改成移动端创作工作台。
- 普通用户界面不默认展示内部错误码或本机绝对路径,也不展示 External Editor Base URL 或 Developer API Key;官方服务地址由客户端构建固定,远端编辑自动使用当前陶泥儿登录态。仅独立 standalone game-chat release 或显式高级自定义模式在隔离配置页维护 External v1 URL/Key,确需诊断的信息进入受控详情或开发模式。
@@ -567,6 +570,9 @@ type ProjectAgentMudPointAttribution = {
4. 主站现有图片编辑器的按钮名称、tool selection、hover/focus/pressed/disabled 行为与工具栏位置不因共享抽取回归;网站账号、钱包、素材库、生成和导出逻辑不下沉共享层。
5. 客户端生成、保存、取消、恢复、登录失效与余额不足只更换视觉承载,不改变 Host Port 调用、幂等身份、草稿 CAS、manifest/revision、Runtime 审批或钱包刷新语义。
6. 阶段一、二冻结合同并让主站消费共享 chrome;2026-08-06 的阶段三至五已经完成客户端中央区、Supervisor 和 Agent Dock 的正式换肤、定向测试与 `1280×800` 验收。独立 Vite 没有登录会话时继续显示真实登录门禁,不为视觉测试增加绕过入口。
7. 普通工作台和 UI Editor 子路由均不显示工作面外层圆角或区块留白;中央区与右侧 Supervisor 的区域底色可辨识,竖向分隔连续、无错位。
8. 右侧正式钱包入口在普通工作台和 UI Editor 子路由都始终可见、键盘可达并能打开余额、充值与使用详情;路由切换不得使入口消失或失去交互。
9. “资源依赖 / 资源类型”以连通分段按钮呈现,点击与键盘操作均只保留一个选中项;使用 Tab 定位和键盘切换时焦点指示清晰、完整,不被容器边界或 `overflow` 裁切。
## 8. 非目标