Repository navigation
docs(mosaic): move the swingset docs house style into the mosaic skill #10634
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| --- | ||
| --- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,267 @@ | ||
| # Documenting Mosaic 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. | ||
|
|
||
| A component's docs are two files in `packages/swingset/src/stories/`: | ||
|
|
||
| - `<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). | ||
|
|
||
| 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. | ||
|
|
||
| ## 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: | ||
|
|
||
| | Group | What lives here | Archetype | | ||
| | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------- | | ||
| | `User Button` | Composed flow UI (e.g. `UserButton`) | C | | ||
| | `User Profile` | Composed flow UI (e.g. `UserProfileProfilePanel`) | C | | ||
| | `Organization Profile` | Composed flow UI (e.g. `OrganizationProfileGeneralPanel`) | C | | ||
| | `Misc` | Standalone composed components (e.g. `OrganizationSwitcher`, `Waitlist`) | C | | ||
| | `Blocks` | Flows assembled from components, wired by the caller's machine (e.g. `Confirmation`, `Destructive`) | C | | ||
| | `Components` | Styled Mosaic components — simple, with a flat variant surface (`Button`, `Input`), or compound (`Card`, `Field`, `Menu`, `Popover`) | A | | ||
| | `Primitives` | Headless primitives from `@clerk/mosaic`'s `src/primitives/` (`Accordion`) | B | | ||
| | `Styles` | Atomic styles that ship as StyleX atoms, not components (`Scroll Area`) | B (adapted) | | ||
| | `Hooks` | Headless hooks (`useForm`) | B (adapted) | | ||
| | `Localization` | The `localization` prop on `MosaicProvider`: catalogs, overrides, locale, and the message helpers | B (adapted) | | ||
|
|
||
| `User Button` / `User Profile` / `Organization Profile` / `Misc` / `Blocks` → `Components` → `Primitives` runs high-level-composition → low-level-primitive. Composed layers are documented as compositions of lower layers (archetype C); leaf layers (Components, Primitives) get full prop/knob docs (archetypes A and B). | ||
|
|
||
| `Styles` and `Hooks` are the non-component layers: there is no element to knob, so they follow | ||
| archetype B's shape (Example → Usage → Parts → Styling) with `Props` replaced by whatever the export | ||
| actually surfaces — an argument table for a style function, a return-value table for a hook. A | ||
| `Styles` entry documents the tokens its atoms read, since those tokens _are_ its API; a | ||
| `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 | ||
| export const meta: StoryMeta = { | ||
| group: 'Components', // exact group string from the table | ||
| title: 'Button', // drives slug + the page <h1> | ||
| label: 'Delete Org', // optional friendlier sidebar text | ||
| status: 'stable', // maturity dot in sidebar + page badge; omit to show no status | ||
| source: 'packages/mosaic/src/components/button/button.tsx', // repo-root path → "View source" | ||
| styles: { | ||
| // Hand-written variant surface — archetype A · simple only | ||
| _variants: { variant: { primary: {}, outline: {} }, size: { sm: {}, md: {} } }, | ||
| _defaultVariants: { variant: 'primary', size: 'md' }, | ||
| }, | ||
| }; | ||
| ``` | ||
|
|
||
| - `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. | ||
| - `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. | ||
|
|
||
| **Every `Components`-layer story file exposes its source so each `<Story>` example renders a code footer.** Add the self-import once, right after the imports: | ||
|
|
||
| ```ts | ||
| 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. | ||
|
Comment on lines
+69
to
+75
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| ### A · simple — flat variant props (`Button`, `Input`) | ||
|
|
||
| Has a declared variant surface, so the page is **knob-driven**: an interactive canvas plus a generated prop table. Required MDX section order: | ||
|
|
||
| ```mdx | ||
| import * as ButtonStories from './button.stories'; | ||
|
|
||
| # Button | ||
|
|
||
| <!-- One present-tense paragraph: what it is, what it's for. --> | ||
|
|
||
| ## Playground | ||
|
|
||
| <Preview | ||
| name='Primary' | ||
| storyModule={ButtonStories} | ||
| /> | ||
|
|
||
| ## Props | ||
|
|
||
| <PropTable meta={ButtonStories.meta} /> | ||
|
|
||
| ## Usage | ||
|
|
||
| <Usage | ||
| component='Button' | ||
| module='@clerk/mosaic/components/button' | ||
| > | ||
| Click me | ||
| </Usage> | ||
|
|
||
| --- | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Sizes | ||
|
|
||
| <Story | ||
| name='Sizes' | ||
| storyModule={ButtonStories} | ||
| /> | ||
|
|
||
| ### Disabled | ||
|
|
||
| <Story | ||
| name='Disabled' | ||
| storyModule={ButtonStories} | ||
| /> | ||
| ``` | ||
|
|
||
| - **Playground / Props / Usage are mandatory and always in this order.** The three share one playground state: editing a row in `<PropTable>` re-renders `<Preview>` above it and regenerates the `<Usage>` snippet below it. | ||
| - 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`) | ||
|
|
||
| A compound styled component (`Popover.Root`/`Popover.Popup`/…) has no single flat prop interface to knob, so there's no `<Preview>` or `<PropTable>`. Document it like a primitive (archetype B) but themed — the difference is the **Styling** section, which lists each part's `.cl-<slot>` class and `data-<axis>` attributes rather than saying "bring your own CSS". Required MDX section order: | ||
|
|
||
| ```mdx | ||
| import * as PopoverStories from './popover.component.stories'; | ||
|
|
||
| # Popover | ||
|
|
||
| <!-- Intro: state it's the styled Mosaic component built on the underlying headless | ||
| primitive, and that it inherits the primitive's behavior/ARIA. --> | ||
|
|
||
| ## Example | ||
|
|
||
| <Story | ||
| name='Default' | ||
| storyModule={PopoverStories} | ||
| /> | ||
|
|
||
| ## Usage | ||
|
|
||
| <!-- A code fence showing the compound parts composed together. --> | ||
|
|
||
| ## Parts | ||
|
|
||
| <!-- Table: part | class (`.cl-<slot>`) | description. --> | ||
|
|
||
| ## Styling | ||
|
|
||
| <!-- Each part's `.cl-<slot>` class and the `data-<axis>` attributes it reflects, plus | ||
| the `--cl-*` tokens it reads, with a CSS example. --> | ||
| ``` | ||
|
|
||
| The story is `meta` (no `styles`) plus a single `Default` export that renders the composed parts. The file pair is named `<name>.component.stories.tsx` / `<name>.component.mdx` so it doesn't collide with the headless `Primitives` entry of the same title (e.g. `Menu`, `Popover` exist in both layers); the `docModules` map disambiguates by group. | ||
|
|
||
| ## Archetype B — headless primitive (`Primitives`) | ||
|
|
||
| No styles, so there's no knob canvas. The single demo renders the primitive **raw (unstyled)** to show only behavior, state, and ARIA wiring. The prop/styling tables are **hand-written** (there is no variant surface to derive them from). Required MDX section order: | ||
|
|
||
| ```mdx | ||
| # Accordion | ||
|
|
||
| <!-- Intro: state plainly that it's a headless primitive that ships no styles, and | ||
| that you target its `data-*` state attributes for appearance. --> | ||
|
|
||
| ## Example | ||
|
|
||
| <Story name='Default' storyModule={AccordionStories} /> <!-- the one thing prose can't show --> | ||
|
|
||
| ## Usage | ||
|
|
||
| <!-- Code fences: basic, then a Controlled example, then notable variations. --> | ||
|
|
||
| ## Parts | ||
|
|
||
| <!-- Table: compound part | default element | description. Note `render` polymorphism | ||
| and which parts throw outside their parent. --> | ||
|
|
||
| ## Props | ||
|
|
||
| <!-- One prop table per part (### `Accordion.Root`, ### `Accordion.Item`, …), | ||
| columns: Prop | Type | Default | Description. Every row declares its default | ||
| in the Default column — see the default-value note below. --> | ||
|
|
||
| ## Styling | ||
|
|
||
| <!-- Table of the `data-*` state attributes each part emits, plus any exposed CSS | ||
| custom properties (e.g. `--cl-accordion-panel-height`) with a CSS example. --> | ||
| ``` | ||
|
|
||
| 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`). | ||
|
|
||
| ## Archetype C — composed layer (`User Button`, `User Profile`, `Organization Profile`, `Misc`, `Blocks`) | ||
|
|
||
| These compose lower layers, so the docs lead with the composition rather than knobs. Required MDX: | ||
|
|
||
| ```mdx | ||
| import * as SwitcherStories from './user-button.stories'; | ||
|
|
||
| # UserButton | ||
|
|
||
| <!-- One paragraph: what state this owns and which lower-layer pieces it wires together. --> | ||
|
|
||
| <Story | ||
| name='Default' | ||
| storyModule={SwitcherStories} | ||
| composition={[ | ||
| { name: 'Avatar', href: '/components/avatar', layer: 'Components' }, | ||
| { name: 'Item', href: '/components/item', layer: 'Components' }, | ||
| { name: 'Menu', href: '/components/menu', layer: 'Components' }, | ||
| { name: 'Popover', href: '/components/popover', layer: 'Components' }, | ||
| ]} | ||
| /> | ||
| ``` | ||
|
|
||
| - The `composition` array names each lower-layer component this one builds on, with a link to that component's page and its `layer`. List every direct dependency so the layering is navigable. | ||
| - The story is `meta` plus a single `Default` export that renders the composed UI with no knobs. | ||
|
|
||
| ## 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: | ||
|
|
||
| ```mdx | ||
| <!-- no --> | ||
|
|
||
| ### Variant | ||
|
|
||
| Which surface the dialog holds, and so the geometry it is given. Not a `size`, | ||
| because these are different surfaces rather than one surface at two widths — a | ||
| second card width would be a size of the `card` variant, with nowhere to sit on | ||
| this axis. | ||
|
|
||
| <!-- yes --> | ||
|
|
||
| ### Variant | ||
|
|
||
| Which surface the dialog holds, and the geometry that comes with it. | ||
| ``` | ||
|
|
||
| ## Before you finish | ||
|
|
||
| - [ ] `meta.source` is set to a repo-root-relative path. | ||
| - [ ] 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). | ||
| - [ ] 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. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: clerk/javascript
Length of output: 41641
Make the
meta.sourcerequirement conditional fortodoentries.Line 55 exempts
todoentries fromsourceuntil the component exists. Line 57 requiressourceon every entry, and the checklist at line 258 repeats that rule. An author who follows line 57 for a planned entry must setsourceto a file that does not exist yet. The "View source" link built from that path can then point to a broken page.State the requirement for implemented entries, and make the checklist match it.
📝 Suggested fix