From 35de167abfbee5b379214fcb28ed586878af42a4 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 2 Nov 2026 13:35:00 -0500 Subject: [PATCH] docs(mosaic): document Flow for moving between screens on one surface --- .changeset/mosaic-flow-component-guidance.md | 2 + .claude/skills/mosaic/SKILL.md | 9 +++- packages/mosaic/AGENTS.md | 2 + packages/mosaic/docs/views.md | 45 ++++++++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 .changeset/mosaic-flow-component-guidance.md diff --git a/.changeset/mosaic-flow-component-guidance.md b/.changeset/mosaic-flow-component-guidance.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/mosaic-flow-component-guidance.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.claude/skills/mosaic/SKILL.md b/.claude/skills/mosaic/SKILL.md index a59614fe2b8..88844ed63a1 100644 --- a/.claude/skills/mosaic/SKILL.md +++ b/.claude/skills/mosaic/SKILL.md @@ -5,7 +5,8 @@ description: >- tokens, `themeProps`), or building a flow — writing the model (the Clerk adapter), the controller (local state, in React state or a state machine: `setup`, states/guards/`invoke`, wired to React with `useMachine`/`useActor`/ - `useSelector`), or the view (rendering), testing a feature, or + `useSelector`), or the view (rendering, including moving between screens with + the `Flow` component), testing a feature, or migrating a legacy / pre-Mosaic component into the model / controller / view split. Use when building, styling, debugging, testing, or migrating anything Mosaic. `packages/mosaic/ARCHITECTURE.md` holds the design-system @@ -26,6 +27,12 @@ Two things live under Mosaic, and this skill covers the how-to for both: each boundary is plain data: no Clerk resource reaches the controller, no machine snapshot reaches the view. +"Flow" means two things here. A feature's model, controller and view are its +flow. `Flow` from `src/components/flow/` is the component that moves between +screens on one card or dialog. If a screen replaces another on the same surface, +use the component: a conditional makes the new screen pop in. See "Moving between +screens on one surface" in `packages/mosaic/docs/views.md`. + ```text model Clerk adapter: reads Clerk hooks and resources, resolves the environment, gates permissions, and answers with plain data plus diff --git a/packages/mosaic/AGENTS.md b/packages/mosaic/AGENTS.md index dd34d96395c..9f623df174d 100644 --- a/packages/mosaic/AGENTS.md +++ b/packages/mosaic/AGENTS.md @@ -60,6 +60,7 @@ Most problems a feature hits already have a shared answer. Find the row for your | Siblings that slide when an item is added or removed | `useLayoutAnimation` (`src/primitives/hooks/`) | | An input that reformats as the user types without moving the caret | `useFormattedInput` (`src/primitives/hooks/`) | | A panel that animates open and closed to its measured size | `usePanelSize` (`src/primitives/hooks/`) | +| One screen replacing another on the same card or dialog | `Flow` (`src/components/flow/`), see [Views](docs/views.md) | | An interaction with an async lifecycle or two values that move together | A machine in the controller file, via `setup()` (`src/machine/`) | | Styles several components share | `src/styles/*.styles.ts` | @@ -73,6 +74,7 @@ Stop and check the table if you are writing any of these in a feature: - a ref map of buttons to restore focus after a removal - `setTimeout` to debounce or to delay a spinner - `stylex.create` inside a `.tsx` file +- a conditional or early return that swaps one screen for another on the same card or dialog ## Nested radii diff --git a/packages/mosaic/docs/views.md b/packages/mosaic/docs/views.md index 3d20e6fb13f..626b3cfe427 100644 --- a/packages/mosaic/docs/views.md +++ b/packages/mosaic/docs/views.md @@ -42,6 +42,51 @@ Two shapes, chosen by whether the slice fetches its own data: (`UserProfileUsernameRowView` calls `useUserProfileEditUsernameController` with its `onSubmit`). Still no Clerk — the effect arrives from above. +## Moving between screens on one surface + +When one screen replaces another on the same card or dialog, render both as steps +of `Flow` (`components/flow`). The surface stays put, its height animates, and the +outgoing screen stays mounted until its exit finishes. A conditional or an early +return makes the new screen pop in instead. + +```tsx + + + {current => ( + <> + + + + + + + + )} + + +``` + +- **The surface sits outside the flow, and a step renders only what goes inside + it.** A screen used as a step of another feature cannot bring its own `Card.Root`. + `ConfirmShell` owns the card and `Reverification` renders only its contents. +- **Nest a flow when a step has steps of its own.** The outer flow keeps its step + mounted, and `useFlowAutoFocus` reaches the inner active step. +- **The controller owns the step.** Going back is direction `-1`. Derive it from the + step when each step has one way in, and hold it in the controller when it does not + (`reverification.controller.ts`). +- **Mark what takes focus with `useFlowAutoFocus`.** Focus moves only for a step + that transitions in, so a flow that opens on a step does not take focus. + +A loading or error state of a single screen is not a step, and neither is the +skeleton shown before a feature has data. Those can stay conditionals. + +The parts, data attributes and CSS variables are in +`src/primitives/flow/README.md`. + ## Where the strings live Every string a view renders comes from the feature's `*.messages.ts`, shaped the