feat(ui): UI redesign — top bar, sliders, timeline, export modal, compare modes#19
feat(ui): UI redesign — top bar, sliders, timeline, export modal, compare modes#19RichardBray wants to merge 27 commits intomainfrom
Conversation
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>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
| 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, | ||
| }; |
There was a problem hiding this comment.
what is the benefit of writing the css here instead of using Tailwind?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
what does this component do?
There was a problem hiding this comment.
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)" }} |
There was a problem hiding this comment.
why are inline styles used here?
There was a problem hiding this comment.
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)" }} |
There was a problem hiding this comment.
why are inlines styles used here and where else are they used? why wasn't tailwind used?
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
would it make sense to pull this from the package.json instead of hardcoding it?
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
should svg icons be their own component?
There was a problem hiding this comment.
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.
| setCanvasRect({ left: r.left, top: r.top, width: r.width, height: r.height }); | ||
| } | ||
| update(); | ||
| const ro = new ResizeObserver(update); |
There was a problem hiding this comment.
what is the resize observer used here for?
There was a problem hiding this comment.
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.
| 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(); |
There was a problem hiding this comment.
put this in a named function, or do you think it is best here?
There was a problem hiding this comment.
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.
…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>
…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>
Summary
encoderSettingstorunGpuExport.Test Plan
🤖 Generated with Claude Code