AppShell: region-transform plugin system + Focus Mode (first built-in plugin)
- Dominant language
- TypeScript
- Stars
- 13.1k
- Forks
- 1.1k
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 687
Description
## Summary
Introduce an **AppShell plugin system** that lets consumers transform each shell region, and use it to ship **Focus Mode** (a coordinated shell state that hides nav regions and expands the body, animated). This adopts the same shape as the existing `Table` plugin model (a named record of transform functions) and applies it to AppShell's fixed named regions.
Origin: reviewer feedback on #4174 ("animate nav surface enter/exit") reframed the work as a coordinated Focus Mode. Rather than a single `focusMode` prop, the direction is a **general region-transform plugin API** — Focus Mode becomes the first built-in plugin.
## Proposed plugin shape
An AppShell plugin is an object of optional per-region transform methods, mirroring `TablePlugin`'s `transformColumns`/`transformTable`/etc.:
```ts
interface AppShellPlugin {
transformShellContext?: (children: ReactNode) => ReactNode;
transformBanner?: (props: BannerProps) => BannerProps;
transformTopNav?: (props: TopNavProps) => TopNavProps;
transformSideNav?: (props: SideNavProps) => SideNavProps;
transformContent?: (props: ContentProps) => ContentProps;
transformMobileNav?: (props: MobileNavProps) => MobileNavProps;
}
// on AppShellProps:
plugins?: Record;
```
Each region's props pass through the (composed) plugin transforms before render. `transformShellContext` wraps the whole subtree (for providers), paralleling `Table`'s `transformTableContext`.
Focus Mode ships as a built-in plugin (likely a hook, e.g. `useFocusMode({ isActive, preset })`) that returns transforms setting the relevant regions hidden and expanding content — consistent with how first-party Table plugins are hooks returning plugin objects.
## Design considerations (for the spec)
- **Composition / ordering.** Table sorts plugins into a canonical order and feeds one plugin's output into the next. Decide the equivalent for AppShell: canonical order? last-writer-wins per region? How do two plugins that both transform `sideNav` compose? (Table's `useBaseTablePlugins` is the reference.)
- **Region prop types must be public + stable.** `BannerProps`/`TopNavProps`/`SideNavProps`/`ContentProps`/`MobileNavProps` become part of the plugin contract — they need to be exported and treated as API surface. Confirm each exists and is export-ready (Content in particular).
- **Hidden ≠ unmounted.** Reuse the existing `React.Activity mode="hidden"` path already used for the mobile drawer (`ActivityWrapper`) so hidden regions stay mounted but inert (out of tab order + a11y tree, state preserved). A region hidden by a transform should go through this, not `display:none`.
- **Animation.** Reuse motion tokens (`--duration-medium` / `--ease-standard`) with a `prefers-reduced-motion` guard, matching MobileNav. No animation on initial mount — regions mount already in their final state; transitions only fire on later changes (guard enter transitions with `@starting-style`, never on first paint). No animation-config props.
- **Focus Mode presets.** Decide the preset vocabulary (e.g. `focus` / `present` / `immersive`) and whether presets are augmentable (module-augmentation precedent: `AppShellVariantMap`).
- **Relationship to SideNav `collapsible`.** Define the boundary between a coordinated Focus Mode and SideNav's existing single-region `collapsible` so there aren't two ways to do the same thing.
- **Non-breaking.** `plugins` absent = today's behavior.
## Background research
A prior design exploration weighed three shapes — a single `focusMode` union prop (A), this region-transform plugin system (B), and per-region boolean props (C) — against in-repo precedents (`Table` plugins, `mobileNav` config union, `AppShellVariantMap` augmentation) and external systems (VS Code Zen Mode, shadcn Sidebar, Mantine AppShell). That writeup documents the tradeoffs of the plugin approach (composition/conflict-resolution cost, discoverability) and the mechanisms to reuse (Activity, motion tokens, zero-jank first paint). It should inform the spec; the plugin direction is the chosen path here.
## Acceptance
- Follows the full Specification Protocol (this is an API change / new extensibility surface) — **spec reviewed before build**.
- `plugins?: Record` with the per-region transform methods above; documented composition/ordering semantics.
- Focus Mode delivered as the first built-in plugin; hidden regions via `Activity mode="hidden"`; token-based motion + reduced-motion; no first-paint animation.
- Region prop types exported and treated as stable API.
- Non-breaking defaults; tests + stories + docs.
Contributor guide
Assessment
This issue has not been assessed yet.