Post-Mortem: Building a CAD Layer on Top of Extracted HTML
TLDR: Three plans were thrown away, one fundamental assumption was wrong, and the feature that works best wasn't in the original spec.
What the plan said
The original plan was simple: inject drag handles on zones and regions, use HTML5 drag-and-drop, reorder the DOM, sync state. One file. Two days.
What shipped took five phases and fixed bugs in each one.
Plan 1: Right-click to open Edit Code
Right-click intercept seemed clean. One event, one handler. Except the existing context menu (contextMenu.js) was already wired to contextmenu on .prose-area. Intercepting it killed the image-insert options.
Thrown away. The correct approach was adding "Edit Code" as a menu item inside the existing context menu and calling openViewCode(targetElement) from there. viewCode.js exports a function; contextMenu.js imports it.
Plan 2: Double-click to open Edit Code
Second attempt: double-click. Natural, discoverable. Except #html-preview is contenteditable. Double-click in contenteditable selects a word. The Monaco dialog opened with a selected word, no editor content, and the browser also fired text selection events under it.
Thrown away. Stuck with context menu. It's correct.
Plan 3: The drag handle was in flow
Drag handles were prepended as <span> children. Zones use CSS Grid. Regions stack in a flex column in some configurations. The handle became a grid item.
In a two-column zone: column 1 got shunted right because the handle occupied column 0. In a one-column zone: all content shifted right because the handle took left-side space. The layout in Selection Mode was completely different from Edit Mode, making drag-and-drop edits unpredictable.
Thrown away. Changed handle to position: absolute; top: 4px; left: 4px. Added position: relative to zones and regions in selection mode. Handle overlays the element without participating in layout. Same visual structure in both modes.
Plan 4: window.monaco in the Edit Code dialog
viewCode.js initially called window.monaco.editor.create(...). Worked in dev. Crashed silently in production. The Monaco editor container stayed blank with no error.
Root cause: Vite bundles Monaco as an ES module. It never assigns window.monaco. The fix is one line: import * as monaco from 'monaco-editor'.
The same lesson appears in every Vite + global library combination. The build tool rewrites bare specifiers; it doesn't expose them as globals.
What survived
The core insight survived every iteration: HTML extracted from a PDF is already a box model. CSS Grid zones, flow regions, and getBoundingClientRect() give you free layout geometry. HTML5 drag-and-drop plus insertBefore is the entire reorder implementation.
The features that weren't in the original spec turned out to be the most important: Shift+marquee select, quadrant drop to create a column grid, floating padding/translate panel. These emerged from actually using the tool.
What broke in the user's hand
- Marquee cleared itself immediately because
_onMarqueeEndhadif (!e.ctrlKey && !e.metaKey) _clearSelection()and Shift is neither - Ghost column always showed on cols-1 zones because ghost count was read from
.pdf-colchildren (zero for cols-1) not from the class name - The column resize divider's
_resizeDragwas nulled after_saveColWidthsinstead of before — stale state inside the sync call - Quadrant drop indicator flickered every
dragoverevent because_removeIndicator()was called at the top of the handler, clearing the CSS class before re-adding it
Lesson
Interaction features on editable surfaces have more hidden constraints than pure UI features. contenteditable, drag events, and position:absolute all have browser-specific behaviors that only appear when the user actually tries to use the tool. Build the interaction and test it with real content before declaring it done.