8956ed6fe6
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.
237 lines
8.2 KiB
TypeScript
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>
|
|
);
|
|
}
|