统一主站画布共享视觉组件
新增共享画布按钮、工具栏、分组与分隔符及作用域样式 主站图片画布默认动作与底部工具栏改用共享实现 补齐 UI 对齐合同、决策记录和定向测试
This commit is contained in:
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user