Skip to content
Open
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
129 changes: 129 additions & 0 deletions packages/dev/s2-docs/scripts/generateAgentSkills.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ const MARKDOWN_DOCS_DIST = path.join(REPO_ROOT, 'packages/dev/s2-docs/dist');
const MDX_PAGES_DIR = path.join(REPO_ROOT, 'packages/dev/s2-docs/pages');
const MARKDOWN_DOCS_SCRIPT = path.join(__dirname, 'generateMarkdownDocs.mjs');
const MIGRATION_REFS_DIR = path.join(REPO_ROOT, 'packages/dev/s2-docs/migration-references');
const AUDIT_SKILL_SOURCE_DIR = path.join(REPO_ROOT, 'packages/dev/s2-docs/skills/spectrum-audit');
const RSP_S2_SKILL_SOURCE_DIR = path.join(
REPO_ROOT,
'packages/dev/s2-docs/skills/react-spectrum-s2'
);
const WELL_KNOWN_DIR = '.well-known';
const WELL_KNOWN_SKILLS_DIR = 'skills';

Expand Down Expand Up @@ -79,6 +84,19 @@ const SKILLS = {
author: 'Adobe',
website: 'https://react-aria.adobe.com/'
}
},
'spectrum-audit': {
name: 'spectrum-audit',
description:
'Audit a codebase for adherence to the Spectrum design system and React Spectrum S2 best practices. Use when a developer asks to audit, review, lint, or check a project for Spectrum/S2 correctness, configuration, styling, accessibility, or component-usage issues.',
kind: 'audit',
license: 'Apache-2.0',
sourceDir: 's2',
compatibility: 'Requires a React project using @react-spectrum/s2.',
metadata: {
author: 'Adobe',
website: 'https://react-spectrum.adobe.com/'
}
}
};

Expand Down Expand Up @@ -665,6 +683,59 @@ Use these when you need more component-by-component or API-level detail:
);
}

function generateAuditSkillMd(skillConfig) {
return (
`${generateFrontmatter(skillConfig)}# Spectrum Audit

Audit a codebase for adherence to the Spectrum design system and React Spectrum S2 best practices, then produce a scored, prioritized report. This skill is **report-only** — it does not modify any files.

## When to use

Use when a developer asks to audit, review, lint, or check a project for Spectrum / S2 correctness, configuration, styling, accessibility, or component-usage issues.

Requires a project with \`@react-spectrum/s2\` installed. If S2 is not a dependency, say so and stop.

## How it works

Run these phases in order.

### Phase 0 — Scope

- Identify the project — or, in a monorepo, the specific package — to audit. Audit the package, not the workspace root.
- Detect the package manager from the lockfile, the bundler (Vite / webpack / Next.js / Parcel / Rollup / ESBuild), and read \`package.json\` dependencies.
- Confirm \`@react-spectrum/s2\` is installed. Establish the source globs to scan (e.g. \`src/**/*.{tsx,jsx,ts,js}\`).

### Phase 1 — Run the checks

Work through each check file in \`references/checks/\`, in order. For every violation, record a finding: \`{file:line, rule, severity, category, fix}\`. Cite only line numbers you actually read or grepped — never invent locations.

- [01 — Setup & configuration](references/checks/01-setup-config.md)
- [02 — Component usage](references/checks/02-component-usage.md)
- [03 — Styling](references/checks/03-styling.md)
- [04 — Accessibility & correctness](references/checks/04-accessibility.md)
- [05 — Versioning & maintenance](references/checks/05-versioning.md)
- [06 — Testing](references/checks/06-testing.md)

The canonical rules behind these checks live in [Implementation guidance](references/docs-implementation-guidance.md) and [Getting started](references/docs-getting-started.md). When you need a component's API or the canonical component list to judge a finding, use the \`react-spectrum-s2\` skill if it is installed.

### Phase 2 — Score

Apply the [scoring rubric](references/scoring-rubric.md) to the recorded findings to compute per-category scores and the overall Spectrum Adherence Score. The score is arithmetic over counted findings — do not estimate it.

### Phase 3 — Report

Write \`SPECTRUM-AUDIT.md\` to the audited project following the [report template](references/report-template.md): headline score, grade, and severity counts; a summary; scores by category; findings grouped by severity (each with a clickable \`file:line\` and a link to its check file); prioritized action items; and what looks good.

### Phase 4 — Hand off

This skill does not edit code. Recommend:

- The \`react-spectrum-s2\` skill to implement the fixes.
- The \`migrate-react-spectrum-v3-to-s2\` skill if Spectrum 1 packages (\`@adobe/react-spectrum\`, \`@react-spectrum/*\`, \`@spectrum-icons/*\`) are present.
`.trimEnd() + '\n'
);
}

/**
* Copy documentation files to the skill's references directory.
*/
Expand Down Expand Up @@ -823,6 +894,52 @@ function writeMigrationReferences(skillDir, sourceDir) {
]);
}

function writeAuditReferences(skillDir, sourceDir) {
const refsDir = path.join(skillDir, 'references');
fs.mkdirSync(refsDir, {recursive: true});

// Copy authored audit check files.
const checksSourceDir = path.join(AUDIT_SKILL_SOURCE_DIR, 'checks');
const checksTargetDir = path.join(refsDir, 'checks');
fs.mkdirSync(checksTargetDir, {recursive: true});
for (const file of fs.readdirSync(checksSourceDir)) {
if (file.endsWith('.md')) {
fs.copyFileSync(path.join(checksSourceDir, file), path.join(checksTargetDir, file));
}
}

// Copy the authored scoring rubric and report template.
for (const file of ['scoring-rubric.md', 'report-template.md']) {
fs.copyFileSync(path.join(AUDIT_SKILL_SOURCE_DIR, file), path.join(refsDir, file));
}

// Reuse the generated getting-started doc as the canonical setup reference.
copyFocusedDocs(sourceDir, skillDir, [['getting-started.md', 'docs-getting-started.md']]);

// Reuse the canonical S2 implementation guidance and component decision tree (single source of
// truth — don't restate the rules in the audit). Resolve the {{...}} doc-link tokens to the
// co-generated react-spectrum-s2 skill's references, which share this skill's parent directory.
const guidanceReplacements = {
'{{guidesBase}}': '../react-spectrum-s2/references/guides/',
'{{componentsBase}}': '../react-spectrum-s2/references/components/',
'{{testingBase}}': '../react-spectrum-s2/references/testing/'
};
fs.writeFileSync(
path.join(refsDir, 'docs-implementation-guidance.md'),
renderCustomMarkdown(
path.join(RSP_S2_SKILL_SOURCE_DIR, 'implementation-guidance.md'),
guidanceReplacements
) + '\n'
);
fs.writeFileSync(
path.join(refsDir, 'docs-component-decision-tree.md'),
renderCustomMarkdown(
path.join(RSP_S2_SKILL_SOURCE_DIR, 'component-decision-tree.md'),
guidanceReplacements
) + '\n'
);
}

function collectSkillFiles(skillDir) {
const files = [];

Expand Down Expand Up @@ -918,6 +1035,18 @@ function generateSkill(skillConfig, wellKnownRoot) {
return skillDir;
}

if (skillConfig.kind === 'audit') {
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), generateAuditSkillMd(skillConfig));
console.log(`Generated ${path.relative(REPO_ROOT, path.join(skillDir, 'SKILL.md'))}`);

writeAuditReferences(skillDir, skillConfig.sourceDir);
console.log(
`Copied audit references to ${path.relative(REPO_ROOT, path.join(skillDir, 'references'))}`
);

return skillDir;
}

const isS2 = skillConfig.name === 'react-spectrum-s2';

// Parse documentation entries
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Check 01 — Setup & configuration

Validates that the project is configured the way [Getting started](../docs-getting-started.md) specifies. Misconfiguration here is high-impact: the `style` macro silently no-ops, CSS bloats, or strings for 30+ languages ship to every user.

Read first: `../docs-getting-started.md` (canonical setup), plus the project's `package.json` and bundler config file.

## Detection inputs (gathered in Phase 0)

- **Package manager:** lockfile — `yarn.lock`, `package-lock.json`, or `pnpm-lock.yaml`.
- **Bundler:** `vite.config.*`, `webpack.config.*`, `next.config.*`, `.parcelrc` / a `parcel` dependency, `rollup.config.*`, or an esbuild build script.
- **Installed deps:** `dependencies` / `devDependencies` in `package.json`.

## Checks

### `@react-spectrum/s2` is installed
- **Detect:** `@react-spectrum/s2` present in `package.json`. If absent, the audit does not apply — stop and tell the user.
- **Severity:** — (precondition)

### Style macro plugin configured
- **Rule:** Non-Parcel bundlers must wire `unplugin-parcel-macros` into the build; Parcel needs ≥ 2.12.0. Without it, `style({...})` calls are never evaluated at build time and components render unstyled.
- **Detect:**
- Vite/Rollup/React Router/ESBuild/webpack/Next: `unplugin-parcel-macros` in deps **and** referenced in the bundler config (`macros.vite()`, `macros.webpack()`, `macros.rollup()`, `macros.esbuild()`).
- Parcel: `parcel` ≥ 2.12.0 (macros are built in) — no plugin needed.
- Next.js: also confirm the app starts with `--webpack` (not Turbopack) — check the `dev`/`build` scripts. `unplugin-parcel-macros` does not work with Turbopack.
- **Severity:** Critical.

### CSS bundle optimization
- **Rule:** All S2 + macro-generated CSS should be combined into a single shared `s2-styles` bundle rather than code-split per route. Atomic CSS overlaps heavily between components, so loading it up front is smaller than duplicating it across chunks.
- **Detect:**
- Parcel: `@parcel/bundler-default.manualSharedBundles` with an `s2-styles` entry in the root `package.json`.
- webpack/Next: `optimization.splitChunks.cacheGroups` with an `s2`/`s2-styles` group testing for `@react-spectrum/s2` and `macro-*.css`.
- Vite/Rollup/React Router: `build.rollupOptions.output.manualChunks` returning `'s2-styles'` for `macro-*.css` and `@react-spectrum/s2/*.css`.
- **Severity:** High.

### lightningcss minification
- **Rule:** Compile/minify CSS with lightningcss — it produces a much smaller bundle and dedupes atomic rules.
- **Detect:** `cssMinify: 'lightningcss'` (Vite/React Router) or `CssMinimizerPlugin.lightningCssMinify` (webpack/Next).
- **Severity:** Medium.

### Locale optimization
- **Rule:** S2 ships localized strings for 30+ languages by default. Projects should install the locale optimization plugin and declare only the languages they support.
- **Detect:** `@react-aria/optimize-locales-plugin` (webpack/Vite/Rollup/React Router/ESBuild) or `@react-aria/parcel-resolver-optimize-locales` (Parcel) installed **and** configured with a `locales` list.
- **Severity:** Medium.

### Single root Provider, wired to the router
- **Rule:** Mount one `Provider` from `@react-spectrum/s2/Provider` at the app root, wired to the client router; SSR frameworks (Next.js, React Router) set `locale` from the server request and render `Provider` with `elementType="html"`.
- **Detect:** exactly one root `Provider`; a `router={{navigate}}` prop; for Next/RR, a server-derived locale. Flag a missing `router`, or SSR setups that don't sync the locale.
- **Severity:** High. (Provider *scope* misuse — stacking, hard-coded `colorScheme` — is in [04-accessibility](04-accessibility.md).)

### React 19
- **Rule:** React 19 is recommended for S2.
- **Detect:** `react` version in `package.json`.
- **Severity:** Low.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Check 02 — Component usage

Validates that the project uses S2 components where they exist, builds custom components on React Aria Components + the `style` macro, and follows S2 composition and collection conventions. This is the most judgment-heavy category — read the component's docs (via the `react-spectrum-s2` skill, if installed) before flagging.

Canonical rules: `../docs-implementation-guidance.md` (Component composition, Collections, Typography, Form fields). Reverse-lookup help ("is there an S2 component for this?"): `../docs-component-decision-tree.md`.

## Checks

### Use an S2 component where one exists
- **Rule:** Prefer S2 components over hand-rolled UI. S2 covers ~85 components — Button, Picker, ComboBox, Menu, Dialog/AlertDialog, Card/CardView, TableView, ListView, Tabs, and more.
- **Detect:** hand-rolled equivalents — `<div role="button|dialog|menu|listbox|tablist">`, custom dropdown/modal/tooltip implementations, native `<button>`/`<select>` styled to look like Spectrum, native `<table>` for data grids. Cross-check candidates against the canonical component list (`@react-spectrum/s2` exports / the `react-spectrum-s2` skill).
- **Severity:** High.

### Custom components build on RAC + the style macro
- **Rule:** When no S2 component fits, build the custom component on React Aria Components (for behavior + accessibility) and style it with the `style` macro — not raw divs with click handlers.
- **Detect:** interactive custom components using bare `<div onClick>` / `<div onKeyDown>` / `<span onClick>` instead of an RAC primitive or an S2 component.
- **Severity:** High (accessibility regressions).

### Typed item components
- **Rule:** Each collection has its own item export — `MenuItem`, `PickerItem`, `ComboBoxItem`, `ListViewItem`, `TreeViewItem`, `Row`/`Cell`/`Column`, `Tag`, `Breadcrumb`, `AccordionItem`, etc. There is no generic `Item`.
- **Detect:** a generic `Item`/`Section` import in S2 code (usually a Spectrum 1 leftover), or the wrong item type nested inside a collection.
- **Severity:** Medium.

### Collection conventions
- **Rule:** Collection items need an `id` (and React `key` when built with `.map`); items whose children aren't plain text need `textValue`; the collection container needs `aria-label`/`aria-labelledby`; virtualized collections must not be wrapped in an `overflow` container; empty/loading/bulk-action UI uses the built-in `renderEmptyState` / `loadingState` / `renderActionBar`.
- **Detect:** items lacking `id`; non-text item children without `textValue`; a collection with no accessible name; an `overflow*` wrapper around `TableView`/`ListView`/`CardView`/`TreeView`/`Menu`/`ListBox`; hand-rolled empty/spinner/bulk-action UI replacing the built-ins.
- **Severity:** High for missing `id` / `aria-label` / `textValue` (breaks selection, keyboard nav, screen readers); Medium for the rest.

### Don't reinvent Card / CardView
- **Rule:** For grids of objects/files/products/people, use `CardView` with a variant (`AssetCard`, `UserCard`, `ProductCard`) or `Card` composed from `CardPreview`/`Content`/`Text`/`Footer`.
- **Detect:** hand-rolled card `<div>` / `<article>` grids reproducing card layout.
- **Severity:** Medium.

### Slot discipline
- **Rule:** Only pass `slot` values the parent documents, and don't inject wrapper elements where a component expects specific slot children.
- **Detect:** `<div>`/`<span>` wrapping collection-item children; `slot=` values the parent component doesn't define (e.g. `slot="close"` on an arbitrary button).
- **Severity:** Medium.
47 changes: 47 additions & 0 deletions packages/dev/s2-docs/skills/spectrum-audit/checks/03-styling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Check 03 — Styling

Validates that styling goes through S2 components and the `style` macro with tokens — not escape hatches, raw CSS, or third-party styling systems. Canonical rules: `../docs-implementation-guidance.md` (Styling and Icons sections).

Search the source globs (e.g. `**/*.{tsx,jsx,ts,js}`) for the patterns below.

## Checks

### No `UNSAFE_style` / `UNSAFE_className`
- **Rule:** Avoid `UNSAFE_style` and `UNSAFE_className`; they bypass tokens and the style layer ordering.
- **Detect:** grep `UNSAFE_style|UNSAFE_className`.
- **Severity:** High.

### No concatenation of macro output
- **Rule:** `style()` results encode style precedence; concatenating them with template literals, `clsx`, `classnames`, or string spaces breaks ordering. Express variation in one `style({...})` call with runtime conditions, or use `mergeStyles`.
- **Detect:** macro results combined via `` `${style(...)} ...` ``, `clsx(`/`classNames(` wrapping `style(`, or Tailwind utility classes alongside S2 components. grep `clsx|classnames`, `tailwind`, and template-literal `className`s containing `style(`.
- **Severity:** High.

### No inline `style` alongside the macro
- **Rule:** Don't combine `className={style({...})}` (or the `styles` prop) with an inline `style={{...}}` on the same element — inline styles bypass tokens and break layer ordering. Inline `style` is only for values genuinely unknowable at build time (e.g. a drag position).
- **Detect:** elements carrying both `style={{` and `style(` / `styles=`.
- **Severity:** Medium.

### Tokens, not raw CSS values
- **Rule:** Macro values are typed tokens, not raw CSS. No hex / `rgb()` / `var()` colors; no string units like `'1rem'` / `'100%'` (use numbers on the 4px grid, or `'full'`); no `'flex-start'` / `'flex-end'` (use `'start'` / `'end'`); no physical sides (use logical `paddingStart`/`marginEnd`/`insetStart`/`borderStartStartRadius` so they flip under RTL).
- **Detect:** inside `style(` / `styles=`: `#` hex colors, `rgb(`, `var(--`, quoted units (`'…rem'`, `'…px'`, `'…%'`), `'flex-start'`/`'flex-end'`, and physical props `paddingLeft|paddingRight|marginLeft|marginRight|borderTopLeftRadius|borderTopRightRadius`.
- **Severity:** Medium. Prefer **semantic** color tokens (`accent`, `neutral`, `negative`, `positive`, `informative`, `notice`) where color carries meaning, over raw hue tokens (`red-…`, `blue-…`).

### The `styles` prop is layout-only
- **Rule:** The `styles` prop on S2 components is restricted to layout properties (margin, width, flex, grid placement, position, z-index, visibility). Non-layout properties belong on a native element's `className={style(...)}`.
- **Detect:** `styles={style({` on an S2 component containing `backgroundColor`, `color`, `font`, `borderWidth`, or `padding`.
- **Severity:** Medium.

### Subpath imports, not the barrel
- **Rule:** Import components from their subpath (`@react-spectrum/s2/Button`), not the package barrel (`@react-spectrum/s2`). Shared types and list-data hooks (`useListData`, `Key`, `Selection`, …) are the exception — they're re-exported from the barrel.
- **Detect:** a named **component** import from the root: `import {Button|Picker|…} from '@react-spectrum/s2'`.
- **Severity:** Low.

### No third-party icon or design-system libraries
- **Rule:** Use S2 icons/illustrations and components. Don't introduce `lucide-react`, `heroicons`, `phosphor-icons`, `react-icons`, or whole design systems (MUI, Chakra, Radix, shadcn/ui, Ant Design) in S2 code.
- **Detect:** imports from those packages.
- **Severity:** Medium for icon libraries; High for a competing design system (overlaps with [02-component-usage](02-component-usage.md)).

### Don't restate default prop values
- **Rule:** Setting a prop to its default — `variant="primary"`, `size="M"`, `density="regular"` — is noise.
- **Detect:** those literal default props on S2 components.
- **Severity:** Low.
Loading