diff --git a/.plop/templates/component/component.mdx.hbs b/.plop/templates/component/component.mdx.hbs
new file mode 100644
index 00000000..afea35cb
--- /dev/null
+++ b/.plop/templates/component/component.mdx.hbs
@@ -0,0 +1,113 @@
+{{!-- Template-authoring note, stripped from the generated file: JSX braces that touch
+Handlebars braces form a triple-stache and break generation. Keep a space inside
+`of={ … }` and let `~` trim it from the output, e.g. `of={ {{~pascalCase name~}} Stories}`.
+Avoid inline `style={{ … }}` in this file for the same reason.
+Markdown tables don't render in this repo's MDX (no remark-gfm); wrap them in a
+`{` … `}` block and escape inner backticks as `\``, like avatar.mdx. --}}
+import { Canvas, Controls, Markdown, Meta, Subtitle, Title } from '@storybook/addon-docs/blocks'
+
+import * as {{pascalCase name}}Stories from './{{dashCase name}}.stories'
+
+
+
+
+
+One-line summary of what {{pascalCase name}} is for.
+
+## Overview
+
+What {{pascalCase name}} is and the problem it solves, in a sentence or two. For design
+guidance (anatomy, when to use, do and don't), see the
+[{{pascalCase name}} handbook page](https://github.com/Doist/component-libraries/blob/main/handbook/components/{{dashCase name}}.md).
+
+{/* Verify this handbook link: the slug isn't always the component's dash-name (e.g.
+ buttons.md, menus.md, switches.md) and some pages don't exist yet. */}
+
+{/* Compound components (multiple exported parts): describe the parts in an Anatomy table.
+
+## Anatomy
+
+{`
+| Part | Renders | Owns |
+| --- | --- | --- |
+| \`<{{pascalCase name}}>\` | the root element | context and coordination between parts |
+| \`<{{pascalCase name}}.Item>\` | one item | its own selection state |
+`}
+*/}
+
+## When to use
+
+{/* Two or more concrete scenarios. Trim to what the handbook doesn't already cover. */}
+
+- Use {{pascalCase name}} when ...
+- Prefer another component when ...
+
+## Basic usage
+
+The simplest real example. Pass `variant` and `children`.
+
+
+
+## Variants
+
+{{pascalCase name}} supports a `primary` and a `secondary` variant.
+
+
+
+{/* Visual states (idle, hover, disabled, loading) get their own section. Add a States
+ story, then uncomment:
+
+## States
+
+
+*/}
+
+## Props
+
+Try the props in the playground below; the table lists each prop with its type and default.
+
+
+
+
+
+{/* Compound components: document each exported part with its own block. Import the parts
+ from the component module (e.g. `import { {{pascalCase name}}Item } from './{{dashCase name}}'`),
+ add `ArgTypes` and `Description` to the blocks import above, then repeat per part:
+
+### `<{{pascalCase name}}.Item>`
+
+
+
+
+*/}
+
+## Custom properties
+
+These CSS custom properties control the {{pascalCase name}} colors. The variant classes set
+them; the values below are what the `primary` variant sets.
+
+{`
+| Property | Default (primary variant) | Purpose |
+| --- | --- | --- |
+| \`--reactist-{{dashCase name}}-tint\` | \`var(--reactist-content-primary)\` | Content color |
+| \`--reactist-{{dashCase name}}-fill\` | \`var(--reactist-framework-fill-selected)\` | Background color |
+`}
+
+{/* Theming variables set on `:root` with literal defaults belong in this table too. For
+ long lists of color variables, a ColorPalette (see avatar.mdx) works better. */}
+
+{/* If integration responsibilities deliberately stay with the consumer (data fetching,
+ persistence, entity conventions), make the boundary explicit:
+
+## What the consumer owns
+
+- **Some responsibility**: what the component expects the consumer to handle, and why.
+*/}
+
+## Accessibility
+
+- **Keyboard**: describe focus order and keys the component handles.
+- **Screen reader**: the name, role, and state it exposes (and via which prop).
+- **Focus**: focus management / trapping behavior, if any.
+
+
diff --git a/.plop/templates/component/component.stories.tsx.hbs b/.plop/templates/component/component.stories.tsx.hbs
index fd7b7bb5..3e93cba9 100644
--- a/.plop/templates/component/component.stories.tsx.hbs
+++ b/.plop/templates/component/component.stories.tsx.hbs
@@ -1,10 +1,11 @@
import * as React from 'react'
-import type { Meta, StoryObj } from '@storybook/react'
import { {{pascalCase name}} } from './{{dashCase name}}'
+import type { Meta, StoryObj } from '@storybook/react-vite'
+
const meta = {
- title: 'Design system/{{pascalCase name}}',
+ title: '{{category}}/{{pascalCase name}}',
component: {{pascalCase name}},
parameters: {
/**
@@ -12,6 +13,8 @@ const meta = {
* Used to highlight the web accessibility standards the component meets or
* whether the component is deprecated.
* Possible values: `accessible`, `partiallyAccessible`, `notAccessible`, `deprecated`
+ * Start at `notAccessible`; raise it only once the component has been through an
+ * accessibility review, so a fresh component never claims a standard it hasn't earned.
* @see .storybook/badges/README.md
*/
badges: ['notAccessible'],
@@ -34,6 +37,39 @@ export default meta
type Story = StoryObj
+export const Default: Story = {
+ render() {
+ return (
+
+ <{{pascalCase name}} variant="primary">Hello world{{pascalCase name}}>
+
+ )
+ },
+}
+
+export const Variants: Story = {
+ render() {
+ return (
+
+ <{{pascalCase name}} variant="primary">Primary{{pascalCase name}}>{' '}
+ <{{pascalCase name}} variant="secondary">Secondary{{pascalCase name}}>
+
+ )
+ },
+}
+
+export const Accessibility: Story = {
+ // Demonstrate what the docs page's Accessibility section describes (the
+ // accessible name, keyboard behavior, decorative usage), not just default content.
+ render() {
+ return (
+
+ <{{pascalCase name}} variant="primary">Accessible content{{pascalCase name}}>
+
+ )
+ },
+}
+
export const Playground: Story = {
args: {
variant: 'primary',
@@ -48,7 +84,7 @@ export const Playground: Story = {
control: { type: 'text' },
},
},
- render(args) {
+ render(args: React.ComponentProps) {
return (
<{{pascalCase name}} {...args} />
diff --git a/.plop/templates/component/component.test.tsx.hbs b/.plop/templates/component/component.test.tsx.hbs
index dbe0405a..e91960a1 100644
--- a/.plop/templates/component/component.test.tsx.hbs
+++ b/.plop/templates/component/component.test.tsx.hbs
@@ -1,5 +1,7 @@
import * as React from 'react'
+
import { render, screen } from '@testing-library/react'
+
import { {{pascalCase name}} } from './{{dashCase name}}'
describe('{{pascalCase name}}', () => {
diff --git a/.plop/templates/component/component.tsx.hbs b/.plop/templates/component/component.tsx.hbs
index f1da8df3..a42def26 100644
--- a/.plop/templates/component/component.tsx.hbs
+++ b/.plop/templates/component/component.tsx.hbs
@@ -1,5 +1,7 @@
import * as React from 'react'
+
import { Box } from '../box'
+
import styles from './{{dashCase name}}.module.css'
type {{pascalCase name}}Props = {
@@ -14,11 +16,7 @@ type {{pascalCase name}}Props = {
}
function {{pascalCase name}}({ children, variant }: {{pascalCase name}}Props) {
- return (
-
- {children}
-
- )
+ return {children}
}
export { {{pascalCase name}} }
diff --git a/.plop/templates/component/index.ts.hbs b/.plop/templates/component/index.ts.hbs
index 45e186b9..cd311cbf 100644
--- a/.plop/templates/component/index.ts.hbs
+++ b/.plop/templates/component/index.ts.hbs
@@ -1,2 +1,2 @@
-export { {{pascalCase name}} } from './{{dashCase name}}'
export type { {{pascalCase name}}Props } from './{{dashCase name}}'
+export { {{pascalCase name}} } from './{{dashCase name}}'
diff --git a/.prettierignore b/.prettierignore
index 61fa2401..189c5fca 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -3,3 +3,5 @@ dist/**
coverage/**
docs/**
lib/**
+# Prettier's MDX parser predates MDX 2/3 and corrupts {/* … */} comments (see PR #1091)
+*.mdx
diff --git a/AGENTS.md b/AGENTS.md
index e2bae8ff..aaa98c8a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -35,7 +35,8 @@ src/
component-name.tsx # Implementation
component-name.module.css # CSS Modules styles
component-name.test.tsx # Tests
- component-name.stories.mdx # Storybook docs (or .tsx)
+ component-name.stories.tsx # Storybook stories (CSF)
+ component-name.mdx # Storybook MDX docs page
styles/
design-tokens.css # Global CSS custom properties (--reactist-*)
utils/
@@ -51,7 +52,7 @@ src/
### File structure
-Every component has `index.ts`, `component.tsx`, `component.module.css`, `component.test.tsx`, and a story file. Use `npm run plop component` to scaffold new components.
+Every component has `index.ts`, `component.tsx`, `component.module.css`, `component.test.tsx`, a `component.stories.tsx` story file, and a `component.mdx` docs page. Use `npm run plop component` to scaffold new components. The `component.mdx` docs page follows the section structure documented in README.md under "Documenting a component"; keep the heading names and order, uncomment the optional sections you fill in, wrap any markdown table in a `` block (no remark-gfm), and note that Prettier is never run on `.mdx` files.
### Implementation patterns
diff --git a/README.md b/README.md
index 26522078..214ac06c 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,25 @@ The generated source files include the component implementation with sample prop
You also need to export your new component by adding a reference to it in the [top-level index file](src/index.ts).
+### Documenting a component
+
+The generator also creates a `.mdx` documentation page next to the stories; its live examples render from the generated `Default`, `Variants`, `Accessibility`, and `Playground` stories. For an existing component that lacks a docs page, `npm run plop docs` scaffolds just the `.mdx` (repoint its canvases at the component's real stories as your first step).
+
+Every docs page follows the same structure, in this order:
+
+1. `` and `` (rendered from the story meta, so the page title matches the sidebar)
+2. **Overview**: what the component is and the problem it solves, linking the matching [design handbook page](https://github.com/Doist/component-libraries/tree/main/handbook/components) for anatomy and usage guidance
+3. **Anatomy** (compound components only): a table mapping each exported part to what it renders and owns
+4. **When to use**: concrete scenarios, including when to prefer another component
+5. **Basic usage**: the simplest real example
+6. **Variants** and **States**: visual variations; rename or replace these with domain sections when the component's modes need it (e.g. a Docked/Overlay split)
+7. **Props**: an interactive playground canvas with the props table directly below it; compound components add a `### ` block with `Description` and `ArgTypes` per exported part, importing the parts at the top of the file
+8. **Custom properties**: the component's CSS custom properties as a Property/Default/Purpose table (a `ColorPalette` is fine for long color lists)
+9. **What the consumer owns** (optional): integration responsibilities the component deliberately leaves to the consumer
+10. **Accessibility**: keyboard, screen reader, and focus behavior, with a demo canvas
+
+Keep the heading names and order. Optional sections are scaffolded as `{/* … */}` comments: uncomment the ones you fill in, delete the ones that don't apply. Constraints consumers must not violate (e.g. "only one modal overlay at a time") get a short callout inside the section they belong to. Markdown tables (Anatomy, Custom properties) must be wrapped in a `{` … `}` block with their inner backticks escaped, since this repo's MDX has no remark-gfm and renders bare tables as literal text (see `avatar.mdx`). Prettier does not format `.mdx` (its MDX parser predates MDX 2/3 and corrupts comments), so match the surrounding formatting by hand.
+
## Storybook
For the first development mode run:
diff --git a/lint-staged.config.js b/lint-staged.config.js
index 06ae75b2..4594fa56 100644
--- a/lint-staged.config.js
+++ b/lint-staged.config.js
@@ -1,5 +1,5 @@
module.exports = {
- './**/*.{js,jsx,ts,tsx,md,mdx,json,css,scss,less}': ['prettier --write'],
+ './**/*.{js,jsx,ts,tsx,md,json,css,scss,less}': ['prettier --write'],
'./**/*.{js,jsx,ts,tsx}': ['npx eslint --format codeframe --fix'],
'src/**/*.{js,jsx,ts,tsx}': ['npx @doist/react-compiler-tracker --stage-record-file'],
}
diff --git a/package.json b/package.json
index e9c72c12..bd145dba 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
"type-check:react18": "tsc --noEmit -p ./tsconfig.react18.json",
"lint": "eslint --format codeframe --cache --ext js,jsx,ts,tsx ./",
"storybook": "storybook dev -p 6006",
- "prettify": "prettier --write \"./**/*.{js,jsx,ts,tsx,json,css,scss,less,md,mdx}\"",
+ "prettify": "prettier --write \"./**/*.{js,jsx,ts,tsx,json,css,scss,less,md}\"",
"plop": "plop"
},
"peerDependencies": {
diff --git a/plopfile.mjs b/plopfile.mjs
index 14bfb621..78a10f15 100644
--- a/plopfile.mjs
+++ b/plopfile.mjs
@@ -23,6 +23,21 @@ function componentGenerator(plop) {
message:
'Component name (e.g. "dropdown select", "sortable-table", "PromotionBanner")',
},
+ {
+ type: 'list',
+ name: 'category',
+ message: 'Storybook category (the section the component belongs to)',
+ choices: [
+ '📐 Layout',
+ '🔤 Typography',
+ '🔘 Buttons & links',
+ '📝 Form',
+ '📑 Menus & tabs',
+ '📊 Data display',
+ '💬 Feedback',
+ '🪟 Overlays',
+ ],
+ },
],
actions() {
@@ -56,12 +71,43 @@ function componentGenerator(plop) {
templateFile: templateFile('component/component.stories.tsx'),
})
+ actions.push({
+ type: 'add',
+ path: 'src/{{dashCase name}}/{{dashCase name}}.mdx',
+ templateFile: templateFile('component/component.mdx'),
+ })
+
return actions
},
})
}
+/** @param {NodePlopAPI} plop */
+function docsGenerator(plop) {
+ plop.setGenerator('docs', {
+ description: 'MDX docs page for an existing component that lacks one',
+
+ prompts: [
+ {
+ type: 'input',
+ name: 'name',
+ message:
+ 'Existing component name, matching its directory (e.g. "toast", "text field")',
+ },
+ ],
+
+ actions: [
+ {
+ type: 'add',
+ path: 'src/{{dashCase name}}/{{dashCase name}}.mdx',
+ templateFile: templateFile('component/component.mdx'),
+ },
+ ],
+ })
+}
+
/** @param {NodePlopAPI} plop */
export default function (plop) {
componentGenerator(plop)
+ docsGenerator(plop)
}