From f9847c8f15c458f9d1bf15f71f4de974dce02c1c Mon Sep 17 00:00:00 2001 From: menghao Date: Thu, 6 Aug 2026 12:54:20 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E4=B8=BB=E7=AB=99=E7=94=BB?= =?UTF-8?q?=E5=B8=83=E5=85=B1=E4=BA=AB=E8=A7=86=E8=A7=89=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增共享画布按钮、工具栏、分组与分隔符及作用域样式 主站图片画布默认动作与底部工具栏改用共享实现 补齐 UI 对齐合同、决策记录和定向测试 --- ...AI游戏创作】项目开发工作台PRD-2026-07-20.md | 23 ++- .../shared-memory/decision-log.md | 9 ++ ...客户端素材创作无限画布阶段一合同-2026-08-05.md | 26 ++- .../src/CanvasChrome.test.tsx | 101 ++++++++++++ .../image-canvas-react/src/CanvasChrome.tsx | 152 ++++++++++++++++++ packages/image-canvas-react/src/index.ts | 1 + packages/image-canvas-react/src/styles.css | 126 +++++++++++++++ .../ImageCanvasBottomToolbarView.test.tsx | 11 ++ .../ImageCanvasBottomToolbarView.tsx | 39 +++-- .../ImageCanvasEditorPrimitives.test.tsx | 4 +- .../ImageCanvasEditorPrimitives.tsx | 21 ++- .../ImageCanvasEditorView.test.tsx | 4 +- .../ImageCanvasPanelDockView.test.tsx | 5 + .../image-editor/ImageCanvasPanelDockView.tsx | 11 +- .../ImageCanvasSelectedLayerToolbarView.tsx | 26 +-- ...mageCanvasUiAssetExtractionOverlayView.tsx | 6 +- src/index.css | 42 +---- 17 files changed, 519 insertions(+), 88 deletions(-) create mode 100644 packages/image-canvas-react/src/CanvasChrome.test.tsx create mode 100644 packages/image-canvas-react/src/CanvasChrome.tsx diff --git a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md index 6363f66b0..763c2a9da 100644 --- a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md +++ b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md @@ -1,6 +1,6 @@ # AI 游戏创作项目开发工作台 PRD -更新时间:`2026-08-05`(实时 manifest、资源焦点状态机与客户端素材创作无限画布合同收口) +更新时间:`2026-08-06`(主站 UI 对齐、实时 manifest、资源焦点状态机与客户端素材创作无限画布合同收口) ## 1. 产品定位 @@ -83,6 +83,16 @@ - 未开放选项使用“视觉不可用但可点击说明原因”,不使用无法触发说明的原生 `disabled`。 - 普通用户暂不开放 Agent.md 编辑和自定义 Skill 安装;后续必须先定义来源审核、版本、权限、沙箱和回滚合同。 +### 3.7 主站 UI 对齐与共享视觉边界 + +- 项目工作台继续保留左侧平台导航、中央主视窗、右侧 Project Supervisor 和底部专业 Agent 状态栏四区结构;主站图片编辑器只作为视觉语言和共享画布组件的事实源,不把其素材库侧栏、账号业务或云端项目外壳整体搬入客户端。 +- 平台主题事实源固定为 `packages/shared/src/theme.css`。画布通用 chrome 固定落在 `@genarrative/image-canvas-react`,主站与 Tauri 必须直接 import 同一组件和作用域样式;客户端不得复制 `src/components/image-editor/`,也不得导入主站完整 `src/index.css`。 +- 第一批共享 chrome 固定覆盖画布动作按钮、工具栏、工具分组和分隔符。按钮的默认、悬停、键盘焦点、选中、禁用和主次色语义由共享层表达;宿主只提供图标、文案、事件与业务禁用条件。 +- 主站账号、钱包、服务端项目、云端素材库和生成面板仍留在网站宿主;客户端本地项目、manifest、Runtime、审批、草稿、生成与正式提交仍留在 Tauri 宿主。共享视觉组件不得读取这些业务事实。 +- 客户端 UI 对齐采用“同视觉、同组件、保留四区布局”,不复制主站整页布局。资源总览、运行视图和 Supervisor 对话继续是 Game Agent 工作台独有语义。 +- 客户端正式产品仍只按 `1280×800` 横屏合同交付;更窄浏览器样式只负责不崩溃和开发兼容,不改成移动端创作工作台。 +- 普通用户界面不默认展示内部错误码、开发者 API 配置或本机绝对路径;确需诊断的信息进入受控详情或开发模式,不与主要动作并列。 + ## 4. 工作台状态机 ### 4.1 主视窗 @@ -401,7 +411,7 @@ type ProjectAgentMudPointAttribution = { - 当前 run 专业状态与项目历史成果分离。 - 默认三专业组,并可展开另外三组。 - 严格审批有效;风险/无需审批可点击查看未开放原因。 -- 橙色低保真视觉与 `1280×800` 横屏边界。 +- 与主站图片编辑器一致的平台轻色主题、共享画布 chrome 与 `1280×800` 横屏边界。 ### P1 @@ -487,6 +497,15 @@ type ProjectAgentMudPointAttribution = { 7. 新增、精修、生成、保存、取消、失败和恢复必须覆盖权威专题 §13 的完整验收矩阵;只完成画布 UI 或只完成本地写文件都不能算正式闭环。 8. 自动定位必须分别证明资源已投影、dependency/type 两份布局都 settled 且存在目标位置、目标卡 DOM 已提交;搜索隐藏走显式清除/定位,任何 commit 最多自动聚焦一次。 +### 7.7 主站 UI 对齐验收 + +1. `packages/image-canvas-react` 暴露共享画布动作按钮、工具栏、工具分组和分隔符;主站图片编辑器直接消费这些组件,旧网站组件只允许保留薄适配,不得继续维护另一份按钮可访问性或选中态实现。 +2. 共享样式全部使用 `.genarrative-image-canvas*` 作用域并消费 `--platform-*` / `--image-canvas-brand-*` token;Tauri 不导入主站完整 `src/index.css`,主站也不复制共享样式回业务 CSS。 +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 实施阶段完成。 + ## 8. 非目标 - 资源总览当前不实现资源卡手动拖动,也不实现通用聚焦工具栏/工具侧边栏、下一迭代版本创建入口、运行版本切换、版本回滚、运行模块扩展、测试切片、运行态消费版本、数值参数或泥点归因。正式版本记录已经成为 manifest 业务真相,但当前只读取、校验和展示已有记录。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index e262d663c..b216db250 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -1,5 +1,14 @@ # 决策记录 +## 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`。主站先改为直接消费共享 chrome,Tauri 在下一 UI 阶段接入同一组件。 +- 依赖边界:共享组件只接受 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` 通过。 +- 关联:`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`、`docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md`、`packages/image-canvas-react/`。 + ## 2026-08-05 无限画布阶段三以 Tauri 持久草稿和项目 mutation 事务形成独立图片闭环 - 决策:阶段三新增独立 `AssetCanvasSurface` 和 Tauri Host Adapter,直接消费共享 core/React/ports;前端状态固定为 `canvas.creating / editing / saving / recovering / failed`,generation 明确使用 unsupported mock。本阶段不接资源总览入口、保存后自动选中或真实 AI,避免提前修改工作台状态合同。 diff --git a/docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md b/docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md index e3fddbcd9..2e23518fb 100644 --- a/docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md +++ b/docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md @@ -1,8 +1,8 @@ # 客户端素材创作无限画布阶段一合同 -更新时间:`2026-08-05` +更新时间:`2026-08-06` -状态:阶段一产品与技术合同已冻结;当前提交只更新文档,不代表代码已经实现。 +状态:阶段一产品与技术合同已冻结;截至 2026-08-06,阶段一、二共享画布 chrome 与主站接入已实现,客户端中央画布换肤留待后续阶段。 本文是网站与 AI 游戏创作 Tauri 客户端共享图片画布能力的下一阶段编码依据。若本文与资源管理阶段七的“美术编辑暂缓”口径冲突,以本文对后续素材创作切片的更新决定为准;资源总览既有布局、依赖图和只读聚焦合同继续有效。 @@ -202,6 +202,28 @@ interface ImageCanvasHostPort { - Web adapter 把草稿、导入、生成、导出和提交映射到现有服务端 editor project、云端素材库及账户/钱包链路。 - Tauri adapter 把草稿、导入、导出和提交映射到本文第 7 至 11 节的本地合同;生成通过 External Editor API,API Key 只从 Tauri 应用配置读取,不能进入项目 sidecar、manifest、事件或错误正文。 +### 3.4 主站 UI 对齐与共享画布 chrome + +实现状态(2026-08-06,阶段一、二):以下四个共享组件及作用域样式已经进入 `@genarrative/image-canvas-react`;主站默认 `EditorIconButton`、底部画布工具栏及画布内直接使用的常规图标/短标签动作已经消费共享按钮,网站专属 `surfaceFloating` / `darkMini` 仍保留薄适配。客户端 `AssetCanvasSurface` 尚未接入,按本节边界留给后续 UI 实施阶段。 + +第一批通用视觉组件固定由 `@genarrative/image-canvas-react` 暴露: + +```ts +CanvasChromeButton +CanvasToolbar +CanvasToolbarGroup +CanvasToolbarDivider +``` + +- `CanvasChromeButton` 统一原生 button 的可访问名称、tooltip、`aria-pressed`、`aria-expanded`、禁用态和画布 action 视觉;图标、短文案、业务事件和禁用条件由宿主传入。共享组件不得 import Lucide、平台账号 store、钱包 store 或宿主请求客户端。 +- `CanvasToolbar` 只负责 `role=toolbar`、可访问名称、plain/floating surface 和横向收纳;`CanvasToolbarGroup`、`CanvasToolbarDivider` 只表达动作分组,不知道具体工具 ID 或业务顺序。 +- 共享 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 逻辑带入共享包。 +- 主题事实源仍是 `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 行为。 + ## 4. 图片首版能力边界 ### 4.1 必须交付 diff --git a/packages/image-canvas-react/src/CanvasChrome.test.tsx b/packages/image-canvas-react/src/CanvasChrome.test.tsx new file mode 100644 index 000000000..a517e7045 --- /dev/null +++ b/packages/image-canvas-react/src/CanvasChrome.test.tsx @@ -0,0 +1,101 @@ +/* @vitest-environment jsdom */ + +import { fireEvent, render, screen } from '@testing-library/react'; +import { createRef } from 'react'; +import { describe, expect, it, vi } from 'vitest'; + +import { + CanvasChromeButton, + CanvasToolbar, + CanvasToolbarDivider, + CanvasToolbarGroup, +} from './CanvasChrome'; + +describe('shared canvas chrome', () => { + it('forwards native button props and exposes shared accessible states', () => { + const onClick = vi.fn(); + const ref = createRef(); + + render( + } + className="host-button" + data-host="main-site" + pressed + expanded={false} + onClick={onClick} + />, + ); + + const button = screen.getByRole('button', { name: '选择工具' }); + expect(button.className).toContain( + 'genarrative-image-canvas__chrome-button', + ); + expect(button.className).toContain('host-button'); + expect(button.getAttribute('type')).toBe('button'); + expect(button.getAttribute('title')).toBe('选择'); + expect(button.getAttribute('aria-pressed')).toBe('true'); + expect(button.getAttribute('aria-expanded')).toBe('false'); + expect(button.getAttribute('data-host')).toBe('main-site'); + expect(ref.current).toBe(button); + + fireEvent.click(button); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('supports a short visible label and native disabled behavior', () => { + const onClick = vi.fn(); + render( + } + disabled + onClick={onClick} + > + 改造 + , + ); + + const button = screen.getByRole('button', { name: '改造素材' }); + expect(button.className).toContain( + 'genarrative-image-canvas__chrome-button--with-label', + ); + expect(button.getAttribute('title')).toBe('改造素材'); + expect(button).toHaveProperty('disabled', true); + + fireEvent.click(button); + expect(onClick).not.toHaveBeenCalled(); + }); + + it('provides a named toolbar with groups and decorative dividers', () => { + render( + + + } /> + + + , + ); + + const toolbar = screen.getByRole('toolbar', { name: '素材工具栏' }); + expect(toolbar.className).toContain('genarrative-image-canvas__toolbar'); + expect(toolbar.className).toContain( + 'genarrative-image-canvas__toolbar--floating', + ); + expect(toolbar.className).toContain('host-toolbar'); + expect(toolbar.getAttribute('aria-orientation')).toBe('horizontal'); + expect(screen.getByTestId('toolbar-group').className).toContain( + 'genarrative-image-canvas__toolbar-group', + ); + expect( + screen.getByTestId('toolbar-divider').getAttribute('aria-hidden'), + ).toBe('true'); + }); +}); diff --git a/packages/image-canvas-react/src/CanvasChrome.tsx b/packages/image-canvas-react/src/CanvasChrome.tsx new file mode 100644 index 000000000..cfec8a54c --- /dev/null +++ b/packages/image-canvas-react/src/CanvasChrome.tsx @@ -0,0 +1,152 @@ +import { + type ButtonHTMLAttributes, + forwardRef, + type HTMLAttributes, + type ReactNode, +} from 'react'; + +function joinClassNames(...classNames: Array) { + return classNames.filter(Boolean).join(' '); +} + +export type CanvasChromeButtonProps = Omit< + ButtonHTMLAttributes, + 'aria-expanded' | 'aria-label' | 'aria-pressed' | 'children' +> & { + label: string; + icon: ReactNode; + children?: ReactNode; + pressed?: boolean; + expanded?: boolean; +}; + +/** + * 画布宿主共享的动作按钮,只承接 chrome、可访问性与原生按钮状态。 + */ +export const CanvasChromeButton = forwardRef< + HTMLButtonElement, + CanvasChromeButtonProps +>(function CanvasChromeButton( + { + label, + icon, + children, + pressed, + expanded, + title = label, + type = 'button', + className, + ...buttonProps + }, + ref, +) { + const hasVisibleLabel = + children !== undefined && children !== null && children !== false; + + return ( + + ); +}); + +export type CanvasToolbarProps = Omit< + HTMLAttributes, + 'aria-label' | 'children' | 'role' +> & { + label: string; + children: ReactNode; + surface?: 'plain' | 'floating'; +}; + +/** 画布宿主共享的横向工具栏容器。 */ +export const CanvasToolbar = forwardRef( + function CanvasToolbar( + { label, children, surface = 'plain', className, ...toolbarProps }, + ref, + ) { + return ( +
+ {children} +
+ ); + }, +); + +export type CanvasToolbarGroupProps = HTMLAttributes & { + children: ReactNode; +}; + +/** 将同类画布动作保持为不可拆散的横向分组。 */ +export const CanvasToolbarGroup = forwardRef< + HTMLSpanElement, + CanvasToolbarGroupProps +>(function CanvasToolbarGroup({ children, className, ...groupProps }, ref) { + return ( + + {children} + + ); +}); + +export type CanvasToolbarDividerProps = Omit< + HTMLAttributes, + 'aria-hidden' | 'children' +>; + +/** 纯视觉工具分隔符,不进入无障碍树。 */ +export const CanvasToolbarDivider = forwardRef< + HTMLSpanElement, + CanvasToolbarDividerProps +>(function CanvasToolbarDivider({ className, ...dividerProps }, ref) { + return ( +