fix: polish platform creation flow interactions

This commit is contained in:
2026-06-06 21:36:38 +08:00
parent 7e6ed91149
commit 50e335ba47
12 changed files with 434 additions and 102 deletions

View File

@@ -6,7 +6,7 @@ import {
Trash2,
X,
} from 'lucide-react';
import { type ReactNode, useEffect, useState } from 'react';
import { type ReactNode, useEffect, useRef, useState } from 'react';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
@@ -28,6 +28,8 @@ export type CreativeImageInputPanelLabels = {
promptReferenceUpload: string;
promptReferencePreviewAlt: string;
closePromptReferencePreview: string;
previewMainImage?: string;
closeMainImagePreview?: string;
history?: string;
};
@@ -37,6 +39,9 @@ export type CreativeImageInputPanelProps = {
disabled?: boolean;
isSubmitting?: boolean;
mainImageMode?: 'edit' | 'preview';
mainImageClickMode?: 'upload' | 'preview';
canUploadMainImage?: boolean;
canUseImageHistory?: boolean;
canRemoveMainImage?: boolean;
canToggleAiRedraw?: boolean;
canUploadPromptReferences?: boolean;
@@ -82,6 +87,9 @@ export function CreativeImageInputPanel({
disabled = false,
isSubmitting = false,
mainImageMode = 'edit',
mainImageClickMode = 'preview',
canUploadMainImage = true,
canUseImageHistory = true,
canRemoveMainImage = true,
canToggleAiRedraw = true,
canUploadPromptReferences,
@@ -117,8 +125,10 @@ export function CreativeImageInputPanel({
onHistoryClick,
onSubmit,
}: CreativeImageInputPanelProps) {
const mainImageInputRef = useRef<HTMLInputElement | null>(null);
const [previewReferenceImage, setPreviewReferenceImage] =
useState<CreativeImageInputReferenceImage | null>(null);
const [isMainImagePreviewOpen, setIsMainImagePreviewOpen] = useState(false);
const [isRemoveImageConfirmOpen, setIsRemoveImageConfirmOpen] =
useState(false);
const showPrompt = mainImageMode === 'preview' || !uploadedImageSrc || aiRedraw;
@@ -127,10 +137,19 @@ export function CreativeImageInputPanel({
const promptReferenceUploadDisabled =
disabled || promptReferenceImages.length >= promptReferenceLimit;
const canEditMainImage = mainImageMode === 'edit';
const isMainImageUploadEnabled = canEditMainImage && canUploadMainImage;
const shouldShowHistoryButton =
canEditMainImage && canUseImageHistory && Boolean(onHistoryClick);
const shouldPreviewMainImage =
mainImageClickMode === 'preview' && Boolean(uploadedImageSrc);
const shouldShowMainImageUploadButton =
isMainImageUploadEnabled && shouldPreviewMainImage;
useEffect(() => {
if (uploadedImageSrc) {
setPreviewReferenceImage(null);
} else {
setIsMainImagePreviewOpen(false);
}
}, [uploadedImageSrc]);
@@ -187,35 +206,48 @@ export function CreativeImageInputPanel({
</div>
<div className={imageFrameClassName}>
<div className={imageCardClassName}>
{canEditMainImage ? (
<>
<input
id={mainImageInputId}
type="file"
accept={mainImageAccept}
disabled={disabled}
aria-label={labels.uploadImage}
onChange={(event) => {
const file = event.currentTarget.files?.[0] ?? null;
event.currentTarget.value = '';
if (file) {
onMainImageFileSelect(file);
}
}}
className="sr-only"
/>
<label
htmlFor={mainImageInputId}
className={`absolute inset-0 z-0 cursor-pointer ${disabled ? 'cursor-not-allowed' : ''}`}
title={
uploadedImageSrc ? labels.replaceImage : labels.uploadImage
{isMainImageUploadEnabled ? (
<input
ref={mainImageInputRef}
id={mainImageInputId}
type="file"
accept={mainImageAccept}
disabled={disabled}
aria-label={labels.uploadImage}
onChange={(event) => {
const file = event.currentTarget.files?.[0] ?? null;
event.currentTarget.value = '';
if (file) {
onMainImageFileSelect(file);
}
>
<span className="sr-only">
{uploadedImageSrc ? labels.replaceImage : labels.uploadImage}
</span>
</label>
</>
}}
className="sr-only"
/>
) : null}
{shouldPreviewMainImage ? (
<button
type="button"
className="absolute inset-0 z-[2] cursor-zoom-in"
aria-label={labels.previewMainImage ?? uploadedImageAlt}
title={labels.previewMainImage ?? uploadedImageAlt}
onClick={() => setIsMainImagePreviewOpen(true)}
/>
) : isMainImageUploadEnabled ? (
<label
htmlFor={mainImageInputId}
className={`absolute inset-0 z-0 cursor-pointer ${disabled ? 'cursor-not-allowed' : ''}`}
title={
uploadedImageSrc
? labels.replaceImage
: labels.uploadImage
}
>
<span className="sr-only">
{uploadedImageSrc
? labels.replaceImage
: labels.uploadImage}
</span>
</label>
) : null}
{uploadedImageSrc ? (
<ResolvedAssetImage
@@ -232,7 +264,19 @@ export function CreativeImageInputPanel({
</span>
)}
<div className="pointer-events-none absolute inset-0 z-[1] bg-[linear-gradient(180deg,rgba(255,255,255,0.12)_0%,rgba(255,255,255,0.04)_42%,rgba(255,255,255,0.18)_100%)]" />
{canEditMainImage && onHistoryClick ? (
{shouldShowMainImageUploadButton ? (
<button
type="button"
disabled={disabled}
onClick={() => mainImageInputRef.current?.click()}
className="absolute bottom-3 right-3 z-10 inline-flex h-10 w-10 items-center justify-center rounded-full border border-white/80 bg-white/94 text-[var(--platform-text-strong)] shadow-sm backdrop-blur transition hover:text-[var(--platform-accent)] disabled:cursor-not-allowed disabled:opacity-55"
aria-label={labels.replaceImage}
title={labels.replaceImage}
>
<ImagePlus className="h-4 w-4" />
</button>
) : null}
{shouldShowHistoryButton ? (
<button
type="button"
disabled={disabled}
@@ -284,7 +328,7 @@ export function CreativeImageInputPanel({
>
<Trash2 className="h-4 w-4" />
</button>
) : canEditMainImage && !uploadedImageSrc ? (
) : isMainImageUploadEnabled && !uploadedImageSrc ? (
<label
htmlFor={mainImageInputId}
className={`absolute bottom-9 left-1/2 z-10 -translate-x-1/2 whitespace-nowrap text-center text-sm font-black text-[var(--platform-text-strong)] drop-shadow-[0_1px_0_rgba(255,255,255,0.82)] transition hover:text-[var(--platform-accent)] sm:bottom-10 ${
@@ -477,6 +521,48 @@ export function CreativeImageInputPanel({
</div>
) : null}
{isMainImagePreviewOpen && uploadedImageSrc ? (
<div
className="platform-modal-backdrop fixed inset-0 z-[82] flex items-center justify-center px-4 py-6"
onClick={() => setIsMainImagePreviewOpen(false)}
>
<div
role="dialog"
aria-modal="true"
aria-labelledby="creative-image-main-preview-title"
className="platform-modal-shell platform-remap-surface w-full max-w-4xl rounded-[1.35rem] p-3 shadow-[0_24px_70px_rgba(15,23,42,0.22)]"
onClick={(event) => event.stopPropagation()}
>
<div className="mb-3 flex items-center justify-between gap-3 px-1">
<div
id="creative-image-main-preview-title"
className="min-w-0 truncate text-sm font-black text-[var(--platform-text-strong)]"
>
{labels.previewMainImage ?? uploadedImageAlt}
</div>
<button
type="button"
aria-label={
labels.closeMainImagePreview ?? labels.closePromptReferencePreview
}
onClick={() => setIsMainImagePreviewOpen(false)}
className="platform-profile-icon-button flex h-8 w-8 shrink-0 items-center justify-center rounded-full"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="max-h-[82vh] overflow-hidden rounded-[1rem] bg-black/5">
<ResolvedAssetImage
src={uploadedImageSrc}
refreshKey={uploadedImageRefreshKey}
alt={uploadedImageAlt}
className="h-full max-h-[82vh] w-full object-contain"
/>
</div>
</div>
</div>
) : null}
{isRemoveImageConfirmOpen ? (
<div className="platform-modal-backdrop fixed inset-0 z-[80] flex items-center justify-center px-4 py-6">
<div