Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/mosaic-flow-component-guidance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
9 changes: 8 additions & 1 deletion .claude/skills/mosaic/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/mosaic/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |

Expand All @@ -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

Expand Down
45 changes: 45 additions & 0 deletions packages/mosaic/docs/views.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<Card.Root renderBranding={renderBranding}>
<Flow.Root
value={step}
direction={step === 'list' ? -1 : 1}
state={props}
>
{current => (
<>
<Flow.Step ids={['list']}>
<ListStep {...current} />
</Flow.Step>
<Flow.Step ids={['create']}>
<CreateStep {...current} />
</Flow.Step>
</>
)}
</Flow.Root>
</Card.Root>
```

- **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
Expand Down
Loading