Skip to content
Draft
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
113 changes: 113 additions & 0 deletions .plop/templates/component/component.mdx.hbs
Original file line number Diff line number Diff line change
@@ -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
`<Markdown>{` … `}</Markdown>` 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'

<Meta of={ {{~pascalCase name~}} Stories} />

<Title />

<Subtitle>One-line summary of what {{pascalCase name}} is for.</Subtitle>

## 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

<Markdown>{`
| Part | Renders | Owns |
| --- | --- | --- |
| \`<{{pascalCase name}}>\` | the root element | context and coordination between parts |
| \`<{{pascalCase name}}.Item>\` | one item | its own selection state |
`}</Markdown>
*/}

## 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`.

<Canvas of={ {{~pascalCase name~}} Stories.Default} />

## Variants

{{pascalCase name}} supports a `primary` and a `secondary` variant.

<Canvas of={ {{~pascalCase name~}} Stories.Variants} />

{/* Visual states (idle, hover, disabled, loading) get their own section. Add a States
story, then uncomment:

## States

<Canvas of={ {{~pascalCase name~}} Stories.States} />
*/}

## Props

Try the props in the playground below; the table lists each prop with its type and default.

<Canvas of={ {{~pascalCase name~}} Stories.Playground} />

<Controls of={ {{~pascalCase name~}} Stories.Playground} />

{/* 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>`

<Description of={ {{~pascalCase name~}} Item} />

<ArgTypes of={ {{~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.

<Markdown>{`
| 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 |
`}</Markdown>

{/* 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.

<Canvas of={ {{~pascalCase name~}} Stories.Accessibility} />
42 changes: 39 additions & 3 deletions .plop/templates/component/component.stories.tsx.hbs
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
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: {
/**
* Storybook toolbar badges
* 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'],
Expand All @@ -34,6 +37,39 @@ export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {
render() {
return (
<section>
<{{pascalCase name}} variant="primary">Hello world</{{pascalCase name}}>
</section>
)
},
}

export const Variants: Story = {
render() {
return (
<section>
<{{pascalCase name}} variant="primary">Primary</{{pascalCase name}}>{' '}
<{{pascalCase name}} variant="secondary">Secondary</{{pascalCase name}}>
</section>
)
},
}

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 (
<section>
<{{pascalCase name}} variant="primary">Accessible content</{{pascalCase name}}>
</section>
)
},
}

export const Playground: Story = {
args: {
variant: 'primary',
Expand All @@ -48,7 +84,7 @@ export const Playground: Story = {
control: { type: 'text' },
},
},
render(args) {
render(args: React.ComponentProps<typeof {{pascalCase name}}>) {
return (
<section>
<{{pascalCase name}} {...args} />
Expand Down
2 changes: 2 additions & 0 deletions .plop/templates/component/component.test.tsx.hbs
Original file line number Diff line number Diff line change
@@ -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}}', () => {
Expand Down
8 changes: 3 additions & 5 deletions .plop/templates/component/component.tsx.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as React from 'react'

import { Box } from '../box'

import styles from './{{dashCase name}}.module.css'

type {{pascalCase name}}Props = {
Expand All @@ -14,11 +16,7 @@ type {{pascalCase name}}Props = {
}

function {{pascalCase name}}({ children, variant }: {{pascalCase name}}Props) {
return (
<Box className={[styles.container, styles[variant]]}>
{children}
</Box>
)
return <Box className={[styles.container, styles[variant]]}>{children}</Box>
}

export { {{pascalCase name}} }
Expand Down
2 changes: 1 addition & 1 deletion .plop/templates/component/index.ts.hbs
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { {{pascalCase name}} } from './{{dashCase name}}'
export type { {{pascalCase name}}Props } from './{{dashCase name}}'
export { {{pascalCase name}} } from './{{dashCase name}}'
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ dist/**
coverage/**
docs/**
lib/**
# Prettier's MDX parser predates MDX 2/3 and corrupts {/* … */} comments (see PR #1091)
*.mdx
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand All @@ -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 `<Markdown>` block (no remark-gfm), and note that Prettier is never run on `.mdx` files.

### Implementation patterns

Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<component-name>.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. `<Title />` and `<Subtitle>` (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 `### <Component.Part>` 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 `<Markdown>{` … `}</Markdown>` 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:
Expand Down
2 changes: 1 addition & 1 deletion lint-staged.config.js
Original file line number Diff line number Diff line change
@@ -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'],
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
46 changes: 46 additions & 0 deletions plopfile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
}
Loading