docs(plan): core-ui component generator (5 phases, ~7 commits)

Implementation plan for the spec at docs/superpowers/specs/2026-05-11-core-
ui-component-generator-design.md. Five phases:

- Phase 1: add splice anchors to core-ui tier barrel templates +
  regenerate the byte-identical ui snapshot
- Phase 2: create the 4 Handlebars template files
  (component.tsx + stories + test + index)
- Phase 3 (TDD): register the setGenerator block, implement
  coreUiComponentActions + printCoreUiComponentNextSteps, add unit
  tests for registration shape, action sequence, and PascalCase
  validation
- Phase 4: scaffolding reference doc + CLAUDE.md Quick Start +
  AGENTS.md Key Commands
- Phase 5: manual smoke test (scaffold one component per tier, verify
  file layout + spliced barrels + core-ui gates) + final repo gates

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 08:51:53 +02:00
parent 5878b8f493
commit 09ac0bba6c

View File

@@ -0,0 +1,955 @@
# core-ui Component Generator Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Ship `pnpm turbo gen core-ui-component` — a plop generator that scaffolds atomic-design components (atom / molecule / organism) into `packages/core-ui/` using the established 4-file pattern (component.tsx, stories.tsx, test.tsx, index.ts), with the new export spliced into the corresponding tier barrel.
**Architecture:** Two-phase delivery. Phase 1 adds `// <gen:atoms>` / `// <gen:molecules>` / `// <gen:organisms>` anchors to the core-ui template's three tier barrels and regenerates the byte-identical snapshot. Phase 2 ships the new generator (4 template files + `setGenerator` block + action helpers + docs). The generator refuses to run when `packages/core-ui/` isn't scaffolded.
**Tech Stack:** TypeScript, Node 22, plop (via `@turbo/gen`), Vitest, Handlebars.
**Spec:** `docs/superpowers/specs/2026-05-11-core-ui-component-generator-design.md` — read first, especially §4 (Phase A template prep) and §5 (Phase B generator).
---
## Phase 0 — Read first
- [ ] **Step 1: Read the spec end-to-end**
Open `docs/superpowers/specs/2026-05-11-core-ui-component-generator-design.md`. The plan maps to its sections 1:1. §4 (template prep), §5 (generator), §6 (testing), §7 (docs), §8 (out of scope) are load-bearing references.
- [ ] **Step 2: Skim existing generator patterns**
Read these sections of `turbo/generators/config.ts`:
- The `feature` generator (line ~32 — top-level package scaffold with kebabCase paths)
- The `event` generator (line ~345 — list prompt + branching actions)
- The `realtime` generator (line ~429 — closest analog to what you're building: list prompt + per-mode actions + print next-steps)
Note the existing local helpers at the bottom of `config.ts`: `camel(input)` (~line 1293), `pascalCase(input)` (~line 1297), `constantCase(input)` (~line 1305). There is no `kebabCase` JS helper — Handlebars `{{kebabCase name}}` is used in template strings, expanded at plop apply-time.
- [ ] **Step 3: Read the canonical component pattern**
Open `turbo/generators/templates/core-package/ui/src/atoms/button/button.tsx.hbs` and the three sibling files (`.stories.tsx.hbs`, `.test.tsx.hbs`, `index.ts.hbs`). The new generator emits files in the same shape, just minimal (no variants/sizes).
---
## Phase 1 — Template prep (anchors + snapshot regen)
**Goal:** Make the core-ui template ready for the new generator by adding splice anchors to its tier barrels. After this phase, freshly scaffolded core-ui packages have `// <gen:atoms>` / `// <gen:molecules>` / `// <gen:organisms>` ready for the generator to splice into.
**Files touched:**
- Modify: `turbo/generators/templates/core-package/ui/src/atoms/index.ts.hbs`
- Modify: `turbo/generators/templates/core-package/ui/src/molecules/index.ts.hbs`
- Modify: `turbo/generators/templates/core-package/ui/src/organisms/index.ts.hbs`
- Modify: `turbo/generators/__snapshots__/core-package/ui.snapshot.json`
### Task 1.1: Add anchors to the 3 tier barrel templates
**Files:**
- Modify: `turbo/generators/templates/core-package/ui/src/atoms/index.ts.hbs`
- Modify: `turbo/generators/templates/core-package/ui/src/molecules/index.ts.hbs`
- Modify: `turbo/generators/templates/core-package/ui/src/organisms/index.ts.hbs`
- [ ] **Step 1: Inspect the current barrel contents**
Run:
```bash
cat turbo/generators/templates/core-package/ui/src/atoms/index.ts.hbs
cat turbo/generators/templates/core-package/ui/src/molecules/index.ts.hbs
cat turbo/generators/templates/core-package/ui/src/organisms/index.ts.hbs
```
Expected output:
```
// atoms/index.ts.hbs
export { Button, type ButtonProps } from "./button/index";
export { Input, type InputProps } from "./input/index";
export { Label, type LabelProps } from "./label/index";
// molecules/index.ts.hbs
export { FormField, type FormFieldProps } from "./form-field/index";
// organisms/index.ts.hbs
export {};
```
- [ ] **Step 2: Add the atoms anchor**
Replace the entire contents of `turbo/generators/templates/core-package/ui/src/atoms/index.ts.hbs` with:
```ts
// <gen:atoms>
export { Button, type ButtonProps } from "./button/index";
export { Input, type InputProps } from "./input/index";
export { Label, type LabelProps } from "./label/index";
```
- [ ] **Step 3: Add the molecules anchor**
Replace the entire contents of `turbo/generators/templates/core-package/ui/src/molecules/index.ts.hbs` with:
```ts
// <gen:molecules>
export { FormField, type FormFieldProps } from "./form-field/index";
```
- [ ] **Step 4: Add the organisms anchor**
Replace the entire contents of `turbo/generators/templates/core-package/ui/src/organisms/index.ts.hbs` with:
```ts
// <gen:organisms>
export {};
```
- [ ] **Step 5: Commit**
```bash
git add turbo/generators/templates/core-package/ui/src/atoms/index.ts.hbs \
turbo/generators/templates/core-package/ui/src/molecules/index.ts.hbs \
turbo/generators/templates/core-package/ui/src/organisms/index.ts.hbs
git commit -m "feat(generators): add splice anchors to core-ui tier barrels"
```
### Task 1.2: Regenerate the ui snapshot
The snapshot at `turbo/generators/__snapshots__/core-package/ui.snapshot.json` hashes the post-scaffold contents of `packages/core-ui/`. Since the tier barrel contents changed in Task 1.1, the snapshot is now stale. The existing `core-package-ui.e2e.test.ts` will FAIL until the snapshot matches.
**Files:**
- Modify: `turbo/generators/__snapshots__/core-package/ui.snapshot.json`
- [ ] **Step 1: Confirm the e2e test currently fails**
Run:
```bash
pnpm --filter @repo/turbo-generators test core-package-ui.e2e
```
Expected: FAIL — at least 3 file hashes mismatch (atoms/index.ts, molecules/index.ts, organisms/index.ts).
- [ ] **Step 2: Regenerate the snapshot via the existing generator into a tmp workspace**
Use the same machinery the e2e test uses, but write the output back to the committed snapshot path:
```bash
pnpm exec tsx <<'TS'
import { mkdtempSync, cpSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { execSync } from "node:child_process";
import { join } from "node:path";
import { computeSnapshot } from "./turbo/generators/lib/snapshot.js";
const REPO = process.cwd();
const TMP = mkdtempSync(join(tmpdir(), "regen-ui-"));
cpSync(REPO, TMP, {
recursive: true,
filter: (src) =>
!src.includes("node_modules") &&
!src.includes(".turbo") &&
!src.includes(".git") &&
!src.includes("packages/core-ui"),
});
execSync(`cd ${TMP} && pnpm install --silent`, { stdio: "inherit" });
execSync(`cd ${TMP} && pnpm turbo gen core-package --args ui`, { stdio: "inherit" });
const snap = computeSnapshot(join(TMP, "packages/core-ui"));
const out = join(REPO, "turbo/generators/__snapshots__/core-package/ui.snapshot.json");
writeFileSync(out, JSON.stringify(snap, null, 2) + "\n");
console.log(`Wrote ${snap.length} entries to ${out}`);
TS
```
If the script fails because plop is interactive (the `core-package` prompt expects a list selection), wrap the generator call to pipe the answer:
```bash
execSync(`cd ${TMP} && echo "ui" | pnpm turbo gen core-package`, { stdio: "inherit" });
```
(`--args ui` may or may not work depending on the turbo/plop version. If it doesn't, fall back to piping.)
- [ ] **Step 3: Run the e2e test to verify it now passes**
```bash
pnpm --filter @repo/turbo-generators test core-package-ui.e2e
```
Expected: PASS.
- [ ] **Step 4: Also verify the other 3 e2e tests still pass (no spillover)**
```bash
pnpm --filter @repo/turbo-generators test
```
Expected: all 17 tests pass (or the current count; should match what was green before this work).
- [ ] **Step 5: Commit**
```bash
git add turbo/generators/__snapshots__/core-package/ui.snapshot.json
git commit -m "feat(generators): regenerate ui snapshot for new tier-barrel anchors"
```
### Task 1.3: Phase 1 verification gate
- [ ] **Step 1: Run all gates from repo root**
```bash
pnpm lint && pnpm typecheck && pnpm test && pnpm turbo boundaries
```
Expected: all green.
(No commit; verification gate only.)
---
## Phase 2 — Generator template files
**Goal:** Create the 4 Handlebars template files that the new `core-ui-component` generator emits.
**Files touched:**
- Create: `turbo/generators/templates/core-ui-component/component.tsx.hbs`
- Create: `turbo/generators/templates/core-ui-component/component.stories.tsx.hbs`
- Create: `turbo/generators/templates/core-ui-component/component.test.tsx.hbs`
- Create: `turbo/generators/templates/core-ui-component/index.ts.hbs`
### Task 2.1: Create all 4 template files
These files are independent stubs; they're created together in one commit since none has tests of its own (the generator's emit behavior is unit-tested in Phase 3).
- [ ] **Step 1: Create the directory**
```bash
mkdir -p turbo/generators/templates/core-ui-component
```
- [ ] **Step 2: Create `component.tsx.hbs`**
Write to `turbo/generators/templates/core-ui-component/component.tsx.hbs`:
```hbs
import { forwardRef, type HTMLAttributes } from "react";
import { cn } from "../../lib/utils";
export interface {{pascalCase name}}Props extends HTMLAttributes<HTMLDivElement> {}
export const {{pascalCase name}} = forwardRef<HTMLDivElement, {{pascalCase name}}Props>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("", className)} {...props} />
),
);
{{pascalCase name}}.displayName = "{{pascalCase name}}";
```
- [ ] **Step 3: Create `component.stories.tsx.hbs`**
Write to `turbo/generators/templates/core-ui-component/component.stories.tsx.hbs`:
```hbs
import type { Meta, StoryObj } from "@storybook/react";
import { {{pascalCase name}} } from "./{{kebabCase name}}";
const meta = {
title: "{{tierTitle}}/{{pascalCase name}}",
component: {{pascalCase name}},
tags: ["autodocs"],
} satisfies Meta<typeof {{pascalCase name}}>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { args: {} };
```
- [ ] **Step 4: Create `component.test.tsx.hbs`**
Write to `turbo/generators/templates/core-ui-component/component.test.tsx.hbs`:
```hbs
import { describe, it, expect } from "vitest";
import { createRef } from "react";
import { renderWithProviders } from "@repo/core-testing/react";
import { screen } from "@testing-library/react";
import { {{pascalCase name}} } from "./{{kebabCase name}}";
describe("{{pascalCase name}}", () => {
it("renders without crashing", () => {
renderWithProviders(<{{pascalCase name}} data-testid="root" />);
expect(screen.getByTestId("root")).toBeInTheDocument();
});
it("applies a passed className alongside its own", () => {
renderWithProviders(<{{pascalCase name}} data-testid="root" className="custom" />);
expect(screen.getByTestId("root")).toHaveClass("custom");
});
it("forwards ref to the underlying element", () => {
const ref = createRef<HTMLDivElement>();
renderWithProviders(<{{pascalCase name}} ref={ref} />);
expect(ref.current).toBeInstanceOf(HTMLDivElement);
});
});
```
- [ ] **Step 5: Create `index.ts.hbs`**
Write to `turbo/generators/templates/core-ui-component/index.ts.hbs`:
```hbs
export { {{pascalCase name}}, type {{pascalCase name}}Props } from "./{{kebabCase name}}";
```
- [ ] **Step 6: Verify file list**
```bash
ls turbo/generators/templates/core-ui-component/
```
Expected: 4 files — `component.stories.tsx.hbs`, `component.test.tsx.hbs`, `component.tsx.hbs`, `index.ts.hbs`.
- [ ] **Step 7: Commit**
```bash
git add turbo/generators/templates/core-ui-component
git commit -m "feat(generators): core-ui-component template files (component + stories + test + barrel)"
```
---
## Phase 3 — Generator registration + action sequence (TDD)
**Goal:** Add the `setGenerator("core-ui-component")` block to `config.ts` with prompts, validation, and the `coreUiComponentActions` + `printCoreUiComponentNextSteps` helpers.
**Files touched:**
- Modify: `turbo/generators/config.ts`
- Modify: `turbo/generators/config.test.ts`
### Task 3.1: Write the failing test for registration shape
**Files:**
- Modify: `turbo/generators/config.test.ts`
- [ ] **Step 1: Append the registration test**
Add this `describe` block at the bottom of `turbo/generators/config.test.ts`:
```ts
describe("core-ui-component generator", () => {
it("is registered with tier and name prompts", () => {
const captured: Array<{ name: string; def: PlopTypes.PlopGeneratorConfig }> = [];
const plopMock = {
setHelper: () => {},
setGenerator: (name: string, def: PlopTypes.PlopGeneratorConfig) =>
captured.push({ name, def }),
} as unknown as PlopTypes.NodePlopAPI;
generator(plopMock);
const entry = captured.find((c) => c.name === "core-ui-component");
expect(entry).toBeDefined();
const prompts = entry!.def.prompts as Array<{ name: string; choices?: unknown[] }>;
expect(prompts.map((p) => p.name)).toEqual(["tier", "name"]);
expect(prompts[0]!.choices).toEqual(["atom", "molecule", "organism"]);
});
});
```
- [ ] **Step 2: Run the test and verify it fails**
```bash
pnpm --filter @repo/turbo-generators test config.test
```
Expected: FAIL — `expect(entry).toBeDefined()` returns undefined because `core-ui-component` isn't registered yet.
- [ ] **Step 3: No commit yet** — the implementation in Task 3.2 will turn this test green.
### Task 3.2: Add the `setGenerator("core-ui-component")` block
**Files:**
- Modify: `turbo/generators/config.ts`
- [ ] **Step 1: Find the right insertion point**
The existing `setGenerator` blocks live inside `export default function generator(plop)`. Look for the `setGenerator("realtime", ...)` block (around line 429) and the `setGenerator("job", ...)` block (around line 501). Insert the new block after `setGenerator("job", ...)` and BEFORE the closing `}` of the `generator` function body.
- [ ] **Step 2: Add the setGenerator block**
Insert this code at the chosen position:
```ts
/**
* Turbo generator: `core-ui-component`
*
* Scaffolds a core-ui atomic-design component (atom / molecule / organism)
* using the established 4-file pattern: `<name>.tsx`, `<name>.stories.tsx`,
* `<name>.test.tsx`, `index.ts`. Splices the new export into the matching
* tier barrel via the `// <gen:<tier>s>` anchor.
*
* Refuses to run unless `packages/core-ui/` exists (see
* `pnpm turbo gen core-package ui`).
*/
plop.setGenerator("core-ui-component", {
description:
"Scaffold a core-ui atomic-design component (atom / molecule / organism)",
prompts: [
{
type: "list",
name: "tier",
message: "Tier:",
choices: ["atom", "molecule", "organism"],
},
{
type: "input",
name: "name",
message: "Component name (PascalCase, e.g. Spinner):",
validate: (input: string) => {
if (!input) return "Required";
if (!/^[A-Z][A-Za-z0-9]*$/.test(input)) {
return "Must be PascalCase (e.g. Spinner, IconButton)";
}
return true;
},
},
],
actions: (answers) => {
const a = answers as {
tier: "atom" | "molecule" | "organism";
name: string;
};
return coreUiComponentActions(a);
},
});
```
(Note: `coreUiComponentActions` doesn't exist yet — TypeScript will flag the reference. That's expected; Task 3.4 adds it.)
- [ ] **Step 3: Run the registration test → PASS**
```bash
pnpm --filter @repo/turbo-generators test config.test
```
Expected: the new test from Task 3.1 PASSES. Other tests may FAIL due to the missing `coreUiComponentActions` function — that's fine; Task 3.4 fixes it.
- [ ] **Step 4: No commit yet** — wait until the action helper is in place.
### Task 3.3: Write the failing test for the action sequence
**Files:**
- Modify: `turbo/generators/config.test.ts`
- [ ] **Step 1: Add the action-shape test**
Append to the `describe("core-ui-component generator", ...)` block:
```ts
it("for each tier, emits 4 add actions, 1 modify, plus guards and print", () => {
const captured: Array<{ name: string; def: PlopTypes.PlopGeneratorConfig }> = [];
const plopMock = {
setHelper: () => {},
setGenerator: (name: string, def: PlopTypes.PlopGeneratorConfig) =>
captured.push({ name, def }),
} as unknown as PlopTypes.NodePlopAPI;
generator(plopMock);
const corePkg = captured.find((c) => c.name === "core-ui-component")!.def;
for (const tier of ["atom", "molecule", "organism"] as const) {
const actions = (corePkg.actions as (a: { tier: string; name: string }) => PlopTypes.ActionType[])(
{ tier, name: "Spinner" },
);
const tierPlural = `${tier}s`;
// 4 `add` actions, one per emitted file
const adds = actions.filter(
(a): a is PlopTypes.AddAction =>
typeof a === "object" && "type" in a && a.type === "add",
);
expect(adds).toHaveLength(4);
const addPaths = adds.map((a) => a.path);
expect(addPaths).toContain(
`packages/core-ui/src/${tierPlural}/{{kebabCase name}}/{{kebabCase name}}.tsx`,
);
expect(addPaths).toContain(
`packages/core-ui/src/${tierPlural}/{{kebabCase name}}/{{kebabCase name}}.stories.tsx`,
);
expect(addPaths).toContain(
`packages/core-ui/src/${tierPlural}/{{kebabCase name}}/{{kebabCase name}}.test.tsx`,
);
expect(addPaths).toContain(
`packages/core-ui/src/${tierPlural}/{{kebabCase name}}/index.ts`,
);
// 1 `modify` action targeting the tier barrel
const modifies = actions.filter(
(a): a is PlopTypes.ModifyAction =>
typeof a === "object" && "type" in a && a.type === "modify",
);
expect(modifies).toHaveLength(1);
expect(modifies[0]!.path).toBe(`packages/core-ui/src/${tierPlural}/index.ts`);
expect(String(modifies[0]!.pattern)).toContain(`<gen:${tierPlural}>`);
// 3 function actions (2 guards + 1 print)
const fns = actions.filter((a) => typeof a === "function");
expect(fns).toHaveLength(3);
}
});
it("PascalCase validator rejects bad names", () => {
const captured: Array<{ name: string; def: PlopTypes.PlopGeneratorConfig }> = [];
const plopMock = {
setHelper: () => {},
setGenerator: (name: string, def: PlopTypes.PlopGeneratorConfig) =>
captured.push({ name, def }),
} as unknown as PlopTypes.NodePlopAPI;
generator(plopMock);
const corePkg = captured.find((c) => c.name === "core-ui-component")!.def;
const nameValidate = (corePkg.prompts as Array<{ name: string; validate?: (i: string) => string | true }>)
.find((p) => p.name === "name")!.validate!;
expect(nameValidate("")).toBe("Required");
expect(nameValidate("spinner")).toContain("PascalCase");
expect(nameValidate("123Foo")).toContain("PascalCase");
expect(nameValidate("Foo-Bar")).toContain("PascalCase");
expect(nameValidate("Spinner")).toBe(true);
expect(nameValidate("IconButton")).toBe(true);
});
```
- [ ] **Step 2: Run → FAIL**
```bash
pnpm --filter @repo/turbo-generators test config.test
```
Expected: the new tests FAIL because `coreUiComponentActions` doesn't exist yet (TypeScript and runtime errors).
### Task 3.4: Implement `coreUiComponentActions`
**Files:**
- Modify: `turbo/generators/config.ts`
- [ ] **Step 1: Find the right insertion point**
The existing helper functions (`camel`, `pascalCase`, `constantCase`, etc.) live at the bottom of `turbo/generators/config.ts`, outside the `generator` function. Look around lines 12901310. Insert `coreUiComponentActions` BEFORE those helpers (it uses them), but AFTER the `generator` function body.
If existing local helpers (`realtimeChannelActions`, etc.) sit between the generator body and the case helpers, insert `coreUiComponentActions` in the same neighborhood.
- [ ] **Step 2: Add the `coreUiComponentActions` helper**
```ts
function coreUiComponentActions(a: {
tier: "atom" | "molecule" | "organism";
name: string;
}): PlopTypes.ActionType[] {
const tierPlural = `${a.tier}s` as const;
const tierTitle = tierPlural[0]!.toUpperCase() + tierPlural.slice(1);
const tierBarrel = `packages/core-ui/src/${tierPlural}/index.ts`;
const componentDir = `packages/core-ui/src/${tierPlural}/{{kebabCase name}}`;
return [
// 1. Guard: core-ui must be scaffolded
() => {
const pkgRoot = join(process.cwd(), "packages", "core-ui");
if (!existsSync(pkgRoot)) {
throw new Error(
`packages/core-ui/ does not exist. Run \`pnpm turbo gen core-package ui\` first.`,
);
}
return "Guard passed — packages/core-ui exists.";
},
// 2. Guard: tier barrel must have its anchor
() => {
assertAnchors(process.cwd(), tierBarrel, [`// <gen:${tierPlural}>`]);
return `Anchor // <gen:${tierPlural}> present in ${tierBarrel}.`;
},
// 3. Emit the 4 component files
{
type: "add",
path: `${componentDir}/{{kebabCase name}}.tsx`,
templateFile: "templates/core-ui-component/component.tsx.hbs",
data: { name: a.name },
},
{
type: "add",
path: `${componentDir}/{{kebabCase name}}.stories.tsx`,
templateFile: "templates/core-ui-component/component.stories.tsx.hbs",
data: { name: a.name, tierTitle },
},
{
type: "add",
path: `${componentDir}/{{kebabCase name}}.test.tsx`,
templateFile: "templates/core-ui-component/component.test.tsx.hbs",
data: { name: a.name },
},
{
type: "add",
path: `${componentDir}/index.ts`,
templateFile: "templates/core-ui-component/index.ts.hbs",
data: { name: a.name },
},
// 4. Splice export into tier barrel.
// The `template` string is itself Handlebars-rendered by plop, so
// {{pascalCase name}} and {{kebabCase name}} are resolved at apply-
// time. This matches the pattern used by the `realtime` and `event`
// generators (no kebabCase JS helper exists in config.ts).
{
type: "modify",
path: tierBarrel,
pattern: new RegExp(`// <gen:${tierPlural}>`),
template: `// <gen:${tierPlural}>\nexport { {{pascalCase name}}, type {{pascalCase name}}Props } from "./{{kebabCase name}}/index";`,
},
// 5. Print next-steps
() => printCoreUiComponentNextSteps(a),
];
}
```
Note: `printCoreUiComponentNextSteps` is referenced but not yet defined. Task 3.5 adds it; for now the TypeScript reference is dangling.
- [ ] **Step 3: Run the action-shape tests → PASS (or partial)**
```bash
pnpm --filter @repo/turbo-generators test config.test
```
Expected: the action-shape test (Task 3.3 step 1's first new test) PASSES. The validator test (Task 3.3 step 1's second new test) also PASSES. The print test (referenced inside the action array) errors at runtime if invoked, but the test only constructs the action array and inspects shape — it does NOT invoke the print function.
- [ ] **Step 4: No commit yet** — print helper still missing.
### Task 3.5: Implement `printCoreUiComponentNextSteps`
**Files:**
- Modify: `turbo/generators/config.ts`
- [ ] **Step 1: Add the print helper alongside the existing print helpers**
Locate the existing print helpers in `config.ts` (e.g., `printHandlerNextSteps`, `printChannelNextSteps` — search for `function print`). Add `printCoreUiComponentNextSteps` in the same neighborhood:
```ts
function printCoreUiComponentNextSteps(a: {
tier: "atom" | "molecule" | "organism";
name: string;
}): string {
const tierPlural = `${a.tier}s`;
const kebab = a.name
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
.toLowerCase();
const pascal = a.name;
return [
"─────────────────────────────────────────────────────────────",
`${pascal} scaffolded into packages/core-ui/src/${tierPlural}/${kebab}/.`,
"",
"Next steps (manual):",
` 1. Implement the component body in ${kebab}.tsx (currently a div`,
` passthrough — change the element/type to fit, add variants/sizes`,
` if needed; see button.tsx for the canonical rich pattern).`,
` 2. Flesh out the Default story in ${kebab}.stories.tsx; add variant`,
` stories if your component takes a variant prop.`,
` 3. Tighten the tests in ${kebab}.test.tsx beyond the smoke-test`,
` baseline.`,
"",
"Verify:",
" pnpm --filter @repo/core-ui lint typecheck test",
" pnpm dev --filter @repo/storybook # view in Storybook",
"─────────────────────────────────────────────────────────────",
].join("\n");
}
```
Note: this function inlines a kebab-case conversion (since there's no `kebabCase` JS helper in `config.ts`). Splitting on lower-to-upper boundaries handles `IconButton``icon-button` and `Spinner``spinner`.
- [ ] **Step 2: Run all generator tests → PASS**
```bash
pnpm --filter @repo/turbo-generators test
```
Expected: all tests PASS (the registration test, the action-shape test, the validator test, plus pre-existing tests).
- [ ] **Step 3: Run typecheck → PASS**
```bash
pnpm --filter @repo/turbo-generators typecheck
```
Expected: no errors.
- [ ] **Step 4: Commit (combine Phase 3 work into one commit)**
```bash
git add turbo/generators/config.ts turbo/generators/config.test.ts
git commit -m "feat(generators): core-ui-component generator + action helpers + tests"
```
---
## Phase 4 — Documentation
**Goal:** Add the new generator to discovery paths: scaffolding reference doc, CLAUDE.md Quick Start, AGENTS.md Key Commands.
**Files touched:**
- Create: `docs/scaffolding/core-ui-component-generator.md`
- Modify: `CLAUDE.md`
- Modify: `AGENTS.md`
### Task 4.1: Write the scaffolding reference doc
**Files:**
- Create: `docs/scaffolding/core-ui-component-generator.md`
- [ ] **Step 1: Look at the parallel doc for context**
```bash
cat docs/scaffolding/core-package-generator.md
```
Note the structure: Title → blurb → Usage section with prompt-walkthrough → Available templates table → Verification section.
- [ ] **Step 2: Create the new doc**
Write to `docs/scaffolding/core-ui-component-generator.md`:
```markdown
# core-ui component generator
`pnpm turbo gen core-ui-component` scaffolds an atomic-design component (atom / molecule / organism) into `packages/core-ui/` using the established 4-file pattern.
**Prerequisite:** `packages/core-ui/` must exist. If your project started from the slim template, scaffold core-ui first via `pnpm turbo gen core-package ui`.
## Usage
```bash
pnpm turbo gen core-ui-component
# → Tier: (use arrow keys)
# atom
# molecule
# organism
# → Component name (PascalCase, e.g. Spinner):
# Spinner
```
The generator emits 4 files into `packages/core-ui/src/<tier>s/<kebab-name>/`:
| File | Purpose |
|---|---|
| `<kebab-name>.tsx` | Component implementation (forwardRef + cn + className passthrough) |
| `<kebab-name>.stories.tsx` | Storybook stories (Meta + StoryObj + one Default story; tier-prefixed title) |
| `<kebab-name>.test.tsx` | Vitest + Testing Library smoke tests (renders, className passthrough, ref forwarding) |
| `index.ts` | Barrel that re-exports the component + Props type |
The new export is also spliced into `packages/core-ui/src/<tier>s/index.ts` immediately after the `// <gen:<tier>s>` anchor, so the component is reachable from the tier barrel (and transitively from the root `@repo/core-ui` export) without any manual wiring.
## Generated scaffold (example: `pnpm turbo gen core-ui-component` → atom → Spinner)
```
packages/core-ui/src/atoms/spinner/
├── spinner.tsx # forwardRef<HTMLDivElement, SpinnerProps>
├── spinner.stories.tsx # title: "Atoms/Spinner"
├── spinner.test.tsx # 3 smoke tests
└── index.ts # export { Spinner, type SpinnerProps }
```
And in `packages/core-ui/src/atoms/index.ts`:
```ts
// <gen:atoms>
export { Spinner, type SpinnerProps } from "./spinner/index";
export { Button, type ButtonProps } from "./button/index";
// ...
```
## Customizing the scaffold
The generated component is a minimal `<div>` passthrough — change the element/type and add variants/sizes to fit. The existing `button.tsx` in `src/atoms/button/` is the canonical reference for a richer component with `variant` and `size` props plus variant lookup tables.
## Verification
After scaffolding:
```bash
pnpm --filter @repo/core-ui lint typecheck test
pnpm dev --filter @repo/storybook # view the new component in Storybook
```
The Storybook stories glob (`packages/core-ui/src/**/*.stories.@(ts|tsx)`) picks up the new file automatically — no Storybook config changes needed.
```
- [ ] **Step 3: Commit**
```bash
git add docs/scaffolding/core-ui-component-generator.md
git commit -m "docs(scaffolding): core-ui component generator reference"
```
### Task 4.2: Update CLAUDE.md Quick Start
**Files:**
- Modify: `CLAUDE.md`
- [ ] **Step 1: Find the Quick Start block**
Open `CLAUDE.md` and search for the line `pnpm turbo gen core-package`. The Quick Start block enumerates all available generators.
- [ ] **Step 2: Add the new line**
Insert this line immediately after the `pnpm turbo gen core-package` line:
```
pnpm turbo gen core-ui-component # Scaffold a core-ui atomic-design component (see docs/scaffolding/core-ui-component-generator.md)
```
The full Quick Start block now contains six `pnpm turbo gen` entries: `feature`, `event`, `job`, `realtime`, `core-package`, `core-ui-component`.
- [ ] **Step 3: Commit**
```bash
git add CLAUDE.md
git commit -m "docs(claude): Quick Start entry for core-ui-component generator"
```
### Task 4.3: Update AGENTS.md Key Commands
**Files:**
- Modify: `AGENTS.md`
- [ ] **Step 1: Find the Key Commands section**
Open `AGENTS.md` and search for the line mentioning `pnpm turbo gen core-package`. The Key Commands section lists available generators.
- [ ] **Step 2: Add the new entry**
Insert a parallel entry for `core-ui-component` alongside the existing five generators. Match the existing entries' format exactly (look at the surrounding lines to see whether they use bullet points, table rows, or inline mentions).
- [ ] **Step 3: Commit**
```bash
git add AGENTS.md
git commit -m "docs(agents): Key Commands entry for core-ui-component generator"
```
---
## Phase 5 — Manual smoke test + final verification
**Goal:** Confirm the generator works end-to-end against a live core-ui.
### Task 5.1: Manual smoke test
This is a manual verification, not committed. It exercises the full flow against the actual workspace.
- [ ] **Step 1: Scaffold core-ui if not already present**
```bash
ls packages/core-ui 2>/dev/null && echo "core-ui already present" || echo "core-ui absent — will scaffold"
```
If core-ui is absent, scaffold it:
```bash
pnpm turbo gen core-package
# Select: ui
```
Then complete the printed next-steps for `gen core-package ui` (add deps to apps, add transpilePackages entry, add Storybook stories glob).
- [ ] **Step 2: Generate one component per tier**
```bash
pnpm turbo gen core-ui-component
# Tier: atom
# Component name: SpinnerAtomSmoke
```
```bash
pnpm turbo gen core-ui-component
# Tier: molecule
# Component name: MenuItemMoleculeSmoke
```
```bash
pnpm turbo gen core-ui-component
# Tier: organism
# Component name: HeaderOrganismSmoke
```
- [ ] **Step 3: Verify the file layout**
```bash
find packages/core-ui/src/atoms/spinner-atom-smoke \
packages/core-ui/src/molecules/menu-item-molecule-smoke \
packages/core-ui/src/organisms/header-organism-smoke
```
Expected (per directory): 4 files matching the pattern `<kebab>.tsx`, `<kebab>.stories.tsx`, `<kebab>.test.tsx`, `index.ts`.
- [ ] **Step 4: Verify tier barrels were spliced**
```bash
head -3 packages/core-ui/src/atoms/index.ts
head -3 packages/core-ui/src/molecules/index.ts
head -3 packages/core-ui/src/organisms/index.ts
```
Each should start with the anchor `// <gen:<tier>s>` followed immediately by the new component's export line.
- [ ] **Step 5: Run core-ui gates**
```bash
pnpm --filter @repo/core-ui lint typecheck test
```
Expected: all green. The 3 new test files (one per smoke-test component) contribute 9 new tests (3 tests × 3 components).
- [ ] **Step 6: Verify Storybook picks up the new stories (optional, requires Storybook running)**
```bash
pnpm dev --filter @repo/storybook
```
Open Storybook in the browser. Confirm three new entries appear: `Atoms/SpinnerAtomSmoke`, `Molecules/MenuItemMoleculeSmoke`, `Organisms/HeaderOrganismSmoke`. Stop Storybook with Ctrl-C when done.
- [ ] **Step 7: Clean up smoke-test components (do NOT commit them)**
```bash
rm -rf packages/core-ui/src/atoms/spinner-atom-smoke
rm -rf packages/core-ui/src/molecules/menu-item-molecule-smoke
rm -rf packages/core-ui/src/organisms/header-organism-smoke
```
Then manually remove the spliced export lines from each tier barrel:
```bash
sed -i.bak '/SpinnerAtomSmoke/d' packages/core-ui/src/atoms/index.ts && rm packages/core-ui/src/atoms/index.ts.bak
sed -i.bak '/MenuItemMoleculeSmoke/d' packages/core-ui/src/molecules/index.ts && rm packages/core-ui/src/molecules/index.ts.bak
sed -i.bak '/HeaderOrganismSmoke/d' packages/core-ui/src/organisms/index.ts && rm packages/core-ui/src/organisms/index.ts.bak
```
- [ ] **Step 8: Confirm no leftover smoke-test artifacts**
```bash
git status
```
Expected: clean (no modifications). If anything is modified, inspect and revert.
### Task 5.2: Final verification gate
- [ ] **Step 1: Run all gates from repo root**
```bash
pnpm lint && pnpm typecheck && pnpm test && pnpm turbo boundaries
```
Expected: all green.
- [ ] **Step 2: Confirm the new generator test count**
```bash
pnpm --filter @repo/turbo-generators test 2>&1 | tail -10
```
Expected: 3 new tests added to `config.test.ts` (registration, action shape, validator). Total generator test count should be the previous count + 3.
- [ ] **Step 3: No commit** — verification gate only. Plan complete.
---
## Notes for the executing agent
- Phases 1 → 2 → 3 → 4 → 5 are sequenced. Don't start Phase 2 until Phase 1's snapshot regen lands; the byte-identical e2e test will fail until both the anchors and the snapshot are in place.
- The TDD cycle in Phase 3 has tests pasted before the implementation — that's intentional. Run the test after each step to confirm the expected failure → expected success transition.
- The manual smoke test in Phase 5 must be cleaned up (Step 7). Do NOT commit smoke-test components into the live core-ui; they're throwaway.
- If `pnpm turbo gen` complains about plop helper resolution (e.g., `kebabCase` not found), the project uses plop's built-in case helpers; no extra registration needed.
- The `printCoreUiComponentNextSteps` function inlines its own kebab-case conversion. Don't try to import a `kebabCase` from elsewhere — there isn't one in `config.ts`.
- Commit cadence: one commit per task (Phase 1 has 2 commits; Phase 2 has 1; Phase 3 has 1 combined; Phase 4 has 3; Phase 5 has 0). Total ≈ 7 commits.