修复画板 Portal 菜单主题继承
新增画板 Portal 主题桥接并同步亮暗主题 让编辑器根节点与 Portal 菜单共享完整品牌 Token 补充静音开关、角色动画与样式契约回归测试 同步图片画布技术方案和 Portal 排障记忆
This commit is contained in:
@@ -128,6 +128,11 @@ describe('ImageCanvasCharacterAnimationPanelView', () => {
|
||||
expect(screen.getByLabelText('当前分辨率').textContent).toBe('720p');
|
||||
expect(screen.getByLabelText('当前比例').textContent).toBe('16:9');
|
||||
expect(menu).toBeTruthy();
|
||||
expect(
|
||||
menu
|
||||
.closest('.image-canvas-editor__portal-theme')
|
||||
?.classList.contains('platform-theme--light'),
|
||||
).toBe(true);
|
||||
expect(screen.getByLabelText('当前状态').textContent).toBe('idle');
|
||||
expect(screen.getByLabelText('当前错误').textContent).toBe('-');
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import type {
|
||||
EditorCharacterAnimationRatio,
|
||||
@@ -19,6 +18,7 @@ import { PlatformFloatingMenu } from '../common/PlatformFloatingMenu';
|
||||
import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { PlatformTextField } from '../common/PlatformTextField';
|
||||
import { ImageCanvasEditorPortal } from './ImageCanvasEditorPortal';
|
||||
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
|
||||
import type {
|
||||
CanvasLayer,
|
||||
@@ -79,11 +79,8 @@ function buildLocalMenuStyle(anchor: HTMLElement | null): CSSProperties {
|
||||
}
|
||||
|
||||
function renderPanelPortal(node: ReactNode) {
|
||||
if (typeof document === 'undefined') {
|
||||
return node;
|
||||
}
|
||||
// 中文注释:参数菜单挂到页面级,避免被角色动画面板滚动边界裁切。
|
||||
return document.body ? createPortal(node, document.body) : node;
|
||||
return <ImageCanvasEditorPortal>{node}</ImageCanvasEditorPortal>;
|
||||
}
|
||||
|
||||
function getRatioLabel(value: EditorCharacterAnimationRatio) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
|
||||
export function ImageCanvasEditorPortal({ children }: { children: ReactNode }) {
|
||||
const platformTheme = useAuthUi()?.platformTheme ?? 'light';
|
||||
|
||||
if (typeof document === 'undefined') {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={`platform-theme platform-theme--${platformTheme} image-canvas-editor__portal-theme`}
|
||||
>
|
||||
{children}
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
@@ -554,6 +554,11 @@ describe('ImageCanvasGenerationComposerView', () => {
|
||||
const paramsPanel = screen.getByRole('menu', {
|
||||
name: '视频参数选项',
|
||||
});
|
||||
expect(
|
||||
paramsPanel
|
||||
.closest('.image-canvas-editor__portal-theme')
|
||||
?.classList.contains('platform-theme--light'),
|
||||
).toBe(true);
|
||||
const ratioChoice = within(paramsPanel).getByRole('button', {
|
||||
name: '比例 16:9',
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import {
|
||||
@@ -24,6 +23,7 @@ import { ImageCanvasCharacterAnimationPanelView } from './ImageCanvasCharacterAn
|
||||
import { ImageCanvasCharacterGenerationComposerView } from './ImageCanvasCharacterGenerationComposerView';
|
||||
import { ImageCanvasCropExpandPanelView } from './ImageCanvasCropExpandPanelView';
|
||||
import { ImageCanvasEditGenerationModalView } from './ImageCanvasEditGenerationModalView';
|
||||
import { ImageCanvasEditorPortal } from './ImageCanvasEditorPortal';
|
||||
import type {
|
||||
CanvasLayer,
|
||||
CharacterAnimationPanelState,
|
||||
@@ -162,10 +162,7 @@ function buildPortalMenuStyle(
|
||||
}
|
||||
|
||||
function renderEditorPortal(node: ReactNode) {
|
||||
if (typeof document === 'undefined') {
|
||||
return node;
|
||||
}
|
||||
return createPortal(node, document.body);
|
||||
return <ImageCanvasEditorPortal>{node}</ImageCanvasEditorPortal>;
|
||||
}
|
||||
|
||||
function resetFailedVideoDialogStatus(dialog: GenerateDialogState) {
|
||||
|
||||
@@ -14,6 +14,16 @@ import type {
|
||||
import { useCanvasGenerationDialogs } from './useCanvasGenerationDialogs';
|
||||
import { useImageCanvasGenerationSurface } from './useImageCanvasGenerationSurface';
|
||||
|
||||
const { mockUseAuthUi } = vi.hoisted(() => ({
|
||||
mockUseAuthUi: vi.fn(() => ({
|
||||
platformTheme: 'light' as 'light' | 'dark',
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../auth/AuthUiContext', () => ({
|
||||
useAuthUi: mockUseAuthUi,
|
||||
}));
|
||||
|
||||
vi.mock('../../services/image-editor/editorImageReference', () => ({
|
||||
resolveEditorImageReferenceDataUrl: vi.fn(async (src: string) => src),
|
||||
}));
|
||||
@@ -35,6 +45,7 @@ let mockedComposerRect: DOMRect | null = null;
|
||||
let originalGetBoundingClientRect: typeof Element.prototype.getBoundingClientRect;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseAuthUi.mockReturnValue({ platformTheme: 'light' });
|
||||
originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;
|
||||
Element.prototype.getBoundingClientRect = function getBoundingClientRect() {
|
||||
if (
|
||||
@@ -302,6 +313,19 @@ describe('useImageCanvasGenerationSurface', () => {
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('bridges the current platform theme into body-level portal menus', () => {
|
||||
mockUseAuthUi.mockReturnValue({ platformTheme: 'dark' });
|
||||
render(<GenerationSurfaceHarness />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '切换规范' }));
|
||||
const menu = screen.getByRole('menu', { name: '生成规范类型' });
|
||||
const themeBridge = menu.closest('.image-canvas-editor__portal-theme');
|
||||
|
||||
expect(themeBridge?.parentElement).toBe(document.body);
|
||||
expect(themeBridge?.classList.contains('platform-theme')).toBe(true);
|
||||
expect(themeBridge?.classList.contains('platform-theme--dark')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders a warning toast when a non-spec layer is picked as character spec', () => {
|
||||
render(<GenerationSurfaceHarness />);
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import type {
|
||||
EditorAssetSnapshot,
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
PlatformFloatingMenuItem,
|
||||
} from '../common/PlatformFloatingMenu';
|
||||
import { PlatformRuntimeStatusToast } from '../common/PlatformRuntimeStatusToast';
|
||||
import { ImageCanvasEditorPortal } from './ImageCanvasEditorPortal';
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
CanvasHistoryAction,
|
||||
@@ -110,10 +110,7 @@ type ImageCanvasGenerationSurfaceOptions = {
|
||||
};
|
||||
|
||||
function renderEditorPortal(node: ReactNode) {
|
||||
if (typeof document === 'undefined') {
|
||||
return node;
|
||||
}
|
||||
return createPortal(node, document.body);
|
||||
return <ImageCanvasEditorPortal>{node}</ImageCanvasEditorPortal>;
|
||||
}
|
||||
|
||||
function buildToolbarPortalMenuStyle(
|
||||
|
||||
+15
-10
@@ -3899,16 +3899,9 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
}
|
||||
}
|
||||
|
||||
.image-canvas-editor {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
color: #1f2937;
|
||||
.image-canvas-editor,
|
||||
.image-canvas-editor__portal-theme,
|
||||
.image-canvas-editor__portal-menu {
|
||||
--image-canvas-brand-accent: var(--platform-accent, #c7653d);
|
||||
--image-canvas-brand-accent-strong: #6f2f21;
|
||||
--image-canvas-brand-fill: var(
|
||||
@@ -3926,6 +3919,18 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
--image-canvas-brand-soft-strong: rgba(238, 208, 183, 0.46);
|
||||
--image-canvas-brand-shadow: rgba(182, 98, 63, 0.16);
|
||||
--image-canvas-brand-focus-ring: rgba(204, 117, 76, 0.18);
|
||||
}
|
||||
|
||||
.image-canvas-editor {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
color: #1f2937;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
Reference in New Issue
Block a user