facebook / facebook/astryx

AppShell: region-transform plugin system + Focus Mode (first built-in plugin)

Open
#4,255 0 comments 0 reactions 1 assignee Claimed by @humbertovirtudes View on GitHub
component enhancement needs-scoping
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

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.