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-docs-one-home.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
1 change: 0 additions & 1 deletion .claude/skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,4 @@ relevant.
| Skill | Use it for |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clerk-monorepo` | Day-to-day work in the monorepo: setup, build/test loops, the package map, changesets, commits, PRs, breaking-change checks. |
| `mosaic` | Mosaic flow UI: authoring machines, controllers, and views, and migrating a legacy component into the split (with parity verification). |
| `verify-clerk-expo` | Proving a `@clerk/expo` change on an iOS simulator or Android emulator, local or borrowed on a CI runner, with a video or screenshots as evidence. |
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ The skill is `.claude/skills/verify-clerk-expo/`, and the tests and the CLI that

## References

- For the Mosaic design system (tokens, StyleX authoring, `MosaicProvider`, the model/controller/view split, migration from the existing system), see `packages/mosaic/ARCHITECTURE.md`. Work under `packages/mosaic` also follows `packages/mosaic/AGENTS.md`.
- For the Mosaic design system (tokens, StyleX authoring, `MosaicProvider`, the model/controller/view split, migration from the existing system), start at `packages/mosaic/AGENTS.md`, which routes each task to its guide in `packages/mosaic/docs/`.
- For dev setup, testing, JSDoc/Typedoc, publishing, changesets, and commit conventions, see `docs/CONTRIBUTING.md`.
- For working in the repo day to day (setup ordering and footguns, the package map, dev-loop recipes, and the breaking-change checklist), the `clerk-monorepo` Claude Code skill in `.claude/skills/clerk-monorepo/` restates these rules in actionable form.
232 changes: 91 additions & 141 deletions packages/mosaic/AGENTS.md

Large diffs are not rendered by default.

592 changes: 0 additions & 592 deletions packages/mosaic/ARCHITECTURE.md

This file was deleted.

24 changes: 24 additions & 0 deletions packages/mosaic/docs/breaking-changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Breaking changes

Read this when a change removes or renames a public export, prop, `--cl-*` token or `.cl-` / `data-` styling hook, or changes a public type or default. It covers the changeset that ships it.

Worked example:

```md
---
'@clerk/mosaic': minor
---

**Breaking:** Rename a `UserButton` prop. Values and defaults are unchanged; TypeScript flags the old name.

| Before | After |
| -------------------- | ------------------ |
| `renderTriggerLabel` | `showTriggerLabel` |

Find usages with `git grep -n renderTriggerLabel`.
```

## Rules

- While `@clerk/mosaic` is on 0.x, a breaking change is a `minor` changeset.
- Start the body with `**Breaking:**`, then give an old-to-new table and a search that finds affected usage, so an agent can migrate from the changelog alone. The changelog ships in the npm package at `node_modules/@clerk/mosaic/CHANGELOG.md`.
404 changes: 100 additions & 304 deletions packages/mosaic/docs/controllers.md

Large diffs are not rendered by default.

45 changes: 21 additions & 24 deletions packages/mosaic/docs/documenting-components.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
# Documenting Mosaic components
# Documenting components

`packages/swingset/CLAUDE.md` explains _how the app works_ and `packages/swingset/README.md` covers the _mechanical wiring_ of adding a component (registry, `docModules`, redirect). This file is the **house style** — _what to write_ once the wiring is in place, so every component page reads consistently.
Read this when you add or change a Swingset page for a Mosaic component, block, hook or style: a `<name>.stories.tsx` or `<name>.mdx` in `packages/swingset/src/stories/`. It covers what to write once the page is wired; how the app works is in `packages/swingset/CLAUDE.md`, and the registry, `docModules` and redirect wiring is in `packages/swingset/README.md`.

A component's docs are two files in `packages/swingset/src/stories/`:
Worked examples: `button.stories.tsx` and `button.mdx` (archetype A, simple), `popover.component.mdx` (A, compound), `accordion.mdx` (B), `user-button.mdx` (C).

- `<name>.stories.tsx` — the live demos (named React exports + a `meta`).
- `<name>.mdx` — the prose page that embeds those demos via the injected MDX tags (`<Preview>`, `<PropTable>`, `<Usage>`, `<Story>`; see the **MDX** architecture note in `packages/swingset/CLAUDE.md` for the mechanism).
## Rules

Pick the archetype below by the component's **layer** (its `meta.group`), then follow that archetype's required MDX section order exactly. Same archetype → same headings in the same order, every time. That uniformity is the whole point.
- A page is a `<name>.stories.tsx` (a `meta`, plus named `export function` stories where the archetype has them; the code footer extracts only function declarations) and a `<name>.mdx`. The archetype decides which stories the file exports and which of `<Preview>`, `<PropTable>`, `<Usage>` and `<Story>` the page uses.
- Pick the archetype by the entry's layer (`meta.group`) and follow its section order exactly. Same archetype, same headings in the same order: that uniformity is the point.
- Always set `meta.source`, as a repo-root-relative path to the file that exports the component.
- Every props table has a **Default** column ([Document the default value](#document-the-default-value)).
- Prose adds what a demo cannot (behavior, accessibility, when to reach for it) and states the API as it is, without rationale or history ([Writing the prose](#writing-the-prose)).
- Import the component explicitly, never with `import *`.

## Layers

`meta.group` places an entry in one of these layers. Group order follows first appearance in the `registry` array. Groups in `ALPHABETICAL_GROUPS` (`packages/swingset/src/lib/registry.ts`) sort their entries by `title`; the rest render in registry order. Within a group, an optional `meta.navigation.category` sub-groups entries under a small collapsible subheading (e.g. `User Profile` splits into `Panels` and `Sections`), collapsed by default unless it contains the active page; category order also follows first appearance in the registry, and uncategorized entries render with no subheading (list them before the categorized ones). Use these exact group strings:
`meta.group` places an entry in one of these layers; how the sidebar orders groups and `meta.navigation.category` subheadings is in `packages/swingset/src/lib/registry.ts`. Use these exact group strings:

| Group | What lives here | Archetype |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
Expand All @@ -35,8 +39,6 @@ actually surfaces — an argument table for a style function, a return-value tab
`Hooks` entry needs only `meta`, with no story exports at all, which is the minimum a section entry
needs.

Archetype A has two forms, chosen by whether the component exposes a single flat set of variant props: **simple** components (`Button`, `Input`) are knob-driven; **compound** components (`Card`, `Field`, `Menu`, `Popover`) have no flat variant props to knob, so they're documented like a primitive but themed. Both are detailed under Archetype A below.

## `meta` conventions (all archetypes)

```ts
Expand All @@ -55,13 +57,10 @@ export const meta: StoryMeta = {
```

- `title` is the component's export name; it produces the slug and is what readers match against code. Set `label` only when the sidebar should read differently (the slug and page heading still come from `title`).
- `status` marks the entry's maturity — `'stable'`, `'wip'`, or `'todo'` (planned, not yet implemented; e.g. `Table`) — rendered as a colored dot (green / amber / gray) to the left of the entry in the sidebar, and as a dot-plus-label badge to the right of the page's `# Title` h1. It is explicit: omitting it displays no status anywhere (not every entry carries one, e.g. `Scroll Area`). A todo entry is `meta` alone plus a one-paragraph MDX page; it skips its archetype's required sections (and `source`) until the component exists.
- `substatus` optionally says _why_ a wip entry is wip — one of the `WipSubstatus` values in `packages/swingset/src/lib/types.ts`. It renders in the page badge (`wip: needs wire-up`) and the sidebar dot's tooltip. Most wip entries won't need one; it is ignored on stable entries.
- `source` is always a path **relative to the monorepo root**, pointing at the file that exports the documented component. Always set it — it powers the "View source" link.
- `status` is `'stable'`, `'wip'` or `'todo'` (planned, not yet built), shown as a dot in the sidebar and a badge on the page. Omit it to show no status. A todo entry is `meta` plus a one-paragraph MDX page, and skips its archetype's sections and `source` until the component exists.
- `substatus` optionally says why a wip entry is wip, as a `WipSubstatus` value (`packages/swingset/src/lib/types.ts`).
- `styles` declares the component's variant props and is **required for archetype A's simple (knob-driven) form** (it generates the knobs and the `<PropTable>`). Keep it in sync with the component's real prop union by hand — StyleX compiles its styles away, so nothing derives it for you. Omit it for compound A components, and for B and C.

Always import the component explicitly — never `import *`.

## Archetype A — styled component (`Components`)

A styled Mosaic component. Which of the two forms below applies is decided by the component's shape, not by preference: if it exposes a single flat set of variant props (declared as `meta.styles`), use the **simple** form; if it's compound — a set of parts with no flat variant props (`Card`, `Field`, `Menu`, `Popover`) — use the **compound** form.
Expand All @@ -72,7 +71,7 @@ A styled Mosaic component. Which of the two forms below applies is decided by th
export { default as __source } from './<name>.stories?raw';
```

That's all the wiring needed — `StoryEmbed` picks `__source` up automatically and renders a collapsible "View code" footer keyed to each example's story function (see the `<Story>` code-footer architecture note in `packages/swingset/CLAUDE.md`). No MDX change is required; keep authoring `<Story name='…' storyModule={…} />` as before. This applies to both A forms (simple and compound) and to every example a Components page ships.
`StoryEmbed` picks it up and renders a "View code" footer for each `<Story>`; no MDX change is needed. This applies to both A forms.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

### A · simple — flat variant props (`Button`, `Input`)

Expand Down Expand Up @@ -128,7 +127,6 @@ import * as ButtonStories from './button.stories';
- The story file exports a primary demo (rendered by `<Preview>`) plus one named export per variation under **Examples**. Each story takes `props: Record<string, unknown>` and casts through a local `knobsAsProps` helper — knobs are dynamically typed, the component isn't.
- Use `<PropTable>`'s `extra` for documenting non-variant props; the `xstyle` escape-hatch row is appended for you.
- Use `<Usage props={{…}}>` to pin static, non-knob props in the generated snippet.
- `<PropTable>` renders `Prop | Type | Default | Value`: the **Default** column is filled from `meta.styles._defaultVariants`, and the **Value** column is the live knob seeded with that default. No per-row default annotation is needed; see _Document the default value_ under Archetype B.

### A · compound — parts, no flat variant props (`Card`, `Field`, `Menu`, `Popover`)

Expand Down Expand Up @@ -202,7 +200,9 @@ No styles, so there's no knob canvas. The single demo renders the primitive **ra

The story is `meta` (no `styles`) plus a single `Default` export that renders the primitive unstyled. Don't add a `<Preview>` or `<PropTable>` — primitives have neither knobs nor a variant surface to drive them.

**Document the default value for every prop in a dedicated Default column.** Every props table — auto and hand-written — has a **Default** column; the `Type` stays a plain union/enum and the default is named in its own column (the convention every component-doc site and TypeDoc's `@default` tag follow), never inlined into the type. The auto `<PropTable>` renders `Prop | Type | Default | Value` and fills Default from `meta.styles._defaultVariants` (the **Value** column is the live knob seeded with that default); hand-written tables render `Prop | Type | Default | Description` and fill it by hand. Name the default member (`'base'`, `'multiple'`, `'bottom-start'`); use `—` when there is no default (a controlled-only or required prop) and append `(required)` for required props; when the default is behavioral rather than a literal, state it in words (`inherits Root`, `falls back to value`).
### Document the default value

Every props table has a **Default** column, and the `Type` stays a plain union, never with the default inlined. The auto `<PropTable>` fills it from `meta.styles._defaultVariants`; hand-written tables (`Prop | Type | Default | Description`) fill it by hand. Name the default member (`'bottom-start'`), use `—` when there is none and append `(required)` for a required prop, and state a behavioral default in words (`inherits Root`).

## Archetype C — composed layer (`User Button`, `User Profile`, `Organization Profile`, `Misc`, `Blocks`)

Expand Down Expand Up @@ -232,12 +232,9 @@ import * as SwitcherStories from './user-button.stories';

## Writing the prose

- Keep the intro to one short, present-tense paragraph: what the thing is and what it's for. For primitives, say explicitly that it's headless and ships no styles.
- Prose should add what a demo can't — behavior, accessibility, when to reach for it — not restate prop names already in the table.
- Describe the API as it is; leave the reasoning that produced it out. No "not a `size`, because…", no rejected alternatives, no history of what a prop used to be — that belongs in the PR description. The docs get the conclusion, stated plainly and briefly.
- Lead every page with the heading hierarchy its archetype prescribes; don't invent new top-level sections or reorder them. Consistency across pages is the goal.

Every sentence of rationale is one the reader has to skim past to find what the thing does:
- Keep the intro to one short, present-tense paragraph: what the thing is and what it's for. For primitives, say it is headless and ships no styles.
- Do not restate prop names already in the table.
- Leave out the reasoning behind the API: no "not a `size`, because…", no rejected alternatives, no history. That belongs in the PR description.

```mdx
<!-- no -->
Expand All @@ -262,6 +259,6 @@ Which surface the dialog holds, and the geometry that comes with it.
- [ ] Story renders.
- [ ] `Components`-layer story files export `__source` (the `?raw` self-import) so every `<Story>` example gets a "View code" footer.
- [ ] MDX sections match the archetype's required order exactly.
- [ ] Every props-table row states its default in the **Default** column (auto `<PropTable>` fills it from `meta.styles._defaultVariants`; `—` / `(required)` when none).
- [ ] Every props-table row states its default in the **Default** column.
- [ ] Wiring done per `packages/swingset/README.md`: `registry.ts`, `DocsViewer.tsx`'s `docModules`, and the `app/(explorer)/page.tsx` redirect if this is now the first component.
- [ ] `pnpm format --filter @clerk/swingset` is clean.
49 changes: 49 additions & 0 deletions packages/mosaic/docs/features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Features

Read this when you add a feature, or a file to one. It covers the model → controller → view split, the files a feature is made of, and how a wrapper composes them; each layer's own rules are in [Models](models.md), [Controllers](controllers.md) and [Views](views.md).

Worked example: `src/features/reverification/` (model, controller, view, types, messages, wrapper and a feature test).

## Rules

- Before you add a feature, check `packages/ui/src/components/` for a legacy version. If one exists, this is a migration: read [Migration](migration.md) first.
- Only a `*.model.ts(x)`, or a shared Clerk-reading hook in `src/hooks/` (`useMosaicEnvironment`, `useMosaicRouter`, `useMosaicSupportEmail`), imports Clerk hooks or calls Clerk resource methods.
- What crosses each boundary is plain data: no Clerk resource reaches the controller, and no machine snapshot reaches the view.
- A feature's machine lives in its `*.controller.ts(x)`, not in `src/machine/` and not in a `*.machine.ts`.
- Import from source paths inside `packages/mosaic`; there are no published subpath barrels.
- A flow is tested as a whole, not per layer ([Testing](testing.md)).

## The layers

Read the split as _where the data comes from_ (model) → _what the user is doing to it_ (controller) → _what that looks like_ (view); the one-paragraph summary is in [AGENTS.md → Layers](../AGENTS.md#layers-in-one-paragraph). A machine is not a layer. It is one of two ways a controller holds its state, chosen by the interaction's complexity ([Controllers → Which one holds the state](controllers.md#which-one-holds-the-state)).

## File shape

A feature lives in `src/features/<feature>/`, one file per role, each prefixed with the feature name. Use `.ts` unless the file renders JSX.

| File | Holds |
| ---------------------------- | --------------------------------------------------------------------------- |
| `<feature>.model.ts(x)` | The Clerk adapter |
| `<feature>.controller.ts(x)` | Local state, a machine if it has one, and action wrapping |
| `<feature>.view.tsx` | Rendering only |
| `<feature>.tsx` | The composition wrapper and the public props type |
| `<feature>.types.ts` | The data contract the model and view share |
| `<feature>.messages.ts` | Every string the feature renders ([Views](views.md#where-the-strings-live)) |
| `<feature>.layout.ts` | Pure derivation, such as which affordance goes in which slot |
| `<feature>.utils.ts` | Pure helpers |
| `<feature>.styles.ts` | `stylex.create` atoms ([StyleX](stylex.md)) |
| `<feature>.skeleton.tsx` | The loading placeholder ([Skeletons](skeletons.md)) |
| `<name>.dialog.tsx` | A dialog the view opens, when it would crowd the view file |

`*.types.ts` holds the contract so that neither the model nor the view owns it, and the two cannot drift.

The other folders under `src/` are named for what they hold; `src/components/` has one folder per component and nothing else. Shared helpers are in the table in [AGENTS.md](../AGENTS.md#reuse-before-you-write), and test helpers are under `src/__tests__/`.

## Composition

Two shapes, chosen by whether the slice fetches its own data.

- **A wrapper composes the layers** when the slice is a connected component. It calls the model, hands it to the controller, branches on `status`, and renders the view with the rest (`src/features/switcher/switcher.tsx`). `loading` renders the caller's `fallback`; `hidden` renders nothing, because signed out is an answer, not a wait.
- **A view owns its controller** when the slice is a leaf that takes its effect as a prop. There is no model: the Clerk call arrives from whoever renders it (`UserProfileUsernameRowView` calls `useUserProfileEditUsernameController` with its `onSubmit`).

Either way, the controller never imports Clerk, and its effects arrive as injected plain functions.
Loading
Loading