web: undo/redo, a clean preview, and a histogram that stays put
Four things the studio owed the visitor: - UNDO/REDO in the header, so a look can be taken back and put back without reloading the photo; a fresh edit clears the redo trail. - Opening a saved frame, or picking a look out of its history, now drops the stale preview buffer instead of leaving the previous render on the stage. - The picked history look is marked in the accent, so it is plain which look the photo is wearing. - The histogram is re-clamped against the photo box on resize, so opening a chip column no longer pushes the overlay past the edge of the canvas. The 'NEW SAVES: FILM STRIP' chip goes: a save already lands in the strip.
This commit is contained in:
@@ -108,10 +108,19 @@ export function Histogram({ url, onClose }: { url: string; onClose: () => void }
|
||||
setPos({ x: Math.max(8, b.width - box.getBoundingClientRect().width - 12), y: 12 });
|
||||
}, [pos]);
|
||||
|
||||
// The photo's own box is what the frame is parked against, and it shrinks
|
||||
// when a column opens beside the stage — not only when the window does. So
|
||||
// the frame watches that box, not the window, and is pulled back inside it.
|
||||
useEffect(() => {
|
||||
const onResize = () => setPos((p) => (p ? clamp(p.x, p.y) : p));
|
||||
window.addEventListener('resize', onResize);
|
||||
return () => window.removeEventListener('resize', onResize);
|
||||
const wrap = boxRef.current?.parentElement;
|
||||
const ro = wrap ? new ResizeObserver(onResize) : null;
|
||||
ro?.observe(wrap as Element);
|
||||
return () => {
|
||||
window.removeEventListener('resize', onResize);
|
||||
ro?.disconnect();
|
||||
};
|
||||
}, [clamp]);
|
||||
|
||||
const start = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
|
||||
@@ -12,7 +12,9 @@ export function TopBar({
|
||||
exporting,
|
||||
savingPhoto,
|
||||
canUndo,
|
||||
canRedo,
|
||||
onUndo,
|
||||
onRedo,
|
||||
onReset,
|
||||
onSavePhoto,
|
||||
onExport,
|
||||
@@ -27,7 +29,9 @@ export function TopBar({
|
||||
exporting: boolean;
|
||||
savingPhoto: boolean;
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
onUndo: () => void;
|
||||
onRedo: () => void;
|
||||
onReset: () => void;
|
||||
onSavePhoto: () => void;
|
||||
onExport: () => void;
|
||||
@@ -63,7 +67,30 @@ export function TopBar({
|
||||
<span className="recipe-name">{recipeName}</span>
|
||||
<span className="spacer" />
|
||||
|
||||
<button type="button" className="btn ghost" disabled={!canUndo} onClick={onUndo}>{t('act.undo')}</button>
|
||||
{/* Both arrows are one icon wide, so the pair reads as one control: the
|
||||
counter-clockwise arrow steps back, the clockwise one forward. */}
|
||||
<button
|
||||
type="button"
|
||||
className="btn ghost icon"
|
||||
data-key="undo"
|
||||
disabled={!canUndo}
|
||||
title={t('act.undo')}
|
||||
aria-label={t('act.undo')}
|
||||
onClick={onUndo}
|
||||
>
|
||||
↺
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn ghost icon"
|
||||
data-key="redo"
|
||||
disabled={!canRedo}
|
||||
title={t('act.redo')}
|
||||
aria-label={t('act.redo')}
|
||||
onClick={onRedo}
|
||||
>
|
||||
↻
|
||||
</button>
|
||||
<button type="button" className="btn ghost" onClick={onReset}>{t('act.reset')}</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user