agentic-team 0.0.0-stage → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +189 -2
- package/bin/build-template.js +208 -0
- package/bin/index.js +1334 -0
- package/bin/platforms.js +510 -0
- package/package.json +41 -4
- package/template/.claude-plugin/plugin.json +10 -0
- package/template/.orchestra/README.md +391 -0
- package/template/.orchestra/blueprints/README.md +87 -0
- package/template/.orchestra/blueprints/api-only.md +46 -0
- package/template/.orchestra/blueprints/component-crud-resource.md +25 -0
- package/template/.orchestra/blueprints/saas-starter.md +69 -0
- package/template/.orchestra/config.yml +293 -0
- package/template/.orchestra/roles/orchestrator.md +105 -0
- package/template/.orchestra/roles/product-manager.md +439 -0
- package/template/.orchestra/scripts/compact-diff.awk +45 -0
- package/template/.orchestra/scripts/context.mjs +373 -0
- package/template/.orchestra/stacks/README.md +81 -0
- package/template/.orchestra/stacks/_lib/common.sh +3095 -0
- package/template/.orchestra/stacks/_repo/setup.sh +1113 -0
- package/template/.orchestra/stacks/backend-hono/claude/commands/endpoint.md +13 -0
- package/template/.orchestra/stacks/backend-hono/claude/skills/api-endpoint/SKILL.md +111 -0
- package/template/.orchestra/stacks/backend-hono/claude/skills/testing/SKILL.md +75 -0
- package/template/.orchestra/stacks/backend-hono/setup.sh +5945 -0
- package/template/.orchestra/stacks/backend-hono/stack.md +195 -0
- package/template/.orchestra/stacks/mobile-android-native/claude/commands/component.md +13 -0
- package/template/.orchestra/stacks/mobile-android-native/claude/skills/design-component/SKILL.md +58 -0
- package/template/.orchestra/stacks/mobile-android-native/setup.sh +607 -0
- package/template/.orchestra/stacks/mobile-android-native/stack.md +138 -0
- package/template/.orchestra/stacks/mobile-expo/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/mobile-expo/claude/skills/design-component/SKILL.md +92 -0
- package/template/.orchestra/stacks/mobile-expo/setup.sh +470 -0
- package/template/.orchestra/stacks/mobile-expo/stack.md +141 -0
- package/template/.orchestra/stacks/mobile-ios-native/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/mobile-ios-native/claude/skills/design-component/SKILL.md +58 -0
- package/template/.orchestra/stacks/mobile-ios-native/setup.sh +468 -0
- package/template/.orchestra/stacks/mobile-ios-native/stack.md +130 -0
- package/template/.orchestra/stacks/web-nextjs/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/web-nextjs/claude/skills/design-component/SKILL.md +100 -0
- package/template/.orchestra/stacks/web-nextjs/claude/skills/design-tokens/SKILL.md +61 -0
- package/template/.orchestra/stacks/web-nextjs/setup.sh +128 -0
- package/template/.orchestra/stacks/web-nextjs/stack.md +170 -0
- package/template/.orchestra/stacks/web-vite-react/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/web-vite-react/claude/skills/design-component/SKILL.md +100 -0
- package/template/.orchestra/stacks/web-vite-react/claude/skills/design-tokens/SKILL.md +61 -0
- package/template/.orchestra/stacks/web-vite-react/setup.sh +185 -0
- package/template/.orchestra/stacks/web-vite-react/stack.md +177 -0
- package/template/.orchestra/trackers/github.sh +467 -0
- package/template/CLAUDE.md +54 -0
- package/template/agents/architect.md +83 -0
- package/template/agents/engineer-high.md +17 -0
- package/template/agents/engineer-low.md +17 -0
- package/template/agents/engineer-max.md +17 -0
- package/template/agents/engineer-medium.md +17 -0
- package/template/agents/engineer-xhigh.md +17 -0
- package/template/agents/lead.md +925 -0
- package/template/agents/milestone-reviewer.md +65 -0
- package/template/agents/orchestrator.md +105 -0
- package/template/agents/product-manager.md +439 -0
- package/template/agents/re-reviewer.md +25 -0
- package/template/agents/reviewer.md +303 -0
- package/template/agents/scaffolder.md +206 -0
- package/template/agents/tracker.md +202 -0
- package/template/commands/blueprint.md +23 -0
- package/template/commands/create-role.md +64 -0
- package/template/commands/export.md +45 -0
- package/template/commands/help.md +56 -0
- package/template/commands/hotfix.md +14 -0
- package/template/commands/orchestrator.md +12 -0
- package/template/commands/pm.md +18 -0
- package/template/commands/rewind.md +63 -0
- package/template/commands/setup.md +15 -0
- package/template/commands/start.md +29 -0
- package/template/commands/status.md +11 -0
- package/template/commands/sync.md +76 -0
- package/template/commands/verifier.md +53 -0
- package/template/hooks/audit-context.sh +166 -0
- package/template/hooks/enforce-role.sh +193 -0
- package/template/hooks/protect-system-files.sh +123 -0
- package/template/hooks/review-rounds.sh +172 -0
- package/template/rules/acceptance-check.orchestra.md +11 -0
- package/template/rules/analytics-kpi.orchestra.md +56 -0
- package/template/rules/code-standards.orchestra.md +35 -0
- package/template/rules/comments.orchestra.md +77 -0
- package/template/rules/commit-format.orchestra.md +20 -0
- package/template/rules/dependency-upgrade.orchestra.md +112 -0
- package/template/rules/design-parity.orchestra.md +181 -0
- package/template/rules/observability.orchestra.md +121 -0
- package/template/rules/performance.orchestra.md +190 -0
- package/template/rules/phase-limits.orchestra.md +3 -0
- package/template/rules/pull-request.orchestra.md +218 -0
- package/template/rules/role-boundaries.orchestra.md +132 -0
- package/template/rules/security.orchestra.md +106 -0
- package/template/rules/stuck-detection.orchestra.md +25 -0
- package/template/rules/testing-standards.orchestra.md +219 -0
- package/template/rules/tracker-authority.orchestra.md +198 -0
- package/template/rules/verification-gate.orchestra.md +56 -0
- package/template/skills/accessibility/SKILL.md +31 -0
- package/template/skills/android-testing/SKILL.md +112 -0
- package/template/skills/architecture/SKILL.md +175 -0
- package/template/skills/auth-setup/SKILL.md +105 -0
- package/template/skills/best-practices/SKILL.md +29 -0
- package/template/skills/code-optimizer/SKILL.md +27 -0
- package/template/skills/core-web-vitals/SKILL.md +32 -0
- package/template/skills/crud-api/SKILL.md +32 -0
- package/template/skills/debug/SKILL.md +28 -0
- package/template/skills/deployment/SKILL.md +104 -0
- package/template/skills/error-handling/SKILL.md +154 -0
- package/template/skills/frontend-design/SKILL.md +30 -0
- package/template/skills/fullstack-infrastructure/SKILL.md +108 -0
- package/template/skills/ios-testing/SKILL.md +100 -0
- package/template/skills/mobile-testing/SKILL.md +102 -0
- package/template/skills/observability/SKILL.md +91 -0
- package/template/skills/project-structure/SKILL.md +114 -0
- package/template/skills/push-notifications/SKILL.md +146 -0
- package/template/skills/react-best-practices/SKILL.md +31 -0
- package/template/skills/realtime/SKILL.md +146 -0
- package/template/skills/review/SKILL.md +82 -0
- package/template/skills/testing/SKILL.md +45 -0
- package/template/skills/web-quality-audit/SKILL.md +47 -0
- package/template/skills/web-testing/SKILL.md +120 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: design-component
|
|
3
|
+
description: "Author a component in the design system — folder layout, API shape, tests, fixture, a11y. Use when adding or changing anything under src/design/."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Authoring a design system component
|
|
7
|
+
|
|
8
|
+
## Folder
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
src/design/{Name}/
|
|
12
|
+
├── {Name}.tsx # the component, nothing else
|
|
13
|
+
├── {Name}.test.tsx # interaction tests + one snapshot
|
|
14
|
+
├── {Name}.fixture.tsx # react-cosmos fixture
|
|
15
|
+
├── types.ts # props and any type this component owns
|
|
16
|
+
└── index.ts # public exports
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then add the component to `src/design/index.ts`.
|
|
20
|
+
|
|
21
|
+
## Before writing
|
|
22
|
+
|
|
23
|
+
1. Does a token exist for every value you are about to use? If not, add the
|
|
24
|
+
token first. Never hardcode a color, radius, spacing, or duration.
|
|
25
|
+
2. Is there a native element with the right semantics? Use it. A `<div>`
|
|
26
|
+
with a click handler is a defect.
|
|
27
|
+
3. Does this component need to know anything about the product? If yes, it
|
|
28
|
+
does not belong in `src/design/`.
|
|
29
|
+
|
|
30
|
+
## API
|
|
31
|
+
|
|
32
|
+
- accept `ref` as a prop — React 19 forwards it, `forwardRef` is legacy
|
|
33
|
+
- merge `className` in, do not overwrite the caller's
|
|
34
|
+
- spread remaining props onto the root
|
|
35
|
+
- variants and sizes are unions (`"primary" | "danger"`), never independent
|
|
36
|
+
booleans that can contradict each other
|
|
37
|
+
- no default that hides a required decision
|
|
38
|
+
|
|
39
|
+
## Tests
|
|
40
|
+
|
|
41
|
+
Interaction first, snapshot second.
|
|
42
|
+
|
|
43
|
+
Browser mode, verified working shape:
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import { expect, test, vi } from "vitest";
|
|
47
|
+
import { page } from "vitest/browser";
|
|
48
|
+
import { render } from "vitest-browser-react";
|
|
49
|
+
import { Button } from "./Button";
|
|
50
|
+
|
|
51
|
+
test("fires when clicked", async () => {
|
|
52
|
+
const onClick = vi.fn();
|
|
53
|
+
await render(<Button onClick={onClick}>Save</Button>);
|
|
54
|
+
await page.getByRole("button", { name: "Save" }).click();
|
|
55
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("does not fire when disabled", async () => {
|
|
59
|
+
const onClick = vi.fn();
|
|
60
|
+
await render(
|
|
61
|
+
<Button disabled onClick={onClick}>
|
|
62
|
+
Save
|
|
63
|
+
</Button>,
|
|
64
|
+
);
|
|
65
|
+
await expect.element(page.getByRole("button", { name: "Save" })).toBeDisabled();
|
|
66
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`render` is async — await it. Query through `page`. Import from
|
|
71
|
+
`vitest/browser`; `@vitest/browser/context` is deprecated.
|
|
72
|
+
|
|
73
|
+
Always query by role **and** accessible name. Role alone passes on a button
|
|
74
|
+
with no name, which is the defect a screen-reader user actually hits.
|
|
75
|
+
|
|
76
|
+
Also cover: focus returns to the trigger when an overlay closes, and one
|
|
77
|
+
snapshot per component for markup drift.
|
|
78
|
+
|
|
79
|
+
`src/components/Button` and `src/components/Select` are the two worked
|
|
80
|
+
examples — the native side of the line and the primitive side. Read them
|
|
81
|
+
before writing a third.
|
|
82
|
+
|
|
83
|
+
If the component wraps a third-party library, test *our* wrapper. A
|
|
84
|
+
Dependabot bump that changes the library's behavior has to turn this suite
|
|
85
|
+
red — that is the entire reason the suite exists.
|
|
86
|
+
|
|
87
|
+
## Accessibility checklist
|
|
88
|
+
|
|
89
|
+
- [ ] correct role, native element preferred
|
|
90
|
+
- [ ] accessible name reachable by screen reader
|
|
91
|
+
- [ ] full keyboard operation
|
|
92
|
+
- [ ] Escape closes, focus returns to trigger (overlays)
|
|
93
|
+
- [ ] focus ring visible in both themes
|
|
94
|
+
- [ ] contrast holds in both themes
|
|
95
|
+
|
|
96
|
+
## Done means
|
|
97
|
+
|
|
98
|
+
Component, types, tests, fixture, barrel export, and a11y checklist — all
|
|
99
|
+
of it. A component without a fixture is invisible to design comparison, and
|
|
100
|
+
a component without interaction tests is untested.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: design-tokens
|
|
3
|
+
description: "Extract tokens from the design source into Tailwind v4 @theme and the semantic theme layer. Use before writing any component, or when the design changes."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Design tokens
|
|
7
|
+
|
|
8
|
+
Tokens come first. A component written before the token system is a
|
|
9
|
+
component that hardcodes values.
|
|
10
|
+
|
|
11
|
+
## Extract every axis
|
|
12
|
+
|
|
13
|
+
Color is the one people remember and the least sufficient. Pull all of it:
|
|
14
|
+
|
|
15
|
+
| Axis | Examples |
|
|
16
|
+
|---|---|
|
|
17
|
+
| color | palette ramps, then semantic roles |
|
|
18
|
+
| spacing | the scale, not ad-hoc values |
|
|
19
|
+
| typography | family, size ramp, weight, line height |
|
|
20
|
+
| radii | control vs surface |
|
|
21
|
+
| shadow | elevation levels |
|
|
22
|
+
| breakpoints | the real ones from the design, not Tailwind defaults |
|
|
23
|
+
| z-index | named layers — overlay, modal, toast |
|
|
24
|
+
| motion | durations and easings |
|
|
25
|
+
|
|
26
|
+
## Two layers
|
|
27
|
+
|
|
28
|
+
**Palette** goes in `@theme`. Raw values, no meaning attached.
|
|
29
|
+
|
|
30
|
+
```css
|
|
31
|
+
@theme {
|
|
32
|
+
--color-brand-500: oklch(0.62 0.19 250);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Semantic** maps meaning onto palette, and is what components use.
|
|
37
|
+
|
|
38
|
+
```css
|
|
39
|
+
:root { --surface: var(--color-white); }
|
|
40
|
+
[data-theme="dark"] { --surface: var(--color-brand-900); }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A component references `--surface`. It never references `--color-brand-500`.
|
|
44
|
+
That is what makes a theme swap a one-file change.
|
|
45
|
+
|
|
46
|
+
## Theming
|
|
47
|
+
|
|
48
|
+
Three states must work: explicit light, explicit dark, and system default.
|
|
49
|
+
|
|
50
|
+
- `[data-theme="dark"]` and `[data-theme="light"]` for an explicit choice
|
|
51
|
+
- `@media (prefers-color-scheme: dark)` guarded with
|
|
52
|
+
`:root:not([data-theme="light"])` for the system default
|
|
53
|
+
|
|
54
|
+
Never define a color only inside a media query — the explicit toggle will
|
|
55
|
+
have nothing to override.
|
|
56
|
+
|
|
57
|
+
## Tailwind v4
|
|
58
|
+
|
|
59
|
+
There is no `tailwind.config.js`. `@theme` is the configuration, and its
|
|
60
|
+
entries become real CSS custom properties. Do not add a JS config to make
|
|
61
|
+
it feel familiar — you will end up with two sources of truth.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Next.js App Router. Biome via the native create-next-app flag, Tailwind v4,
|
|
3
|
+
# react-cosmos workbench, Vitest browser mode, Playwright e2e.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
DIR="${1:?usage: setup.sh <target-dir>}"
|
|
7
|
+
LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../_lib" && pwd)"
|
|
8
|
+
# shellcheck source=../_lib/common.sh
|
|
9
|
+
source "$LIB/common.sh"
|
|
10
|
+
|
|
11
|
+
# --biome is native now, so we never install eslint just to remove it.
|
|
12
|
+
# --disable-git because we are already inside a repo.
|
|
13
|
+
# create-next-app does not create intermediate directories, and when the
|
|
14
|
+
# parent is missing it reports "The application path is not writable, please
|
|
15
|
+
# check folder permissions" — which sends you after the wrong thing entirely.
|
|
16
|
+
mkdir -p "$(dirname "$DIR")"
|
|
17
|
+
|
|
18
|
+
log "scaffolding next.js into $DIR"
|
|
19
|
+
npx --yes create-next-app@latest "$DIR" \
|
|
20
|
+
--ts --app --src-dir --tailwind --biome --use-npm --skip-install --disable-git --yes </dev/null
|
|
21
|
+
|
|
22
|
+
assert_scaffolded "$DIR"
|
|
23
|
+
cd "$DIR"
|
|
24
|
+
|
|
25
|
+
setup_yarn
|
|
26
|
+
yarn_install
|
|
27
|
+
|
|
28
|
+
# create-next-app writes its own CLAUDE.md, which would collide with the
|
|
29
|
+
# stack's convention pack. AGENTS.md is kept — it does not clash.
|
|
30
|
+
rm -f CLAUDE.md
|
|
31
|
+
|
|
32
|
+
setup_typescript
|
|
33
|
+
setup_node_pin
|
|
34
|
+
setup_typecheck_script
|
|
35
|
+
|
|
36
|
+
add_latest @sentry/nextjs posthog-js
|
|
37
|
+
add_latest --dev react-cosmos
|
|
38
|
+
setup_vitest_browser
|
|
39
|
+
# The API seam, faked at the network layer. Mocking our own fetch wrapper
|
|
40
|
+
# instead would leave the wrapper — the thing that breaks when the API
|
|
41
|
+
# changes — untested.
|
|
42
|
+
add_latest --dev msw
|
|
43
|
+
setup_axe
|
|
44
|
+
setup_playwright_e2e "yarn build && yarn start --port 3000" "http://localhost:3000"
|
|
45
|
+
setup_axe_e2e
|
|
46
|
+
setup_bundle_budget
|
|
47
|
+
|
|
48
|
+
pkg_set scripts.cosmos="cosmos" scripts.lint="biome check ." scripts.format="biome format --write ."
|
|
49
|
+
|
|
50
|
+
# Next generates LayoutProps/PageProps into .next/types. On a clean checkout —
|
|
51
|
+
# which is what CI is — they do not exist yet and tsc fails on them, so
|
|
52
|
+
# generate before typechecking.
|
|
53
|
+
pkg_set scripts.typecheck="next typegen && tsc -b --noEmit"
|
|
54
|
+
# --json, or npm writes the string "false" and bundlers treat it as a glob.
|
|
55
|
+
pkg_set sideEffects=false
|
|
56
|
+
|
|
57
|
+
log "writing design system skeleton"
|
|
58
|
+
mkdir -p src/design/helpers
|
|
59
|
+
cat > src/design/tokens.css <<'CSS'
|
|
60
|
+
@import "tailwindcss";
|
|
61
|
+
|
|
62
|
+
/* Populate from the Claude Design source before writing any component.
|
|
63
|
+
Color is one axis of several — do not stop there. */
|
|
64
|
+
@theme {
|
|
65
|
+
--color-brand-50: oklch(0.97 0.02 250);
|
|
66
|
+
--color-brand-500: oklch(0.62 0.19 250);
|
|
67
|
+
--color-brand-900: oklch(0.35 0.12 250);
|
|
68
|
+
--radius-control: 0.5rem;
|
|
69
|
+
--font-sans: system-ui, sans-serif;
|
|
70
|
+
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
|
|
71
|
+
--duration-fast: 120ms;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
:root { --surface: var(--color-white); --on-surface: var(--color-brand-900); }
|
|
75
|
+
[data-theme="dark"] { --surface: var(--color-brand-900); --on-surface: var(--color-brand-50); }
|
|
76
|
+
@media (prefers-color-scheme: dark) {
|
|
77
|
+
:root:not([data-theme="light"]) {
|
|
78
|
+
--surface: var(--color-brand-900); --on-surface: var(--color-brand-50);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
CSS
|
|
82
|
+
|
|
83
|
+
printf 'export type ThemeName = "light" | "dark";\nexport type Size = "sm" | "md" | "lg";\n' > src/design/types.ts
|
|
84
|
+
printf 'export * from "./types";\n' > src/design/index.ts
|
|
85
|
+
|
|
86
|
+
# create-next-app writes its own globals.css and imports it from the root
|
|
87
|
+
# layout. Without this the tokens file exists and is never loaded.
|
|
88
|
+
GLOBALS="$(find src/app -maxdepth 1 -name 'globals.css' | head -1)"
|
|
89
|
+
if [ -n "$GLOBALS" ]; then
|
|
90
|
+
# tokens.css already does `@import "tailwindcss"` — drop the duplicate.
|
|
91
|
+
node -e '
|
|
92
|
+
const fs = require("fs");
|
|
93
|
+
const f = process.argv[1];
|
|
94
|
+
let s = fs.readFileSync(f, "utf8");
|
|
95
|
+
s = s.replace(/^@import\s+["\x27]tailwindcss["\x27];\s*$/m, "");
|
|
96
|
+
if (!s.includes("design/tokens.css")) {
|
|
97
|
+
s = `@import "../design/tokens.css";\n` + s;
|
|
98
|
+
}
|
|
99
|
+
fs.writeFileSync(f, s.trimStart() + "\n");
|
|
100
|
+
' "$GLOBALS"
|
|
101
|
+
log "design tokens imported from $GLOBALS"
|
|
102
|
+
else
|
|
103
|
+
warn "globals.css not found — import src/design/tokens.css manually or tokens will not load"
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# create-next-app --biome installs biome at whatever version its template
|
|
107
|
+
# pinned and writes a bare biome.json. Running the shared setup brings the
|
|
108
|
+
# version to current and applies the config the rest of the stack relies on
|
|
109
|
+
# — tailwindDirectives, and excludes so build output is not linted.
|
|
110
|
+
setup_biome
|
|
111
|
+
|
|
112
|
+
install_pre_commit_gate
|
|
113
|
+
setup_integrations_dir
|
|
114
|
+
setup_ui_primitives client
|
|
115
|
+
|
|
116
|
+
setup_coverage
|
|
117
|
+
setup_mutation
|
|
118
|
+
|
|
119
|
+
# create-next-app ships "@types/node": "^20" and friends. Nothing above
|
|
120
|
+
# touches them, so without this the repo is born several majors behind.
|
|
121
|
+
refresh_scaffold_deps
|
|
122
|
+
# Everything above was generated after setup_biome formatted the scaffold.
|
|
123
|
+
# Without this the first commit is rejected by the gate we just installed.
|
|
124
|
+
yarn biome check --write . >/dev/null 2>&1 || true
|
|
125
|
+
|
|
126
|
+
assert_verification_scripts
|
|
127
|
+
assert_scaffold_clean
|
|
128
|
+
log "web-nextjs ready in $DIR"
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Web — Next.js (App Router)
|
|
2
|
+
|
|
3
|
+
## Who you are
|
|
4
|
+
|
|
5
|
+
You are a **senior frontend engineer** who understands where code runs.
|
|
6
|
+
Server Components by default; `"use client"` only where interactivity
|
|
7
|
+
genuinely requires it, and as far down the tree as possible. A page that
|
|
8
|
+
ships the whole app to the browser because one leaf needed state is a
|
|
9
|
+
defect.
|
|
10
|
+
|
|
11
|
+
SOLID, KISS, YAGNI. No overengineering — when it starts feeling clever,
|
|
12
|
+
simplify.
|
|
13
|
+
|
|
14
|
+
## Structure
|
|
15
|
+
|
|
16
|
+
Everything under `src/`.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
src/
|
|
20
|
+
├── app/ # routes, layouts, server components
|
|
21
|
+
├── design/ # the UI kit. no business logic. ever.
|
|
22
|
+
│ ├── tokens.css
|
|
23
|
+
│ ├── types.ts
|
|
24
|
+
│ ├── helpers/
|
|
25
|
+
│ ├── index.ts
|
|
26
|
+
│ └── Button/
|
|
27
|
+
│ ├── Button.tsx
|
|
28
|
+
│ ├── Button.test.tsx
|
|
29
|
+
│ ├── Button.fixture.tsx
|
|
30
|
+
│ ├── types.ts
|
|
31
|
+
│ └── index.ts
|
|
32
|
+
└── ...
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Every component gets its own folder: component, tests, fixture, types,
|
|
36
|
+
`index.ts`. Uniform, no exceptions.
|
|
37
|
+
|
|
38
|
+
`src/design/` holds nothing that is not design. A component may contain
|
|
39
|
+
logic about itself; never about your product.
|
|
40
|
+
|
|
41
|
+
## Server and client
|
|
42
|
+
|
|
43
|
+
- Design system components are presentational and mostly client-safe, but
|
|
44
|
+
do not add `"use client"` reflexively — only when the component actually
|
|
45
|
+
uses state, effects, or browser APIs.
|
|
46
|
+
- Data fetching belongs in Server Components, not in `useEffect`.
|
|
47
|
+
- Never leak a secret into a client component. It ships to the browser.
|
|
48
|
+
|
|
49
|
+
## Order of work
|
|
50
|
+
|
|
51
|
+
Tokens before components. Always.
|
|
52
|
+
|
|
53
|
+
1. Extract every token from the design source — color, spacing, typography,
|
|
54
|
+
radii, shadows, breakpoints, z-index, motion.
|
|
55
|
+
2. Write them into `src/design/tokens.css` under `@theme`.
|
|
56
|
+
3. Add the semantic layer and the dark variant.
|
|
57
|
+
4. Then write components.
|
|
58
|
+
|
|
59
|
+
Hardcoded colors or spacing are bugs. Reject them in review.
|
|
60
|
+
|
|
61
|
+
## Tailwind v4
|
|
62
|
+
|
|
63
|
+
Configuration is CSS, not `tailwind.config.js`. `@theme` entries become
|
|
64
|
+
real CSS custom properties, so tokens and theming are one system. Do not
|
|
65
|
+
add a JS config file.
|
|
66
|
+
|
|
67
|
+
## Where a component comes from
|
|
68
|
+
|
|
69
|
+
One question decides it:
|
|
70
|
+
|
|
71
|
+
> Does the browser already give me the role, the keyboard behaviour and the
|
|
72
|
+
> focus management?
|
|
73
|
+
|
|
74
|
+
**Yes — write it.** `button`, `input`, `select`, `textarea`, `a`, `table`,
|
|
75
|
+
`details`, `dialog` with `showModal()`, the Popover API. These carry their
|
|
76
|
+
semantics for free, and a library on top of them buys a dependency and
|
|
77
|
+
nothing else. Prefer the platform, and check its support at the time rather
|
|
78
|
+
than trusting a table written here — that is the same rule dependencies
|
|
79
|
+
follow.
|
|
80
|
+
|
|
81
|
+
**No — use an unstyled primitive.** Combobox, listbox with custom content,
|
|
82
|
+
menu with typeahead, tabs, date picker, slider. There is no element for
|
|
83
|
+
these, so hand-writing one means re-implementing the WAI-ARIA authoring
|
|
84
|
+
practice: roving focus, typeahead, dismissal layers, focus return,
|
|
85
|
+
`aria-activedescendant`. That work is not a component, it is a library, and
|
|
86
|
+
this stack runs axe as an assertion — so a hand-rolled one fails the gate
|
|
87
|
+
over and over instead of once, at the library choice.
|
|
88
|
+
|
|
89
|
+
`react-aria-components` is the default. It ships behaviour and **no styling**,
|
|
90
|
+
it is tree-shaken per component, and it covers the patterns that have no
|
|
91
|
+
element — including the date picker, which nothing should hand-roll. Radix is
|
|
92
|
+
an acceptable substitute; a styled kit like shadcn or MUI is not, because it
|
|
93
|
+
brings someone else's opinion about your product.
|
|
94
|
+
|
|
95
|
+
Tailwind stays. It is utility CSS, not components — it styles the elements we
|
|
96
|
+
write.
|
|
97
|
+
|
|
98
|
+
### The line is not a style preference
|
|
99
|
+
|
|
100
|
+
Take the worked `Select` and swap the primitive's `Popover` for a plain
|
|
101
|
+
`div`, which is what a hand-rolled version is. Opening still works, selecting
|
|
102
|
+
still works, and **axe still passes**. The only thing that breaks is Escape
|
|
103
|
+
returning focus to the trigger — a keyboard user is silently dropped at the
|
|
104
|
+
top of the document.
|
|
105
|
+
|
|
106
|
+
That is the whole argument. The failure is invisible to the linter, invisible
|
|
107
|
+
to a snapshot, and invisible in review. It is only visible to an interaction
|
|
108
|
+
test, which is why `src/components/Select/Select.test.tsx` has one.
|
|
109
|
+
|
|
110
|
+
A primitive is **not** an `src/integrations/` seam — it is neither an SDK call
|
|
111
|
+
nor an HTTP API, so it lives with the component that uses it. Its coverage
|
|
112
|
+
comes from the interaction test driving the real thing, which is a stronger
|
|
113
|
+
check than a line-count gate.
|
|
114
|
+
## Memory — the collector cannot end what you still hold
|
|
115
|
+
|
|
116
|
+
React does not free a subscription for you, and the collector cannot free
|
|
117
|
+
what a live closure still references. So:
|
|
118
|
+
|
|
119
|
+
- **Every effect that starts something returns the function that stops
|
|
120
|
+
it.** `addEventListener` → `removeEventListener`; `setInterval` →
|
|
121
|
+
`clearInterval`; `subscribe` → `unsubscribe`; a fetch → an
|
|
122
|
+
`AbortController` aborted in the cleanup. The gate checks that the cleanup
|
|
123
|
+
exists; only you can check that it stops the right thing.
|
|
124
|
+
- State that only grows is a leak with a UI. A list that appends on every
|
|
125
|
+
event and never trims, a map of every id ever seen — bound it, or key it
|
|
126
|
+
by what is on screen.
|
|
127
|
+
- A closure created in render captures the render's state and is recreated
|
|
128
|
+
every render. That is by design; it becomes a cost when it is passed to a
|
|
129
|
+
memoized child or a long list. `.claude/rules/performance.orchestra.md`
|
|
130
|
+
says where that matters.
|
|
131
|
+
- A module-level cache is process-lifetime — and on the server it is shared
|
|
132
|
+
by every request. Give it a maximum and an eviction rule, and never put a
|
|
133
|
+
request's data in it.
|
|
134
|
+
|
|
135
|
+
## Testing
|
|
136
|
+
|
|
137
|
+
Coverage target is effectively total. The deciding rule:
|
|
138
|
+
|
|
139
|
+
> If one of our functions calls into a third-party library, that function
|
|
140
|
+
> has a test. When Dependabot bumps the library and behavior changes, the
|
|
141
|
+
> test must fail.
|
|
142
|
+
|
|
143
|
+
- **Interaction tests** (Vitest browser mode, Playwright driver) — primary.
|
|
144
|
+
- **Snapshots** — secondary, markup drift only. Snapshot-only is untested.
|
|
145
|
+
- **E2E** (`@playwright/test`) — flows across routes.
|
|
146
|
+
|
|
147
|
+
Server Components need tests too: assert what they render given data, and
|
|
148
|
+
that they do not reach for browser APIs.
|
|
149
|
+
|
|
150
|
+
## Accessibility
|
|
151
|
+
|
|
152
|
+
Structural. Every component: correct role and native element first,
|
|
153
|
+
reachable accessible name, full keyboard operation, Escape and focus return
|
|
154
|
+
for overlays, visible focus in both themes, contrast holding in both.
|
|
155
|
+
|
|
156
|
+
## Component API
|
|
157
|
+
|
|
158
|
+
Forward `ref`, merge `className`, spread the rest. Variants and sizes are
|
|
159
|
+
unions, not booleans that can contradict. Props types live in the
|
|
160
|
+
component's folder and export from its `index.ts`.
|
|
161
|
+
|
|
162
|
+
## Workbench
|
|
163
|
+
|
|
164
|
+
`react-cosmos` (`npm run cosmos`). Every component has a fixture — it is
|
|
165
|
+
the catalog and the stable render target for design comparison.
|
|
166
|
+
|
|
167
|
+
## Tooling
|
|
168
|
+
|
|
169
|
+
Biome for lint and format, installed natively by `create-next-app --biome`.
|
|
170
|
+
No ESLint, no Prettier. TypeScript strict with `noUncheckedIndexedAccess`.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Scaffold a new design system component.
|
|
2
|
+
|
|
3
|
+
Usage: `/web:component {Name}`
|
|
4
|
+
|
|
5
|
+
1. Load the `design-component` skill and follow it exactly.
|
|
6
|
+
2. Confirm every value the component needs has a token. If one is missing,
|
|
7
|
+
add it to `src/design/tokens.css` first — do not hardcode.
|
|
8
|
+
3. Create `src/design/{Name}/` with the component, types, tests, fixture,
|
|
9
|
+
and `index.ts`.
|
|
10
|
+
4. Export it from `src/design/index.ts`.
|
|
11
|
+
5. Run `npm run typecheck && npm test && npm run lint`.
|
|
12
|
+
6. Report the a11y checklist with each item explicitly checked or flagged.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: design-component
|
|
3
|
+
description: "Author a component in the design system — folder layout, API shape, tests, fixture, a11y. Use when adding or changing anything under src/design/."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Authoring a design system component
|
|
7
|
+
|
|
8
|
+
## Folder
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
src/design/{Name}/
|
|
12
|
+
├── {Name}.tsx # the component, nothing else
|
|
13
|
+
├── {Name}.test.tsx # interaction tests + one snapshot
|
|
14
|
+
├── {Name}.fixture.tsx # react-cosmos fixture
|
|
15
|
+
├── types.ts # props and any type this component owns
|
|
16
|
+
└── index.ts # public exports
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then add the component to `src/design/index.ts`.
|
|
20
|
+
|
|
21
|
+
## Before writing
|
|
22
|
+
|
|
23
|
+
1. Does a token exist for every value you are about to use? If not, add the
|
|
24
|
+
token first. Never hardcode a color, radius, spacing, or duration.
|
|
25
|
+
2. Is there a native element with the right semantics? Use it. A `<div>`
|
|
26
|
+
with a click handler is a defect.
|
|
27
|
+
3. Does this component need to know anything about the product? If yes, it
|
|
28
|
+
does not belong in `src/design/`.
|
|
29
|
+
|
|
30
|
+
## API
|
|
31
|
+
|
|
32
|
+
- accept `ref` as a prop — React 19 forwards it, `forwardRef` is legacy
|
|
33
|
+
- merge `className` in, do not overwrite the caller's
|
|
34
|
+
- spread remaining props onto the root
|
|
35
|
+
- variants and sizes are unions (`"primary" | "danger"`), never independent
|
|
36
|
+
booleans that can contradict each other
|
|
37
|
+
- no default that hides a required decision
|
|
38
|
+
|
|
39
|
+
## Tests
|
|
40
|
+
|
|
41
|
+
Interaction first, snapshot second.
|
|
42
|
+
|
|
43
|
+
Browser mode, verified working shape:
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import { expect, test, vi } from "vitest";
|
|
47
|
+
import { page } from "vitest/browser";
|
|
48
|
+
import { render } from "vitest-browser-react";
|
|
49
|
+
import { Button } from "./Button";
|
|
50
|
+
|
|
51
|
+
test("fires when clicked", async () => {
|
|
52
|
+
const onClick = vi.fn();
|
|
53
|
+
await render(<Button onClick={onClick}>Save</Button>);
|
|
54
|
+
await page.getByRole("button", { name: "Save" }).click();
|
|
55
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("does not fire when disabled", async () => {
|
|
59
|
+
const onClick = vi.fn();
|
|
60
|
+
await render(
|
|
61
|
+
<Button disabled onClick={onClick}>
|
|
62
|
+
Save
|
|
63
|
+
</Button>,
|
|
64
|
+
);
|
|
65
|
+
await expect.element(page.getByRole("button", { name: "Save" })).toBeDisabled();
|
|
66
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`render` is async — await it. Query through `page`. Import from
|
|
71
|
+
`vitest/browser`; `@vitest/browser/context` is deprecated.
|
|
72
|
+
|
|
73
|
+
Always query by role **and** accessible name. Role alone passes on a button
|
|
74
|
+
with no name, which is the defect a screen-reader user actually hits.
|
|
75
|
+
|
|
76
|
+
Also cover: focus returns to the trigger when an overlay closes, and one
|
|
77
|
+
snapshot per component for markup drift.
|
|
78
|
+
|
|
79
|
+
`src/components/Button` and `src/components/Select` are the two worked
|
|
80
|
+
examples — the native side of the line and the primitive side. Read them
|
|
81
|
+
before writing a third.
|
|
82
|
+
|
|
83
|
+
If the component wraps a third-party library, test *our* wrapper. A
|
|
84
|
+
Dependabot bump that changes the library's behavior has to turn this suite
|
|
85
|
+
red — that is the entire reason the suite exists.
|
|
86
|
+
|
|
87
|
+
## Accessibility checklist
|
|
88
|
+
|
|
89
|
+
- [ ] correct role, native element preferred
|
|
90
|
+
- [ ] accessible name reachable by screen reader
|
|
91
|
+
- [ ] full keyboard operation
|
|
92
|
+
- [ ] Escape closes, focus returns to trigger (overlays)
|
|
93
|
+
- [ ] focus ring visible in both themes
|
|
94
|
+
- [ ] contrast holds in both themes
|
|
95
|
+
|
|
96
|
+
## Done means
|
|
97
|
+
|
|
98
|
+
Component, types, tests, fixture, barrel export, and a11y checklist — all
|
|
99
|
+
of it. A component without a fixture is invisible to design comparison, and
|
|
100
|
+
a component without interaction tests is untested.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: design-tokens
|
|
3
|
+
description: "Extract tokens from the design source into Tailwind v4 @theme and the semantic theme layer. Use before writing any component, or when the design changes."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Design tokens
|
|
7
|
+
|
|
8
|
+
Tokens come first. A component written before the token system is a
|
|
9
|
+
component that hardcodes values.
|
|
10
|
+
|
|
11
|
+
## Extract every axis
|
|
12
|
+
|
|
13
|
+
Color is the one people remember and the least sufficient. Pull all of it:
|
|
14
|
+
|
|
15
|
+
| Axis | Examples |
|
|
16
|
+
|---|---|
|
|
17
|
+
| color | palette ramps, then semantic roles |
|
|
18
|
+
| spacing | the scale, not ad-hoc values |
|
|
19
|
+
| typography | family, size ramp, weight, line height |
|
|
20
|
+
| radii | control vs surface |
|
|
21
|
+
| shadow | elevation levels |
|
|
22
|
+
| breakpoints | the real ones from the design, not Tailwind defaults |
|
|
23
|
+
| z-index | named layers — overlay, modal, toast |
|
|
24
|
+
| motion | durations and easings |
|
|
25
|
+
|
|
26
|
+
## Two layers
|
|
27
|
+
|
|
28
|
+
**Palette** goes in `@theme`. Raw values, no meaning attached.
|
|
29
|
+
|
|
30
|
+
```css
|
|
31
|
+
@theme {
|
|
32
|
+
--color-brand-500: oklch(0.62 0.19 250);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Semantic** maps meaning onto palette, and is what components use.
|
|
37
|
+
|
|
38
|
+
```css
|
|
39
|
+
:root { --surface: var(--color-white); }
|
|
40
|
+
[data-theme="dark"] { --surface: var(--color-brand-900); }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A component references `--surface`. It never references `--color-brand-500`.
|
|
44
|
+
That is what makes a theme swap a one-file change.
|
|
45
|
+
|
|
46
|
+
## Theming
|
|
47
|
+
|
|
48
|
+
Three states must work: explicit light, explicit dark, and system default.
|
|
49
|
+
|
|
50
|
+
- `[data-theme="dark"]` and `[data-theme="light"]` for an explicit choice
|
|
51
|
+
- `@media (prefers-color-scheme: dark)` guarded with
|
|
52
|
+
`:root:not([data-theme="light"])` for the system default
|
|
53
|
+
|
|
54
|
+
Never define a color only inside a media query — the explicit toggle will
|
|
55
|
+
have nothing to override.
|
|
56
|
+
|
|
57
|
+
## Tailwind v4
|
|
58
|
+
|
|
59
|
+
There is no `tailwind.config.js`. `@theme` is the configuration, and its
|
|
60
|
+
entries become real CSS custom properties. Do not add a JS config to make
|
|
61
|
+
it feel familiar — you will end up with two sources of truth.
|