完成 Game Agent 画布 UI 对齐
Project CI / Repository checks (pull_request) Failing after 7s
Project CI / Backend tests (pull_request) Failing after 17s
Project CI / Frontend tests (pull_request) Failing after 2m11s
Project CI / Native shell tests (pull_request) Failing after 3m59s

中央素材画布接入共享 chrome 与平台主题 token
统一 Supervisor、主视窗和 Agent Dock 的横屏布局
补齐按钮状态、非裁剪和 1280×800 布局测试
同步技术方案、PRD 与项目长期记忆
This commit is contained in:
2026-08-06 13:30:52 +08:00
parent f4fed35660
commit 0c0e7f6382
10 changed files with 678 additions and 303 deletions
@@ -22,6 +22,10 @@ import {
transformCanvasLayers,
} from '@genarrative/image-canvas-core';
import {
CanvasChromeButton,
CanvasToolbar,
CanvasToolbarDivider,
CanvasToolbarGroup,
CanvasViewport as SharedCanvasViewport,
CanvasWorld,
LayerRenderer,
@@ -29,6 +33,22 @@ import {
useCanvasHistory,
ZoomControls,
} from '@genarrative/image-canvas-react';
import {
ArrowLeft,
Check,
ImagePlus,
Maximize2,
Minus,
Plus,
Redo2,
RotateCcw,
Save,
Sparkles,
Trash2,
Undo2,
Upload,
X,
} from 'lucide-react';
import {
type ChangeEvent,
type PointerEvent as ReactPointerEvent,
@@ -339,6 +359,7 @@ export function AssetCanvasSurface({
const [documentVersion, setDocumentVersion] = useState(0);
const [canvasSize, setCanvasSize] = useState({ width: 900, height: 640 });
const viewportElementRef = useRef<HTMLDivElement | null>(null);
const importInputRef = useRef<HTMLInputElement | null>(null);
const layersRef = useRef(layers);
const viewportRef = useRef(viewport);
const backgroundRef = useRef(backgroundColor);
@@ -1322,106 +1343,141 @@ export function AssetCanvasSurface({
data-session-id={sessionId}
aria-label="素材创作无限画布"
>
<header className="asset-canvas-surface__toolbar">
<button
type="button"
onClick={
lifecycle.kind === 'canvas.generating'
? stopWaitingForGeneration
: cancelCanvas
}
disabled={lifecycle.kind === 'canvas.saving'}
<header className="asset-canvas-surface__toolbar-shell">
<CanvasToolbar
label="素材画布工具栏"
surface="plain"
className="asset-canvas-surface__toolbar"
>
<CanvasToolbarGroup>
<CanvasChromeButton
label={
lifecycle.kind === 'canvas.generating'
? '停止等待并返回'
: '取消并返回'
}
icon={<ArrowLeft size={15} aria-hidden="true" />}
onClick={
lifecycle.kind === 'canvas.generating'
? stopWaitingForGeneration
: cancelCanvas
}
disabled={lifecycle.kind === 'canvas.saving'}
>
{lifecycle.kind === 'canvas.generating' ? '停止等待' : '返回'}
</CanvasChromeButton>
<CanvasChromeButton
label="导入图片"
title="导入本地图片"
icon={<Upload size={15} aria-hidden="true" />}
onClick={() => importInputRef.current?.click()}
disabled={
lifecycle.kind === 'canvas.saving' ||
lifecycle.kind === 'canvas.generating'
}
>
</CanvasChromeButton>
</CanvasToolbarGroup>
<CanvasToolbarDivider />
<CanvasToolbarGroup>
<CanvasChromeButton
label="撤销"
icon={<Undo2 size={15} aria-hidden="true" />}
onClick={undo}
disabled={!canUndo || lifecycle.kind === 'canvas.generating'}
/>
<CanvasChromeButton
label="重做"
icon={<Redo2 size={15} aria-hidden="true" />}
onClick={redo}
disabled={!canRedo || lifecycle.kind === 'canvas.generating'}
/>
<CanvasChromeButton
label="删除"
title="删除选中图层"
icon={<Trash2 size={15} aria-hidden="true" />}
onClick={deleteSelected}
disabled={
!selectedLayerIds.length ||
lifecycle.kind === 'canvas.generating'
}
/>
</CanvasToolbarGroup>
<CanvasToolbarDivider />
<CanvasToolbarGroup>
<CanvasChromeButton
label="AI 生成图片"
icon={<Sparkles size={15} aria-hidden="true" />}
onClick={openGenerationDialog}
expanded={generationDialog !== null}
disabled={
lifecycle.kind === 'canvas.saving' ||
lifecycle.kind === 'canvas.generating'
}
>
AI
</CanvasChromeButton>
</CanvasToolbarGroup>
</CanvasToolbar>
<div
className="asset-canvas-surface__savebar"
aria-label="素材保存设置"
>
{lifecycle.kind === 'canvas.generating'
? '停止等待并返回'
: '取消并返回'}
</button>
<label className="asset-canvas-surface__import">
<input
aria-label="导入本地图片"
type="file"
accept="image/png,image/jpeg,image/webp"
multiple
aria-label="素材名称"
value={assetName}
maxLength={80}
onChange={(event) => setAssetName(event.target.value)}
disabled={lifecycle.kind === 'canvas.generating'}
/>
<input
aria-label="素材类型"
value={assetKind}
maxLength={64}
onChange={(event) => setAssetKind(event.target.value)}
disabled={lifecycle.kind === 'canvas.generating'}
/>
<select
aria-label="导出格式"
value={exportMediaType}
onChange={(event) =>
setExportMediaType(
event.target.value as 'image/png' | 'image/jpeg' | 'image/webp',
)
}
>
<option value="image/png">PNG</option>
<option value="image/jpeg">JPEG</option>
<option value="image/webp">WebP</option>
</select>
<CanvasChromeButton
label="保存到项目"
icon={<Save size={15} aria-hidden="true" />}
className="asset-canvas-surface__save"
onClick={() => void saveAsset()}
disabled={
lifecycle.kind === 'canvas.saving' ||
lifecycle.kind === 'canvas.generating'
}
onChange={(event) => void handleImport(event)}
/>
</label>
<button
type="button"
onClick={undo}
disabled={!canUndo || lifecycle.kind === 'canvas.generating'}
>
</button>
<button
type="button"
onClick={redo}
disabled={!canRedo || lifecycle.kind === 'canvas.generating'}
>
</button>
<button
type="button"
onClick={deleteSelected}
disabled={
!selectedLayerIds.length || lifecycle.kind === 'canvas.generating'
}
>
</button>
<button
type="button"
onClick={openGenerationDialog}
>
</CanvasChromeButton>
</div>
<input
ref={importInputRef}
className="asset-canvas-surface__import-input"
aria-label="导入本地图片"
type="file"
accept="image/png,image/jpeg,image/webp"
multiple
disabled={
lifecycle.kind === 'canvas.saving' ||
lifecycle.kind === 'canvas.generating'
}
>
AI
</button>
<span className="asset-canvas-surface__spacer" />
<input
aria-label="素材名称"
value={assetName}
maxLength={80}
onChange={(event) => setAssetName(event.target.value)}
disabled={lifecycle.kind === 'canvas.generating'}
onChange={(event) => void handleImport(event)}
/>
<input
aria-label="素材类型"
value={assetKind}
maxLength={64}
onChange={(event) => setAssetKind(event.target.value)}
disabled={lifecycle.kind === 'canvas.generating'}
/>
<select
aria-label="导出格式"
value={exportMediaType}
onChange={(event) =>
setExportMediaType(
event.target.value as 'image/png' | 'image/jpeg' | 'image/webp',
)
}
>
<option value="image/png">PNG</option>
<option value="image/jpeg">JPEG</option>
<option value="image/webp">WebP</option>
</select>
<button
type="button"
className="asset-canvas-surface__save"
onClick={() => void saveAsset()}
disabled={
lifecycle.kind === 'canvas.saving' ||
lifecycle.kind === 'canvas.generating'
}
>
</button>
</header>
{generationDialog ? (
@@ -1439,13 +1495,11 @@ export function AssetCanvasSurface({
<strong id="asset-canvas-generation-title">
{generationDialog === 'edit' ? 'AI 图片生成' : '确认图片生成'}
</strong>
<button
type="button"
aria-label="关闭图片生成面板"
<CanvasChromeButton
label="关闭图片生成面板"
icon={<X size={16} aria-hidden="true" />}
onClick={closeGenerationDialog}
>
×
</button>
/>
</header>
{generationDialog === 'edit' ? (
<div className="asset-canvas-surface__generation-form">
@@ -1531,33 +1585,49 @@ export function AssetCanvasSurface({
</fieldset>
) : null}
<footer>
<button type="button" onClick={closeGenerationDialog}>
<CanvasChromeButton
label="取消"
title="取消图片生成"
icon={<X size={15} aria-hidden="true" />}
onClick={closeGenerationDialog}
>
</button>
<button type="button" onClick={showGenerationConfirmation}>
</CanvasChromeButton>
<CanvasChromeButton
label="继续确认"
title="继续确认图片生成"
icon={<Check size={15} aria-hidden="true" />}
className="asset-canvas-surface__primary-action"
onClick={showGenerationConfirmation}
>
</button>
</CanvasChromeButton>
</footer>
</div>
) : (
<div className="asset-canvas-surface__generation-confirmation">
<p>
{generationAspectRatio}{generationImageSize}
{assetName} manifest
</p>
<p>
使
{assetName}
</p>
<p></p>
<footer>
<button
type="button"
<CanvasChromeButton
label="返回修改"
title="返回修改生成设置"
icon={<ArrowLeft size={15} aria-hidden="true" />}
onClick={() => setGenerationDialog('edit')}
>
</button>
<button type="button" onClick={confirmGeneration}>
</CanvasChromeButton>
<CanvasChromeButton
label="确认并生成"
icon={<ImagePlus size={15} aria-hidden="true" />}
className="asset-canvas-surface__primary-action"
onClick={confirmGeneration}
>
</button>
</CanvasChromeButton>
</footer>
</div>
)}
@@ -1582,9 +1652,13 @@ export function AssetCanvasSurface({
aria-label="图片生成进度"
/>
<p></p>
<button type="button" onClick={stopWaitingForGeneration}>
<CanvasChromeButton
label="停止等待并返回"
icon={<ArrowLeft size={15} aria-hidden="true" />}
onClick={stopWaitingForGeneration}
>
</button>
</CanvasChromeButton>
</div>
</section>
) : null}
@@ -1602,24 +1676,32 @@ export function AssetCanvasSurface({
<strong>{generationFailureTitle(lifecycle.code)}</strong>
<p>{lifecycle.message}</p>
<div className="asset-canvas-surface__operation-actions">
<button type="button" onClick={cancelCanvas}>
<CanvasChromeButton
label="返回资源总览"
icon={<ArrowLeft size={15} aria-hidden="true" />}
onClick={cancelCanvas}
>
</button>
</CanvasChromeButton>
{!lifecycle.reconciliationRequired ? (
<>
<button
type="button"
<CanvasChromeButton
label="返回修改"
title="返回修改生成设置"
icon={<Undo2 size={15} aria-hidden="true" />}
onClick={() => reopenGenerationAfterFailure('edit')}
>
</button>
<button
type="button"
className="asset-canvas-surface__save"
</CanvasChromeButton>
<CanvasChromeButton
label="重新确认"
title="重新确认图片生成"
icon={<RotateCcw size={15} aria-hidden="true" />}
className="asset-canvas-surface__primary-action"
onClick={() => reopenGenerationAfterFailure('confirm')}
>
</button>
</CanvasChromeButton>
</>
) : null}
</div>
@@ -1774,25 +1856,29 @@ export function AssetCanvasSurface({
}}
>
{(actions) => (
<div className="asset-canvas-surface__zoom">
<button
type="button"
aria-label="缩小画布"
<CanvasToolbar
label="画布缩放"
surface="floating"
className="asset-canvas-surface__zoom"
>
<CanvasChromeButton
label="缩小画布"
icon={<Minus size={15} aria-hidden="true" />}
onClick={actions.zoomOut}
/>
<CanvasChromeButton
label={`复位画布,当前 ${actions.displayPercent}`}
icon={<Maximize2 size={15} aria-hidden="true" />}
onClick={actions.fit}
>
</button>
<button type="button" onClick={actions.fit}>
{actions.displayPercent}
</button>
<button
type="button"
aria-label="放大画布"
</CanvasChromeButton>
<CanvasChromeButton
label="放大画布"
icon={<Plus size={15} aria-hidden="true" />}
onClick={actions.zoomIn}
>
+
</button>
</div>
/>
</CanvasToolbar>
)}
</ZoomControls>
{minimapModel ? (
@@ -1,4 +1,10 @@
.asset-canvas-surface {
--image-canvas-brand-accent: var(--platform-button-primary-solid);
--image-canvas-brand-accent-strong: var(--platform-text-strong);
--image-canvas-brand-border-soft: var(--platform-icon-border);
--image-canvas-brand-border-strong: var(--platform-nav-active-border);
--image-canvas-brand-focus-ring: var(--platform-input-focus-ring);
--image-canvas-brand-soft: var(--platform-nav-active-fill);
position: relative;
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
@@ -6,74 +12,87 @@
height: 100%;
min-height: 0;
overflow: hidden;
border-radius: 16px;
background: #e8e6e2;
color: #2d2926;
border-radius: inherit;
background: var(--platform-overlay-fill);
color: var(--platform-text-strong);
}
.asset-canvas-surface--loading {
place-items: center;
}
.asset-canvas-surface__toolbar {
.asset-canvas-surface__toolbar-shell {
z-index: 4;
display: flex;
display: grid;
min-width: 0;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-bottom: 1px solid rgba(63, 54, 48, 0.12);
background: rgba(252, 251, 249, 0.96);
padding: 8px 10px 9px;
border-bottom: 1px solid var(--platform-line-soft);
background: var(--platform-desktop-topbar-fill);
}
.asset-canvas-surface button,
.asset-canvas-surface input,
.asset-canvas-surface select,
.asset-canvas-surface__import {
min-height: 34px;
border: 1px solid rgba(63, 54, 48, 0.18);
border-radius: 9px;
background: #fff;
color: inherit;
font: inherit;
.asset-canvas-surface__toolbar {
width: 100%;
max-width: 100%;
padding-block: 1px 3px;
}
.asset-canvas-surface button,
.asset-canvas-surface__import {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 11px;
cursor: pointer;
}
.asset-canvas-surface button:disabled {
cursor: default;
opacity: 0.45;
}
.asset-canvas-surface input,
.asset-canvas-surface select {
.asset-canvas-surface__savebar {
display: grid;
grid-template-columns:
minmax(108px, 1.35fr) minmax(88px, 0.8fr) minmax(70px, auto)
auto;
min-width: 0;
align-items: center;
gap: 7px;
}
.asset-canvas-surface__savebar input,
.asset-canvas-surface__savebar select,
.asset-canvas-surface__generation-form select {
min-width: 0;
min-height: 34px;
border: 1px solid var(--platform-surface-border);
border-radius: 8px;
background: var(--platform-input-fill);
color: var(--platform-text-base);
font: inherit;
padding: 0 9px;
}
.asset-canvas-surface__import input {
.asset-canvas-surface__savebar input:focus-visible,
.asset-canvas-surface__savebar select:focus-visible,
.asset-canvas-surface__generation-form select:focus-visible,
.asset-canvas-surface__generation-form textarea:focus-visible {
border-color: var(--platform-surface-hover-border);
outline: none;
box-shadow: 0 0 0 3px var(--platform-input-focus-ring);
}
.asset-canvas-surface__import-input {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
}
.asset-canvas-surface__spacer {
flex: 1;
.asset-canvas-surface .asset-canvas-surface__primary-action,
.asset-canvas-surface .asset-canvas-surface__save {
border-color: var(--platform-button-primary-border);
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
box-shadow: var(--platform-nav-active-shadow);
}
.asset-canvas-surface__save {
border-color: #c7653d !important;
background: #c7653d !important;
color: #fff !important;
.asset-canvas-surface
.asset-canvas-surface__primary-action:not(:disabled):hover,
.asset-canvas-surface .asset-canvas-surface__save:not(:disabled):hover {
border-color: var(--platform-button-primary-border);
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
}
.asset-canvas-surface[data-state='canvas.generating']
@@ -90,18 +109,25 @@
display: grid;
place-items: center;
padding: 16px;
background: rgba(35, 31, 28, 0.48);
background: color-mix(
in srgb,
var(--platform-text-strong) 36%,
transparent
);
backdrop-filter: blur(5px);
}
.asset-canvas-surface__generation-dialog {
display: grid;
width: min(560px, 100%);
max-height: min(700px, calc(100dvh - 32px));
max-height: calc(100% - 32px);
min-height: 0;
overflow: auto;
border: 1px solid rgba(63, 54, 48, 0.16);
border: 1px solid var(--platform-modal-border);
border-radius: 16px;
background: #fcfbf9;
box-shadow: 0 24px 80px rgba(35, 31, 28, 0.24);
background: var(--platform-modal-fill);
color: var(--platform-text-strong);
box-shadow: var(--platform-panel-shadow);
}
.asset-canvas-surface__generation-dialog > header,
@@ -114,12 +140,7 @@
}
.asset-canvas-surface__generation-dialog > header {
border-bottom: 1px solid rgba(63, 54, 48, 0.1);
}
.asset-canvas-surface__generation-dialog > header button {
min-width: 34px;
padding: 0;
border-bottom: 1px solid var(--platform-line-soft);
}
.asset-canvas-surface__generation-form,
@@ -132,18 +153,20 @@
.asset-canvas-surface__generation-form label {
display: grid;
gap: 6px;
color: var(--platform-text-base);
font-size: 13px;
font-weight: 700;
}
.asset-canvas-surface__generation-form textarea {
box-sizing: border-box;
width: 100%;
resize: vertical;
border: 1px solid rgba(63, 54, 48, 0.18);
border: 1px solid var(--platform-surface-border);
border-radius: 10px;
padding: 10px;
background: #fff;
color: inherit;
background: var(--platform-input-fill);
color: var(--platform-text-strong);
font: inherit;
}
@@ -158,7 +181,7 @@
max-height: 180px;
gap: 8px;
overflow: auto;
border: 1px solid rgba(63, 54, 48, 0.12);
border: 1px solid var(--platform-subpanel-border);
border-radius: 10px;
padding: 10px;
}
@@ -166,6 +189,7 @@
.asset-canvas-surface__generation-form fieldset label {
display: flex;
align-items: center;
font-weight: 500;
}
.asset-canvas-surface__generation-form fieldset input {
@@ -180,7 +204,11 @@
.asset-canvas-surface__viewport {
grid-row: 2;
min-height: 0;
background-image: radial-gradient(rgba(45, 41, 38, 0.12) 1px, transparent 1px);
background-color: var(--platform-neutral-bg);
background-image: radial-gradient(
var(--platform-line-soft) 1px,
transparent 1px
);
background-size: 18px 18px;
}
@@ -194,19 +222,27 @@
place-items: center;
overflow: auto;
padding: 20px;
background: rgba(45, 41, 38, 0.34);
background: color-mix(
in srgb,
var(--platform-text-strong) 30%,
transparent
);
backdrop-filter: blur(4px);
}
.asset-canvas-surface__operation-card {
display: grid;
width: min(440px, 100%);
max-height: 100%;
min-height: 0;
gap: 14px;
border: 1px solid rgba(63, 54, 48, 0.14);
overflow: auto;
border: 1px solid var(--platform-modal-border);
border-radius: 18px;
padding: 22px;
background: #fcfbf9;
box-shadow: 0 20px 64px rgba(35, 31, 28, 0.2);
background: var(--platform-modal-fill);
color: var(--platform-text-strong);
box-shadow: var(--platform-panel-shadow);
}
.asset-canvas-surface__operation-card strong {
@@ -215,8 +251,9 @@
.asset-canvas-surface__operation-card p {
margin: 0;
color: #6f655f;
color: var(--platform-text-base);
line-height: 1.55;
overflow-wrap: anywhere;
}
.asset-canvas-surface__operation-card progress {
@@ -225,18 +262,18 @@
overflow: hidden;
border: 0;
border-radius: 999px;
accent-color: #c7653d;
accent-color: var(--platform-accent);
}
.asset-canvas-surface__operation-kicker {
color: #9c4c2d;
color: var(--platform-warm-text);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
}
.asset-canvas-surface__operation-card--failed {
border-color: rgba(164, 65, 45, 0.24);
border-color: var(--platform-button-danger-border);
}
.asset-canvas-surface__operation-actions {
@@ -252,9 +289,9 @@
bottom: -8px;
width: 16px;
height: 16px;
border: 2px solid #fff;
border: 2px solid var(--platform-input-fill);
border-radius: 50%;
background: #c7653d;
background: var(--platform-accent);
cursor: nwse-resize;
}
@@ -268,14 +305,8 @@
}
.asset-canvas-surface__zoom {
display: flex;
max-width: min(100%, 280px);
justify-content: flex-end;
gap: 4px;
}
.asset-canvas-surface__zoom button {
min-height: 30px;
background: rgba(255, 255, 255, 0.94);
}
.asset-canvas-surface .genarrative-image-canvas__minimap {
@@ -283,28 +314,45 @@
height: 84px;
padding: 0;
overflow: hidden;
background: rgba(255, 255, 255, 0.94);
border: 1px solid var(--platform-surface-border);
background: var(--platform-input-fill);
box-shadow: var(--platform-nav-active-shadow);
}
.asset-canvas-surface .genarrative-image-canvas__minimap-layer {
background: rgba(199, 101, 61, 0.32);
background: var(--platform-warm-bg);
}
.asset-canvas-surface .genarrative-image-canvas__minimap-viewport {
border: 1px solid #c7653d;
border: 1px solid var(--platform-accent);
}
.asset-canvas-surface__status {
z-index: 4;
display: flex;
min-height: 34px;
max-height: 96px;
align-items: center;
flex-wrap: wrap;
gap: 12px;
overflow-y: auto;
padding: 8px 12px;
border-top: 1px solid rgba(63, 54, 48, 0.12);
background: rgba(252, 251, 249, 0.96);
border-top: 1px solid var(--platform-line-soft);
background: var(--platform-desktop-topbar-fill);
color: var(--platform-text-base);
font-size: 12px;
line-height: 1.4;
scrollbar-gutter: stable;
}
.asset-canvas-surface__status > span {
min-width: 0;
overflow-wrap: anywhere;
}
.asset-canvas-surface__status > span:first-child {
color: var(--platform-text-strong);
font-weight: 800;
}
@media (max-width: 760px) {
@@ -314,13 +362,22 @@
border-radius: 0;
}
.asset-canvas-surface__toolbar {
overflow-x: auto;
.asset-canvas-surface__toolbar-shell {
padding-inline: 8px;
}
.asset-canvas-surface__toolbar input {
width: 112px;
flex: 0 0 auto;
.asset-canvas-surface__savebar {
grid-template-columns: minmax(96px, 1fr) minmax(72px, 0.75fr) 70px;
}
.asset-canvas-surface__save {
grid-column: 1 / -1;
width: 100%;
}
.asset-canvas-surface__viewport-tools {
right: 8px;
bottom: 52px;
}
.asset-canvas-surface__generation-options {
+192 -70
View File
@@ -3929,10 +3929,11 @@ iframe.preview-frame {
.game-project-workbench {
width: calc(100vw - 72px);
max-width: none;
gap: 12px;
padding-top: 40px;
padding-bottom: 0;
color: #4b342c;
min-height: 0;
gap: 10px;
padding: 40px 8px 8px;
overflow: hidden;
color: var(--platform-text-base);
}
.launcher-main:has(.game-project-workbench) {
@@ -3944,7 +3945,7 @@ iframe.preview-frame {
z-index: 0;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(360px, 0.36fr);
gap: 12px;
gap: 10px;
height: clamp(560px, calc(100dvh - 206px), 780px);
min-width: 0;
min-height: 0;
@@ -3954,16 +3955,21 @@ iframe.preview-frame {
.game-workbench-chat {
min-width: 0;
overflow: hidden;
border: 1px solid #ead7ce;
border: 1px solid var(--platform-desktop-panel-border);
border-radius: 16px;
background: #fffdfa;
box-shadow: 0 14px 36px rgb(103 65 48 / 8%);
background: var(--platform-desktop-panel-fill);
}
.game-workbench-stage {
position: relative;
display: grid;
grid-template-rows: auto auto minmax(0, 1fr);
box-shadow: var(--platform-panel-shadow);
}
.game-workbench-chat {
border-color: var(--platform-line-soft);
box-shadow: var(--platform-nav-active-shadow);
}
.game-workbench-stage[data-resource-view-state^='resources.asset-canvas'] {
@@ -3983,8 +3989,8 @@ iframe.preview-frame {
gap: 12px;
min-height: 48px;
padding: 8px 12px;
border-bottom: 1px solid #f0dfd7;
background: #fff8f4;
border-bottom: 1px solid var(--platform-line-soft);
background: var(--platform-desktop-topbar-fill);
}
.game-workbench-tabs,
@@ -3997,8 +4003,9 @@ iframe.preview-frame {
.game-workbench-tabs {
padding: 3px;
border: 1px solid var(--platform-subpanel-border);
border-radius: 999px;
background: #f3ded3;
background: var(--platform-nav-fill);
}
.game-workbench-tabs button,
@@ -4012,28 +4019,39 @@ iframe.preview-frame {
border: 1px solid transparent;
border-radius: 999px;
background: transparent;
color: #735448;
color: var(--platform-button-ghost-text);
font-size: 12px;
font-weight: 700;
white-space: nowrap;
cursor: pointer;
}
.game-workbench-tabs button:focus-visible,
.game-workbench-view-actions button:focus-visible,
.game-workbench-approval-trigger:focus-visible,
.game-agent-dock-more:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--platform-input-focus-ring);
}
.game-workbench-tabs button.is-active,
.game-workbench-view-actions button.is-active {
border-color: #d7845c;
background: #d7845c;
color: #fff;
border-color: var(--platform-button-primary-border);
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
}
.game-workbench-tabs button:disabled,
.game-workbench-tabs button[data-unavailable='true'] {
color: #a99086;
color: var(--platform-text-muted);
cursor: not-allowed;
opacity: 0.6;
}
.game-workbench-view-actions button {
border-color: #ead6cc;
background: #fff;
border-color: var(--platform-surface-border);
background: var(--platform-button-secondary-fill);
color: var(--platform-button-secondary-text);
}
.game-resource-reorder-status {
@@ -4850,8 +4868,8 @@ iframe.preview-frame {
align-items: center;
gap: 9px;
padding: 12px 14px;
border-bottom: 1px solid #f0dfd7;
background: #fff8f4;
border-bottom: 1px solid var(--platform-line-soft);
background: var(--platform-subpanel-fill);
}
.game-workbench-chat > header > div {
@@ -4867,21 +4885,23 @@ iframe.preview-frame {
min-height: 28px;
margin-left: auto;
padding: 0 9px;
border: 1px solid #e2c7ba;
border: 1px solid var(--platform-surface-border);
border-radius: 999px;
background: #fff;
color: #9c5d42;
background: var(--platform-button-ghost-fill);
color: var(--platform-button-ghost-text);
font-size: 10px;
font-weight: 700;
white-space: nowrap;
cursor: pointer;
}
.game-workbench-chat > header strong {
color: #a95531;
color: var(--platform-text-strong);
font-size: 13px;
}
.game-workbench-chat > header small {
color: #a08a80;
color: var(--platform-text-soft);
font-size: 10px;
}
@@ -4890,8 +4910,8 @@ iframe.preview-frame {
width: 30px;
height: 30px;
border-radius: 50%;
background: #d9794c;
color: #fff;
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
font-size: 12px;
font-weight: 900;
place-items: center;
@@ -4908,24 +4928,28 @@ iframe.preview-frame {
}
.game-workbench-chat .project-supervisor-conversation {
grid-template-rows: minmax(120px, 1fr) minmax(0, auto) auto auto auto;
grid-template-rows: minmax(96px, 1fr) minmax(0, auto) auto auto auto;
height: 100%;
min-height: 0;
overflow: hidden;
}
.game-workbench-chat .project-supervisor-message-list {
min-height: 120px;
min-height: 96px;
max-height: none;
overflow-y: auto;
border: 1px solid #f1e4de;
background: #fffdfb;
border: 1px solid var(--platform-subpanel-border);
background: var(--platform-input-fill);
scrollbar-gutter: stable;
}
.game-workbench-chat .agent-runtime-status {
max-height: clamp(190px, 30dvh, 300px);
max-height: clamp(120px, 24dvh, 240px);
min-height: 0;
overflow-y: auto;
border-color: var(--platform-subpanel-border);
background: var(--platform-neutral-bg);
color: var(--platform-text-strong);
overscroll-behavior: contain;
scrollbar-gutter: stable;
}
@@ -4936,8 +4960,8 @@ iframe.preview-frame {
z-index: 3;
gap: 5px;
padding-bottom: 7px;
border-bottom: 1px solid #d8dde5;
background: #f8fafd;
border-bottom: 1px solid var(--platform-line-soft);
background: var(--platform-neutral-bg);
}
.project-runtime-empty-state {
@@ -4946,12 +4970,12 @@ iframe.preview-frame {
place-content: center;
justify-items: center;
gap: 10px;
color: #d9794c;
color: var(--platform-accent);
text-align: center;
}
.project-runtime-empty-state strong {
color: #55443c;
color: var(--platform-text-strong);
font-size: 13px;
}
@@ -4973,6 +4997,73 @@ iframe.preview-frame {
.game-workbench-chat .project-runtime-professional-list article {
gap: 3px;
padding: 7px 8px;
border-color: var(--platform-subpanel-border);
background: var(--platform-input-fill);
}
.game-workbench-chat .project-runtime-overview,
.game-workbench-chat
.agent-runtime-status
.project-runtime-pending-command {
background: var(--platform-warm-bg);
}
.game-workbench-chat .project-runtime-overview > span,
.game-workbench-chat .project-runtime-plan-progress span,
.game-workbench-chat .project-runtime-professional-list article > header span,
.game-workbench-chat .project-runtime-professional-list article p,
.game-workbench-chat .project-supervisor-workspace-status {
color: var(--platform-text-base);
}
.game-workbench-chat .project-runtime-plan-progress progress {
accent-color: var(--platform-accent);
}
.game-workbench-chat
.agent-runtime-status
.project-runtime-professional-list {
border-top-color: var(--platform-line-soft);
}
.game-workbench-chat .project-runtime-professional-actions button {
border-color: var(--platform-surface-border);
background: var(--platform-button-secondary-fill);
color: var(--platform-button-secondary-text);
white-space: nowrap;
}
.game-workbench-chat .project-runtime-professional-actions button:last-child,
.game-workbench-chat .project-runtime-recovery > button {
border-color: var(--platform-button-primary-border);
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
}
.game-workbench-chat
.agent-runtime-status
.project-runtime-recovery {
border-color: var(--platform-button-danger-border);
background: var(--platform-button-danger-fill);
}
.game-workbench-chat .project-runtime-recovery > span {
color: var(--platform-button-danger-text);
}
.game-workbench-chat .project-runtime-recovery > span small {
color: var(--platform-text-base);
}
.game-workbench-chat .message--user {
color: var(--platform-warm-text);
}
.game-workbench-chat .message-action,
.game-workbench-chat .message-history-more {
border-color: var(--platform-surface-border);
background: var(--platform-button-secondary-fill);
color: var(--platform-button-secondary-text);
}
.game-workbench-chat .project-runtime-professional-list .project-runtime-error {
@@ -4989,6 +5080,11 @@ iframe.preview-frame {
.game-workbench-chat .project-supervisor-composer {
grid-template-columns: minmax(0, 1fr) 82px;
align-items: stretch;
position: relative;
z-index: 2;
padding-top: 8px;
border-top: 1px solid var(--platform-line-soft);
background: var(--platform-desktop-panel-fill);
}
.game-workbench-chat .project-supervisor-composer textarea {
@@ -4996,12 +5092,26 @@ iframe.preview-frame {
min-height: 72px;
max-height: 72px;
resize: none;
border-color: var(--platform-surface-border);
background: var(--platform-input-fill);
color: var(--platform-text-strong);
}
.game-workbench-chat .project-supervisor-composer textarea:focus-visible {
border-color: var(--platform-surface-hover-border);
outline: none;
box-shadow: 0 0 0 3px var(--platform-input-focus-ring);
}
.game-workbench-chat .project-supervisor-composer button {
width: auto;
height: auto;
min-height: 72px;
border-color: var(--platform-button-primary-border);
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
font-weight: 800;
white-space: nowrap;
}
.game-agent-dock {
@@ -5011,46 +5121,49 @@ iframe.preview-frame {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
gap: 6px;
overflow: visible;
min-height: 66px;
padding: 8px 12px;
border: 1px solid #ead7ce;
border-radius: 16px;
background: #f7e6dc;
box-shadow: 0 12px 28px rgb(103 65 48 / 10%);
min-height: 58px;
padding: 6px 10px;
border: 1px solid var(--platform-line-soft);
border-radius: 14px;
background: var(--platform-subpanel-fill);
box-shadow: var(--platform-nav-active-shadow);
}
.game-agent-dock-more {
flex: 0 0 auto;
min-height: 40px;
min-height: 38px;
padding: 0 12px;
border: 1px solid #d9b8a8;
border: 1px solid var(--platform-surface-border);
border-radius: 12px;
background: rgb(255 255 255 / 72%);
color: #765447;
background: var(--platform-button-ghost-fill);
color: var(--platform-button-ghost-text);
font-size: 11px;
font-weight: 800;
white-space: nowrap;
cursor: pointer;
}
.game-agent-dock-more:hover {
border-color: #c98361;
color: #a7522f;
border-color: var(--platform-surface-hover-border);
color: var(--platform-text-strong);
}
.game-agent-dock-item {
position: relative;
display: grid;
grid-template-columns: 34px minmax(0, 1fr);
flex: 1 1 128px;
align-items: center;
gap: 8px;
min-width: 150px;
padding: 7px 10px;
border: 1px solid transparent;
min-width: 0;
max-width: 170px;
padding: 6px 8px;
border: 1px solid var(--platform-icon-border);
border-radius: 12px;
background: rgb(255 255 255 / 68%);
color: #584137;
background: var(--platform-button-ghost-fill);
color: var(--platform-text-base);
text-align: left;
cursor: default;
}
@@ -5060,8 +5173,8 @@ iframe.preview-frame {
width: 34px;
height: 34px;
border-radius: 12px;
background: #d27a50;
color: #fff;
background: var(--platform-nav-active-fill);
color: var(--platform-warm-text);
font-size: 12px;
font-weight: 900;
place-items: center;
@@ -5069,16 +5182,24 @@ iframe.preview-frame {
.game-agent-dock-item > span:nth-child(2) {
display: grid;
min-width: 0;
gap: 2px;
}
.game-agent-dock-item strong {
overflow: hidden;
color: var(--platform-text-strong);
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.game-agent-dock-item small {
color: #96776a;
overflow: hidden;
color: var(--platform-text-soft);
font-size: 9px;
text-overflow: ellipsis;
white-space: nowrap;
}
.game-agent-dock-item.is-running::after {
@@ -5088,8 +5209,8 @@ iframe.preview-frame {
width: 7px;
height: 7px;
border-radius: 50%;
background: #5ec46b;
box-shadow: 0 0 0 3px rgb(94 196 107 / 16%);
background: var(--platform-success-text);
box-shadow: 0 0 0 3px var(--platform-success-border);
content: '';
}
@@ -5106,18 +5227,18 @@ iframe.preview-frame {
}
.game-agent-dock-item.is-waiting::after {
background: #df8a16;
box-shadow: 0 0 0 3px rgb(223 138 22 / 16%);
background: var(--platform-warm-text);
box-shadow: 0 0 0 3px var(--platform-warm-border);
}
.game-agent-dock-item.is-failed::after {
background: #d64545;
box-shadow: 0 0 0 3px rgb(214 69 69 / 16%);
background: var(--platform-button-danger-text);
box-shadow: 0 0 0 3px var(--platform-button-danger-border);
}
.game-agent-dock-item.is-completed::after {
background: #2878d4;
box-shadow: 0 0 0 3px rgb(40 120 212 / 16%);
background: var(--platform-accent);
box-shadow: 0 0 0 3px var(--platform-nav-active-border);
}
.game-agent-dock-detail {
@@ -5128,11 +5249,11 @@ iframe.preview-frame {
display: none;
width: 220px;
padding: 10px 12px;
border: 1px solid #dcc2b5;
border: 1px solid var(--platform-modal-border);
border-radius: 10px;
background: #4f433d;
color: #fff;
box-shadow: 0 12px 30px rgb(50 30 22 / 20%);
background: var(--platform-modal-fill);
color: var(--platform-text-strong);
box-shadow: var(--platform-panel-shadow);
transform: translateX(-50%);
}
@@ -5286,7 +5407,7 @@ iframe.preview-frame {
cursor: pointer;
}
@media (min-width: 761px) and (min-height: 680px) {
@media (min-width: 761px) {
.game-project-workbench {
grid-template-rows: minmax(0, 1fr) auto;
height: 100dvh;
@@ -5385,6 +5506,7 @@ iframe.preview-frame {
.game-agent-dock-item {
width: 100%;
max-width: none;
}
.game-agent-dock-detail {
@@ -2175,28 +2175,35 @@ export function registerProjectWorkbenchFoundationTests() {
);
expect(styles).toMatch(
/\.game-project-workbench\s*\{[^}]*width:\s*calc\(100vw - 72px\)[^}]*padding-top:\s*40px/s,
/\.game-project-workbench\s*\{[^}]*width:\s*calc\(100vw - 72px\)[^}]*padding:\s*40px 8px 8px[^}]*overflow:\s*hidden/s,
);
expect(styles).toMatch(
/@media \(min-width: 761px\) and \(min-height: 680px\)[\s\S]*?\.game-project-workbench\s*\{[^}]*grid-template-rows:\s*minmax\(0, 1fr\) auto[^}]*height:\s*100dvh/,
/@media \(min-width: 761px\)[\s\S]*?\.game-project-workbench\s*\{[^}]*grid-template-rows:\s*minmax\(0, 1fr\) auto[^}]*height:\s*100dvh/,
);
expect(styles).toMatch(
/\.game-workbench-chat \.project-supervisor-conversation\s*\{[^}]*grid-template-rows:\s*minmax\(120px, 1fr\) minmax\(0, auto\) auto auto auto[^}]*height:\s*100%[^}]*overflow:\s*hidden/s,
/\.game-workbench-chat \.project-supervisor-conversation\s*\{[^}]*grid-template-rows:\s*minmax\(96px, 1fr\) minmax\(0, auto\) auto auto auto[^}]*height:\s*100%[^}]*overflow:\s*hidden/s,
);
expect(styles).toMatch(
/\.game-workbench-chat \.agent-runtime-status\s*\{[^}]*max-height:\s*clamp\(190px, 30dvh, 300px\)[^}]*overflow-y:\s*auto/s,
/\.game-workbench-chat \.agent-runtime-status\s*\{[^}]*max-height:\s*clamp\(120px, 24dvh, 240px\)[^}]*overflow-y:\s*auto/s,
);
expect(styles).toMatch(
/\.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/s,
/\.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,
);
expect(styles).toMatch(
/\.game-workbench-chat \.project-supervisor-composer button\s*\{[^}]*min-height:\s*72px[^}]*white-space:\s*nowrap/s,
);
const agentDockRule = styles.match(/\.game-agent-dock\s*\{([^}]*)\}/s);
expect(agentDockRule?.[1]).toContain('overflow: visible;');
expect(agentDockRule?.[1]).not.toContain('overflow-x:');
expect(agentDockRule?.[1]).toContain('z-index: 30;');
expect(agentDockRule?.[1]).toContain('isolation: isolate;');
expect(agentDockRule?.[1]).toContain('min-height: 58px;');
expect(styles).toMatch(
/\.game-agent-dock-item\s*\{[^}]*flex:\s*1 1 128px[^}]*min-width:\s*0[^}]*max-width:\s*170px/s,
);
expect(styles).toMatch(
/@media \(max-width: 760px\)[\s\S]*?\.game-workbench-layout\s*\{[^}]*height:\s*auto/,
);
@@ -416,6 +416,82 @@ afterEach(() => {
});
describe('Tauri 素材创作无限画布独立 Surface', () => {
it('直接消费共享 chrome,并暴露工具栏结构与原生按钮状态', async () => {
const memory = memoryHost();
renderSurface(memory.host);
await screen.findByText('画布可编辑');
const toolbar = screen.getByRole('toolbar', { name: '素材画布工具栏' });
expect(
toolbar.classList.contains('genarrative-image-canvas__toolbar'),
).toBe(true);
expect(
toolbar.querySelectorAll('.genarrative-image-canvas__toolbar-group'),
).toHaveLength(3);
expect(
toolbar.querySelectorAll('.genarrative-image-canvas__toolbar-divider'),
).toHaveLength(2);
const generate = screen.getByRole('button', { name: 'AI 生成图片' });
expect(
generate.classList.contains(
'genarrative-image-canvas__chrome-button',
),
).toBe(true);
expect(generate.getAttribute('aria-expanded')).toBe('false');
expect(
(screen.getByRole('button', { name: '撤销' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '重做' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole('button', { name: '删除' }) as HTMLButtonElement)
.disabled,
).toBe(true);
fireEvent.click(generate);
expect(generate.getAttribute('aria-expanded')).toBe('true');
expect(screen.getByRole('dialog', { name: 'AI 图片生成' })).toBeTruthy();
expect(
screen
.getByRole('button', { name: '继续确认' })
.classList.contains('genarrative-image-canvas__chrome-button'),
).toBe(true);
expect(screen.getByRole('button', { name: '取消' })).toBeTruthy();
});
it('让生成、失败、保存与取消区域使用内部滚动而不被固定高度裁剪', () => {
const css = readFileSync(
resolve(
process.cwd(),
'apps/ai-game-creator-shell/src/features/asset-canvas/assetCanvasSurface.css',
),
'utf8',
);
expect(css).toMatch(
/\.asset-canvas-surface__generation-dialog\s*\{[^}]*max-height:\s*calc\(100% - 32px\)[^}]*overflow:\s*auto/s,
);
expect(css).toMatch(
/\.asset-canvas-surface__operation-overlay\s*\{[^}]*overflow:\s*auto/s,
);
expect(css).toMatch(
/\.asset-canvas-surface__operation-card\s*\{[^}]*max-height:\s*100%[^}]*overflow:\s*auto/s,
);
expect(css).toMatch(
/\.asset-canvas-surface__status\s*\{[^}]*max-height:\s*96px[^}]*overflow-y:\s*auto/s,
);
expect(css).toMatch(
/\.asset-canvas-surface__savebar\s*\{[^}]*grid-template-columns:/s,
);
expect(css).toMatch(
/@media \(max-width: 760px\)[\s\S]*?\.asset-canvas-surface__save\s*\{[^}]*grid-column:\s*1 \/ -1[^}]*width:\s*100%/s,
);
});
it('取消时先把草稿标记为 cancelled,再返回资源总览', async () => {
const memory = memoryHost();
const { onCancel } = renderSurface(memory.host);
@@ -85,6 +85,8 @@
### 3.7 主站 UI 对齐与共享视觉边界
实现状态(2026-08-06):主站与 Tauri 已完成同源 chrome 接入。Tauri 现有中央素材画布直接消费共享动作按钮、工具栏、工具组和分隔符;工作台外围继续保留四区结构,并以平台 token 统一中央壳、Supervisor、Agent Dock、状态提示和主要操作。生成、保存、登录、计费、草稿、manifest、Runtime 和审批语义未随换肤改变。
- 项目工作台继续保留左侧平台导航、中央主视窗、右侧 Project Supervisor 和底部专业 Agent 状态栏四区结构;主站图片编辑器只作为视觉语言和共享画布组件的事实源,不把其素材库侧栏、账号业务或云端项目外壳整体搬入客户端。
- 平台主题事实源固定为 `packages/shared/src/theme.css`。画布通用 chrome 固定落在 `@genarrative/image-canvas-react`,主站与 Tauri 必须直接 import 同一组件和作用域样式;客户端不得复制 `src/components/image-editor/`,也不得导入主站完整 `src/index.css`
- 第一批共享 chrome 固定覆盖画布动作按钮、工具栏、工具分组和分隔符。按钮的默认、悬停、键盘焦点、选中、禁用和主次色语义由共享层表达;宿主只提供图标、文案、事件与业务禁用条件。
@@ -502,10 +504,10 @@ type ProjectAgentMudPointAttribution = {
1. `packages/image-canvas-react` 暴露共享画布动作按钮、工具栏、工具分组和分隔符;主站图片编辑器直接消费这些组件,旧网站组件只允许保留薄适配,不得继续维护另一份按钮可访问性或选中态实现。
2. 共享样式全部使用 `.genarrative-image-canvas*` 作用域并消费 `--platform-*` / `--image-canvas-brand-*` tokenTauri 不导入主站完整 `src/index.css`,主站也不复制共享样式回业务 CSS。
3. 后续客户端接入完成后,`1280×800` 下中央画布工具栏保持单行或受控横向收纳,中文动作不得逐字换行;聊天输入、主要保存动作和底部 Agent Dock 始终可见,document/body 不产生页面级溢出。
3. 客户端接入完成后,`1280×800` 下中央画布工具栏保持单行或受控横向收纳,中文动作不得逐字换行;聊天输入、主要保存动作和底部 Agent Dock 始终可见,document/body 不产生页面级溢出。
4. 主站现有图片编辑器的按钮名称、tool selection、hover/focus/pressed/disabled 行为与工具栏位置不因共享抽取回归;网站账号、钱包、素材库、生成和导出逻辑不下沉共享层。
5. 客户端生成、保存、取消、恢复、登录失效与余额不足只更换视觉承载,不改变 Host Port 调用、幂等身份、草稿 CAS、manifest/revision、Runtime 审批或钱包刷新语义。
6. 阶段一、二冻结合同并让主站消费共享 chrome,不修改客户端中央素材画布;客户端中央区、Supervisor 和 Agent Dock 的正式换肤在后续同一 UI 实施阶段完成
6. 阶段一、二冻结合同并让主站消费共享 chrome2026-08-06 的阶段三至五已经完成客户端中央区、Supervisor 和 Agent Dock 的正式换肤、定向测试与 `1280×800` 验收。独立 Vite 没有登录会话时继续显示真实登录门禁,不为视觉测试增加绕过入口
## 8. 非目标
@@ -3,10 +3,10 @@
## 2026-08-06 Game Agent 画布视觉以主站 token 与共享 chrome 为唯一来源
- 背景:网站与 Tauri 已经共同消费 `image-canvas-core/react` 的 viewport、selection、renderer 和 history,但客户端素材画布仍维护独立的文字工具栏、按钮和状态外观,`1280×800` 下会出现动作逐字换行、主次不清和工作台四区视觉漂移。直接复制主站 `src/components/image-editor` 或整包 `src/index.css` 会重新形成宿主分叉和隐式全局依赖。
- 决策:保留 Game Agent 左导航、中央主视窗、右侧 Supervisor 和底部 Agent Dock 四区布局;平台颜色、边框、文本和 surface 继续以 `packages/shared/src/theme.css` 为事实源,画布动作按钮、工具栏、分组与分隔符进入现有 `@genarrative/image-canvas-react`。主站先改为直接消费共享 chromeTauri 在下一 UI 阶段接入同一组件
- 决策:保留 Game Agent 左导航、中央主视窗、右侧 Supervisor 和底部 Agent Dock 四区布局;平台颜色、边框、文本和 surface 继续以 `packages/shared/src/theme.css` 为事实源,画布动作按钮、工具栏、分组与分隔符进入现有 `@genarrative/image-canvas-react`。主站和 Tauri 均直接消费同一 chromeTauri 宿主只保留业务接线和布局适配
- 依赖边界:共享组件只接受 React props、图标节点、短文案和事件,不读取账号、钱包、项目、生成、Tauri 或 HTTP。主站 `EditorIconButton` 只保留 Lucide/特殊浮层薄适配,主站业务 CSS 只保留定位和专属状态;Tauri 禁止导入网站完整 CSS。内部错误码、开发者 API 配置和本机绝对路径不作为普通用户默认视觉内容。
- 分期:阶段一、二更新权威合同、抽取共享 chrome 并让主站消费,不修改客户端 `AssetCanvasSurface`;阶段三、四统一中央素材画布、Supervisor 与 Agent Dock阶段五验证与修复必须和客户端实施同一任务完成
- 验证:共享 React 组件与主站图片编辑器 7 个定向测试文件共 `92/92` 通过,覆盖可访问性、状态、透传、工具列表、选中态与 option hover/focus;主站与客户端 typecheck、编码检查和 `git diff --check`过。
- 分期:阶段一、二更新权威合同、抽取共享 chrome 并让主站消费;阶段三、四统一中央素材画布、Supervisor 与 Agent Dock阶段五在同一变更中补齐测试、真实视口测量和发现回归修复
- 验证:共享 chrome 与 Tauri Surface 定向测试 `20/20` 通过,覆盖可访问性、pressed/expanded/disabled、工具组/分隔符、生成/失败/保存/取消和非裁剪 CSS;AppSurface 横屏合同 `1/1` 通过,锁定 Supervisor composer、Agent Dock 与 `100dvh` 两行工作台。客户端/全仓 typecheck、定向 ESLint、编码和 diff 检查通过。完整 AppSurface 本次运行是 `352/354` 通过:preview 权限用例单跑通过,属于全量异步顺序波动;仍失败的“画板 API Key”运行配置用例不涉及本次 diff,留在原业务范围处理。应用内浏览器 `1280×800` 实测 window/document/body client 与 scroll 均为 `1280×800`;真实入口没有登录会话时保持登录门禁,不增加测试绕过。
- 关联:`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md``docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md``packages/image-canvas-react/`
## 2026-08-05 无限画布阶段三以 Tauri 持久草稿和项目 mutation 事务形成独立图片闭环
@@ -30,6 +30,14 @@
- 验证:共享 React 测试重复 mount/unmount 后 wheel add/remove 数量相等、ResizeObserver 精确 disconnect,并挂两个含各自小地图的 viewport,确认只更新目标实例;网站与 Tauri 壳分别 typecheck/build。
- 关联:`packages/image-canvas-react/src/useImageCanvasViewportControls.ts``packages/image-canvas-react/src/CanvasPortal.tsx`、根目录与 `apps/ai-game-creator-shell` 的 Vite 配置。
## 桌面工作台不要让 Supervisor 内容高度挤掉输入区和 Agent Dock
- 现象:`1280×800` 或更矮窗口中,Supervisor 的消息、Runtime 状态和错误正文共同按内容高度增长,聊天输入被推到栏外;底部 Dock 使用固定宽度卡片时还会在中等宽度造成页面级横向溢出。中央画布的生成卡和状态栏若同时固定高度并隐藏 overflow,进度、失败、重试、保存或取消动作会被裁掉。
- 原因:四区工作台没有把“主区内部滚动”和“页面级滚动”分开;Supervisor 的长状态没有独立上限,Dock 卡片不能收缩,宿主又用全局按钮/固定行高样式覆盖共享 chrome。
- 处理:桌面工作台使用 `100dvh` 两行网格,第一行 `minmax(0, 1fr)` 承载中央区与 Supervisor,第二行承载 Dock;消息和 Runtime 分别内部滚动,composer 保持最后一行并设置明确层级。Dock 卡片使用可收缩 flex 与文本省略。画布 dialog、进度/失败卡和状态栏设置 `min-height: 0`、受限最大高度与内部滚动;宿主不再覆盖所有按钮,只为主要动作和布局提供 token 化薄样式。
- 验证:AppSurface CSS 合同检查 `100dvh`、Supervisor composer、Runtime 内滚动、Dock 常驻/可收缩;素材画布测试检查生成 dialog、operation card、状态栏和窄屏保存动作不会被裁剪。真实入口在 `1280×800` 测量 document/body client 与 scroll 一致;登录门禁不可为视觉测试绕过。
- 关联:`apps/ai-game-creator-shell/src/styles.css``apps/ai-game-creator-shell/src/features/asset-canvas/assetCanvasSurface.css``apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts``apps/ai-game-creator-shell/tests/assetCanvasSurface.test.tsx`
## 正式素材提交不能把多文件写入或 Tauri 事件误当成一次原子动作
- 现象:图片已经落到 `assets/` 但 manifest 没有资产,或 manifest 已追加而 project revision/草稿仍是旧值;进程在 emit 前后退出后,用户重试又得到第二份图片、第二个 asset 或重复选中。
@@ -960,3 +960,11 @@ game-project/
- 应用重启时 `accepted/running` 只恢复 GET`prepared` 只以私有账本原请求字节和原键恢复同一 POST。用户停止等待只失效当前 focus generation;不可取消的远端操作继续 reconcile,迟到 completed 可以更新原项目但不能抢焦点。配置缺失、结果引用不稳定、下载/提交失败和未知结果都不得显示资源创建成功。
- 最终审计后 Tauri 撤销/重做直接使用与网站相同的共享 `useCanvasHistory`,共享 history 同时恢复 viewport、选择、图层位置和缩放边界;Tauri 自身只保留 Pointer 事件接线,平移、缩放、选择、图层变换和 renderer 算法继续来自共享 core/react。
- 正式事务把首个快照、全部快照和 journal 写入后但 ledger 写入前纳入故障矩阵。恢复发现无 ledger 的 transaction 时,只有目录内容全部属于受控快照/临时文件、正式目标文件不存在、manifest 与 project revision 仍精确等于 before,才清理目录并返回 rolled-back;任何未知文件或权威状态变化都失败关闭。同一 commit/idempotency 身份在清理后可重放,最终仍只登记一份资产。
## 2026-08-06 Game Agent 画布 UI 对齐阶段三至五
- 中央 `AssetCanvasSurface` 沿用现有 create/refine、生成、保存、取消和恢复接线,直接消费 `@genarrative/image-canvas-react``CanvasChromeButton / CanvasToolbar / CanvasToolbarGroup / CanvasToolbarDivider`。宿主不再维护全局按钮皮肤,只保留字段、面板定位、主要动作和平台 token 映射;主站完整 `src/index.css` 没有进入 Tauri。
- 顶部动作与保存设置分层,工具组保持中文单行并允许工具栏受控横向收纳;生成设置仍是独立 dialog。进度/失败卡、dialog 和底部状态栏使用内部滚动,不再由固定高度或父级 overflow 裁剪,保存/确认使用平台 primary 层级。
- 工作台仍是左导航、中央主视窗、右 Project Supervisor、底部 Agent Dock 四区。中央区使用主面板层级;Supervisor 的消息/Runtime 独立滚动且 composer 常驻;Dock 改为较轻的持续状态 surface 与可收缩卡片。桌面网格按 `100dvh` 将 Dock 固定在第二行,`≤760px` 只保留开发兼容堆叠。
- AppSurface/CSS 合同锁定 `1280×800` 下的工作台无页面级溢出、Supervisor composer、主要保存动作和 Dock 可见;共享/Tauri 定向用例锁定按钮名称、工具组/分隔符、pressed/expanded/disabled 和生成/失败/保存/取消非裁剪。真实 Vite 页面测得 window/document/body client 与 scroll 均为 `1280×800`;无登录会话时按既有安全合同停在登录门禁,不增加或使用测试绕过。
- 本次只调整 UI 和测试/文档,未改变图片生成请求、登录态、泥点计费、钱包刷新、Host Port、Tauri command/event、草稿 CAS、manifest/revision、幂等恢复、Runtime、审批、资源投影或保存事务。
@@ -2,7 +2,7 @@
更新时间:`2026-08-06`
状态:阶段一产品与技术合同已冻结;截至 2026-08-06,客户登录态图片生成、泥点计费链路、中央进度/失败态Tauri 正式资产提交已落地,后续增量仍受本文合同约束。
状态:阶段一产品与技术合同已冻结;截至 2026-08-06,客户登录态图片生成、泥点计费链路、中央进度/失败态Tauri 正式资产提交及画布 UI 对齐阶段三至五已落地,后续增量仍受本文合同约束。
本文是网站与 AI 游戏创作 Tauri 客户端共享图片画布能力的下一阶段编码依据。若本文与资源管理阶段七的“美术编辑暂缓”口径冲突,以本文对后续素材创作切片的更新决定为准;资源总览既有布局、依赖图和只读聚焦合同继续有效。
@@ -204,7 +204,7 @@ interface ImageCanvasHostPort {
### 3.4 主站 UI 对齐与共享画布 chrome
实现状态(2026-08-06,阶段一、二):以下四个共享组件及作用域样式已经进入 `@genarrative/image-canvas-react`;主站默认 `EditorIconButton`、底部画布工具栏及画布内直接使用的常规图标/短标签动作已经消费共享按钮,网站专属 `surfaceFloating` / `darkMini` 仍保留薄适配。客户端 `AssetCanvasSurface` 尚未接入,按本节边界留给后续 UI 实施阶段
实现状态(2026-08-06,阶段一至五):以下四个共享组件及作用域样式已经进入 `@genarrative/image-canvas-react`;主站默认 `EditorIconButton`、底部画布工具栏及画布内直接使用的常规图标/短标签动作已经消费共享按钮,网站专属 `surfaceFloating` / `darkMini` 仍保留薄适配。客户端 `AssetCanvasSurface` 的常规动作、三组工具、两个分隔符、保存/生成/失败动作和浮动缩放栏也已直接消费同一 chrome;宿主只保留业务回调、字段、面板布局和平台 token 映射
第一批通用视觉组件固定由 `@genarrative/image-canvas-react` 暴露:
@@ -220,7 +220,7 @@ CanvasToolbarDivider
- 共享 CSS 继续位于 `packages/image-canvas-react/src/styles.css`,选择器必须以 `.genarrative-image-canvas` 前缀开头。颜色优先消费宿主继承的 `--image-canvas-brand-*`,再回退 `--platform-*`,最后才使用包内安全默认值。
- 主站现役 `EditorIconButton` 可以保留为传入 Lucide icon 的薄 adapter,但默认画布按钮必须委托 `CanvasChromeButton`;特殊网站浮层或暗色媒体按钮仍可留在宿主,不能为了共享视觉把网站 portal、账号或媒体业务下沉。
- 主站底部画布工具栏必须委托 `CanvasToolbar``CanvasToolbarGroup``CanvasToolbarDivider`,位置、引导态和工具 option hover 仍由网站宿主控制。共享抽取完成后,主站业务 CSS 只保留定位和主站专属状态,不重复维护通用按钮、浮动 surface、分组和分隔符样式。
- Tauri 在后续 UI 实施阶段消费同一组件;本阶段不修改 `AssetCanvasSurface` 的 JSX/CSS,不把生成、保存、草稿、manifest 或 Runtime 逻辑带入共享包。
- Tauri 直接消费同一组件,且没有把生成、保存、草稿、manifest、账号、钱包、HTTP 或 Runtime 逻辑带入共享包;这些事实继续由现有 Host Adapter 和宿主回调负责
- 主题事实源仍是 `packages/shared/src/theme.css`。Tauri 只能导入共享主题、`@genarrative/image-canvas-react/styles.css` 和自身宿主样式,禁止导入网站整包 `src/index.css`
- 共享组件测试必须覆盖名称、默认 `type=button`、pressed/expanded、禁用、带短标签动作、floating toolbar surface 和 ref/class/event 透传;主站测试继续锁定工具列表、选中态和 option tool 的 hover/focus 行为。
@@ -957,3 +957,12 @@ confirmation-required
- `accepted/running` 重启只查询原 operation`prepared` 只以账本中的原 endpoint、原始字节和原键恢复同一 POST。轮询超时、临时鉴权/网关错误或结果未知保持可恢复,不自动转 failed 或换键扣费。
- 用户“停止等待”只撤销当前 session 的聚焦意图,不伪装远端取消。后台 command 可以继续安全 reconcile;应用退出后由项目恢复扫描继续原 operation。迟到完成可以更新原项目权威缓存和 manifest,但不能切回旧画布、清搜索或抢焦点。
- 登录失效、余额不足、平台生成服务配置错误、远端 terminal failed、稳定引用缺失、下载失败、staging/正式提交失败都不得返回资源创建成功。客户模式不得把缺少开发者 API Key 显示为失败原因。只有 terminal failed 才允许用户通过新的独立确认建立新 intent;其它未知或对账状态必须保留原身份。
## 15. 2026-08-06 画布 UI 对齐阶段三至五收口
- 阶段三没有新建平行画布。现有 `AssetCanvasSurface` 直接使用 `CanvasChromeButton``CanvasToolbar``CanvasToolbarGroup``CanvasToolbarDivider`,覆盖返回、导入、撤销、重做、删除、AI 生成、保存、生成确认、失败重试、停止等待和缩放。共享组件只接收图标节点、名称、事件和原生状态,不读取 Tauri、账号、钱包或 HTTP。
- Tauri 样式只导入 `packages/shared/src/theme.css``@genarrative/image-canvas-react/styles.css` 与客户端宿主样式,没有导入主站 `src/index.css`。宿主删除了覆盖全部按钮的重复视觉规则,只保留保存/确认主操作、字段、弹窗、进度/失败卡、状态栏和 viewport 定位;颜色、边框、surface、文字、focus 和主次动作均使用平台 token。
- 生成设置继续通过独立 dialog 展示。生成/失败卡和底部状态使用受限高度加内部滚动,保存设置在窄宽下换为两行并保持主按钮整行可见;工具组不拆字,工具栏需要时横向收纳。普通确认文案不显示 manifest、幂等实现、开发者 API 配置或本机绝对路径。
- 阶段四保留左导航、中央主视窗、右侧 Project Supervisor 和底部 Agent Dock 四区 DOM。中央壳使用主面板层级;Supervisor 降为协作面板,其消息和 Runtime 状态内部滚动、composer 固定在栏底;Dock 使用较轻 surface 和可收缩状态卡,不与中央主要操作争抢层级。
- 桌面 `>760px` 统一使用 `100dvh` 的两行工作台网格,第一行是内部滚动的主视窗/Supervisor,第二行是常驻 Dock。正式 `1280×800` 基线下页面级尺寸合同为 window/document/body `1280×800`;无登录会话的真实 Vite 入口按安全合同停在登录页,不绕过门禁,工作台内部由 AppSurface、共享 chrome 和 CSS 合同测试提供可重复证据。
- 阶段五定向测试覆盖共享 chrome 的 pressed、Tauri 实际消费的工具组/分隔符、按钮名称、expanded/disabled、生成确认、失败重试、保存/取消,以及生成 dialog、进度/失败卡、状态栏、Supervisor composer 和 Agent Dock 的非裁剪/可见性合同。生成请求、登录态、泥点计费、Host Port、草稿 CAS、manifest/revision、Runtime、审批和保存事务均未改变。