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:
2026-09-18 15:47:08 +07:00
parent 78fe2b097b
commit abaa980f93
4 changed files with 167 additions and 39 deletions
+10 -1
View File
@@ -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>) => {
+28 -1
View File
@@ -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"