Skip to content

fix: keep folded sections aligned after window resize#210

Open
PathGao wants to merge 3 commits into
alecdotdev:masterfrom
PathGao:codex/fix-fold-layout-resize
Open

fix: keep folded sections aligned after window resize#210
PathGao wants to merge 3 commits into
alecdotdev:masterfrom
PathGao:codex/fix-fold-layout-resize

Conversation

@PathGao

@PathGao PathGao commented Jul 12, 2026

Copy link
Copy Markdown

Problem

After the window is resized (or moved between displays with different scale factors), expanded heading sections lose alignment with their content. Two visible forms:

  • a section-sized gap between a section and the next heading, or
  • the next heading overlapping the previous section's still-visible text.

Root cause: folding animates grid-template-rows: 1fr ⇄ 0fr on .foldable-content-wrapper. After content reflows (narrower window → paragraphs wrap taller), the WebView does not reliably recompute the 1fr track, so the wrapper's layout height goes stale relative to its rendered content, while overflow: visible lets the content paint outside it.

Fix

  • Replace the fractional grid track with an explicit measured height: the wrapper animates height: var(--fold-content-height) (collapsed: height: 0).
  • New src/lib/utils/foldLayout.tsobserveFoldLayout(root) attaches a single ResizeObserver to each expanded .content-inner, batches all writes into one animation frame, and measures innermost wrappers first so nested folds stay consistent.
  • Reflow-driven writes are applied without animating (suppress the transition around the write, force one layout, restore it), so on resize the wrapper tracks its content instantly; the 0.25 s fold/unfold animation is fully preserved.
  • MarkdownViewer starts observation after each preview render and disconnects before the preview HTML is replaced.

Testing

  • scripts/foldLayout.test.ts (new) + whole scripts/*.test.ts suite passes; svelte-check reports 0 errors/0 warnings.
  • Verified in a bundled macOS build across two displays with different scaling: shrinking the window no longer overlaps the following section (previously the wrapper lagged ~340 ms behind the reflowed content on every resize), expanding/collapsing still animates.
  • Repro fixture included: samples/fold-resize-regression.md.

🤖 Generated with Claude Code

PathGao and others added 3 commits July 13, 2026 00:25
Replace the CSS grid 1fr/0fr fold animation with an explicit measured
height. WebView failed to recompute the 1fr grid track after content
reflow on resize or cross-display moves, leaving stale gaps or overlap.

observeFoldLayout() watches each expanded .content-inner with a
ResizeObserver, batches writes through one animation frame, and publishes
the measured scrollHeight as --fold-content-height (innermost wrappers
first so nested measurements stay consistent). The wrapper animates that
explicit height, so expanded sections track their content and collapsed
sections stay at height 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The height transition is meant only for fold/unfold. Because a resize
also changes the measured height, the wrapper animated toward the new
height over 0.25s while its content had already reflowed, leaving the
next section overlapping the still-visible overflow on every resize.

Suppress the transition around each measured write and force one layout
to commit it, then restore the stylesheet transition so fold toggles
still animate. Verified in a standalone harness: on resize the wrapper
now matches its content instantly (97->97, 173->173 across widths) while
the fold-toggle duration stays 0.25s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@PathGao

PathGao commented Jul 12, 2026

Copy link
Copy Markdown
Author

Why this only manifests on large, heavily-sectioned documents — supplementary analysis that may help review/testing.

The defect itself is size-independent: with an animatable 1fr track on an auto-height container, the wrapper's track size must be re-resolved whenever descendant content reflows. That depends on the engine's incremental-layout invalidation chain (dirty-marking → upward propagation → track re-resolution), and under this combination (animatable fr × indefinite height × deep text reflow) a link in that chain can be silently dropped, leaving a stale track. (We did not pinpoint the exact dropped link in WebKit source; the symptom and failure class are confirmed.)

What scales with document size is the bug's exposure, via four conditions:

  1. Wrapper count — every heading adds a grid container; one stale track shifts everything below it, and P(at least one goes stale) grows with n.
  2. Reflow magnitude — long wrapping paragraphs change height dramatically on width change; small docs mis-position by invisible pixels, large docs by whole viewports.
  3. Reflow waves — large files load in two phases (50 KB preview, then full content via requestIdleCallback) plus async rich-content rendering (mermaid/KaTeX/highlight); each wave re-tests the invalidation chain after tracks were resolved.
  4. Offscreen layout — during resize most wrappers of a long document are outside the viewport, where layout shortcuts make a skipped re-resolution most likely (inferred, not instrumented).

This is also why the fix is exposure-independent: ResizeObserver is a spec-guaranteed post-layout observation of committed sizes — it fires regardless of viewport position or which wave caused the change, so the conditions above now only scale the number of correctly-handled notifications, not the failure probability.

Test suggestion: a large fixture (many h2/h3 sections, long wrapping paragraphs, a table + code block) resized narrow↔wide reproduces the original behavior far more reliably than small samples.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant