diff --git a/docs/plans/2026-02-21-mobile-friendly-impl.md b/docs/plans/2026-02-21-mobile-friendly-impl.md new file mode 100644 index 0000000..81291ae --- /dev/null +++ b/docs/plans/2026-02-21-mobile-friendly-impl.md @@ -0,0 +1,399 @@ +# Mobile-Friendly Frontend Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Make the webapp usable on mobile devices (≤768px) with tab-based navigation, full-screen solver modal, and single-finger touch pan. + +**Architecture:** CSS media queries restyle the existing desktop layout. A small JS module handles tab switching. No new pages, no server detection, no backend changes. + +**Tech Stack:** Vanilla CSS media queries, vanilla JS DOM manipulation. + +**Design doc:** `docs/plans/2026-02-21-mobile-friendly-design.md` + +--- + +### Task 1: Add tab bar HTML + +**Files:** +- Modify: `unredact/static/index.html:35` (inside `#workspace`, before `#left-panel`) + +**Step 1: Add the tab bar markup** + +Insert after `
` (line 35) and before `
` (line 36): + +```html +
+ + +
+``` + +**Step 2: Add DOM references** + +In `unredact/static/dom.js`, add at the end of the export block (before the `showToast` function, around line 57): + +```javascript +export const mobileTabs = document.getElementById("mobile-tabs"); +export const leftPanel = document.getElementById("left-panel"); +``` + +**Step 3: Verify manually** + +Open the app in a browser. The tab bar should be invisible on desktop (we'll hide it with CSS in Task 3). On narrow viewports it won't be styled yet — that's expected. + +**Step 4: Commit** + +```bash +git add unredact/static/index.html unredact/static/dom.js +git commit -m "feat: add mobile tab bar markup" +``` + +--- + +### Task 2: Add tab switching JS + +**Files:** +- Modify: `unredact/static/main.js` (add tab init + auto-switch on redaction select) + +**Step 1: Add tab switching logic** + +At the top of `main.js`, add to the imports from `dom.js` (line 3): `mobileTabs`, `leftPanel`, `rightPanel`. + +Note: `rightPanel` is already imported via `viewport.js`. Check current imports in `main.js` — if `rightPanel` and `leftPanel` aren't imported there, add them. + +Then add a new function after the `deleteRedaction` function (~line 358): + +```javascript +// ── Mobile tab switching ── + +function switchTab(tab) { + if (!mobileTabs) return; + mobileTabs.querySelectorAll(".mobile-tab").forEach(btn => { + btn.classList.toggle("active", btn.dataset.tab === tab); + }); + leftPanel.classList.toggle("mobile-active", tab === "redactions"); + rightPanel.classList.toggle("mobile-active", tab === "document"); +} + +function initMobileTabs() { + if (!mobileTabs) return; + mobileTabs.addEventListener("click", (e) => { + const btn = /** @type {HTMLElement} */ (e.target).closest(".mobile-tab"); + if (btn?.dataset.tab) switchTab(btn.dataset.tab); + }); + // Default: Document tab active + switchTab("document"); +} +``` + +**Step 2: Call initMobileTabs from init** + +Find the main init block (look for where `initViewport()`, `initPopover()`, `initSolver()` are called). Add `initMobileTabs();` alongside them. + +**Step 3: Auto-switch to Document tab on redaction select** + +In the `activateRedaction` function (~line 329), add at the end (before the closing `}`): + +```javascript + switchTab("document"); +``` + +This ensures that tapping a redaction in the list on mobile switches to the Document view. + +**Step 4: Verify manually** + +At this point the JS is wired up but has no visual effect (CSS not added yet). No errors in console. + +**Step 5: Commit** + +```bash +git add unredact/static/main.js +git commit -m "feat: add mobile tab switching logic" +``` + +--- + +### Task 3: Add responsive CSS + +**Files:** +- Modify: `unredact/static/style.css` (append media query block at end of file) + +**Step 1: Add the full media query block** + +Append to the end of `style.css`: + +```css +/* ── Mobile responsive (≤768px) ── */ + +.mobile-tabs { + display: none; +} + +@media (max-width: 768px) { + /* Header: compact */ + header { + padding: 0.5rem 1rem; + } + header h1 { + font-size: 1.1rem; + } + .subtitle { + font-size: 0.7rem; + } + + /* Main: tighter padding */ + main { + padding: 0.5rem; + } + + /* Controls bar: wrap on narrow screens */ + #controls { + flex-wrap: wrap; + gap: 0.5rem; + padding: 0.4rem 0.6rem; + } + + /* Tab bar: visible */ + .mobile-tabs { + display: flex; + gap: 0; + flex-shrink: 0; + border-bottom: 1px solid var(--border); + background: var(--bg-surface); + border-radius: 8px 8px 0 0; + } + .mobile-tab { + flex: 1; + padding: 0.6rem 0; + background: none; + border: none; + color: var(--text-dim); + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + border-bottom: 2px solid transparent; + transition: color 0.15s, border-color 0.15s; + } + .mobile-tab.active { + color: var(--accent); + border-bottom-color: var(--accent); + } + + /* Workspace: stack vertically */ + #workspace { + flex-direction: column; + } + + /* Left panel: hidden by default, full-width when active */ + #left-panel { + display: none; + width: 100%; + border-radius: 0 0 8px 8px; + } + #left-panel.mobile-active { + display: block; + } + + /* Right panel: hidden by default, full-size when active */ + #right-panel { + display: none; + border-radius: 0 0 8px 8px; + } + #right-panel.mobile-active { + display: block; + flex: 1; + } + + /* Popover: full-screen modal */ + #popover { + position: fixed; + inset: 0; + width: 100%; + border-radius: 0; + z-index: 50; + background: rgba(20, 20, 40, 0.98); + backdrop-filter: none; + } + + /* Font toolbar: full-width, no right offset */ + #font-toolbar { + right: 0.5rem; + font-size: 0.7rem; + gap: 0.4rem; + } + + /* Touch targets: larger buttons */ + .tb-btn { + width: 2.2rem; + height: 2.2rem; + font-size: 0.85rem; + } + + /* Solve controls: slightly larger inputs for touch */ + .solve-controls select, + .solve-controls input { + padding: 4px 6px; + font-size: 0.85rem; + } + .solve-btn { + padding: 8px 20px; + } + + /* Popover close button: larger touch target */ + .popover-close-btn { + width: 2.2rem; + height: 2.2rem; + font-size: 0.9rem; + } +} +``` + +**Step 2: Test at different widths** + +Open Chrome DevTools, toggle device toolbar. Check: +- At 768px and below: tabs visible, panels toggle, popover is full-screen +- At 769px and above: no visual change from current desktop layout + +**Step 3: Commit** + +```bash +git add unredact/static/style.css +git commit -m "feat: add responsive CSS for mobile layout" +``` + +--- + +### Task 4: Add single-finger touch pan + +**Files:** +- Modify: `unredact/static/viewport.js:157-196` (touch handling section) + +**Step 1: Add single-finger pan alongside existing two-finger pinch** + +Replace the entire touch handling section (lines 157-196) with: + +```javascript + // Touch: single-finger pan + two-finger pinch-zoom + let lastTouches = null; + let singleTouch = null; + + rightPanel.addEventListener("touchstart", (e) => { + if (isPopoverArea(e)) return; + if (e.touches.length === 2) { + e.preventDefault(); + singleTouch = null; + lastTouches = Array.from(e.touches); + } else if (e.touches.length === 1) { + singleTouch = { + startX: e.touches[0].clientX, + startY: e.touches[0].clientY, + startPanX: state.panX, + startPanY: state.panY, + moved: false, + }; + } + }, { passive: false }); + + rightPanel.addEventListener("touchmove", (e) => { + if (isPopoverArea(e)) return; + if (e.touches.length === 2 && lastTouches) { + e.preventDefault(); + singleTouch = null; + const [t0, t1] = e.touches; + const [p0, p1] = lastTouches; + + const oldDist = Math.hypot(p1.clientX - p0.clientX, p1.clientY - p0.clientY); + const newDist = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY); + const zoomDelta = newDist / oldDist; + + const oldMidX = (p0.clientX + p1.clientX) / 2; + const oldMidY = (p0.clientY + p1.clientY) / 2; + const newMidX = (t0.clientX + t1.clientX) / 2; + const newMidY = (t0.clientY + t1.clientY) / 2; + + state.panX -= (newMidX - oldMidX) / state.zoom; + state.panY -= (newMidY - oldMidY) / state.zoom; + + const rect = rightPanel.getBoundingClientRect(); + const sx = newMidX - rect.left; + const sy = newMidY - rect.top; + zoomTo(state.zoom * zoomDelta, sx, sy, false); + + lastTouches = Array.from(e.touches); + } else if (e.touches.length === 1 && singleTouch) { + const dx = e.touches[0].clientX - singleTouch.startX; + const dy = e.touches[0].clientY - singleTouch.startY; + if (Math.abs(dx) > 2 || Math.abs(dy) > 2) singleTouch.moved = true; + if (singleTouch.moved) { + e.preventDefault(); + state.panX = singleTouch.startPanX - dx / state.zoom; + state.panY = singleTouch.startPanY - dy / state.zoom; + applyTransform(false); + } + } + }, { passive: false }); + + rightPanel.addEventListener("touchend", (e) => { + if (e.touches.length === 0) { + lastTouches = null; + singleTouch = null; + } else if (e.touches.length === 1) { + lastTouches = null; + } + }); +``` + +Key behaviors: +- Single finger drag: pans the document (same as mouse drag) +- Two finger pinch: zooms (existing behavior, preserved) +- Short taps (no movement): fall through to click handlers for redaction selection +- `isPopoverArea` check prevents pan when touching popover/toolbar controls + +**Step 2: Test on mobile or with Chrome DevTools touch simulation** + +- Single finger drag should pan the document +- Pinch zoom should still work +- Tapping a redaction should still select it (not pan) + +**Step 3: Commit** + +```bash +git add unredact/static/viewport.js +git commit -m "feat: add single-finger touch pan for mobile" +``` + +--- + +### Task 5: Final integration test + +**Step 1: Test full mobile workflow** + +Using Chrome DevTools device toolbar (e.g. iPhone 14 or Pixel 7 preset): + +1. Upload a PDF — drop zone should be usable +2. Tab bar visible: "Document" and "Redactions" +3. Click "Redactions" tab — left panel shows, right panel hides +4. Click "Detect Redactions" — redactions appear in list +5. Click a redaction — auto-switches to Document tab, pans to redaction +6. Popover opens as full-screen modal +7. Run solver — results list scrollable +8. Click a result — preview renders on document +9. Close popover with X +10. Single-finger pan works +11. Pinch-zoom works +12. Font toolbar wraps properly, buttons are tap-friendly + +**Step 2: Test desktop is unchanged** + +Resize browser to >768px. Verify: +- No tab bar visible +- Split panel layout intact +- Popover as overlay in right panel (not full-screen) +- All existing functionality works + +**Step 3: Final commit if any fixes needed** + +```bash +git add -A +git commit -m "fix: mobile layout adjustments" +```