Engineering Journal
Pdf Processor
Pdf Processor

Post-Mortem: Building a CAD Layer on Top of Extracted HTML

2026-05-21

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

All four were one-line fixes once diagnosed.

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.

Read this post in the full Engineering Journal →