Ctrl K
ComunidadGuías…

docs/solutions/architecture-patterns/a-layout-breakpoint-is-encoded-in-more-places-than-it-is-defined.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
---
title: A layout breakpoint is encoded in more places than it is defined
date: 2026-08-25
category: architecture-patterns
module: split-layout
problem_type: architecture_pattern
component: frontend
severity: high
applies_when:
  - "Changing a viewport/layout threshold (a breakpoint, a min-width gate, a split-vs-stacked switch)"
  - "Unifying two thresholds that previously differed per platform (web vs desktop app)"
  - "Auditing why a class-applied CSS block duplicates a media query"
tags: [breakpoint, media-query, split-layout, e2e, css-drift, tauri]
---

# A layout breakpoint is encoded in more places than it is defined

## Context

Issue #6417 (implemented in PR #7165, open as of this writing) moved the dashboard's split-layout threshold from 1600px (web) / 900px (desktop) to a single 900px constant. The threshold turned out to be *encoded* in far more places than it was *defined*: nine TS/CSS literals, an entire duplicated CSS block, an e2e viewport table, and localStorage key semantics. Each encoding looked independent, and one of them (the 900/1600 split in `panels.css` vs `panel-layout.ts`) had already produced a shipped drift bug — #6426, where saved bottom-zone panels vanished for desktop windows in the 900–1599px band.

The expensive discoveries were the encodings that did not *mention* the number:

- **A class-applied CSS block duplicating a media query.** `.main-content.desktop-grid { ... }` re-stated the whole split layout so the desktop app could get it below the web's 1600px gate. Once the thresholds unified at 900px, the block became provably dead: the Tauri window minimum width (1200, `src-tauri/tauri.conf.json`) keeps the desktop app inside the `@media (min-width: 900px)` query at all times. The duplicate had also been *masking* an inconsistency — a zoomed desktop webview below 900 CSS px kept the class-driven split while the JS zone logic (`getEffectiveUltraWide()`) had already switched to stacked.
- **An e2e viewport table that encoded the breakpoint as behavior, not as a number.** `e2e/dashboard-news-request-budget.spec.ts` pinned viewport→scroll-owner pairs (`1280px → 'main-content'`). Nothing greps for "1600" there; the old threshold lived in the *expectation* that 1280px is a stacked layout. CI went red only after the change shipped to the PR.

## Guidance

When changing a layout threshold, sweep every **encoding**, not every **occurrence of the number**:

1. **Define it once** in a zero-import TS module (`src/app/split-layout.ts`, `SPLIT_LAYOUT_MIN_WIDTH`) and make every TS site reference it.
2. **CSS cannot import it** — media queries keep literals. Pin them with an alignment test that imports the constant and asserts the CSS text (`tests/responsive-zone-listener.test.mjs` asserts the `@media (min-width: 900px)` gate, the `(max-width: 899px)` hide, the grid-track floors, and that no `1600` survives in the runtime files).
3. **Audit class-applied CSS blocks that duplicate a media query.** They exist to serve a platform the media query excluded; after a threshold change, check whether the platform is still outside the query. If the platform's window floor (Tauri `minWidth`) now sits inside it, the class block is dead — delete it and its JS class, and add a `doesNotMatch` guard so it stays deleted.
4. **Sweep e2e specs for viewport tables.** Search `e2e/` for `setViewportSize` and viewport→expectation tables; any viewport between the old and new thresholds changes layout mode, and expectations like scroll owner (`main-content` stacked vs `panels-grid` split) flip. Keep a case on each side of the new threshold so both modes stay covered.
5. **Check threshold-conditional state.** Storage keys written per-mode (`map-height` vs `map-split-height`), settings-export prefix lists, and zone-reconciliation listeners all branch on the same predicate — moving the threshold moves which users hit which branch.

## Why This Matters

A threshold defined in one place but encoded in many is the exact shape that produced #6426: two encodings drifted 700px apart and silently hid user panels for an entire viewport band. Encodings that don't contain the number (viewport tables, duplicated class blocks, mode-scoped keys) are invisible to a literal grep, so they surface as CI reds or field bugs after the "complete" change. The constant-plus-alignment-test structure converts future drift from a field bug into a test failure at the PR gate.

## When to Apply

- Any change to a `@media (min-width/max-width)` gate that JS also branches on
- Unifying or splitting per-platform thresholds
- Deleting or adding a platform-specific layout class (`desktop-grid`-style) — first prove the platform's window bounds against the media query it duplicates

## Examples

The e2e encoding that did not contain the number (failed CI on PR #7165 until updated):

```ts
// e2e/dashboard-news-request-budget.spec.ts — the OLD threshold lived in
// this expectation, not in any "1600" literal:
{ label: 'desktop', width: 1280, height: 720, scrollOwner: 'main-content' } // pre-#6417
{ label: 'desktop', width: 1280, height: 720, scrollOwner: 'panels-grid' }  // post-#6417
{ label: 'narrow-desktop', width: 850, height: 720, scrollOwner: 'main-content' } // keeps the stacked side covered
```

The dead-block proof (why `.desktop-grid` could be deleted):

```
Tauri window minWidth = 1200  >=  SPLIT_LAYOUT_MIN_WIDTH = 900
=> the desktop app always matches @media (min-width: 900px)
=> every rule in .main-content.desktop-grid { ... } is shadowed
```

## Related

- #6417 (feature), PR #7165 (implementation), #6426 (the drift bug this structure closes off)
- [verification-grep-must-cover-every-file-type-it-claims](../conventions/verification-grep-must-cover-every-file-type-it-claims.md) — the same root cause shape: a value duplicated across file types, where the sweep must enumerate every encoding surface
- [deleting-a-parameter-doesnt-delete-the-branch-it-used-to-select](../conventions/deleting-a-parameter-doesnt-delete-the-branch-it-used-to-select.md) — the inverse audit: a change silently killing or activating a branch elsewhere
- [desktop-cls-immediate-tier-panel-mounts-need-deferred-shells](../performance-issues/desktop-cls-immediate-tier-panel-mounts-need-deferred-shells.md) — same dashboard layout surface; watch field CLS after breakpoint changes