统一主站画布共享视觉组件
Project CI / Repository checks (pull_request) Failing after 8s
Project CI / Backend tests (pull_request) Failing after 8s
Project CI / Frontend tests (pull_request) Successful in 2m55s
Project CI / Native shell tests (pull_request) Successful in 13m59s

新增共享画布按钮、工具栏、分组与分隔符及作用域样式
主站图片画布默认动作与底部工具栏改用共享实现
补齐 UI 对齐合同、决策记录和定向测试
This commit is contained in:
2026-08-06 12:54:20 +08:00
parent 050d06ddb5
commit f9847c8f15
17 changed files with 519 additions and 88 deletions
@@ -67,6 +67,17 @@ describe('ImageCanvasBottomToolbarView', () => {
const user = userEvent.setup();
const { toolbar, switchTool } = renderToolbar();
expect(toolbar.className).toContain('genarrative-image-canvas__toolbar');
expect(toolbar.className).toContain(
'genarrative-image-canvas__toolbar--floating',
);
expect(
toolbar.querySelectorAll('.genarrative-image-canvas__toolbar-group'),
).toHaveLength(11);
expect(
toolbar.querySelectorAll('.genarrative-image-canvas__toolbar-divider'),
).toHaveLength(2);
const toolExpectations = [
['选择工具', 'select'],
['抓手工具', 'hand'],
@@ -1,3 +1,8 @@
import {
CanvasToolbar,
CanvasToolbarDivider,
CanvasToolbarGroup,
} from '@genarrative/image-canvas-react';
import {
AppWindow,
Clapperboard,
@@ -77,27 +82,22 @@ export function ImageCanvasBottomToolbarView({
onCloseToolOptions,
}: ImageCanvasBottomToolbarViewProps) {
return (
<div
<CanvasToolbar
className={[
'image-canvas-editor__bottom-toolbar',
highlight ? 'image-canvas-editor__bottom-toolbar--guided' : '',
]
.filter(Boolean)
.join(' ')}
role="toolbar"
aria-label="AI画布工具栏"
label="AI画布工具栏"
surface="floating"
onPointerDown={(event) => event.stopPropagation()}
>
{canvasTools.map(({ id, label, icon: Icon, separatorBefore }) => {
if (isToolbarOptionTool(id)) {
return (
<span key={id} className="image-canvas-editor__bottom-tool-group">
{separatorBefore ? (
<span
aria-hidden="true"
className="image-canvas-editor__bottom-toolbar-divider"
/>
) : null}
<CanvasToolbarGroup key={id}>
{separatorBefore ? <CanvasToolbarDivider /> : null}
<span
ref={
id === 'spec'
@@ -124,22 +124,19 @@ export function ImageCanvasBottomToolbarView({
label={label}
title={label}
icon={Icon}
pressed={hasPersistentPressedState(id) && effectiveTool === id}
pressed={
hasPersistentPressedState(id) && effectiveTool === id
}
onClick={() => onSwitchTool(id)}
/>
</span>
</span>
</CanvasToolbarGroup>
);
}
return (
<span key={id} className="image-canvas-editor__bottom-tool-group">
{separatorBefore ? (
<span
aria-hidden="true"
className="image-canvas-editor__bottom-toolbar-divider"
/>
) : null}
<CanvasToolbarGroup key={id}>
{separatorBefore ? <CanvasToolbarDivider /> : null}
<EditorIconButton
label={label}
title={label}
@@ -147,9 +144,9 @@ export function ImageCanvasBottomToolbarView({
pressed={hasPersistentPressedState(id) && effectiveTool === id}
onClick={() => onSwitchTool(id)}
/>
</span>
</CanvasToolbarGroup>
);
})}
</div>
</CanvasToolbar>
);
}
@@ -9,7 +9,7 @@ import {
SidebarMediaItem,
} from './ImageCanvasEditorPrimitives';
test('editor icon button delegates accessible icon chrome to the platform primitive', () => {
test('editor icon button delegates accessible icon chrome to the shared canvas primitive', () => {
render(
<EditorIconButton
label="素材选择模式"
@@ -23,7 +23,7 @@ test('editor icon button delegates accessible icon chrome to the platform primit
const button = screen.getByRole('button', { name: '素材选择模式' });
expect(button.className).toContain('platform-icon-button');
expect(button.className).toContain('genarrative-image-canvas__chrome-button');
expect(button.className).toContain('image-canvas-editor__icon-button');
expect(button.getAttribute('title')).toBe('选择');
expect(button.getAttribute('aria-pressed')).toBe('true');
@@ -1,3 +1,4 @@
import { CanvasChromeButton } from '@genarrative/image-canvas-react';
import type {
ComponentType,
DragEventHandler,
@@ -39,6 +40,24 @@ export function EditorIconButton({
variant,
onClick,
}: EditorIconButtonProps) {
const iconNode = <Icon className="h-4 w-4" />;
if (variant === undefined || variant === 'platformIcon') {
return (
<CanvasChromeButton
type={type}
className={className}
label={label}
title={title}
disabled={disabled}
pressed={pressed}
expanded={expanded}
onClick={onClick}
icon={iconNode}
/>
);
}
return (
<PlatformIconButton
variant={variant}
@@ -50,7 +69,7 @@ export function EditorIconButton({
aria-pressed={pressed}
aria-expanded={expanded}
onClick={onClick}
icon={<Icon className="h-4 w-4" />}
icon={iconNode}
/>
);
}
@@ -2176,7 +2176,9 @@ describe('ImageCanvasEditorView', () => {
});
expect(screen.queryByRole('button', { name: '画布小地图' })).toBeNull();
expect(backgroundButton.className).toContain('platform-icon-button');
expect(backgroundButton.className).toContain(
'genarrative-image-canvas__chrome-button',
);
const minimapToggle = within(panelToolbar).getByRole('button', {
name: '切换小地图',
});
@@ -119,6 +119,11 @@ describe('ImageCanvasPanelDockView', () => {
});
const panel = screen.getByRole('dialog', { name: '画布背景设置' });
expect(
screen
.getByRole('button', { name: '画布背景色' })
.getAttribute('aria-expanded'),
).toBe('true');
const spectrumButton = within(panel).getByRole('button', {
name: '画布背景色盘',
});
@@ -1,4 +1,8 @@
import { Minimap, ZoomControls } from '@genarrative/image-canvas-react';
import {
CanvasChromeButton,
Minimap,
ZoomControls,
} from '@genarrative/image-canvas-react';
import {
ImagePlus,
Layers,
@@ -20,7 +24,6 @@ import {
PlatformFloatingMenu,
PlatformFloatingMenuItem,
} from '../common/PlatformFloatingMenu';
import { PlatformIconButton } from '../common/PlatformIconButton';
import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton';
import {
CANVAS_BACKGROUND_OPTIONS,
@@ -394,10 +397,10 @@ export function ImageCanvasPanelDockView({
)}
</ZoomControls>
<div className="image-canvas-editor__background-control">
<PlatformIconButton
<CanvasChromeButton
label="画布背景色"
title="画布背景色"
aria-expanded={isBackgroundSettingsOpen}
expanded={isBackgroundSettingsOpen}
onClick={onToggleBackgroundSettings}
icon={
<span
@@ -1,3 +1,4 @@
import { CanvasChromeButton } from '@genarrative/image-canvas-react';
import {
Crop,
Download,
@@ -10,7 +11,6 @@ import {
} from 'lucide-react';
import type { CSSProperties } from 'react';
import { PlatformIconButton } from '../common/PlatformIconButton';
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
import type { CanvasLayer } from './ImageCanvasEditorTypes';
import { isQuickEditUnsupportedAssetKind } from './ImageCanvasGenerationModel';
@@ -57,7 +57,7 @@ export function ImageCanvasSelectedLayerToolbarView({
aria-label="素材工具栏"
onPointerDown={(event) => event.stopPropagation()}
>
<PlatformIconButton
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label="改造"
title="改造"
@@ -65,7 +65,7 @@ export function ImageCanvasSelectedLayerToolbarView({
onClick={() => onOpenRedrawPanel(selectedLayer)}
>
<span>改造</span>
</PlatformIconButton>
</CanvasChromeButton>
<EditorIconButton
label="下载按钮"
title="下载按钮"
@@ -91,7 +91,7 @@ export function ImageCanvasSelectedLayerToolbarView({
>
{!isQuickEditUnsupportedAssetKind(selectedLayer) ? (
<>
<PlatformIconButton
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label="快速编辑"
title="快速编辑"
@@ -99,7 +99,7 @@ export function ImageCanvasSelectedLayerToolbarView({
onClick={() => onOpenQuickEditPanel(selectedLayer)}
>
<span>快速编辑</span>
</PlatformIconButton>
</CanvasChromeButton>
<span
aria-hidden="true"
className="image-canvas-editor__floating-toolbar-divider"
@@ -123,7 +123,7 @@ export function ImageCanvasSelectedLayerToolbarView({
</>
) : null}
{selectedLayer.assetKind === 'icon-spritesheet' ? (
<PlatformIconButton
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label={
isPersistingAssetKind
@@ -157,10 +157,10 @@ export function ImageCanvasSelectedLayerToolbarView({
? '拆图中'
: '拆分图集'}
</span>
</PlatformIconButton>
</CanvasChromeButton>
) : null}
{selectedLayer.assetKind === 'ui-design' ? (
<PlatformIconButton
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label="提取素材"
title="提取素材"
@@ -168,10 +168,10 @@ export function ImageCanvasSelectedLayerToolbarView({
onClick={() => onExtractUiDesignAssets(selectedLayer)}
>
<span>提取素材</span>
</PlatformIconButton>
</CanvasChromeButton>
) : null}
{selectedLayer.assetKind === 'character' ? (
<PlatformIconButton
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label="生成动画"
title="生成动画"
@@ -179,13 +179,13 @@ export function ImageCanvasSelectedLayerToolbarView({
onClick={() => onOpenCharacterAnimationPanel(selectedLayer)}
>
<span>生成动画</span>
</PlatformIconButton>
</CanvasChromeButton>
) : null}
<span
aria-hidden="true"
className="image-canvas-editor__floating-toolbar-divider"
/>
<PlatformIconButton
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label="改造"
title="改造"
@@ -193,7 +193,7 @@ export function ImageCanvasSelectedLayerToolbarView({
onClick={() => onOpenRedrawPanel(selectedLayer)}
>
<span>改造</span>
</PlatformIconButton>
</CanvasChromeButton>
<EditorIconButton
label="下载按钮"
title="下载按钮"
@@ -1,3 +1,4 @@
import { CanvasChromeButton } from '@genarrative/image-canvas-react';
import {
Check,
ChevronDown,
@@ -14,7 +15,6 @@ import { useState } from 'react';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformFloatingMenu } from '../common/PlatformFloatingMenu';
import { PlatformIconButton } from '../common/PlatformIconButton';
import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
import type { CanvasLayer, CanvasViewport } from './ImageCanvasEditorTypes';
@@ -314,9 +314,9 @@ export function ImageCanvasUiAssetExtractionOverlayView({
const Icon = option.icon;
const isActive = state.tool === option.value;
return (
<PlatformIconButton
<CanvasChromeButton
key={option.value}
aria-pressed={isActive}
pressed={isActive}
className={
isActive
? 'image-canvas-editor__ui-extraction-tool--active'
+3 -39
View File
@@ -4064,11 +4064,8 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
font-weight: 750;
}
.image-canvas-editor__icon-button,
.image-canvas-editor__project-back-button,
.image-canvas-editor__floating-toolbar button,
.image-canvas-editor__bottom-toolbar button,
.image-canvas-editor__reset-button {
.image-canvas-editor__floating-toolbar button {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -4082,23 +4079,13 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
color 160ms ease;
}
.image-canvas-editor__icon-button:hover,
.image-canvas-editor__floating-toolbar button:hover,
.image-canvas-editor__bottom-toolbar button:hover,
.image-canvas-editor__reset-button:hover {
.image-canvas-editor__floating-toolbar button:hover {
transform: translateY(-1px);
border-color: var(--image-canvas-brand-border-strong);
background: var(--image-canvas-brand-soft);
color: var(--image-canvas-brand-accent-strong);
}
.image-canvas-editor__icon-button {
width: 2.2rem;
height: 2.2rem;
flex-shrink: 0;
border-radius: 0.45rem;
}
.image-canvas-editor__sidebar-header-actions {
display: inline-flex;
gap: 0.35rem;
@@ -5011,21 +4998,12 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
font-weight: 820;
}
.image-canvas-editor__floating-toolbar button,
.image-canvas-editor__bottom-toolbar button,
.image-canvas-editor__reset-button {
.image-canvas-editor__floating-toolbar button {
width: 2.25rem;
height: 2.25rem;
border-radius: 0.45rem;
}
.image-canvas-editor__bottom-tool-group {
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.image-canvas-editor__bottom-toolbar-divider,
.image-canvas-editor__floating-toolbar-divider {
display: inline-block;
width: 1px;
@@ -6578,15 +6556,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
left: 50%;
bottom: 0.85rem;
z-index: 18;
display: inline-flex;
gap: 0.35rem;
max-width: min(calc(100% - 6.6rem), 34rem);
overflow-x: auto;
border: 1px solid var(--image-canvas-brand-border-soft);
border-radius: 0.5rem;
background: #ffffff;
padding: 0.35rem;
box-shadow: 0 18px 38px rgba(15, 23, 42, 0.16);
transform: translateX(-50%);
}
@@ -6638,12 +6608,6 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
text-shadow: 0 2px 7px rgba(127, 65, 25, 0.2);
}
.image-canvas-editor__bottom-toolbar button[aria-pressed='true'] {
border-color: var(--image-canvas-brand-border-strong);
background: var(--image-canvas-brand-soft);
color: var(--image-canvas-brand-accent-strong);
}
.image-canvas-editor__bottom-toolbar-option-wrap,
.image-canvas-editor__spec-tool-wrap {
position: relative;