Skip to content

feat(ui): UI redesign — top bar, sliders, timeline, export modal, compare modes#19

Open
RichardBray wants to merge 27 commits intomainfrom
feat/ui-redesign
Open

feat(ui): UI redesign — top bar, sliders, timeline, export modal, compare modes#19
RichardBray wants to merge 27 commits intomainfrom
feat/ui-redesign

Conversation

@RichardBray
Copy link
Copy Markdown
Member

@RichardBray RichardBray commented Apr 20, 2026

Summary

  • Top bar restructure (filename left, Save/SaveAsNew/Export right), SaveBar with "Saved ✓" clean state, thin-line sliders, CSS token scale for radius/padding.
  • Adaptive timeline ruler + transport row with precise HH:MM:SS:FF timecode.
  • Expanded LookInfoModal (chips for keywords/characteristics); new ExportModal with codec (H.264/H.265/ProRes 422), quality→CRF, and output path; server wires through encoderSettings to runGpuExport.
  • View mode toolbar with split-compare and reference-compare overlays using CSS clip-path (WebGPU renderer unchanged).

Test Plan

  • New tests pass: SaveBar, timelineTicks, ExportModal
  • Load video → top bar shows filename/Save/Saved ✓/Export; sliders are hairline; timeline shows adaptive ruler + centered timecode
  • Export modal → H.264/H.265/ProRes switches extension; Quality disables for ProRes; export completes
  • Split mode → divider drag syncs original/processed; Reference mode → upload letterboxed image
  • Right-click look → Info modal shows description + chip metadata

🤖 Generated with Claude Code

Comment on lines +47 to +67
const overlayStyle: React.CSSProperties = {
position: "absolute",
left: canvasRect.left,
top: canvasRect.top,
width: canvasRect.width,
height: canvasRect.height,
clipPath: `inset(0 0 0 ${position * 100}%)`,
pointerEvents: "none",
objectFit: mode === "reference" ? "contain" : "fill",
background: mode === "reference" ? "#000" : "transparent",
zIndex: 15,
};

const dividerStyle: React.CSSProperties = {
position: "absolute",
left: canvasRect.left + canvasRect.width * position,
top: canvasRect.top,
height: canvasRect.height,
transform: "translateX(-1px)",
zIndex: 20,
};
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the benefit of writing the css here instead of using Tailwind?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These styles depend on runtime values from canvasRect (left/top/width/height from getBoundingClientRect), so they can not be static Tailwind classes. Inline style is the straightforward way to apply dynamic pixel values.

canvasRect: { left: number; top: number; width: number; height: number } | null;
}

export function CompareOverlay({ mode, position, onPositionChange, overlaySrc, isVideo, videoRef, canvasRect }: Props) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does this component do?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It renders the A/B compare overlay used by the two compare view modes: in split mode it draws the shader-processed video on top of the original and clips it with a draggable vertical divider; in reference mode it overlays a user-uploaded reference image the same way. It also keeps a second <video> element time-synced with the main preview so the split stays in step during playback.

<div
className="bg-zinc-800 border border-zinc-700 rounded-xl p-5 max-w-sm w-full mx-4 shadow-2xl"
className="bg-zinc-800 border border-zinc-700 max-w-sm w-full mx-4 shadow-2xl"
style={{ borderRadius: "var(--radius-md)", padding: "var(--pad-modal)" }}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are inline styles used here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These inline styles are just applying shared design tokens defined as CSS custom properties (--radius-md, --pad-modal, --pad-btn, etc.) so all modals stay visually consistent. Tailwind v4 can reference vars via arbitrary values (rounded-[var(--radius-md)]), but inline style={{ borderRadius: "var(--radius-md)" }} reads a bit cleaner for token usage. Happy to switch to the arbitrary-value form if you prefer a single styling approach.

<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50" onClick={onCancel}>
<div
className="bg-zinc-800 border border-zinc-700 max-w-md w-full mx-4 shadow-2xl"
style={{ borderRadius: "var(--radius-md)", padding: "var(--pad-modal)" }}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are inlines styles used here and where else are they used? why wasn't tailwind used?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same reason as the DeleteLookModal thread: these are pulling shared design tokens from CSS custom properties (--radius-md, --radius-sm, --pad-modal) so spacing/radius is centralized. The same pattern appears in the other modals (DeleteLookModal, NewLookModal, LookInfoModal) and a few spots in App.tsx. Let me know if you want them converted to rounded-[var(--radius-md)]-style Tailwind arbitrary values — happy to normalize.


<div className="flex items-center justify-between px-3 py-2 border-t border-zinc-800 text-[11px] text-zinc-500">
<span>Hance</span>
<span>v0.2.0</span>
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to pull this from the package.json instead of hardcoding it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — done. Now reads pkg.version from the root package.json instead of the hardcoded string.

aria-label={playing ? "Pause" : "Play"}
>
{playing ? (
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should svg icons be their own component?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair — could extract them into an Icons.tsx entry (we already have ImportIcon/NoLookIcon/PlusIcon there). Left inline here because play/pause are only used by the transport and toggling between them is tightly coupled to the playing state in this component. Happy to lift them out if you would rather keep all SVGs centralized.

Comment thread packages/ui/app/App.tsx Outdated
setCanvasRect({ left: r.left, top: r.top, width: r.width, height: r.height });
}
update();
const ro = new ResizeObserver(update);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the resize observer used here for?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The preview canvas can change size/position (window resize, sidebar changes, the app layout reflowing). The overlay components (CompareOverlay, reference-image picker, etc.) are absolutely positioned against the viewport, so they need the canvass current bounding rect to stay aligned. ResizeObserver + scroll/resize listeners keep canvasRect` in sync with those changes.

Comment thread packages/ui/app/App.tsx Outdated
Comment on lines +483 to +489
const i = document.createElement("input");
i.type = "file"; i.accept = "image/*";
i.onchange = () => {
const f = i.files?.[0]; if (!f) return;
setReferenceImage(URL.createObjectURL(f));
};
i.click();
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

put this in a named function, or do you think it is best here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair — inline onClick is a bit dense. Could pull it into a named handleChooseReferenceImage handler. Left it inline because it is only referenced from this one button and the logic is short (open a file picker and set state), but I am happy to extract it if you would prefer.

Richard Oliver Bray and others added 5 commits April 20, 2026 12:20
…ments

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace inline style={{ borderRadius/padding }} CSS var references with
Tailwind utility classes (rounded-sm, p-btn, p-modal, etc.) and move
padding definitions into a @layer components block. Also wire sidebar
version label to package.json and extract reference image picker into a
named function.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Richard Oliver Bray and others added 5 commits April 21, 2026 15:33
…nvas API

- Remove /api/look-thumbnail endpoint and server-side FFmpeg thumbnail pipeline
- Add client-side lookThumbnails module using Canvas for look previews
- Replace reference.jpg with smaller reference.webp asset
- Add loading spinner fallback for missing thumbnails
- Copy assets directory in UI build script
- Preserve existing thumbnail URLs across look list refreshes
… artifacts

Restrict /api/export outputName to a basename matching [A-Za-z0-9._-]+\.(mp4|mov),
closing a path-traversal gap where names containing `..` or NUL bytes could escape
the export tempdir. Also remove committed .superpowers/brainstorm tool output and
ignore the directory going forward.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant