Engineering Journal
Pdf Processor
Pdf Processor

Diff Ui Redesign Postmortem

2026-05-30

Post-Mortem: Diff Tab Redesign, Mobile Drag Fix, and the CSS Specificity Trap

TLDR: Three UI problems, three different root causes. The column detection fix was algorithmic. The mobile drag was an axis-detection oversight. The CSS specificity bug was a cascade law I already know but applied wrong under time pressure.


What We Set Out To Do

Four tasks entered this session:

  1. Fix raiko-aistats: 0 column splits on all 9 pages of a two-column LaTeX PDF.
  2. Fix visual-diff mobile drag: stacked layout (< 1024px) used horizontal clientX even though the divider was now vertical.
  3. Add touch support to the compare diff resizer: mouse-only, no mobile drag.
  4. Redesign the diff tab chrome: two rows of controls eating 82px before any content appeared.
The column detection fix was already covered in its own post-mortem. This one is about everything else.

The Mobile Drag Problem

The visual-diff layout switches to flex-direction: column at 1024px. The original initDividerResize() always read clientX and called outerWidth() on the first pane. Neither is meaningful when the axis is vertical.

The fix sounds simple: detect which axis the layout is using. The trap was how to detect it. You cannot use a window width check because the breakpoint is a CSS media query and can be overridden. The correct source of truth is:

getComputedStyle($layout[0]).flexDirection === 'column'

Computed style reads what CSS actually applied, not what the JavaScript thinks the breakpoint should be. This check runs at drag start, not at init, so it handles viewport resizes between page load and drag attempt.

The same pattern drives cursor choice: row-resize vs col-resize, and whether to write flex: 0 0 ${topPct}% to height or width.


The Diff Tab Redesign

Two problems with the original design:

The redesign collapses everything into a single 36px bar. Three pill groups (Rich/Plain, Split/Unified, Word/Char) sit left-aligned in a flex row. Stats (N added, N removed) sit right-aligned. The pill group uses a container background with a raised active-pill shadow -- the standard segmented control pattern.

This required no HTML restructuring of the diff panels themselves. Only the chrome above them changed.


The CSS Specificity Disaster

After the redesign, #view-diff started rendering on top of every other tab. The panels were supposed to be display: none when inactive. The diff panel was always visible.

Initial read of the user's report: "It's not the height. It's either you rename it view-diff where the name that is being referred to is probably compare-diff or something like that."

That sentence is about an ID mismatch hypothesis. I checked the IDs. They matched. The real culprit was in the CSS cascade.

The .view-panel rule sets display: none on all panels. A later rule .diff-layout set display: flex. These are both single-class selectors with equal specificity. Source order breaks the tie. .diff-layout appears later in the file. It wins. Every .view-panel.diff-layout element gets display: flex whether it is the active tab or not.

The fix is two characters wide: add .view-panel to the diff-layout rule.

/ Before — overrides display:none for all panels: /
.diff-layout {
    display: flex;
    flex-direction: column;
}

/ After — only sets flex direction, never fights display:none: / .view-panel.diff-layout { flex-direction: column; }

Two-class specificity (0,2,0) beats the single-class .view-panel rule (0,1,0), so the active-tab rule wins when it needs to. The inactive tabs keep display: none.


What Failed

Wrong hypothesis first. The user's wording pointed toward an ID mismatch. I checked IDs first. That was the wrong tree. The cascade investigation was second. In hindsight: "always visible" is a specificity smell, not a naming smell.

The display: flex on a layout helper class. Adding layout properties to a semantic class that gets applied alongside view-panel is the setup for this exact problem. A layout class should set layout properties (direction, wrap, gap). It should not set display unless it is the element that owns the display decision. .view-panel owns the display decision here. .diff-layout does not.


What Survived

The session closed with a clean build. The four items that entered finished as fixed.
Read this post in the full Engineering Journal →