Files
Genarrative/src/components/image-editor/ImageCanvasIconSpritesheetComposerView.tsx
T
JenkenB 8956ed6fe6 Unify canvas generation placement and UI changes
Add unified placement and UI updates for canvas generation: docs require ImageCanvasGenerationPlacementModel to compute placeholder positions (avoid overlaps, center viewport on placement) and limit frontend video model entries. Refactor UI components and tests: expose div props on PlatformFloatingMenu, make PlatformInlineOptionButton forwardRef, replace inline placeholder actions with ImageCanvasGenerationImageOptionsView in basic generation composer, enhance character animation panel with parameter menu portal and option choices, add reference chip UI, and update many tests to match new interactions and labels. These changes align generation workflows, accessibility labels, and positioning behavior across editor panels.
2026-06-18 11:40:19 +08:00

237 lines
8.2 KiB
TypeScript

import {
type CSSProperties,
type Dispatch,
type ReactNode,
type RefObject,
type SetStateAction,
} from 'react';
import { ImageIcon } from 'lucide-react';
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
import {
PlatformFloatingMenu,
PlatformFloatingMenuItem,
} from '../common/PlatformFloatingMenu';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformTextField } from '../common/PlatformTextField';
import {
DEFAULT_ICON_DESCRIPTIONS,
EDITOR_GENERATION_MUD_POINT_CONFIG,
ICON_DESCRIPTION_LIMIT,
} from './ImageCanvasGenerationModel';
import type {
GenerateDialogState,
SpecGenerationType,
UploadTarget,
} from './ImageCanvasEditorTypes';
import { ImageCanvasGenerationImageOptionsView } from './ImageCanvasGenerationImageOptionsView';
type ImageCanvasIconSpritesheetComposerViewProps = {
dialog: GenerateDialogState;
style: CSSProperties;
iconSpecButtonRef: RefObject<HTMLButtonElement | null>;
isIconSpecMenuOpen: boolean;
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
setIsIconSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
setIsPickingIconSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
renderEditorPortal: (node: ReactNode) => ReactNode;
buildPortalMenuStyle: (
anchor: HTMLElement | null,
placement: 'above' | 'below',
) => CSSProperties;
onOpenSpecDialog: (specType: SpecGenerationType) => void;
onRequestUpload: (target: UploadTarget) => void;
onUpdateIconDescription: (index: number, value: string) => void;
onAddIconDescription: () => void;
onRememberImageModel: (model: string) => void;
onSubmit: (dialog: GenerateDialogState) => void;
};
export function ImageCanvasIconSpritesheetComposerView({
dialog,
style,
iconSpecButtonRef,
isIconSpecMenuOpen,
setGenerateDialog,
setIsIconSpecMenuOpen,
setIsPickingIconSpecFromCanvas,
renderEditorPortal,
buildPortalMenuStyle,
onOpenSpecDialog,
onRequestUpload,
onUpdateIconDescription,
onAddIconDescription,
onRememberImageModel,
onSubmit,
}: ImageCanvasIconSpritesheetComposerViewProps) {
const descriptions = dialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS;
const pickIconSpecFromCanvas = () => {
setIsPickingIconSpecFromCanvas(true);
setIsIconSpecMenuOpen(false);
};
const openIconSpecDialog = () => {
setIsIconSpecMenuOpen(false);
onOpenSpecDialog('icon');
};
const requestIconSpecUpload = () => {
setIsIconSpecMenuOpen(false);
onRequestUpload('icon-spec');
};
return (
<form
className="image-canvas-editor__icon-composer"
style={style}
role="dialog"
aria-label="生成图标素材"
onPointerDown={(event) => event.stopPropagation()}
onSubmit={(event) => {
event.preventDefault();
if (dialog.status !== 'generating') {
onSubmit(dialog);
}
}}
>
<div className="image-canvas-editor__reference-strip">
<span className="image-canvas-editor__character-spec-wrap">
<button
ref={iconSpecButtonRef}
type="button"
className="image-canvas-editor__icon-spec-card image-canvas-editor__reference-chip image-canvas-editor__reference-chip--icon image-canvas-editor__reference-chip--upload"
disabled={dialog.status === 'generating'}
aria-label={dialog.iconSpecReference?.label ?? '图标素材规范'}
onClick={() => setIsIconSpecMenuOpen((open) => !open)}
>
<span className="image-canvas-editor__reference-chip-icon image-canvas-editor__icon-spec-preview">
{dialog.iconSpecReference?.src ? (
<img
src={dialog.iconSpecReference.src}
alt=""
aria-hidden="true"
/>
) : (
<ImageIcon className="h-4 w-4" aria-hidden="true" />
)}
</span>
<span className="image-canvas-editor__icon-spec-copy">
<span className="image-canvas-editor__icon-spec-eyebrow">
图标规范
</span>
<span className="image-canvas-editor__icon-spec-title">
{dialog.iconSpecReference?.label ?? '待选择'}
</span>
</span>
<span className="image-canvas-editor__icon-spec-state">
{dialog.iconSpecReference ? '已绑定' : '待绑定'}
</span>
</button>
</span>
{isIconSpecMenuOpen
? renderEditorPortal(
<PlatformFloatingMenu
className="image-canvas-editor__character-spec-menu image-canvas-editor__portal-menu"
label="图标素材规范来源"
placement="top-start"
style={buildPortalMenuStyle(iconSpecButtonRef.current, 'above')}
>
<PlatformFloatingMenuItem
className="image-canvas-editor__context-menu-item"
onClick={pickIconSpecFromCanvas}
>
从画布中选择
</PlatformFloatingMenuItem>
<PlatformFloatingMenuItem
className="image-canvas-editor__context-menu-item"
onClick={openIconSpecDialog}
>
新建图标素材规范
</PlatformFloatingMenuItem>
<PlatformFloatingMenuItem
className="image-canvas-editor__context-menu-item"
onClick={requestIconSpecUpload}
>
上传图片
</PlatformFloatingMenuItem>
</PlatformFloatingMenu>,
)
: null}
</div>
<div className="image-canvas-editor__field-block">
<PlatformFieldLabel
variant="field"
className="image-canvas-editor__field-title"
>
素材描述
</PlatformFieldLabel>
<div className="image-canvas-editor__icon-description-list">
{descriptions.map((description, index) => (
<label
key={index}
className="image-canvas-editor__icon-description-card"
>
<span className="image-canvas-editor__icon-description-index">
{String(index + 1).padStart(2, '0')}
</span>
<span className="image-canvas-editor__icon-description-title">
素材描述 {index + 1}
</span>
<PlatformTextField
aria-label={`素材描述${index + 1}`}
value={description}
disabled={dialog.status === 'generating'}
size="sm"
density="compact"
className="image-canvas-editor__icon-description-input"
onChange={(event) =>
onUpdateIconDescription(index, event.target.value)
}
/>
</label>
))}
</div>
</div>
{dialog.status === 'failed' ? (
<PlatformStatusMessage
tone="error"
surface="platform"
size="xs"
className="image-canvas-editor__generate-status"
role="alert"
>
{dialog.errorMessage}
</PlatformStatusMessage>
) : null}
<div className="image-canvas-editor__icon-footer">
<button
type="button"
className="image-canvas-editor__character-reference-add"
disabled={
dialog.status === 'generating' ||
descriptions.length >= ICON_DESCRIPTION_LIMIT
}
onClick={onAddIconDescription}
>
添加素材描述
</button>
</div>
<div className="image-canvas-editor__generation-composer-footer">
<ImageCanvasGenerationImageOptionsView
dialog={dialog}
setGenerateDialog={setGenerateDialog}
includeDimensions
onRememberImageModel={onRememberImageModel}
optionLabelPrefix="生成图片"
cost={EDITOR_GENERATION_MUD_POINT_CONFIG.icon}
submitLabel="生成"
submitAriaLabel="生成"
renderEditorPortal={renderEditorPortal}
buildPortalMenuStyle={buildPortalMenuStyle}
/>
</div>
</form>
);
}