fix(ui): polish editor layouts and confirm scene delete
Stretch key action buttons, center empty audio/image pickers, and require confirmation before deleting a scene. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+175
-121
@@ -2380,68 +2380,73 @@ function CampaignInspector({
|
||||
<div className={styles.dropHintOverlay}>{t('drop.hintAudio')}</div>
|
||||
) : null}
|
||||
{mediaAssets.filter((a) => a.type === 'audio').length === 0 ? (
|
||||
<div className={[styles.muted, styles.spanSm].join(' ')}>{t('campaign.noFiles')}</div>
|
||||
) : (
|
||||
<div className={styles.audioList}>
|
||||
{mediaAssets
|
||||
.filter((a) => a.type === 'audio')
|
||||
.map((a) => (
|
||||
<div key={a.id} className={styles.audioRow}>
|
||||
<span className={styles.audioName}>{a.originalName}</span>
|
||||
<span className={styles.audioControls}>
|
||||
<label className={styles.checkboxLabelSm}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={audioById.get(a.id)?.autoplay ?? false}
|
||||
onChange={(e) => {
|
||||
const next = audioRefs.map((x) =>
|
||||
x.assetId === a.id ? { ...x, autoplay: e.target.checked } : x,
|
||||
);
|
||||
onAudioRefsChange(next);
|
||||
}}
|
||||
/>
|
||||
<span className={styles.spanXs}>{t('campaign.auto')}</span>
|
||||
</label>
|
||||
<label className={styles.checkboxLabelSm}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={audioById.get(a.id)?.loop ?? false}
|
||||
onChange={(e) => {
|
||||
const next = audioRefs.map((x) =>
|
||||
x.assetId === a.id ? { ...x, loop: e.target.checked } : x,
|
||||
);
|
||||
onAudioRefsChange(next);
|
||||
}}
|
||||
/>
|
||||
<span className={styles.spanXs}>{t('campaign.loop')}</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
title={t('campaign.removeTitle')}
|
||||
className={styles.audioRemove}
|
||||
onClick={() => {
|
||||
onAudioRefsChange(audioRefs.filter((x) => x.assetId !== a.id));
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className={styles.audioRemoveIcon}
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
height={16}
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M9 3h6a1 1 0 0 1 1 1v1h4v2H4V5h4V4a1 1 0 0 1 1-1zm1 5h2v9h-2V8zm4 0h2v9h-2V8zM7 8h2v9H7V8zm9-3H8v1h8V5zM6 21a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8H6v13z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className={styles.audioDropEmpty}>
|
||||
<div className={[styles.muted, styles.spanSm].join(' ')}>{t('campaign.noFiles')}</div>
|
||||
<Button onClick={onUploadAudio}>{t('campaign.upload')}</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.audioList}>
|
||||
{mediaAssets
|
||||
.filter((a) => a.type === 'audio')
|
||||
.map((a) => (
|
||||
<div key={a.id} className={styles.audioRow}>
|
||||
<span className={styles.audioName}>{a.originalName}</span>
|
||||
<span className={styles.audioControls}>
|
||||
<label className={styles.checkboxLabelSm}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={audioById.get(a.id)?.autoplay ?? false}
|
||||
onChange={(e) => {
|
||||
const next = audioRefs.map((x) =>
|
||||
x.assetId === a.id ? { ...x, autoplay: e.target.checked } : x,
|
||||
);
|
||||
onAudioRefsChange(next);
|
||||
}}
|
||||
/>
|
||||
<span className={styles.spanXs}>{t('campaign.auto')}</span>
|
||||
</label>
|
||||
<label className={styles.checkboxLabelSm}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={audioById.get(a.id)?.loop ?? false}
|
||||
onChange={(e) => {
|
||||
const next = audioRefs.map((x) =>
|
||||
x.assetId === a.id ? { ...x, loop: e.target.checked } : x,
|
||||
);
|
||||
onAudioRefsChange(next);
|
||||
}}
|
||||
/>
|
||||
<span className={styles.spanXs}>{t('campaign.loop')}</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
title={t('campaign.removeTitle')}
|
||||
className={styles.audioRemove}
|
||||
onClick={() => {
|
||||
onAudioRefsChange(audioRefs.filter((x) => x.assetId !== a.id));
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className={styles.audioRemoveIcon}
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
height={16}
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M9 3h6a1 1 0 0 1 1 1v1h4v2H4V5h4V4a1 1 0 0 1 1-1zm1 5h2v9h-2V8zm4 0h2v9h-2V8zM7 8h2v9H7V8zm9-3H8v1h8V5zM6 21a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8H6v13z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button onClick={onUploadAudio}>{t('campaign.upload')}</Button>
|
||||
</>
|
||||
)}
|
||||
<Button onClick={onUploadAudio}>{t('campaign.upload')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -2653,68 +2658,73 @@ function SceneInspector({
|
||||
<div className={styles.dropHintOverlay}>{t('drop.hintAudio')}</div>
|
||||
) : null}
|
||||
{mediaAssets.filter((a) => a.type === 'audio').length === 0 ? (
|
||||
<div className={[styles.muted, styles.spanSm].join(' ')}>{t('campaign.noFiles')}</div>
|
||||
) : (
|
||||
<div className={styles.audioList}>
|
||||
{mediaAssets
|
||||
.filter((a) => a.type === 'audio')
|
||||
.map((a) => (
|
||||
<div key={a.id} className={styles.audioRow}>
|
||||
<span className={styles.audioName}>{a.originalName}</span>
|
||||
<span className={styles.audioControls}>
|
||||
<label className={styles.checkboxLabelSm}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={audioById.get(a.id)?.autoplay ?? false}
|
||||
onChange={(e) => {
|
||||
const next = audioRefs.map((x) =>
|
||||
x.assetId === a.id ? { ...x, autoplay: e.target.checked } : x,
|
||||
);
|
||||
onAudioRefsChange(next);
|
||||
}}
|
||||
/>
|
||||
<span className={styles.spanXs}>{t('campaign.auto')}</span>
|
||||
</label>
|
||||
<label className={styles.checkboxLabelSm}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={audioById.get(a.id)?.loop ?? false}
|
||||
onChange={(e) => {
|
||||
const next = audioRefs.map((x) =>
|
||||
x.assetId === a.id ? { ...x, loop: e.target.checked } : x,
|
||||
);
|
||||
onAudioRefsChange(next);
|
||||
}}
|
||||
/>
|
||||
<span className={styles.spanXs}>{t('campaign.loop')}</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
title={t('scene.removeTitle')}
|
||||
className={styles.audioRemove}
|
||||
onClick={() => {
|
||||
onAudioRefsChange(audioRefs.filter((x) => x.assetId !== a.id));
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className={styles.audioRemoveIcon}
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
height={16}
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M9 3h6a1 1 0 0 1 1 1v1h4v2H4V5h4V4a1 1 0 0 1 1-1zm1 5h2v9h-2V8zm4 0h2v9h-2V8zM7 8h2v9H7V8zm9-3H8v1h8V5zM6 21a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8H6v13z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className={styles.audioDropEmpty}>
|
||||
<div className={[styles.muted, styles.spanSm].join(' ')}>{t('campaign.noFiles')}</div>
|
||||
<Button onClick={onUploadMedia}>{t('campaign.upload')}</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.audioList}>
|
||||
{mediaAssets
|
||||
.filter((a) => a.type === 'audio')
|
||||
.map((a) => (
|
||||
<div key={a.id} className={styles.audioRow}>
|
||||
<span className={styles.audioName}>{a.originalName}</span>
|
||||
<span className={styles.audioControls}>
|
||||
<label className={styles.checkboxLabelSm}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={audioById.get(a.id)?.autoplay ?? false}
|
||||
onChange={(e) => {
|
||||
const next = audioRefs.map((x) =>
|
||||
x.assetId === a.id ? { ...x, autoplay: e.target.checked } : x,
|
||||
);
|
||||
onAudioRefsChange(next);
|
||||
}}
|
||||
/>
|
||||
<span className={styles.spanXs}>{t('campaign.auto')}</span>
|
||||
</label>
|
||||
<label className={styles.checkboxLabelSm}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={audioById.get(a.id)?.loop ?? false}
|
||||
onChange={(e) => {
|
||||
const next = audioRefs.map((x) =>
|
||||
x.assetId === a.id ? { ...x, loop: e.target.checked } : x,
|
||||
);
|
||||
onAudioRefsChange(next);
|
||||
}}
|
||||
/>
|
||||
<span className={styles.spanXs}>{t('campaign.loop')}</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
title={t('scene.removeTitle')}
|
||||
className={styles.audioRemove}
|
||||
onClick={() => {
|
||||
onAudioRefsChange(audioRefs.filter((x) => x.assetId !== a.id));
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className={styles.audioRemoveIcon}
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
height={16}
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M9 3h6a1 1 0 0 1 1 1v1h4v2H4V5h4V4a1 1 0 0 1 1-1zm1 5h2v9h-2V8zm4 0h2v9h-2V8zM7 8h2v9H7V8zm9-3H8v1h8V5zM6 21a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8H6v13z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button onClick={onUploadMedia}>{t('campaign.upload')}</Button>
|
||||
</>
|
||||
)}
|
||||
<Button onClick={onUploadMedia}>{t('campaign.upload')}</Button>
|
||||
</div>
|
||||
<div className={styles.spacer6} />
|
||||
<div className={styles.labelSm}>{t('scene.branching')}</div>
|
||||
@@ -2754,6 +2764,7 @@ function SceneListCard({
|
||||
const thumbUrl = useAssetUrl(scene.previewThumbAssetId);
|
||||
const previewUrl = useAssetUrl(scene.previewAssetId);
|
||||
const [menu, setMenu] = useState<{ x: number; y: number } | null>(null);
|
||||
const [pendingDelete, setPendingDelete] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!menu) return;
|
||||
@@ -2909,8 +2920,8 @@ function SceneListCard({
|
||||
role="menuitem"
|
||||
className={styles.sceneCtxDanger}
|
||||
onClick={() => {
|
||||
onDeleteScene(scene.id);
|
||||
setMenu(null);
|
||||
setPendingDelete(true);
|
||||
}}
|
||||
>
|
||||
{t('common.delete')}
|
||||
@@ -2920,6 +2931,49 @@ function SceneListCard({
|
||||
document.body,
|
||||
)
|
||||
: null}
|
||||
{pendingDelete
|
||||
? createPortal(
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('common.close')}
|
||||
className={styles.modalBackdrop}
|
||||
onClick={() => setPendingDelete(false)}
|
||||
/>
|
||||
<div role="dialog" aria-modal="true" className={styles.modalDialog}>
|
||||
<div className={styles.modalHeader}>
|
||||
<div className={styles.modalTitle}>{t('confirmDeleteScene.title')}</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('common.close')}
|
||||
className={styles.modalClose}
|
||||
onClick={() => setPendingDelete(false)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.fieldGrid}>
|
||||
<div className={styles.muted}>
|
||||
{t('confirmDeleteScene.body', { name: scene.title })}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.modalFooter}>
|
||||
<Button onClick={() => setPendingDelete(false)}>{t('common.cancel')}</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setPendingDelete(false);
|
||||
onDeleteScene(scene.id);
|
||||
}}
|
||||
>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>,
|
||||
document.body,
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user