facebook / facebook/astryx

LayoutPanel: responsive hideBelow prop (replace hand-rolled useMediaQuery panel hiding in templates)

Open
#3,339 0 comments 0 reactions 1 assignee Claimed by @cixzhang View on GitHub
component enhancement
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.