Storybook + component API
Load the one file that answers your question.
| File |
The question it answers |
rules/props-state-shape.md |
What shape should this prop be — object or scalars, union or flags, controlled or not — and does it deserve to exist? |
rules/composition-and-slots.md |
Should this be children, a prop, a slot, context, or a compound API — and what does a slotted element owe the primitive? |
rules/vocabulary-registries.md |
Where do a status's label, icon and colour come from, and when does a badge render nothing? |
rules/story-titles.md |
Where does this story land in the sidebar, and does it need an explicit title? |
rules/story-args-and-coverage.md |
Which states earn a story, and how do the Controls stay wired to the real component? |
rules/play-assertions.md |
Can this play function fail, and how do I assert against a portal or a just-opened overlay? |
rules/a11y.md |
What does axe not catch here, and what must a component name for itself? |
traps.md |
Something passed that should not have. Read this before debugging a green-but-wrong story. |
RUBRIC.md |
Grading a diff across nine dimensions, D→A+, with the anti-criteria that look like quality and are not. |
Already enforced — do not re-litigate, and do not restate in prose
These fail vp run check. Treat a violation as a build error, not a style opinion, and do not
write a guideline that repeats one.
The house rules are registered in webapp/tools/oxlint/index.ts — read it rather than trusting a
list, since a rule can be added without this file changing. Those that reach a story file:
hephaestus/typed-story-meta — a meta naming a component must be satisfies Meta<typeof X>; a
gallery meta naming no component may be bare Meta.
hephaestus/play-must-assert — a play that never reaches an assertion. It reads a getBy* used
as a click target as an assertion, so it holds only the floor; whether the play checks the
outcome is still a review question.
hephaestus/no-story-a11y-override — parameters.a11y or globals.a11y on a meta or a story.
Either one alone takes the component out of the accessibility suite while it still reports green.
hephaestus/no-redundant-in-the-document — expect(getBy…).toBeInTheDocument(). A bare
await expect(getBy…) is vitest/valid-expect, which catches it for every subject.
hephaestus/no-within-canvas-element — within(canvasElement) when the play function was handed
canvas.
The ones that only make sense in a story file are scoped to **/*.stories.tsx in the overrides
block of webapp/.oxlintrc.json rather than named in its top-level rules. A house rule missing
from both is simply off, and nothing reports that.
Beyond oxlint:
scripts/check-story-prose.ts (gate:stories) — <p> in a comment Storybook publishes.
scripts/check-story-sort.ts (gate:story-sort) — a title segment missing from storySort.order,
or an order entry no story claims.
scripts/check-presentational-components.ts (gate:components) — a component importing the
query layer, and a story installing MSW handlers. Its allowlist is shrink-only.
webapp/.storybook/preview.tsx sets a11y: { test: "error" } project-wide. Adding a per-story
override is a finding, not a fix.
Not here
/composition-patterns owns the React 19 API shape (react19-no-forwardref), render props, and
the generic compound-component pattern. This skill states only what those cost here.
1---2name: storybook-components3description: Component API and Storybook conventions for the Hephaestus webapp. Use when writing or changing a component under `webapp/src/components/**`, writing or reviewing a `*.stories.tsx`, designing a component's props, or grading a webapp diff in review. Covers props/state shape, Base UI `render=` slots, the vocabulary registries, story titles, args and Controls, play-function assertions, and the accessibility posture. Ships a grading rubric.4---56# Storybook + component API78Load the one file that answers your question.910| File | The question it answers |11|---|---|12| `rules/props-state-shape.md` | What shape should this prop be — object or scalars, union or flags, controlled or not — and does it deserve to exist? |13| `rules/composition-and-slots.md` | Should this be `children`, a prop, a slot, context, or a compound API — and what does a slotted element owe the primitive? |14| `rules/vocabulary-registries.md` | Where do a status's label, icon and colour come from, and when does a badge render nothing? |15| `rules/story-titles.md` | Where does this story land in the sidebar, and does it need an explicit `title`? |16| `rules/story-args-and-coverage.md` | Which states earn a story, and how do the Controls stay wired to the real component? |17| `rules/play-assertions.md` | Can this play function fail, and how do I assert against a portal or a just-opened overlay? |18| `rules/a11y.md` | What does axe not catch here, and what must a component name for itself? |19| `traps.md` | Something passed that should not have. Read this before debugging a green-but-wrong story. |20| `RUBRIC.md` | Grading a diff across nine dimensions, D→A+, with the anti-criteria that look like quality and are not. |2122## Already enforced — do not re-litigate, and do not restate in prose2324These fail `vp run check`. Treat a violation as a build error, not a style opinion, and do not25write a guideline that repeats one.2627The house rules are registered in `webapp/tools/oxlint/index.ts` — read it rather than trusting a28list, since a rule can be added without this file changing. Those that reach a story file:2930- `hephaestus/typed-story-meta` — a `meta` naming a `component` must be `satisfies Meta<typeof X>`; a31 gallery meta naming no component may be bare `Meta`.32- `hephaestus/play-must-assert` — a `play` that never reaches an assertion. It reads a `getBy*` used33 as a click target as an assertion, so it holds only the floor; whether the play checks the34 **outcome** is still a review question.35- `hephaestus/no-story-a11y-override` — `parameters.a11y` or `globals.a11y` on a meta or a story.36 Either one alone takes the component out of the accessibility suite while it still reports green.37- `hephaestus/no-redundant-in-the-document` — `expect(getBy…).toBeInTheDocument()`. A bare38 `await expect(getBy…)` is `vitest/valid-expect`, which catches it for every subject.39- `hephaestus/no-within-canvas-element` — `within(canvasElement)` when the play function was handed40 `canvas`.4142The ones that only make sense in a story file are scoped to `**/*.stories.tsx` in the `overrides`43block of `webapp/.oxlintrc.json` rather than named in its top-level `rules`. A house rule missing44from both is simply off, and nothing reports that.4546Beyond oxlint:4748- `scripts/check-story-prose.ts` (`gate:stories`) — `<p>` in a comment Storybook publishes.49- `scripts/check-story-sort.ts` (`gate:story-sort`) — a title segment missing from `storySort.order`,50 or an `order` entry no story claims.51- `scripts/check-presentational-components.ts` (`gate:components`) — a component importing the52 query layer, and a story installing MSW handlers. Its allowlist is shrink-only.53- `webapp/.storybook/preview.tsx` sets `a11y: { test: "error" }` project-wide. Adding a per-story54 override is a finding, not a fix.5556## Not here5758`/composition-patterns` owns the React 19 API shape (`react19-no-forwardref`), render props, and59the generic compound-component pattern. This skill states only what those cost *here*.