Skip to content

Fix responsive layout: desktop scroll bug, mobile flow, touch targets - #11

Open
rowkav09 wants to merge 6 commits into
mainfrom
responsive-layout-overhaul
Open

Fix responsive layout: desktop scroll bug, mobile flow, touch targets#11
rowkav09 wants to merge 6 commits into
mainfrom
responsive-layout-overhaul

Conversation

@rowkav09

@rowkav09 rowkav09 commented Aug 11, 2026

Copy link
Copy Markdown
Owner

What was broken

  • Desktop: the three-column grid used align-items: stretch, so the preview panel was forced to match the tallest sidebar's height (~2100px). At 1440x900 the preview panel was 2178px tall while the actual rendered content was 483px, centered in ~800px of empty black space, and the whole page required scrolling to ~2350px on a 900px viewport.
  • Laptop (1024-1279px): the grid only split into columns at xl (1280px), so a common size like 1024x768 got one full-width column with a two-button toggle stretched to ~970px.
  • Touch targets: every button measured 38px tall (below the ~44px guideline) and range sliders had a 6px hit box.
  • Mobile: the page stacked every desktop panel in document order with the preview shown before the upload control, and buried Export under ~2150px of renderer/image/background/colour sliders.
  • No way to remove an uploaded image (only "reset settings", which didn't touch the image).
  • text-slate-500 labels measured ~4.4:1 contrast against the black background, just under WCAG AA's 4.5:1 for small text.
  • The dither-compare modal didn't manage focus at all (nowhere on open, nowhere on close, Tab could escape behind the overlay).

No horizontal page overflow was found anywhere in the original code, at any viewport - that part already worked and wasn't touched beyond preserving it.

What changed

  • Desktop/laptop (lg: and up, 1024px+) is now a fixed-height workspace: header + a 100dvh-bounded row where settings, preview, and export each scroll independently instead of the whole document stretching. Sidebar width is clamp(240px, 22vw, 300px) instead of a fixed 292px only above 1280px.
  • Mobile (below 1024px) now follows Upload → Adjust → Inspect → Export in document order (previously CSS order put the preview before the upload control). The bulkiest control clusters ("Image controls", "Background") are grouped into closed-by-default <details> sections, cutting total mobile page height by roughly a quarter and moving Export right after the preview instead of the very bottom.
  • Buttons, selects, and colour swatches raised to min-h-11 (44px); slider tracks widened from 6-8px to 24px. Toggle switches and slider track thickness are a deliberate exception (see commit message) rather than forced to exactly 44px.
  • Added a "Remove" button next to "Browse" to clear a loaded image.
  • text-slate-500text-slate-400 (~8:1 contrast) for all labels/status text.
  • Dither-compare modal now focuses its Close button on open, cycles Tab within itself, and returns focus to the trigger button on close.
  • Fixed a latent type hole (updateAdjustment was typed for number only, so the one string-valued setting relied on an as any cast) - made it generic per-key.
  • Removed a stray tracked dev_server.log.

Testing

  • npm run lint, npm run typecheck, npm test (22 tests), npm run build all pass.
  • Measured (not eyeballed) zero horizontal overflow and zero unintended page scroll at 1440x900, 1024x768, 768x1024, 390x844, 360x800, a very short 1440x400 window, and zoom-equivalent 1152x720 / 960x600 / 720x450.
  • Verified portrait (400x1600), landscape, and transparent test images: aspect ratio preserved, no distortion, and a 202-row portrait render correctly scrolls inside its own preview panel without expanding the page.
  • Verified keyboard shortcuts (R/C/U) still fire on the page and are still suppressed while a form control is focused; verified modal focus behavior directly.
  • Checked browser console/network: only the expected 503 from /api/uses (no local Upstash Redis configured, which is documented existing behavior).

Known limitations

Summary by CodeRabbit

  • UI Improvements
    • Improved the layout for desktop screens and made controls easier to use with larger touch targets.
    • Refined control styling and added collapsible image settings.
  • Accessibility
    • Improved keyboard navigation and focus handling in the image comparison dialog.
    • Focus now returns to the control that opened the dialog after it closes.
  • Bug Fixes
    • Removing an image now reliably clears related preview and comparison states.

dev_server.log was a stray output file committed by accident; it has
no purpose in version control.
The three-column grid used the default align-items: stretch, so the
preview panel was forced to match the tallest sidebar's natural height
(~2100px), leaving roughly 800px of empty black space around a much
smaller rendered image and requiring the whole page to scroll well
past the viewport on ordinary desktop sizes.

Give the workspace a fixed viewport height on lg+ screens and let each
panel (settings, preview, export) scroll independently instead. This
also replaces the xl-only (1280px) column breakpoint with a lg one
(1024px) using clamp()-based sidebar widths, so common laptop sizes
like 1024x768 get a real multi-column layout instead of one full-width
stacked column.
On phones the page previously stacked every desktop panel in document
order with the preview shown before the upload control, and buried
Export beneath ~2150px of renderer/image/background/colour controls
(12+ range sliders with no grouping). That made Export - the action
used every single time - the hardest thing to reach.

- Drop the CSS order hack that showed the preview before the upload
  dropzone; content now follows the natural Upload -> Adjust -> Inspect
  -> Export order at every breakpoint.
- Group "Image controls" and "Background" behind a collapsible
  <details> section (closed by default), since they are the least
  frequently touched controls and by far the tallest. This cuts total
  mobile page height by roughly a quarter and puts Export right after
  the preview instead of at the very bottom.
- Fix a latent type hole this surfaced: updateAdjustment took `value:
  number` for every ImageAdjustments key, so the one non-numeric key
  (fitMode) was silently passed through an `as any` cast. Made it
  generic over the key so the value type is checked per-field.
Every button measured 38px tall and range sliders had only a 6px hit
box - both below the ~44px minimum recommended for touch. Applied
min-h-11 across buttons and selects, widened range slider tracks
(h-1.5/h-2 to h-6), and enlarged colour swatches (32px to 40px tall).

Toggle switches (Invert, Foreground/background separation) and the
range slider track height are pragmatic exceptions: sized up from
their originals but kept below 44px, since forcing a switch or a
fine-adjustment slider to a full 44px square would look oversized
next to their labels without meaningfully improving usability - the
switch and slider thumb, not the track, is what gets touched.
The only prior way to clear a loaded image was to reload the page -
"Reset image settings" only reset sliders, not the image itself. Add
a Remove button next to Browse (shown once an image is loaded) that
revokes the object URL and returns the preview to its empty state.
- text-slate-500 on the app's black background measures ~4.4:1, just
  under the 4.5:1 WCAG AA threshold for the small (10-12px) label and
  status text it was used for everywhere. Move to text-slate-400
  (~8:1) throughout.
- The dither-compare modal took focus nowhere on open and released it
  nowhere on close, and Tab could escape onto controls hidden behind
  the overlay. Focus the Close button on open, cycle Tab within the
  dialog while it's open, and return focus to the triggering button
  on close.
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
halftone Ready Ready Preview Aug 11, 2026 10:57am

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a96f5919-4cc4-4770-9e72-443515978a54

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The editor now provides responsive desktop layouts, larger touch targets, collapsible image settings, image removal cleanup, stronger adjustment typing, and keyboard focus management for the comparison dialog. Development server logs are excluded from version control.

Changes

Image editor UI

Layer / File(s) Summary
Image lifecycle and adjustment state
src/app/page.tsx
Image removal revokes object URLs, clears image and generated-art state, hides comparisons, and updates status. updateAdjustment now preserves key-specific value types.
Responsive editor layout and controls
src/app/page.tsx
The page uses responsive desktop panels, larger controls, collapsible image settings, and an image Remove action.
Comparison dialog focus management
src/app/page.tsx
The dialog focuses its close button, traps Tab focus, and restores focus to its trigger when closed.

Development log exclusion

Layer / File(s) Summary
Ignore development server log
.gitignore
Adds dev_server.log to the ignore rules.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ComparisonTrigger
  participant ComparisonDialog
  participant CloseButton
  ComparisonTrigger->>ComparisonDialog: Open comparison dialog
  ComparisonDialog->>CloseButton: Focus close button
  ComparisonDialog->>ComparisonDialog: Trap Tab focus
  CloseButton->>ComparisonDialog: Close dialog
  ComparisonDialog->>ComparisonTrigger: Restore focus
Loading

Possibly related PRs

  • rowkav09/halftone#9: Shares the comparison modal, image state removal, and adjustment controls.

Suggested labels: bug, enhancement

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main responsive layout changes, including desktop scrolling, mobile flow, and larger touch targets.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@rowkav09
rowkav09 marked this pull request as ready for review August 11, 2026 12:00
@coderabbitai coderabbitai Bot added bug Something isn't working enhancement New feature or request labels Aug 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/page.tsx`:
- Around line 306-317: Update the page-level keyboard shortcut handler near the
showDitherCompare dialog logic to return immediately whenever showDitherCompare
is true, before processing the R, C, or U shortcuts. Gate that handler directly
rather than relying on propagation control from the dialog’s later window
listener, while preserving shortcut behavior when the dialog is closed.
- Line 754: Reorder the grid children around the main layout container so the
mobile stacking order is Upload, all adjustment controls (Renderer and Image),
preview/Inspect, then Export. Update the aside section identified around the
affected grid items, preserving the existing desktop column placement and
responsive behavior.
- Line 308: Update the selector string in the dialog focusable-elements query to
use double quotes, while retaining single quotes around the nested tabindex
attribute value; leave the selector contents and querySelectorAll call
unchanged.
- Around line 439-450: Update removeImage to also increment renderRequestRef and
clear isRendering during image cleanup, invalidating any pending renderArt
request so it cannot restore removed art or state.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e97b0ab5-795e-4ab4-bd03-dde01002211a

📥 Commits

Reviewing files that changed from the base of the PR and between d8892b3 and d2706d7.

⛔ Files ignored due to path filters (1)
  • dev_server.log is excluded by !**/*.log
📒 Files selected for processing (2)
  • .gitignore
  • src/app/page.tsx

Comment thread src/app/page.tsx
Comment on lines +306 to 317
if (event.key === "Escape") { onClose(); return; }
if (event.key !== "Tab") return;
const focusable = dialogRef.current?.querySelectorAll<HTMLElement>('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (!focusable?.length) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); }
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [onClose]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Prevent page shortcuts while the dialog is open.

The page listener that starts at Line 707 registers R, C, and U shortcuts before this modal listener. Those shortcuts still run when focus is in the modal. R can reset settings and U can open the upload picker behind an aria-modal dialog.

Gate the page shortcut handler on showDitherCompare. Do not rely on propagation control in this later window listener.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/page.tsx` around lines 306 - 317, Update the page-level keyboard
shortcut handler near the showDitherCompare dialog logic to return immediately
whenever showDitherCompare is true, before processing the R, C, or U shortcuts.
Gate that handler directly rather than relying on propagation control from the
dialog’s later window listener, while preserving shortcut behavior when the
dialog is closed.

Comment thread src/app/page.tsx
if (event.key === "Escape") onClose();
if (event.key === "Escape") { onClose(); return; }
if (event.key !== "Tab") return;
const focusable = dialogRef.current?.querySelectorAll<HTMLElement>('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use a double-quoted selector string.

Line 308 uses a single-quoted string. Use a double-quoted string and single quotes for the nested attribute value.

Proposed fix
-      const focusable = dialogRef.current?.querySelectorAll<HTMLElement>('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
+      const focusable = dialogRef.current?.querySelectorAll<HTMLElement>("button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])");

As per coding guidelines: Use two-space indentation, semicolons, double-quoted strings, and Tailwind utility classes in TSX.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const focusable = dialogRef.current?.querySelectorAll<HTMLElement>('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
const focusable = dialogRef.current?.querySelectorAll<HTMLElement>("button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/page.tsx` at line 308, Update the selector string in the dialog
focusable-elements query to use double quotes, while retaining single quotes
around the nested tabindex attribute value; leave the selector contents and
querySelectorAll call unchanged.

Source: Coding guidelines

Comment thread src/app/page.tsx
Comment on lines +439 to +450
const removeImage = useCallback(() => {
loadRequestRef.current += 1;
if (fileUrlRef.current) URL.revokeObjectURL(fileUrlRef.current);
fileUrlRef.current = null;
imageRef.current = null;
setLoadedImage(null);
setImageUrl(null);
setImageReady(false);
setGeneratedArt(null);
setShowComparison(false);
setStatus("Image removed. Choose another image or switch to text.");
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Invalidate pending renders when removing an image.

removeImage invalidates only loadRequestRef. If renderArt is awaiting generateArtFromImage, its request remains current and it can restore removed art, status, and rendering state after this cleanup.

Increment renderRequestRef and clear isRendering when removing the image.

Proposed fix
 const removeImage = useCallback(() => {
   loadRequestRef.current += 1;
+  renderRequestRef.current += 1;
   if (fileUrlRef.current) URL.revokeObjectURL(fileUrlRef.current);
   fileUrlRef.current = null;
   imageRef.current = null;
   setLoadedImage(null);
   setImageUrl(null);
   setImageReady(false);
+  setIsRendering(false);
   setGeneratedArt(null);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const removeImage = useCallback(() => {
loadRequestRef.current += 1;
if (fileUrlRef.current) URL.revokeObjectURL(fileUrlRef.current);
fileUrlRef.current = null;
imageRef.current = null;
setLoadedImage(null);
setImageUrl(null);
setImageReady(false);
setGeneratedArt(null);
setShowComparison(false);
setStatus("Image removed. Choose another image or switch to text.");
}, []);
const removeImage = useCallback(() => {
loadRequestRef.current += 1;
renderRequestRef.current += 1;
if (fileUrlRef.current) URL.revokeObjectURL(fileUrlRef.current);
fileUrlRef.current = null;
imageRef.current = null;
setLoadedImage(null);
setImageUrl(null);
setImageReady(false);
setIsRendering(false);
setGeneratedArt(null);
setShowComparison(false);
setStatus("Image removed. Choose another image or switch to text.");
}, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/page.tsx` around lines 439 - 450, Update removeImage to also
increment renderRequestRef and clear isRendering during image cleanup,
invalidating any pending renderArt request so it cannot restore removed art or
state.

Comment thread src/app/page.tsx

<div className="grid flex-1 gap-4 xl:grid-cols-[292px_minmax(0,1fr)_292px]">
<section className="order-2 space-y-3 rounded-md border border-white/10 bg-white/[0.03] p-3 backdrop-blur-sm xl:order-1">
<div className="grid flex-1 gap-4 lg:min-h-0 lg:grid-cols-[clamp(240px,22vw,300px)_minmax(0,1fr)_clamp(240px,22vw,300px)]">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Restore the required mobile workflow order.

Below the lg breakpoint, the grid stacks the left section, preview, and then this aside. This aside renders Export before Renderer and Image controls. Mobile users therefore reach Inspect and Export before all adjustments.

Reorganize the mobile layout so all adjustment controls precede the preview and Export follows it. This conflicts with the stated Upload → Adjust → Inspect → Export workflow.

Also applies to: 783-794

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/page.tsx` at line 754, Reorder the grid children around the main
layout container so the mobile stacking order is Upload, all adjustment controls
(Renderer and Image), preview/Inspect, then Export. Update the aside section
identified around the affected grid items, preserving the existing desktop
column placement and responsive behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant