DEV Community

Cover image for Debugging Mobile Drag & CSS Specificity in a Real-Time PDF Diff Tool
Bonzai2Carn
Bonzai2Carn

Posted on • Originally published at ginexys.com

Debugging Mobile Drag & CSS Specificity in a Real-Time PDF Diff Tool

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'
Enter fullscreen mode Exit fullscreen mode

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:

  • Two separate rows of controls (mode tabs + a toolbar row) consumed ~82px before any content.
  • The layout and precision controls were visually grouped but semantically separated.

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, which is 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;
}
Enter fullscreen mode Exit fullscreen mode

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

  • Pill group segmented controls are a permanent pattern in this codebase now.
  • Axis-detecting drag via getComputedStyle is the correct approach for responsive dividers.
  • Touch support via { passive: false } and e.touches?.[0] ?? e is now consistent across both dividers.

The session closed with a clean build. The four items that entered finished as fixed.

Top comments (2)

Collapse
 
nazar_boyko profile image
Nazar Boyko

"Always visible is a specificity smell, not a naming smell" is the line I'm keeping from this. Chasing the ID mismatch first is so relatable, and the real lesson, that a layout helper class should never own the display decision, is one of those things that quietly saves you an afternoon once it clicks. Clean writeup.

Collapse
 
frank_signorini profile image
Frank

Interesting insight on column-detection being algorithmic!