LayoutPanel: responsive hideBelow prop (replace hand-rolled useMediaQuery panel hiding in templates)
- Dominant language
- TypeScript
- Stars
- 13k
- Forks
- 1.1k
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 690
Description
## Problem
Multi-column "frame + side panel" page templates need a panel that **disappears below a breakpoint** so the primary content keeps its width on small screens. Today every template implements this responsiveness by hand: call `useMediaQuery(...)` in the page component, compute a boolean, and conditionally render (or `undefined`-out) a `LayoutPanel`. The panel component itself has no responsive awareness.
My first instinct was that these templates were rebuilding a whole "split frame / resizable inspector" layout that should be a component. **On inspection that's not true** — and per the spec protocol's "compose before creating" triage, it's worth writing down why, because the real gap is smaller and prop-shaped.
## What's already solved (do NOT build a new component for this)
The resizable inspector pattern is **already cleanly composed** from existing primitives. incident-console (#3316):
```tsx
const inspectorPanel = useResizable({ defaultSize: 380, minSizePx: 320, maxSizePx: 480 });
// …
…}
end={ isNarrow ? undefined : (
<>
…
)}
/>
```
`Layout` + `LayoutPanel` + `ResizeHandle` + `useResizable` already deliver header/content/panel framing and drag-resize. A `SplitFrame`-style wrapper would just re-wrap these and duplicate `Layout`'s slot API — a thin wrapper the surface-area audit would reject. So: **no new component.**
## The actual gap: responsive panel visibility is hand-rolled
What *is* repeated across templates is the breakpoint-hide logic, done in page code instead of on the panel:
- **incident-console (#3316):** `const isNarrow = useMediaQuery('(max-width: 1024px)')` → `end={ isNarrow ? undefined : () }`.
- **messaging-shell (#3323):** `const isThreadHidden = useMediaQuery('(max-width: 1024px)')`, `const isSidebarHidden = useMediaQuery('(max-width: 768px)')` → both a `start` `LayoutPanel` and an `end` `LayoutPanel` conditionally rendered on those booleans.
- **deployment-detail (#3330):** `const isCompact = useMediaQuery('(max-width: 768px)')` gating panel/column layout.
Four hand-rolled `useMediaQuery` calls across these templates exist purely to hide/show a `LayoutPanel` at a width threshold. That's the same intent (hide this panel below breakpoint X) re-expressed each time, in JS, with SSR/hydration caveats that `useMediaQuery` carries.
## Proposed API (small, on the existing component)
Add a responsive-visibility prop to `LayoutPanel` so the panel owns its own breakpoint behavior — mirroring how `Card`/`LayoutContent`/`LayoutPanel` already own `isScrollable` and `padding` rather than pushing them to callers:
```tsx
// Panel hides itself below the given breakpoint — no useMediaQuery in page code
{threadPanel}
```
- `hideBelow?: 'sm' | 'md' | 'lg' | 'xl'` (align to the system's breakpoint tokens), implemented with a CSS `@media` display toggle inside the component's stylex — no runtime measurement, no hydration flash.
- Prior art: Chakra's `hideBelow` / `hideFrom` utilities and `Show`/`Hide` components solve exactly this; the CSS-media approach avoids `useMediaQuery`'s SSR pitfalls.
- Naming to settle in discussion: `hideBelow` (Chakra-aligned, panel-scoped) vs. a boolean tied to a fixed layout breakpoint. Leaning `hideBelow` for expressiveness.
### Explicitly out of scope
- No `SplitFrame`/`InspectorLayout` component — existing composition already covers it.
- Resize behavior is unchanged; `useResizable` + `ResizeHandle` stay as-is.
---
# Specification Protocol (abbreviated — this is a prop, and the triage is the point)
- **Phase 1 Triage:** Gap = responsive panel visibility. Composition check: the *layout* is already composable (that's the finding); only the breakpoint-hide is duplicated. Not domain-specific → belongs on the core `LayoutPanel`.
- **Phase 2 Internal research:** 4 hand-rolled `useMediaQuery`-to-hide-a-panel instances across #3316/#3323/#3330; all express "hide this panel under N px." `LayoutPanel` already owns analogous concerns (`padding`, `isScrollable`, `resizable`).
- **Phase 3 External research:** Chakra `hideBelow`/`hideFrom` + `Show`/`Hide`; MUI `sx={{ display: {xs:'none', md:'block'} }}`; Tailwind responsive `hidden md:block`. Industry consensus: express responsive visibility declaratively via CSS media, not JS measurement.
- **Phase 4 Use cases:** (simple) hide one end panel below `lg`; (configured) different breakpoints for start vs end panel — messaging-shell's exact case; (composed) inside `Layout` `start`/`end` slots alongside `ResizeHandle`; (edge) panel that is also `resizable` — hide takes precedence below breakpoint; (migration) delete the `useMediaQuery` + conditional and add `hideBelow`.
- **Phase 5 API:** `hideBelow?: Breakpoint` via `@media (max-width: …) { display: none }` in stylex. No new state, no measurement.
- **Phase 6 Surface audit:** no new exports — one prop on an existing component. Rejects the tempting `SplitFrame` wrapper as a duplicate of `Layout`.
- **Phase 7 Review:** utility/layout family; prop independence preserved (hide is orthogonal to `padding`/`isScrollable`/`resizable`); tokened breakpoints only.
- **Phase 8 Arbitration:** open item = prop name (`hideBelow` vs boolean) and whether to also add `hideFrom`/`showBelow` for symmetry; candidate for a quick vibe test if contested.
- **Phase 9 Finalize:** pending sign-off on `hideBelow` name + breakpoint token set. Note: this depends on the system having named breakpoint tokens exposed to stylex — confirm during build.
Contributor guide
Assessment
This issue has not been assessed yet.