mirror of
https://github.com/PR0M3TH3AN/unredact.git
synced 2026-10-27 08:40:29 +00:00
docs: add mobile-responsive implementation plan
7-task plan for bottom-sheet responsive redesign covering HTML restructuring, desktop/mobile CSS, sheet snap behavior, tab switching, and integration. Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
1 parent
534b3fbe33
commit
3a590e17c8
1 file changed
+817
@@ -0,0 +1,817 @@
|
||||
# Mobile-Responsive Redesign Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Replace the broken tab-based mobile layout with a draggable bottom sheet that keeps the document viewer always visible, while preserving the desktop sidebar+overlay layout.
|
||||
|
||||
**Architecture:** A single `#bottom-sheet` element serves as the left sidebar on desktop (>768px) and a fixed-position bottom overlay on mobile (<=768px). On mobile, JS moves the popover, font-toolbar, and text-edit-bar DOM elements into the sheet's tab panes. The document viewer (`#right-panel`) is never hidden, keeping canvas coordinates valid. Sheet snap state (peek/half/full) is managed via CSS custom properties and touch events on the drag handle.
|
||||
|
||||
**Tech Stack:** Vanilla CSS (media queries, custom properties, transitions), vanilla JS (DOM manipulation, touch events, matchMedia).
|
||||
|
||||
**Design doc:** `docs/plans/2026-02-21-mobile-responsive-design.md`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Restructure HTML and update DOM refs
|
||||
|
||||
**Files:**
|
||||
- Modify: `unredact/static/index.html`
|
||||
- Modify: `unredact/static/dom.js`
|
||||
|
||||
**Step 1: Replace the mobile-tabs and left-panel with the bottom-sheet container**
|
||||
|
||||
In `unredact/static/index.html`, replace the `#mobile-tabs` div (lines 36-39) and `#left-panel` div (lines 40-44) with:
|
||||
|
||||
```html
|
||||
<div id="bottom-sheet">
|
||||
<div id="sheet-handle"><div class="handle-bar"></div></div>
|
||||
<div id="sheet-tabs">
|
||||
<button class="sheet-tab active" data-tab="solve">Solve</button>
|
||||
<button class="sheet-tab" data-tab="edit">Edit</button>
|
||||
<button class="sheet-tab" data-tab="list">List</button>
|
||||
</div>
|
||||
<div id="sheet-content">
|
||||
<div id="tab-solve" class="tab-pane active"></div>
|
||||
<div id="tab-edit" class="tab-pane"></div>
|
||||
<div id="tab-list" class="tab-pane">
|
||||
<button id="detect-btn" disabled>Detect Redactions</button>
|
||||
<div id="ocr-status"></div>
|
||||
<div id="redaction-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
The full `#workspace` block should now look like:
|
||||
|
||||
```html
|
||||
<div id="workspace">
|
||||
<div id="bottom-sheet">
|
||||
<div id="sheet-handle"><div class="handle-bar"></div></div>
|
||||
<div id="sheet-tabs">
|
||||
<button class="sheet-tab active" data-tab="solve">Solve</button>
|
||||
<button class="sheet-tab" data-tab="edit">Edit</button>
|
||||
<button class="sheet-tab" data-tab="list">List</button>
|
||||
</div>
|
||||
<div id="sheet-content">
|
||||
<div id="tab-solve" class="tab-pane active"></div>
|
||||
<div id="tab-edit" class="tab-pane"></div>
|
||||
<div id="tab-list" class="tab-pane">
|
||||
<button id="detect-btn" disabled>Detect Redactions</button>
|
||||
<div id="ocr-status"></div>
|
||||
<div id="redaction-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="right-panel">
|
||||
<div id="font-toolbar" hidden>
|
||||
<!-- ... unchanged ... -->
|
||||
</div>
|
||||
<div id="popover" hidden>
|
||||
<!-- ... unchanged ... -->
|
||||
</div>
|
||||
<div id="doc-container">
|
||||
<img id="doc-image" alt="Document page">
|
||||
<canvas id="overlay-canvas"></canvas>
|
||||
</div>
|
||||
<div id="text-edit-bar" hidden>
|
||||
<!-- ... unchanged ... -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Note: `#popover`, `#font-toolbar`, `#text-edit-bar` remain inside `#right-panel`. They stay there on desktop. On mobile, JS will move them into the sheet's tab panes (Task 5).
|
||||
|
||||
**Step 2: Update dom.js**
|
||||
|
||||
In `unredact/static/dom.js`, replace the `mobileTabs` and `leftPanel` exports (lines 57-58) with:
|
||||
|
||||
```javascript
|
||||
export const bottomSheet = document.getElementById("bottom-sheet");
|
||||
export const sheetHandle = document.getElementById("sheet-handle");
|
||||
export const sheetTabs = document.getElementById("sheet-tabs");
|
||||
export const tabSolve = document.getElementById("tab-solve");
|
||||
export const tabEdit = document.getElementById("tab-edit");
|
||||
export const tabList = document.getElementById("tab-list");
|
||||
```
|
||||
|
||||
**Step 3: Update main.js imports**
|
||||
|
||||
In `unredact/static/main.js`, replace `leftPanel, mobileTabs` in the import block (line 8) with `bottomSheet, sheetTabs, tabSolve, tabEdit, tabList`:
|
||||
|
||||
```javascript
|
||||
import {
|
||||
dropZone, fileInput, uploadSection, viewerSection, docImage,
|
||||
canvas, pageInfo, prevBtn, nextBtn, redactionListEl, detectBtn,
|
||||
rightPanel, bottomSheet, sheetTabs, tabSolve, tabEdit, tabList, fontSelect,
|
||||
solveAccept, gapValue, showToast,
|
||||
} from './dom.js';
|
||||
```
|
||||
|
||||
**Step 4: Delete the old switchTab and initMobileTabs functions**
|
||||
|
||||
In `main.js`, delete the entire mobile tab switching section (lines 359-378):
|
||||
|
||||
```javascript
|
||||
// DELETE: lines 359-378 (switchTab function, initMobileTabs function)
|
||||
```
|
||||
|
||||
Also delete `switchTab("document");` from the `activateRedaction` function (line 346). We'll add the new sheet behavior in Task 6.
|
||||
|
||||
Also delete `initMobileTabs();` from the init block (line 639).
|
||||
|
||||
**Step 5: Verify**
|
||||
|
||||
Open the app in a desktop browser. The page should load without JS errors. The left sidebar should show the Detect button and redaction list (from `#tab-list`). The sheet handle and tabs should be visible but unstyled (we'll hide them on desktop in Task 2). The popover, font toolbar, and text edit bar should still work when a redaction is selected (they're still inside `#right-panel`).
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add unredact/static/index.html unredact/static/dom.js unredact/static/main.js
|
||||
git commit -m "refactor: replace left-panel with bottom-sheet container"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Desktop CSS — sheet as sidebar
|
||||
|
||||
**Files:**
|
||||
- Modify: `unredact/static/style.css`
|
||||
|
||||
**Step 1: Replace `#left-panel` rules with `#bottom-sheet` rules**
|
||||
|
||||
Find the `#left-panel` rule block (around line 138-144):
|
||||
|
||||
```css
|
||||
/* Left panel — scrollable redaction list */
|
||||
#left-panel {
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
background: var(--bg-surface);
|
||||
border-radius: 8px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```css
|
||||
/* Bottom sheet — sidebar on desktop, bottom overlay on mobile */
|
||||
#bottom-sheet {
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
background: var(--bg-surface);
|
||||
border-radius: 8px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Handle and tabs hidden on desktop */
|
||||
#sheet-handle { display: none; }
|
||||
#sheet-tabs { display: none; }
|
||||
|
||||
/* Tab panes: only list visible on desktop */
|
||||
#sheet-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.tab-pane { display: none; }
|
||||
#tab-list { display: block; }
|
||||
```
|
||||
|
||||
**Step 2: Add sheet handle and tab styling (will only show on mobile)**
|
||||
|
||||
Add these rules after the `#bottom-sheet` block (these define the base look, mobile CSS will make them visible):
|
||||
|
||||
```css
|
||||
#sheet-handle {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 0;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
#sheet-handle:active { cursor: grabbing; }
|
||||
|
||||
.handle-bar {
|
||||
width: 36px;
|
||||
height: 4px;
|
||||
background: var(--text-muted);
|
||||
border-radius: 2px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#sheet-tabs {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.sheet-tab {
|
||||
flex: 1;
|
||||
padding: 6px 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-dim);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
}
|
||||
.sheet-tab.active {
|
||||
color: var(--accent);
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Delete old mobile CSS**
|
||||
|
||||
Delete the entire mobile section at the bottom of style.css — the `.mobile-tabs` base rule (lines 777-779) AND the full `@media (max-width: 768px)` block (lines 781-902). We'll rewrite mobile CSS from scratch in Task 3.
|
||||
|
||||
**Step 4: Verify**
|
||||
|
||||
Desktop browser: the app should look identical to before. The sidebar shows the List tab content (detect button + redaction list). Sheet handle and tabs are hidden. Popover and toolbars still work as overlays in the right panel.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add unredact/static/style.css
|
||||
git commit -m "feat: desktop CSS for bottom-sheet as sidebar"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Mobile CSS — sheet as bottom overlay
|
||||
|
||||
**Files:**
|
||||
- Modify: `unredact/static/style.css`
|
||||
|
||||
**Step 1: Add the mobile media query block**
|
||||
|
||||
Append to the end of `style.css`:
|
||||
|
||||
```css
|
||||
/* ── Mobile (≤768px) ── */
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
/* Workspace: full column layout */
|
||||
#workspace {
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Right panel: always visible, takes available space above sheet */
|
||||
#right-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Bottom sheet: fixed overlay at bottom */
|
||||
#bottom-sheet {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: var(--sheet-height, 60px);
|
||||
z-index: 40;
|
||||
border-radius: 12px 12px 0 0;
|
||||
background: var(--bg-surface);
|
||||
border-top: 1px solid var(--border);
|
||||
transition: height 0.25s ease-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Handle and tabs: visible on mobile */
|
||||
#sheet-handle { display: block; }
|
||||
#sheet-tabs { display: flex; }
|
||||
|
||||
/* Sheet content: fill remaining space, scroll */
|
||||
#sheet-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
|
||||
/* All tab panes hidden by default; .active shown */
|
||||
.tab-pane { display: none; }
|
||||
.tab-pane.active { display: block; }
|
||||
|
||||
/* ── Elements moved into sheet tabs on mobile ── */
|
||||
|
||||
/* Popover: static flow inside solve tab (no close button needed) */
|
||||
#popover:not([hidden]) {
|
||||
position: static;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
backdrop-filter: none;
|
||||
z-index: auto;
|
||||
overflow-y: visible;
|
||||
}
|
||||
.popover-header { display: none; }
|
||||
|
||||
/* Font toolbar: static flow inside edit tab */
|
||||
#font-toolbar:not([hidden]) {
|
||||
position: static;
|
||||
width: 100%;
|
||||
right: auto;
|
||||
left: auto;
|
||||
top: auto;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
z-index: auto;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6rem;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* Text edit bar: static flow inside edit tab */
|
||||
#text-edit-bar:not([hidden]) {
|
||||
position: static;
|
||||
width: 100%;
|
||||
left: auto;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
z-index: auto;
|
||||
padding: 8px 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Touch targets: larger buttons */
|
||||
.tb-btn {
|
||||
width: 2.2rem;
|
||||
height: 2.2rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Solve controls: larger inputs for touch */
|
||||
.solve-controls select,
|
||||
.solve-controls input {
|
||||
padding: 4px 6px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.solve-btn { padding: 8px 20px; }
|
||||
|
||||
/* Delete button: always visible on touch (no hover) */
|
||||
.redaction-delete { opacity: 1; }
|
||||
|
||||
/* Viewer section needs bottom margin for sheet peek height */
|
||||
#viewer-section {
|
||||
padding-bottom: var(--sheet-height, 60px);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Verify**
|
||||
|
||||
Open Chrome DevTools, toggle device toolbar to a phone preset (e.g. iPhone 14, 390x844):
|
||||
|
||||
- The right panel (document viewer) should fill the screen
|
||||
- A thin bottom sheet should be visible at the bottom (60px, peek height) with a handle bar
|
||||
- The sheet tabs should be visible: Solve | Edit | List
|
||||
- At this point, tabs won't work yet (JS not wired) but the layout should be correct
|
||||
- Desktop should still look exactly the same
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add unredact/static/style.css
|
||||
git commit -m "feat: mobile CSS for bottom sheet overlay"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Sheet snap behavior (JS)
|
||||
|
||||
**Files:**
|
||||
- Modify: `unredact/static/main.js`
|
||||
|
||||
**Step 1: Add sheet snap constants and state**
|
||||
|
||||
At the top of `main.js`, after the imports, add:
|
||||
|
||||
```javascript
|
||||
// ── Sheet snap management ──
|
||||
|
||||
const SNAP_PEEK = 60;
|
||||
const SNAP_HALF_RATIO = 0.45;
|
||||
const SNAP_FULL_RATIO = 0.9;
|
||||
|
||||
/** @type {'peek'|'half'|'full'} */
|
||||
let sheetSnap = 'peek';
|
||||
|
||||
function getSnapHeight(snap) {
|
||||
const vh = window.innerHeight;
|
||||
switch (snap) {
|
||||
case 'peek': return SNAP_PEEK;
|
||||
case 'half': return Math.round(vh * SNAP_HALF_RATIO);
|
||||
case 'full': return Math.round(vh * SNAP_FULL_RATIO);
|
||||
}
|
||||
}
|
||||
|
||||
function setSheetSnap(snap) {
|
||||
sheetSnap = snap;
|
||||
const h = getSnapHeight(snap);
|
||||
document.documentElement.style.setProperty('--sheet-height', h + 'px');
|
||||
bottomSheet.style.height = h + 'px';
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Add sheet drag handling**
|
||||
|
||||
Below the snap functions, add:
|
||||
|
||||
```javascript
|
||||
function initSheetDrag() {
|
||||
const handle = document.getElementById('sheet-handle');
|
||||
if (!handle) return;
|
||||
|
||||
let dragState = null;
|
||||
|
||||
handle.addEventListener('touchstart', (e) => {
|
||||
if (e.touches.length !== 1) return;
|
||||
dragState = {
|
||||
startY: e.touches[0].clientY,
|
||||
startHeight: bottomSheet.offsetHeight,
|
||||
};
|
||||
bottomSheet.style.transition = 'none';
|
||||
}, { passive: true });
|
||||
|
||||
handle.addEventListener('touchmove', (e) => {
|
||||
if (!dragState) return;
|
||||
e.preventDefault();
|
||||
const dy = dragState.startY - e.touches[0].clientY;
|
||||
const newH = Math.max(SNAP_PEEK, Math.min(
|
||||
getSnapHeight('full'),
|
||||
dragState.startHeight + dy
|
||||
));
|
||||
bottomSheet.style.height = newH + 'px';
|
||||
document.documentElement.style.setProperty('--sheet-height', newH + 'px');
|
||||
}, { passive: false });
|
||||
|
||||
const endDrag = () => {
|
||||
if (!dragState) return;
|
||||
bottomSheet.style.transition = '';
|
||||
const currentH = bottomSheet.offsetHeight;
|
||||
|
||||
// Find nearest snap point
|
||||
const peekH = getSnapHeight('peek');
|
||||
const halfH = getSnapHeight('half');
|
||||
const fullH = getSnapHeight('full');
|
||||
|
||||
const peekDist = Math.abs(currentH - peekH);
|
||||
const halfDist = Math.abs(currentH - halfH);
|
||||
const fullDist = Math.abs(currentH - fullH);
|
||||
|
||||
if (peekDist <= halfDist && peekDist <= fullDist) setSheetSnap('peek');
|
||||
else if (halfDist <= fullDist) setSheetSnap('half');
|
||||
else setSheetSnap('full');
|
||||
|
||||
dragState = null;
|
||||
};
|
||||
|
||||
handle.addEventListener('touchend', endDrag);
|
||||
handle.addEventListener('touchcancel', endDrag);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Initialize sheet on page load**
|
||||
|
||||
In the init block at the bottom of `main.js`, add (where `initMobileTabs()` used to be):
|
||||
|
||||
```javascript
|
||||
// ── Initialize sheet (mobile only) ──
|
||||
const isMobile = () => window.matchMedia('(max-width: 768px)').matches;
|
||||
|
||||
if (isMobile()) {
|
||||
setSheetSnap('peek');
|
||||
initSheetDrag();
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Verify**
|
||||
|
||||
On mobile viewport: the sheet should be 60px tall (peek). Dragging the handle up should resize the sheet. Releasing should snap to the nearest snap point with a smooth transition. On desktop: no effect (sheet is a sidebar, handle is hidden).
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add unredact/static/main.js
|
||||
git commit -m "feat: sheet drag and snap behavior for mobile"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Tab switching and DOM element relocation
|
||||
|
||||
**Files:**
|
||||
- Modify: `unredact/static/main.js`
|
||||
- Modify: `unredact/static/dom.js`
|
||||
|
||||
**Step 1: Add DOM movement functions**
|
||||
|
||||
In `main.js`, add after the sheet snap code:
|
||||
|
||||
```javascript
|
||||
// ── Mobile layout: move controls into sheet tabs ──
|
||||
|
||||
let elementsInSheet = false;
|
||||
|
||||
function moveElementsToSheet() {
|
||||
if (elementsInSheet) return;
|
||||
const popover = document.getElementById('popover');
|
||||
const fontToolbar = document.getElementById('font-toolbar');
|
||||
const textEditBar = document.getElementById('text-edit-bar');
|
||||
|
||||
tabSolve.appendChild(popover);
|
||||
tabEdit.appendChild(fontToolbar);
|
||||
tabEdit.appendChild(textEditBar);
|
||||
elementsInSheet = true;
|
||||
}
|
||||
|
||||
function moveElementsToPanel() {
|
||||
if (!elementsInSheet) return;
|
||||
const popover = document.getElementById('popover');
|
||||
const fontToolbar = document.getElementById('font-toolbar');
|
||||
const textEditBar = document.getElementById('text-edit-bar');
|
||||
|
||||
rightPanel.insertBefore(fontToolbar, rightPanel.querySelector('#doc-container'));
|
||||
rightPanel.insertBefore(popover, rightPanel.querySelector('#doc-container'));
|
||||
rightPanel.appendChild(textEditBar);
|
||||
elementsInSheet = false;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Add tab switching**
|
||||
|
||||
```javascript
|
||||
// ── Sheet tab switching ──
|
||||
|
||||
function switchSheetTab(tab) {
|
||||
sheetTabs.querySelectorAll('.sheet-tab').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.tab === tab);
|
||||
});
|
||||
document.querySelectorAll('#sheet-content .tab-pane').forEach(pane => {
|
||||
pane.classList.toggle('active', pane.id === 'tab-' + tab);
|
||||
});
|
||||
}
|
||||
|
||||
function initSheetTabs() {
|
||||
sheetTabs.addEventListener('click', (e) => {
|
||||
const btn = /** @type {HTMLElement} */ (e.target).closest('.sheet-tab');
|
||||
if (btn?.dataset.tab) switchSheetTab(btn.dataset.tab);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Handle viewport changes**
|
||||
|
||||
```javascript
|
||||
// ── Responsive layout management ──
|
||||
|
||||
function handleLayoutChange() {
|
||||
if (isMobile()) {
|
||||
moveElementsToSheet();
|
||||
setSheetSnap(sheetSnap);
|
||||
} else {
|
||||
moveElementsToPanel();
|
||||
document.documentElement.style.removeProperty('--sheet-height');
|
||||
bottomSheet.style.height = '';
|
||||
}
|
||||
}
|
||||
|
||||
window.matchMedia('(max-width: 768px)').addEventListener('change', handleLayoutChange);
|
||||
```
|
||||
|
||||
**Step 4: Wire up initialization**
|
||||
|
||||
Update the init block at the bottom of `main.js`. Replace the existing mobile init code with:
|
||||
|
||||
```javascript
|
||||
// ── Initialize sheet and tabs ──
|
||||
initSheetTabs();
|
||||
initSheetDrag();
|
||||
handleLayoutChange();
|
||||
```
|
||||
|
||||
**Step 5: Verify**
|
||||
|
||||
Mobile viewport:
|
||||
- Sheet tabs should be visible: Solve | Edit | List
|
||||
- Clicking List tab should show the detect button and redaction list
|
||||
- Clicking Solve or Edit should show empty panes (controls not moved yet until popover opens)
|
||||
- Resizing from mobile to desktop should move elements back to their desktop positions
|
||||
|
||||
Desktop:
|
||||
- Sidebar shows redaction list as before
|
||||
- Popover, font toolbar, text edit bar work as overlays in right panel
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add unredact/static/main.js
|
||||
git commit -m "feat: sheet tab switching and responsive DOM relocation"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Integrate popover/main flow with sheet
|
||||
|
||||
**Files:**
|
||||
- Modify: `unredact/static/popover.js`
|
||||
- Modify: `unredact/static/main.js`
|
||||
|
||||
**Step 1: Export isMobile and sheet control from main.js**
|
||||
|
||||
We need popover.js to be able to control the sheet. The cleanest way is to have main.js export sheet control functions. But since popover.js can't import from main.js (it would create a circular dependency), we'll use the existing callback pattern.
|
||||
|
||||
In `main.js`, add a function that popover.js can call via a registered callback:
|
||||
|
||||
```javascript
|
||||
/** @type {((tab: string, snap: string) => void)|null} */
|
||||
let _onSheetChange = null;
|
||||
export function setOnSheetChange(fn) { _onSheetChange = fn; }
|
||||
```
|
||||
|
||||
Wait — main.js doesn't export anything currently. Instead, let's keep it simpler: have main.js provide sheet helpers to popover.js via the existing pattern.
|
||||
|
||||
Actually, the cleanest approach: move `switchSheetTab`, `setSheetSnap`, and `isMobile` into a shared location. But to minimize changes, let's just have main.js handle all sheet interactions, and popover.js just calls `openPopover`/`closePopover` as before. Main.js reacts to those calls.
|
||||
|
||||
**Revised approach — modify main.js's activateRedaction and add a popover close handler:**
|
||||
|
||||
In `main.js`, update `activateRedaction`:
|
||||
|
||||
```javascript
|
||||
function activateRedaction(id) {
|
||||
const r = state.redactions[id];
|
||||
if (!r) return;
|
||||
|
||||
state.activeRedaction = id;
|
||||
|
||||
state.panX = r.x + r.w / 2;
|
||||
state.panY = r.y + r.h / 2;
|
||||
applyTransform(true);
|
||||
|
||||
renderRedactionList();
|
||||
renderCanvas();
|
||||
|
||||
if (r.status === "analyzed" || r.status === "approved") {
|
||||
openPopover(id);
|
||||
if (isMobile()) {
|
||||
switchSheetTab('solve');
|
||||
setSheetSnap('half');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Update the popover close handler in main.js**
|
||||
|
||||
Find `setOnPopoverClose(stopSolve)` in the init block and change it to:
|
||||
|
||||
```javascript
|
||||
setOnPopoverClose(() => {
|
||||
stopSolve();
|
||||
if (isMobile()) {
|
||||
switchSheetTab('list');
|
||||
setSheetSnap('peek');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Step 3: Update popover.js closePopover**
|
||||
|
||||
In `popover.js`, update `closePopover` to also deselect the active redaction on mobile:
|
||||
|
||||
```javascript
|
||||
export function closePopover() {
|
||||
popoverEl.hidden = true;
|
||||
fontToolbar.hidden = true;
|
||||
textEditBar.hidden = true;
|
||||
if (_onClose) _onClose();
|
||||
}
|
||||
```
|
||||
|
||||
This stays the same — the `_onClose` callback in main.js now handles the sheet snap.
|
||||
|
||||
**Step 4: Handle popover open on mobile**
|
||||
|
||||
The `openPopover` function in `popover.js` sets `popoverEl.hidden = false`, `fontToolbar.hidden = false`, `textEditBar.hidden = false`. On mobile, these elements are inside the sheet's tab panes. Setting `hidden = false` removes the `[hidden]` attribute, which lets them display (since the `[hidden] { display: none !important; }` rule hides them when hidden).
|
||||
|
||||
This should work as-is! When the Solve tab is active and `popoverEl.hidden = false`, the popover shows inside the tab. When the Edit tab is active, `fontToolbar.hidden = false` and `textEditBar.hidden = false` make those elements visible.
|
||||
|
||||
**Step 5: Verify**
|
||||
|
||||
Mobile viewport:
|
||||
1. Upload a PDF
|
||||
2. Click "Detect Redactions"
|
||||
3. Once redactions appear in the List tab, tap one
|
||||
4. Sheet should snap to half, Solve tab should be active, solver controls visible
|
||||
5. Tapping Edit tab should show font toolbar and text edit bar
|
||||
6. Closing popover (via close button on desktop, or just deselecting) should snap sheet to peek
|
||||
|
||||
Desktop: all behavior unchanged.
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add unredact/static/main.js unredact/static/popover.js
|
||||
git commit -m "feat: wire sheet snap and tab switching to popover flow"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Polish, touch targets, and cleanup
|
||||
|
||||
**Files:**
|
||||
- Modify: `unredact/static/style.css`
|
||||
- Modify: `unredact/static/main.js`
|
||||
|
||||
**Step 1: Add tap-on-canvas to deselect on mobile**
|
||||
|
||||
In `main.js`, in the canvas `mousedown` listener (the first one, around line 382), add deselection when tapping empty area on mobile:
|
||||
|
||||
After the existing `if (hit)` block, add an `else`:
|
||||
|
||||
```javascript
|
||||
if (hit) {
|
||||
e.stopPropagation();
|
||||
activateRedaction(hit.id);
|
||||
} else if (isMobile() && state.activeRedaction) {
|
||||
state.activeRedaction = null;
|
||||
closePopover();
|
||||
renderRedactionList();
|
||||
renderCanvas();
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Add redaction list tap behavior for mobile**
|
||||
|
||||
In `main.js`, in the `renderRedactionList` function, the `div.addEventListener("click", () => activateRedaction(r.id))` already exists. On mobile, tapping a redaction in the List tab should switch to Solve tab and snap to half. This is already handled in `activateRedaction` from Task 6. No changes needed.
|
||||
|
||||
**Step 3: Verify the full mobile workflow**
|
||||
|
||||
Using Chrome DevTools device toolbar (e.g. iPhone 14):
|
||||
|
||||
1. Load the page — upload drop zone visible, no sheet yet (viewer section hidden)
|
||||
2. Upload a PDF — viewer section appears, sheet visible at peek (60px)
|
||||
3. List tab shows "Detect Redactions" button
|
||||
4. Click "Detect Redactions" — redactions appear in list
|
||||
5. Tap a redaction in list — sheet snaps to half, Solve tab active with controls
|
||||
6. Run solver — results appear, scrollable in sheet
|
||||
7. Tap a result — preview shows on document above
|
||||
8. Tap Edit tab — font controls and text inputs visible
|
||||
9. Adjust font/position — changes reflected in document preview above
|
||||
10. Tap Accept — solution saved
|
||||
11. Tap empty canvas area — sheet snaps to peek, deselects redaction
|
||||
12. Drag handle up — sheet expands to full
|
||||
13. Drag handle down — sheet snaps to peek
|
||||
14. Pinch-zoom on document — works (document viewer never hidden)
|
||||
15. Single-finger pan — works
|
||||
|
||||
**Step 4: Test desktop is unchanged**
|
||||
|
||||
Resize to >768px:
|
||||
- Left sidebar with redaction list
|
||||
- Popover overlay in right panel
|
||||
- Font toolbar at top of right panel
|
||||
- Text edit bar at bottom
|
||||
- All zoom/pan/interaction unchanged
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add unredact/static/style.css unredact/static/main.js
|
||||
git commit -m "fix: mobile polish — tap-to-deselect and workflow fixes"
|
||||
```
|
||||
Reference in new issue
Block a user