Files
agentic-dev/docs/superpowers/plans/2026-05-11-core-ui-component-generator.md
Danijel Martinek 09ac0bba6c 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>
2026-05-11 08:51:53 +02:00

35 KiB
Raw Blame History

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:

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:

// <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:

// <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:

// <gen:organisms>
export {};
  • Step 5: Commit
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:

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:

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:

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
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)
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
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
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
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:

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:

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:

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:

export { {{pascalCase name}}, type {{pascalCase name}}Props } from "./{{kebabCase name}}";
  • Step 6: Verify file list
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
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:

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

  /**
   * 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
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:

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
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
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)
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:

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 IconButtonicon-button and Spinnerspinner.

  • Step 2: Run all generator tests → PASS
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
pnpm --filter @repo/turbo-generators typecheck

Expected: no errors.

  • Step 4: Commit (combine Phase 3 work into one commit)
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

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:

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

// <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:

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

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
pnpm turbo gen core-ui-component
# Tier: atom
# Component name: SpinnerAtomSmoke
pnpm turbo gen core-ui-component
# Tier: molecule
# Component name: MenuItemMoleculeSmoke
pnpm turbo gen core-ui-component
# Tier: organism
# Component name: HeaderOrganismSmoke
  • Step 3: Verify the file layout
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
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
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)
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)
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:

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
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
pnpm lint && pnpm typecheck && pnpm test && pnpm turbo boundaries

Expected: all green.

  • Step 2: Confirm the new generator test count
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.