diff --git a/lib/.storybook/preview.ts b/lib/.storybook/preview.ts index 34843fdc..110eea4c 100644 --- a/lib/.storybook/preview.ts +++ b/lib/.storybook/preview.ts @@ -29,6 +29,12 @@ if (window?.navigator?.userAgent?.includes('Chromatic')) { // A blinking cursor is captured on-or-off depending on the frame; freeze it to a // stable solid block so the terminal contributes no non-determinism. cfg.terminal.cursorBlink = false; + // Snap pane splits/restores to their final geometry instead of tweening. A + // mid-tween split resizes panes through many transient widths, and xterm's DOM + // renderer can latch a frame where a pane is still narrow — freezing its last + // line clipped (`user@dormouse:~$` → `user@do`) even after the layout settles. + // Instant geometry removes that race at the source. + cfg.layout.animate = false; } // Collect all CSS variable names across all themes for cleanup diff --git a/lib/src/cfg.ts b/lib/src/cfg.ts index 9ea91f83..e141b562 100644 --- a/lib/src/cfg.ts +++ b/lib/src/cfg.ts @@ -34,4 +34,13 @@ export const cfg = { * stable solid block rather than being captured mid-blink (non-deterministic). */ cursorBlink: true, }, + layout: { + /** When false, Lath pane geometry changes (split / restore / kill / drag) apply + * instantly with no tween. Disabled under Chromatic: a mid-tween split resizes + * panes through many transient widths (briefly near-zero), and xterm's DOM + * renderer can latch onto one of those frames and leave a pane painted blank or + * clipped (`user@dormouse:~$` → `user@do`) even after the geometry settles. + * Snapping straight to the final geometry removes that whole race. */ + animate: true, + }, }; diff --git a/lib/src/components/wall/lath-wall-engine.ts b/lib/src/components/wall/lath-wall-engine.ts index 94b84dc5..8e3bd3b5 100644 --- a/lib/src/components/wall/lath-wall-engine.ts +++ b/lib/src/components/wall/lath-wall-engine.ts @@ -24,6 +24,7 @@ import { createAnimator, } from '../../lib/lath/animator'; import { prefersReducedMotion } from '../../lib/ui-geometry'; +import { cfg } from '../../cfg'; import { UNNAMED_PANEL_TITLE } from '../../lib/terminal-registry'; import type { ResolvedSplitDirection as DorResolvedSplitDirection } from 'dor/commands/types'; import { @@ -163,9 +164,11 @@ export function createLathWallEngine( ): LathWallEngine { const snapshot = (): LathWallSnapshot => store.getSnapshot(); - // 0 under reduced motion, so entry/exit/tween all collapse to instant through the - // very same code path. Tests inject a fixed duration (or 0) via `opts`. - const durationMs = opts?.durationMs ?? (prefersReducedMotion() ? 0 : LATH_MOTION_MS); + // 0 under reduced motion (or when layout animation is disabled, e.g. Chromatic), + // so entry/exit/tween all collapse to instant through the very same code path. + // Tests inject a fixed duration (or 0) via `opts`. + const durationMs = + opts?.durationMs ?? (!cfg.layout.animate || prefersReducedMotion() ? 0 : LATH_MOTION_MS); const animator = createAnimator({ durationMs, easing: LATH_EASING }); // Presentation-only side state (never in the store snapshot): the frame/wake