Skip to content

Commit a1d6f73

Browse files
committed
docs: update storybook asset showcases and ui-pattern stories
1 parent 8755d8a commit a1d6f73

16 files changed

Lines changed: 831 additions & 340 deletions

File tree

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
---
2+
name: promoting-composites-to-components
3+
description: >-
4+
Promote a storybook UI-pattern composite (examples/src/storybook/ui-patterns/components/composites/*.tsx)
5+
into a published @axiscommunications/fluent-* component package in this repo. USE WHEN the user wants to
6+
turn a demo/composite (e.g. Pagination) into a real publishable component, asks whether a component is
7+
"dev ready" / publishable, asks how to scaffold a new component package, or asks how to add unit tests,
8+
Playwright system tests, or accessibility testing the way the original repo does it. Covers the
9+
dev-readiness checklist, the package scaffolding recipe, vitest + Testing Library a11y patterns, the
10+
examples/system-test Playwright setup, and the repo's publishing/best-practice conventions.
11+
---
12+
13+
# Promoting a composite into a publishable Fluent component
14+
15+
Goal: take a reference composite that currently lives only in the examples app
16+
(`examples/src/storybook/ui-patterns/components/composites/*.tsx`) and turn it into
17+
a real, releasable package under `components/<name>/`, matching the conventions of
18+
the existing published components (slider, stepper, topbar, side-navigation,
19+
password-input, empty-view).
20+
21+
## Two tiers: UI patterns vs components
22+
23+
This repo is intentionally split into two tiers. They are named differently on
24+
purpose — "patterns" are not yet "components".
25+
26+
| Tier | Location | Owner | Status |
27+
|------|----------|-------|--------|
28+
| **UI patterns** (design-controlled staging) | `examples/src/storybook/ui-patterns/components/composites/*.tsx` | Design | Unpublished; the `examples` package is `"private": true`, so patterns iterate freely with no release/semver pressure. This is where the heavy lift (UX, visuals, tokens, a11y intent, variants) happens. |
29+
| **Components** (internal source lib) | `components/<name>/` | Dev contributors | Published `@axiscommunications/fluent-*`, versioned, contract-bound. Opened up for dev contribution once the pattern is settled. |
30+
31+
Promotion (pattern → component) is the handoff gate between the two tiers, and the
32+
rest of this skill is that runbook. Two distinct sign-offs gate it:
33+
34+
- **Design-ready** (design owns): behavior, visuals, tokens, accessibility intent,
35+
and variants are settled. The pattern can now be promoted.
36+
- **Dev-ready / publishable** (dev + maintainers own): the **public API is the
37+
contract** — prop shape, peer deps, tests, docs, and semver impact are reviewed.
38+
A pattern can look perfect yet still have an awkward API (see the Pagination
39+
prop-surface / string-interpolation notes below), so this is a separate decision,
40+
not just a visual sign-off. The repo is at major v12 — breaking a published API
41+
is costly, so lock the API here, not in the pattern tier.
42+
43+
`advanced-data-grid` (`0.1.0`, `"private": true`) is the in-between case: promoted
44+
into `components/` structurally but still gated from publishing while it stabilizes.
45+
46+
## 1. Decide if it should be a component at all
47+
48+
Promote a composite only when it is reusable, self-contained, and design-system
49+
aligned. Good signals (use Pagination as the worked example):
50+
51+
- It already uses `forwardRef`, typed props, token-driven `makeStyles`, and a11y
52+
labels — i.e. it is written like a component, not a one-off demo.
53+
- It depends only on Fluent peers (`@fluentui/react-components`,
54+
`@fluentui/react-icons`) — the same peer model every component package uses.
55+
- It fills a real gap (e.g. Pagination pairs with the published
56+
`usePageController` hook in `hooks/`, which ships the logic but no UI).
57+
58+
Refine the API before publishing: trim oversized prop surfaces, prefer accepting a
59+
controller object over duplicating its outputs, and replace brittle string
60+
interpolation (`"X - Y of Z".replace(...)`) with formatter callbacks for i18n.
61+
Also resolve overlap with existing components (e.g. advanced-data-grid already has
62+
its own pagination — decide whether it should consume the shared component).
63+
64+
## 2. Dev-readiness / publishable checklist
65+
66+
A package is "dev ready" (installable by another developer) when ALL of:
67+
68+
- [ ] `components/<name>/package.json` has **no** `"private": true` (the only
69+
private component today is `advanced-data-grid` at `0.1.0`).
70+
- [ ] `name` is `@axiscommunications/fluent-<name>`, `version` matches the shared
71+
monorepo version (all packages release together — currently `12.6.1`).
72+
- [ ] `exports` maps `.``./lib/index.d.ts` (types) + `./lib/index.js` (import),
73+
and `files` is `["lib"]`.
74+
- [ ] `peerDependencies` declare `@fluentui/react-components`,
75+
`@fluentui/react-icons`, `react`, `react-dom` (NOT direct deps).
76+
- [ ] `publishConfig.registry` is `https://npm.pkg.github.com/` (GitHub Packages,
77+
not public npm) — consumers need an `.npmrc` scope mapping + auth token.
78+
- [ ] Barrel `src/index.ts` exports the component(s) + all public types.
79+
- [ ] A `docs/README.md` exists (install + usage + props table + Accessibility).
80+
- [ ] At least one `*.spec.tsx` unit test (vitest + Testing Library).
81+
- [ ] `pnpm build`, `pnpm --filter @axiscommunications/fluent-<name> lint`, and
82+
`test` all pass.
83+
84+
## 3. Scaffold the package
85+
86+
Mirror an existing small package (side-navigation is a good template). Layout:
87+
88+
```
89+
components/<name>/
90+
├── src/
91+
│ ├── index.ts # barrel: components + types + style hooks
92+
│ ├── <name>.tsx # implementation (move from the composite)
93+
│ ├── <name>.types.ts # Props + item/sub-types
94+
│ ├── <name>.styles.ts # makeStyles + classNames map
95+
│ ├── <name>.spec.tsx # vitest unit + a11y tests
96+
│ └── setupTest.ts # imports "@testing-library/jest-dom"
97+
├── docs/README.md
98+
├── package.json # copy from side-navigation, rename, no "private"
99+
├── tsconfig.json # extends ../../tsconfig.base.json
100+
├── tsconfig.build.json # emits .d.ts into lib/
101+
├── vitest.config.ts # jsdom, globals, setupFiles ./src/setupTest.ts
102+
└── depcheck.yml
103+
```
104+
105+
Per-package scripts (identical across packages):
106+
107+
| Script | Runs |
108+
|--------|------|
109+
| `build` | `pnpm build:types && pnpm build:esm` |
110+
| `build:esm` | `esbuild --format=esm --bundle --sourcemap --packages=external --outdir=lib src/index.ts` |
111+
| `build:types` | `tsc -p tsconfig.build.json` |
112+
| `lint` | `tsc --noEmit && biome check` |
113+
| `test` | `vitest run` |
114+
115+
Then add the package to the examples app as a `workspace:*` dependency and update
116+
the relevant story to import from the new package instead of the local composite.
117+
118+
## 4. Unit tests + accessibility testing (vitest + Testing Library)
119+
120+
Component-level tests live next to the source as `*.spec.tsx` and run on
121+
`vitest run` (jsdom, globals). Pattern — see
122+
`components/side-navigation/src/side-navigation.spec.tsx`:
123+
124+
- Always render inside a Fluent provider:
125+
`render(<FluentProvider theme={webLightTheme}>{ui}</FluentProvider>)`.
126+
- Assert by **role + accessible name**, not by class or test id:
127+
`getByRole("button", { name: "Home" })`.
128+
- Verify a11y semantics directly: selected item exposes
129+
`aria-current="page"`, groups expose `aria-expanded`, toggles expose an
130+
accessible label. Use `toHaveAttribute(...)` from `@testing-library/jest-dom`.
131+
- Cover controlled vs uncontrolled, disabled items, and keyboard/click
132+
interaction (`fireEvent.click`, `vi.fn()` for callbacks).
133+
134+
Storybook also runs the **a11y addon** (`@storybook/addon-a11y`, configured in
135+
`examples/.storybook/main.ts`) — every story gets an automated axe audit in the
136+
Accessibility panel. When you add stories for the new component, check that panel
137+
is clean and document an `## Accessibility` section in the story and the README
138+
(landmark role, `aria-*`, reduced-motion handling), as the existing components do.
139+
140+
## 5. Playwright system tests (examples/system-test)
141+
142+
End-to-end/system tests drive the real examples app in a browser. They live in
143+
`examples/system-test/` and end in `.stest.ts`. Setup:
144+
145+
- **Config:** `examples/playwright.config.ts` extends
146+
`examples/playwright.confg.base.ts`; project name
147+
`fluent-components:stest:chromium`, `testMatch: ["system-test/**.stest.ts"]`,
148+
viewport 1280×720, chromium `channel: process.env.BROWSER ?? "chrome"`.
149+
- **Run:** `cd examples && pnpm stest` (or `pnpm stest:ui`). Locally the suite
150+
targets the Vite dev server at `http://127.0.0.1:3000/fluent-components/`; CI
151+
(`CI=true`) targets the published GH Pages site. See
152+
`system-test/util/common.ts` (`getRootPath`).
153+
- **Dev server gotcha:** start it as
154+
`pnpm --filter examples exec vite --host 127.0.0.1` — plain `pnpm dev` binds
155+
only to `localhost`/IPv6 `::1` and refuses `127.0.0.1`.
156+
- **Browser gotcha:** branded Chrome needs sudo; instead
157+
`pnpm exec playwright install chromium` and run with `BROWSER=chromium`.
158+
- **Routing:** the app uses HashRouter — navigate to `${getRootPath()}#/<route>`.
159+
160+
Authoring pattern (page-object model):
161+
162+
1. Add a model in `system-test/models/<name>-page.model.ts` exposing locators
163+
(prefer `getByRole` / `getByLabel`; fall back to `getByTestId` for toolbar
164+
controls) and actions, plus a `goto()` that navigates and waits for the root
165+
to be visible. Example: `models/advanced-data-grid-page.model.ts`.
166+
2. Register it as a fixture in `system-test/util/test.ts`
167+
(`base.extend<TTestFixtures>({ ... })`).
168+
3. Write the spec as `system-test/<name>.stest.ts` using
169+
`import { test } from "./util/test"` and `expect` from `@playwright/test`;
170+
structure with `test.describe` + `test.beforeEach(({ fixture }) => fixture.goto())`.
171+
Example: `advanced-data-grid.stest.ts`.
172+
173+
Scope locators to a stable root (e.g. `.axis-<Component>`) and use `.first()` when
174+
a story renders multiple instances of the component.
175+
176+
## 6. Best practices from the repo
177+
178+
- **Tooling:** Biome (not ESLint/Prettier) for lint+format; 2-space indent,
179+
80-col width; no unused imports/vars (errors in Biome AND tsc strict).
180+
- **Conventional commits** (commitlint): `type(scope): message``feat`, `fix`,
181+
`docs`, `refactor`, `test`, `chore`, etc. Required for the release pipeline.
182+
- **Class names** are prefixed `axis-<ComponentName>` (e.g. `axis-Slider`),
183+
exposed via a `classNames` map in `*.styles.ts`.
184+
- **Separate files per concern:** `.tsx` logic, `.types.ts` types, `.styles.ts`
185+
styles; barrel `index.ts` re-exports components + types + style hooks.
186+
- **Styling:** `makeStyles` + `mergeClasses` + Fluent `tokens`; never raw hex.
187+
- **Peer deps, not direct deps** for `@fluentui/*` and `react`; internal packages
188+
via the pnpm `workspace:` protocol.
189+
- **CI** (`.github/workflows/verify.yml`) runs commit lint → format check → lint →
190+
build → test → unused-deps on every PR. Merging a release commit
191+
(`pnpm exec release major|minor|patch`) auto-publishes all packages.
192+
- Don't add a second divergent implementation of an existing capability — wire the
193+
new component into existing consumers (stories, data grid) instead.

examples/.storybook/preview.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ const preview: Preview = {
7777
storySort: {
7878
order: [
7979
"UI patterns",
80+
["Introduction", "*"],
8081
"Components",
8182
"Assets",
8283
"Utilities",

0 commit comments

Comments
 (0)