Developer Export — Bundle + Playground Implementation Plan

Goal: Ship a one-command CLI that snapshots a Tostada library into a committable design/ bundle (tokens.css, Tailwind preset, layout-principles.md, design-system.json, AGENTS.md, README.md), plus a new top-level Developer page whose Library browser lets a dev browse every token/component/principle, tweak a component's snippet against a live preview, and export the bundle as a ZIP.

Architecture: A new framework-agnostic src/export/ module holds pure generator functions (library, manifest) → string and a buildBundle() that returns the design/ file map. The browser path zips that map (+ a tostada.json snapshot) via fflate; the Node path is an esbuild-bundled copy the existing bin/tostada.js imports to write the folder to disk. The Developer page reuses the preview-host iframe (token broadcast + #variant/<name>?axis=value adapters) and the existing getSnippet/fallbackSnippet; freeform snippet editing compiles JSX with sucrase + new Function inside a new preview-host live route. The old DocumentationPage is removed.

Tech Stack: React 19 + TypeScript (Vite), Tailwind v4, Zustand + immer, React Router 7, localStorage. Tests: Vitest + @testing-library + jsdom (unit/component), Playwright (E2E). New deps: sucrase (runtime, live-eval), esbuild (build-time, Node CLI bundle). fflate already present.


Scope check

This spec spans two subsystems that could be separate plans:

  • A — Export engine + CLI (Tasks 1–9): pure generators, buildBundle, zipForUi, the Node bundle, and the tostada export subcommand. Ships working software on its own (the CLI emits a correct bundle; the browser can already download it via a temporary button).
  • B — Developer page (Tasks 10–17): the Library browser, three detail views, the live-eval playground, and DocumentationPage removal.

They share src/export/, so I'm keeping one plan with a natural checkpoint at Task 9 (engine + CLI green, bundle correct) before the UI work. If you want to ship in two PRs, cut at Task 9. Each task still ends green + committed.


Plan-level decisions (lock before any code)

The spec left a few things implicit at the file level. Resolving them here so the engineer doesn't guess:

  1. CLI file stays bin/tostada.js (not bin/tostada-ui.js). package.json bin is { "tostada": "./bin/tostada.js" } and the command is tostada. We extend the existing file with subcommand routing: bare tostada keeps serving the GUI (today's behavior); tostada export runs the new path. The spec's "bin/tostada-ui.js" refers to the package tostada-ui, not a rename of the file.
  2. Developer page is a top-level route /developer in AppShell (the spec says "new top-level Developer section"). We remove the documentation sub-route + nav entry from LibraryPage and delete DocumentationPage. A Developer link is added to TopNav.
  3. Node bundle of src/export/ is produced by an esbuild script (scripts/build-cli.mjs) writing dist-cli/export.mjs, with the shadcn _manifest.json embedded via resolveJsonModule. dist-cli is added to package.json files and a build:cli script. The generators must stay pure string functions with zero browser/React imports so they bundle cleanly for Node.
  4. Live-eval uses sucrase (transform(code, { transforms: ['jsx','typescript'] })) + new Function, executed inside preview-host on a new #live route + tostada:snippet message, with the bundled shadcn components as scope (via import.meta.glob('./shadcn/*.tsx', { eager: true })). The editor is a plain <textarea> for v1 (no CodeMirror dep) — "light and tight".
  5. theme.css curated set (§9.4): map exactly these semantic tokens to Tailwind v4 @theme keys — bg-*--color-*, fg-*--color-*, border-*--color-*, the typography-* family/size → --font-*/--text-*, and radius--radius-*. The generator reads tier === 'semantic' tokens and maps by id prefix; unknown prefixes are skipped (not an error). Final id list is pinned in Task 2.

File structure overview

Create:

src/export/
├── index.ts                     # barrel: re-exports generators + buildBundle (Node entry)
├── types.ts                     # ExportLibrary, ComponentDoc, BundleFiles, ManifestShape
├── componentDocs.ts             # join manifest + componentRules + snippets → ComponentDoc[]
├── componentDocs.test.ts
├── themeCss.ts                  # generateThemeCss (Tailwind v4 @theme)
├── themeCss.test.ts
├── layoutPrinciplesMd.ts        # generateLayoutPrinciplesMd (v2)
├── layoutPrinciplesMd.test.ts
├── agentsMd.ts                  # generateAgentsMd (5 sections)
├── agentsMd.test.ts
├── designSystemJson.ts          # generateDesignSystemJson (v2 + components)
├── designSystemJson.test.ts
├── readmeMd.ts                  # generateReadmeMd
├── readmeMd.test.ts
├── bundle.ts                    # buildBundle(library, manifest) → BundleFiles
├── bundle.test.ts
├── zipForUi.ts                  # browser ZIP wrapper (design/ + tostada.json)
└── zipForUi.test.ts

scripts/
└── build-cli.mjs                # esbuild: src/export/index.ts → dist-cli/export.mjs

src/components/pages/developer/
├── DeveloperPage.tsx            # top-level: header (Export bundle) · guide · browser
├── DeveloperPage.test.tsx
├── HowToInstallGuide.tsx
├── HowToInstallGuide.test.tsx
├── LibraryBrowser.tsx           # left list: search + filters + 4 categories
├── LibraryBrowser.test.tsx
├── useLibraryItems.ts           # compose items from tokens/manifest/principles
├── useLibraryItems.test.ts
├── detail/TokenDetail.tsx
├── detail/TokenDetail.test.tsx
├── detail/PrincipleDetail.tsx
├── detail/PrincipleDetail.test.tsx
├── detail/ComponentDetail.tsx   # preview + rules + snippet + controls
├── detail/ComponentDetail.test.tsx
├── SnippetEditor.tsx            # textarea + debounce + post to live preview
├── SnippetEditor.test.tsx
└── ComponentControls.tsx        # variant-axis disclosure
    ComponentControls.test.tsx

preview-host/src/live/
├── LiveSnippetRenderer.tsx      # #live route: sucrase compile + render in scope
└── scope.ts                     # named-export map from import.meta.glob

e2e/
└── developer-export.spec.ts

Modify:

package.json                                       # name already tostada-ui; add sucrase, esbuild, build:cli, files:["dist","dist-cli","bin"]
bin/tostada.js                                     # subcommand routing: serve (default) | export
src/components/layout/AppShell.tsx                 # add /developer route
src/components/layout/TopNav.tsx                   # add Developer link
src/components/pages/LibraryPage.tsx               # remove documentation route + nav entry
preview-host/src/App.tsx                           # add #live route
src/lib/docs.ts                                    # generateTokensCss moves to src/export (re-export shim or relocate)
REBUILD_PLAN.md · docs/changelog.md · CLAUDE.md · README.md · docs/library/documentation-export.md

Delete:

src/components/pages/documentation/DocumentationPage.tsx   # folds into Developer page

Conventions every task follows

  • Every task ends with npm run lint && npm run build && npm run test green plus one commit. Playwright runs at Task 18.
  • Run all npm/npx commands from /mnt/c/Apps/Uni/tostada (the project root holds the configs).
  • Path alias @/*./src/*. Tests live next to the file under test.
  • The src/export/ generators are pure — input is plain data (ExportLibrary, ManifestShape), output is string. No useStore, no window, no React. This is what lets the same code run in the browser and in Node.
  • Commit style: feat(devexport): …, chore(devexport): …, test(devexport): …, docs(devexport): ….
  • The pre-existing repo lint errors (RethemerPage, input.tsx, etc.) are out of scope — keep new files lint-clean; don't touch unrelated files.

Shared types (defined in Task 1, referenced throughout)

// src/export/types.ts
import type { TokenMap, DesignToken } from '@/tokens/types'
import type {
  ShellPrinciple, LayoutPrinciple, OverlayPrinciple, NavigationPrinciple,
} from '@/principles/v2/types'
import type { ComponentRules } from '@/principles/componentRules'

/** The subset of the exportLibrary() payload the generators consume. */
export interface ExportLibrary {
  name:           string
  tokens:         TokenMap
  shells:         ShellPrinciple[]
  layouts:        LayoutPrinciple[]
  overlays:       OverlayPrinciple[]
  navigation:     NavigationPrinciple[]
  componentRules: Record<string, ComponentRules>
}

/** Minimal shadcn manifest shape the generators read (a subset of _manifest.json). */
export interface ManifestComponent {
  name:           string
  variants:       { axes: Record<string, string[]>; defaults: Record<string, string> } | null
  installCommand: string
  namedExports:   string[]
  demoSource:     string | null
  tokensUsed:     string[]
}
export interface ManifestShape {
  components: ManifestComponent[]
  adapters:   string[]
}

/** One component's resolved docs — feeds AGENTS.md, design-system.json, the browser. */
export interface ComponentDoc {
  name:     string
  hasRules: boolean
  hasVariants: boolean
  axes:     Record<string, string[]>
  rules:    { base: { do: string[]; dont: string[]; notes: string };
              perVariant: Record<string, { do: string[]; dont: string[]; notes: string }> }
  snippet:  { install: string; imports: string; usage: string }
}

export type BundleFiles = Record<string, string>  // filename (no design/ prefix) → contents

export const BUNDLE_VERSION = 1

Re-export from @/tokens/types, @/principles/v2/types, and DesignToken as needed. These types are import-only at compile time, so they don't break the Node bundle (esbuild strips types).


Task 0 — Baseline check

Files: none.

  • Step 1: npm run test — expect the current 139 tests green.
  • Step 2: npm run build — expect a clean build.
  • Step 3: npx playwright test --list — expect 19 specs.
  • Step 4: No commit — baseline only.

Task 1 — Export module scaffold + relocate generateTokensCss

Files:

  • Create: src/export/types.ts, src/export/index.ts
  • Create: src/export/tokensCss.ts, src/export/tokensCss.test.ts
  • Modify: src/lib/docs.ts (re-export generateTokensCss from the new home for back-compat), src/components/pages/documentation/DocumentationPage.tsx import (will be deleted in Task 10 anyway — leave import working)

Why: Establish the pure module + move the token-CSS generator out of docs.ts so the bundle builder and CLI share one implementation. generateTokensCss is already pure.

  • Step 1: Write src/export/types.ts with the Shared types block above.

  • Step 2: Write the failing test src/export/tokensCss.test.ts:

    import { describe, it, expect } from 'vitest'
    import { generateTokensCss } from '@/export/tokensCss'
    import { DEFAULT_TOKENS } from '@/tokens/defaults'
    
    const tokenMap = Object.fromEntries(DEFAULT_TOKENS.map((t) => [t.id, t]))
    
    describe('generateTokensCss', () => {
      it('emits :root with primitive/semantic/shadcn sections', () => {
        const css = generateTokensCss('My Lib', tokenMap)
        expect(css).toMatch(/:root\s*\{/)
        expect(css).toContain('/* primitive */')
        expect(css).toContain('/* semantic */')
      })
      it('emits a .dark block when dark values differ', () => {
        const css = generateTokensCss('My Lib', tokenMap)
        expect(css).toMatch(/\.dark\s*\{/)
      })
    })
    
  • Step 3: npm run test -- export/tokensCss — fails (module missing).

  • Step 4: Implement src/export/tokensCss.ts — move the generateTokensCss function and its resolveLight/resolveDark helpers verbatim from src/lib/docs.ts (lines 9–72). Keep imports pointing at @/tokens/*.

  • Step 5: Update src/lib/docs.ts — delete the moved code; add export { generateTokensCss } from '@/export/tokensCss' so existing importers keep working until Task 10.

  • Step 6: Write src/export/index.ts barrel: export * from './types' and export { generateTokensCss } from './tokensCss' (more added per task).

  • Step 7: npm run test -- export/tokensCss green; npm run build green.

  • Step 8: Commit: chore(devexport): scaffold src/export + relocate generateTokensCss.


Task 2 — generateThemeCss (Tailwind v4 @theme)

Files:

  • Create: src/export/themeCss.ts, src/export/themeCss.test.ts
  • Modify: src/export/index.ts

Why: React/Tailwind audience consumes tokens as utilities. Curated mapping per plan-decision #5.

  • Step 1: Write the failing test src/export/themeCss.test.ts:

    import { describe, it, expect } from 'vitest'
    import { generateThemeCss } from '@/export/themeCss'
    import { DEFAULT_TOKENS } from '@/tokens/defaults'
    
    const tokenMap = Object.fromEntries(DEFAULT_TOKENS.map((t) => [t.id, t]))
    
    describe('generateThemeCss', () => {
      it('wraps mappings in an @theme block', () => {
        const css = generateThemeCss(tokenMap)
        expect(css).toMatch(/@theme\s*\{/)
      })
      it('maps a bg-* semantic token to a --color-* var referencing the token var', () => {
        const css = generateThemeCss(tokenMap)
        // e.g. --color-bg-accent: var(--bg-accent);
        expect(css).toMatch(/--color-bg-accent:\s*var\(--bg-accent\);/)
      })
      it('skips token ids whose prefix is not in the curated map', () => {
        const css = generateThemeCss(tokenMap)
        expect(css).not.toContain('var(--this-prefix-does-not-exist)')
      })
    })
    

    Before implementing, run node -e "const t=require('./src/tokens/defaults');" is not viable (TS). Instead, in Step 4 pin the curated prefixes by reading DEFAULT_TOKENS ids: grep "id: '" src/tokens/defaults.ts to confirm the real bg-*/fg-*/border-*/typography-*/radius ids exist.

  • Step 2: npm run test -- export/themeCss — fails.

  • Step 3: Implement src/export/themeCss.ts:

    import type { TokenMap } from '@/tokens/types'
    
    // Curated prefix → Tailwind @theme namespace. Unknown prefixes are skipped.
    const PREFIX_MAP: { test: (id: string) => boolean; themeVar: (id: string) => string }[] = [
      { test: (id) => id.startsWith('bg-'),     themeVar: (id) => `--color-${id}` },
      { test: (id) => id.startsWith('fg-'),     themeVar: (id) => `--color-${id}` },
      { test: (id) => id.startsWith('border-'), themeVar: (id) => `--color-${id}` },
      { test: (id) => id === 'radius',          themeVar: ()   => `--radius` },
    ]
    
    export function generateThemeCss(tokens: TokenMap): string {
      const lines: string[] = []
      for (const t of Object.values(tokens)) {
        if (t.tier !== 'semantic') continue
        const rule = PREFIX_MAP.find((p) => p.test(t.id))
        if (!rule) continue
        lines.push(`  ${rule.themeVar(t.id)}: var(--${t.id});`)
      }
      const header = `/* Tailwind v4 @theme — maps semantic tokens to utilities. @import after tokens.css. */`
      return `${header}\n\n@theme {\n${lines.join('\n')}\n}\n`
    }
    

    Pin the final prefix list to the real ids confirmed in Step 1's grep (add typography-*--font-*/--text-* only if those semantic ids exist; otherwise omit and note in §9.4).

  • Step 4: npm run test -- export/themeCss green; add export { generateThemeCss } from './themeCss' to the barrel.

  • Step 5: npm run build green; commit: feat(devexport): add generateThemeCss (Tailwind v4 @theme).


Task 3 — generateLayoutPrinciplesMd (v2)

Files:

  • Create: src/export/layoutPrinciplesMd.ts, src/export/layoutPrinciplesMd.test.ts
  • Modify: src/export/index.ts

Why: The export must serialize the v2 model (shells/layouts-with-blocks/overlays/navigation/applicationTags), not the M1 shape docs.ts still emits.

  • Step 1: Write the failing test using the seeded default preset:

    import { describe, it, expect } from 'vitest'
    import { generateLayoutPrinciplesMd } from '@/export/layoutPrinciplesMd'
    import { DEFAULT_PRINCIPLES_STATE as P } from '@/principles/v2/defaults'
    
    const lib = { shells: P.shells, layouts: P.layouts, overlays: P.overlays, navigation: P.navigation }
    
    describe('generateLayoutPrinciplesMd', () => {
      it('renders the shell, its content area, and the 7 layouts', () => {
        const md = generateLayoutPrinciplesMd(lib)
        expect(md).toContain('## Shells')
        expect(md).toContain('App shell')
        expect(md).toContain('## Layouts')
        expect(md).toContain('Dashboard')
      })
      it('renders a layout block composition with usage labels', () => {
        const md = generateLayoutPrinciplesMd(lib)
        expect(md).toContain('Stats row')        // a default block
        expect(md).toContain('KPIs across the top')
      })
      it('renders overlays and navigation levels', () => {
        const md = generateLayoutPrinciplesMd(lib)
        expect(md).toContain('## Overlays')
        expect(md).toContain('Drawer')
        expect(md).toContain('## Navigation')
        expect(md).toContain('horizontal-nav')
      })
      it('renders application tags with polarity', () => {
        const md = generateLayoutPrinciplesMd(lib)
        expect(md).toMatch(/content-type:\s*dashboard/)
      })
    })
    
  • Step 2: npm run test -- export/layoutPrinciplesMd — fails.

  • Step 3: Implement generateLayoutPrinciplesMd(lib: Pick<ExportLibrary,'shells'|'layouts'|'overlays'|'navigation'>): string. Sections ## Shells / ## Layouts / ## Overlays / ## Navigation. For each shell: name, variant, contentArea, responsive.breakpoints. For each layout: name, variant, blocks (name + usage + flex/grid summary), applicationTags (render key:value (polarity)). For each overlay: name, kind, per-kind config, regions, tags. For each navigation: bound container + ordered levels (#order component @ location) + tags. Reuse the markdown-table helper pattern from docs.ts.

  • Step 4: npm run test -- export/layoutPrinciplesMd green; barrel export.

  • Step 5: npm run build green; commit: feat(devexport): add v2 generateLayoutPrinciplesMd.


Task 4 — componentDocs (join manifest + rules + snippets)

Files:

  • Create: src/export/componentDocs.ts, src/export/componentDocs.test.ts
  • Modify: src/export/index.ts

Why: One resolved ComponentDoc[] feeds AGENTS.md (Task 5), design-system.json (Task 6), and the Library browser (Task 12). Build it once.

  • Step 1: Write the failing test:

    import { describe, it, expect } from 'vitest'
    import { buildComponentDocs } from '@/export/componentDocs'
    import type { ManifestShape } from '@/export/types'
    
    const manifest: ManifestShape = {
      adapters: ['button'],
      components: [
        { name: 'button', installCommand: 'npx shadcn@latest add button',
          namedExports: ['Button'], demoSource: null, tokensUsed: [],
          variants: { axes: { variant: ['default','outline'], size: ['default','sm'] }, defaults: {} } },
        { name: 'card', installCommand: 'npx shadcn@latest add card',
          namedExports: ['Card'], demoSource: '<Card/>', tokensUsed: [], variants: null },
      ],
    }
    const componentRules = {
      button: { base: { do: ['Use for primary actions'], dont: ['Don’t use for nav'], notes: 'n' },
                perVariant: { 'variant:outline': { do: ['Secondary actions'], dont: [], notes: '' } } },
    }
    
    describe('buildComponentDocs', () => {
      it('marks adapter components as having variants + rules', () => {
        const docs = buildComponentDocs(manifest, componentRules as never)
        const button = docs.find((d) => d.name === 'button')!
        expect(button.hasVariants).toBe(true)
        expect(button.hasRules).toBe(true)
        expect(button.axes.variant).toContain('outline')
        expect(button.rules.perVariant['variant:outline'].do).toContain('Secondary actions')
        expect(button.snippet.usage).toContain('Button') // from getSnippet adapter
      })
      it('falls back to the demo snippet + empty rules for non-adapter, un-ruled components', () => {
        const docs = buildComponentDocs(manifest, {} as never)
        const card = docs.find((d) => d.name === 'card')!
        expect(card.hasRules).toBe(false)
        expect(card.hasVariants).toBe(false)
        expect(card.snippet.imports).toContain('Card') // fallbackSnippet from namedExports
      })
    })
    
  • Step 2: npm run test -- export/componentDocs — fails.

  • Step 3: Implement buildComponentDocs(manifest, componentRules) → ComponentDoc[]:

    • For each manifest.components: axes = c.variants?.axes ?? {}, hasVariants = Object.keys(axes).length > 0.
    • rules: from componentRules[c.name] via resolveRules (import from @/principles/componentRules); hasRules = base has any do/dont/notes OR any perVariant entry.
    • snippet: getSnippet(c.name, {}) if manifest.adapters.includes(c.name), else fallbackSnippet({ componentName, installCommand, namedExports, demoSource }). Import both from @/lib/componentSnippets.

      Note: getSnippet/fallbackSnippet/resolveRules are pure and browser/Node-safe (no React, no window) — safe for the esbuild bundle.

  • Step 4: green; barrel export buildComponentDocs.

  • Step 5: npm run build green; commit: feat(devexport): add buildComponentDocs (manifest × rules × snippets).


Task 5 — generateAgentsMd (5 sections)

Files:

  • Create: src/export/agentsMd.ts, src/export/agentsMd.test.ts
  • Modify: src/export/index.ts

Why: The AI-native deliverable — the single file the dev's agent reads. Five sections per spec §5/§7.

  • Step 1: Write the failing test:

    import { describe, it, expect } from 'vitest'
    import { generateAgentsMd } from '@/export/agentsMd'
    import type { ComponentDoc } from '@/export/types'
    
    const docs: ComponentDoc[] = [{
      name: 'button', hasRules: true, hasVariants: true,
      axes: { variant: ['default','outline'] },
      rules: { base: { do: ['Primary actions'], dont: ['Not for nav'], notes: 'note' },
               perVariant: { 'variant:outline': { do: ['Secondary'], dont: [], notes: '' } } },
      snippet: { install: 'npx shadcn@latest add button', imports: 'import { Button }', usage: '<Button/>' },
    }]
    
    describe('generateAgentsMd', () => {
      const md = generateAgentsMd('My Lib', docs)
      it('has all five sections', () => {
        expect(md).toContain('## Orientation')
        expect(md).toContain('## Workflow — using a component')
        expect(md).toContain('## Workflow — composing a page')
        expect(md).toContain('## Components')
        expect(md).toContain('## Notes')
      })
      it('emits a per-component block with do/dont/notes + snippet + per-variant override', () => {
        expect(md).toContain('### button')
        expect(md).toContain('Primary actions')
        expect(md).toContain('Not for nav')
        expect(md).toContain('npx shadcn@latest add button')
        expect(md).toContain('variant:outline')
      })
      it('emits the no-rules placeholder for un-ruled components', () => {
        const md2 = generateAgentsMd('L', [{ ...docs[0], name: 'card', hasRules: false,
          rules: { base: { do: [], dont: [], notes: '' }, perVariant: {} } }])
        expect(md2).toContain('_No usage rules yet._')
      })
    })
    
  • Step 2: npm run test -- export/agentsMd — fails.

  • Step 3: Implement generateAgentsMd(libraryName, docs: ComponentDoc[]): string producing the five sections exactly as §7 "generateAgentsMd output shape": Orientation (paths + @import './design/tokens.css'), the two numbered workflows (static prose from §7), Components (one ### <name> block per doc — Do/Don't bullets, Notes, fenced ```jsx snippet of install+imports+usage, per-variant overrides only when Object.keys(rules.perVariant).length), and the Notes seam (the ## Notes (this session) template + the instruction to append to ./.tostada-notes.md). Un-ruled component → _No usage rules yet._ under Do/Don't/Notes.

  • Step 4: green; barrel export.

  • Step 5: npm run build green; commit: feat(devexport): add generateAgentsMd.


Task 6 — generateDesignSystemJson (v2 + components)

Files:

  • Create: src/export/designSystemJson.ts, src/export/designSystemJson.test.ts
  • Modify: src/export/index.ts

Why: Machine-readable form; must carry components (rules+snippet) + v2 layoutPrinciples and ignore the M1 principles array.

  • Step 1: Write the failing test:

    import { describe, it, expect } from 'vitest'
    import { generateDesignSystemJson } from '@/export/designSystemJson'
    import { DEFAULT_PRINCIPLES_STATE as P } from '@/principles/v2/defaults'
    import { DEFAULT_TOKENS } from '@/tokens/defaults'
    import type { ComponentDoc } from '@/export/types'
    
    const lib = {
      name: 'My Lib',
      tokens: Object.fromEntries(DEFAULT_TOKENS.map((t) => [t.id, t])),
      shells: P.shells, layouts: P.layouts, overlays: P.overlays, navigation: P.navigation,
      componentRules: {},
    }
    const docs: ComponentDoc[] = [{ name: 'button', hasRules: false, hasVariants: false, axes: {},
      rules: { base: { do: [], dont: [], notes: '' }, perVariant: {} },
      snippet: { install: 'i', imports: 'im', usage: 'u' } }]
    
    describe('generateDesignSystemJson', () => {
      it('round-trips and carries tokens, layoutPrinciples, components', () => {
        const json = JSON.parse(generateDesignSystemJson(lib as never, docs))
        expect(json.name).toBe('My Lib')
        expect(json.version).toBe(1)
        expect(json.tokens.semantic.length).toBeGreaterThan(0)
        expect(json.layoutPrinciples.shells).toHaveLength(1)
        expect(json.layoutPrinciples.layouts).toHaveLength(7)
        expect(json.components[0].name).toBe('button')
      })
      it('does not include an M1 principles array', () => {
        const json = JSON.parse(generateDesignSystemJson(lib as never, docs))
        expect(json.principles).toBeUndefined()
      })
    })
    
  • Step 2: npm run test -- export/designSystemJson — fails.

  • Step 3: Implement — payload { name, version: BUNDLE_VERSION, generatedAt, tokens: {primitive,semantic,shadcn}, layoutPrinciples: {shells,layouts,overlays,navigation}, components: docs }. JSON.stringify(payload, null, 2). (Note: generatedAt makes output non-deterministic; the bundle test in Task 7 asserts on a regex/version, not byte-equality.)

  • Step 4: green; barrel export.

  • Step 5: npm run build green; commit: feat(devexport): add v2 generateDesignSystemJson.


Task 7 — generateReadmeMd + buildBundle

Files:

  • Create: src/export/readmeMd.ts, src/export/readmeMd.test.ts, src/export/bundle.ts, src/export/bundle.test.ts

  • Modify: src/export/index.ts

  • Step 1: Write readmeMd.test.ts:

    import { describe, it, expect } from 'vitest'
    import { generateReadmeMd } from '@/export/readmeMd'
    describe('generateReadmeMd', () => {
      it('documents every bundle file', () => {
        const md = generateReadmeMd('My Lib')
        for (const f of ['tokens.css','theme.css','layout-principles.md','design-system.json','AGENTS.md']) {
          expect(md).toContain(f)
        }
        expect(md).toContain("@import './design/tokens.css'")
      })
    })
    
  • Step 2: Write bundle.test.ts:

    import { describe, it, expect } from 'vitest'
    import { buildBundle } from '@/export/bundle'
    import { DEFAULT_PRINCIPLES_STATE as P } from '@/principles/v2/defaults'
    import { DEFAULT_TOKENS } from '@/tokens/defaults'
    
    const manifest = { adapters: [], components: [
      { name: 'card', installCommand: 'npx shadcn@latest add card', namedExports: ['Card'],
        demoSource: '<Card/>', tokensUsed: [], variants: null }] }
    const lib = {
      name: 'My Lib', tokens: Object.fromEntries(DEFAULT_TOKENS.map((t) => [t.id, t])),
      shells: P.shells, layouts: P.layouts, overlays: P.overlays, navigation: P.navigation,
      componentRules: {},
    }
    
    describe('buildBundle', () => {
      const files = buildBundle(lib as never, manifest as never)
      it('returns the six design/ files, all non-empty', () => {
        expect(Object.keys(files).sort()).toEqual(
          ['AGENTS.md','README.md','design-system.json','layout-principles.md','theme.css','tokens.css'])
        for (const v of Object.values(files)) expect(v.length).toBeGreaterThan(0)
      })
    })
    
  • Step 3: Run both — fail.

  • Step 4: Implement generateReadmeMd(libraryName) (one section per file, plain language, the @import line) and buildBundle(library, manifest):

    export function buildBundle(library: ExportLibrary, manifest: ManifestShape): BundleFiles {
      const docs = buildComponentDocs(manifest, library.componentRules)
      return {
        'tokens.css':           generateTokensCss(library.name, library.tokens),
        'theme.css':            generateThemeCss(library.tokens),
        'layout-principles.md': generateLayoutPrinciplesMd(library),
        'design-system.json':   generateDesignSystemJson(library, docs),
        'AGENTS.md':            generateAgentsMd(library.name, docs),
        'README.md':            generateReadmeMd(library.name),
      }
    }
    
  • Step 5: both green; barrel export buildBundle, generateReadmeMd.

  • Step 6: npm run build green; commit: feat(devexport): add generateReadmeMd + buildBundle.


Task 8 — zipForUi (browser ZIP wrapper)

Files:

  • Create: src/export/zipForUi.ts, src/export/zipForUi.test.ts
  • Modify: src/export/index.ts

Why: The UI Export button needs a ZIP with design/<file> + tostada.json at the root. fflate is already a dep.

  • Step 1: Write the failing test:

    import { describe, it, expect } from 'vitest'
    import { unzipSync, strFromU8 } from 'fflate'
    import { zipForUi } from '@/export/zipForUi'
    
    describe('zipForUi', () => {
      it('prefixes design/ and adds tostada.json at the root', () => {
        const files = { 'tokens.css': ':root{}', 'README.md': '# x' }
        const tostadaJson = '{"name":"My Lib"}'
        const zip = zipForUi(files, tostadaJson)
        const out = unzipSync(zip)
        expect(Object.keys(out).sort()).toEqual(['design/README.md','design/tokens.css','tostada.json'])
        expect(strFromU8(out['design/tokens.css'])).toBe(':root{}')
        expect(strFromU8(out['tostada.json'])).toBe(tostadaJson)
      })
    })
    
  • Step 2: npm run test -- export/zipForUi — fails.

  • Step 3: Implement:

    import { zipSync, strToU8 } from 'fflate'
    import type { BundleFiles } from './types'
    export function zipForUi(files: BundleFiles, tostadaJson: string): Uint8Array {
      const entries: Record<string, Uint8Array> = { 'tostada.json': strToU8(tostadaJson) }
      for (const [name, contents] of Object.entries(files)) entries[`design/${name}`] = strToU8(contents)
      return zipSync(entries)
    }
    
  • Step 4: green; barrel export.

  • Step 5: npm run build green; commit: feat(devexport): add zipForUi.

Checkpoint candidate is Task 9 (next) — engine done after it.


Task 9 — Node bundle + tostada export CLI subcommand

Files:

  • Create: scripts/build-cli.mjs, e2e/../ (node test below)
  • Modify: package.json (scripts + deps + files), bin/tostada.js
  • Create: src/export/cli.test.ts (Vitest, runs the built CLI via node)

Why: The Node path. esbuild bundles src/export/index.ts (+ embedded manifest) so bin/tostada.js can import generators without TS.

  • Step 1: Add deps + scripts. npm i -D esbuild. In package.json: add "build:cli": "node scripts/build-cli.mjs", set "files": ["dist", "dist-cli", "bin"], and make "build" run tsc -b && vite build && npm run build:cli.

  • Step 2: Write scripts/build-cli.mjs:

    import { build } from 'esbuild'
    await build({
      entryPoints: ['src/export/index.ts'],
      bundle: true, platform: 'node', format: 'esm', target: 'node18',
      outfile: 'dist-cli/export.mjs',
      loader: { '.json': 'json' },
      // Embed the shadcn manifest so the CLI ships with the component list.
      define: {},
    })
    // Also copy the manifest next to the bundle for the CLI to read.
    import { copyFileSync, mkdirSync } from 'node:fs'
    mkdirSync('dist-cli', { recursive: true })
    copyFileSync('preview-host/src/shadcn/_manifest.json', 'dist-cli/_manifest.json')
    

    src/export/index.ts must not transitively import anything browser-only. getSnippet/fallbackSnippet/resolveRules/docs helpers are pure. If esbuild errors on a browser import, that import is the bug — fix the offending generator to stay pure (the test in Task 4 guards this).

  • Step 3: Write the failing node integration test src/export/cli.test.ts:

    import { describe, it, expect, beforeAll } from 'vitest'
    import { execFileSync } from 'node:child_process'
    import { mkdtempSync, writeFileSync, existsSync, readFileSync } from 'node:fs'
    import { tmpdir } from 'node:os'
    import { join } from 'node:path'
    
    // A minimal tostada.json fixture (exportLibrary payload).
    const fixture = JSON.stringify({
      name: 'CLI Lib', tokens: {}, shells: [], layouts: [], overlays: [], navigation: [],
      componentRules: {},
    })
    
    describe('tostada export CLI', () => {
      beforeAll(() => { execFileSync('npm', ['run', 'build:cli'], { stdio: 'ignore' }) })
    
      it('writes the design/ folder from a cwd tostada.json', () => {
        const dir = mkdtempSync(join(tmpdir(), 'tostada-cli-'))
        writeFileSync(join(dir, 'tostada.json'), fixture)
        execFileSync('node', [join(process.cwd(), 'bin/tostada.js'), 'export'], { cwd: dir })
        for (const f of ['tokens.css','theme.css','layout-principles.md','design-system.json','AGENTS.md','README.md']) {
          expect(existsSync(join(dir, 'src/design', f))).toBe(true)
        }
        expect(readFileSync(join(dir, 'src/design/AGENTS.md'), 'utf8').length).toBeGreaterThan(0)
      })
    
      it('exits non-zero with a clear message when no tostada.json is found', () => {
        const dir = mkdtempSync(join(tmpdir(), 'tostada-cli-empty-'))
        let failed = false; let out = ''
        try { execFileSync('node', [join(process.cwd(), 'bin/tostada.js'), 'export'], { cwd: dir, encoding: 'utf8' }) }
        catch (e: any) { failed = true; out = String(e.stderr ?? e.stdout ?? '') }
        expect(failed).toBe(true)
        expect(out).toMatch(/Couldn't find .*tostada\.json/)
      })
    })
    
  • Step 4: npm run test -- export/cli — fails.

  • Step 5: Modify bin/tostada.js — add subcommand routing at the top:

    const [, , subcommand, ...rest] = process.argv
    if (subcommand === 'export') {
      const { runExport } = await import('./export-cmd.mjs')
      await runExport(rest)
      process.exit(0)
    }
    // …existing GUI-serving code unchanged for the no-subcommand case…
    

    Create bin/export-cmd.mjs:

    import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
    import { join, resolve } from 'node:path'
    import { fileURLToPath } from 'node:url'
    import { buildBundle } from '../dist-cli/export.mjs'
    
    const here = fileURLToPath(new URL('.', import.meta.url))
    const manifest = JSON.parse(readFileSync(join(here, '..', 'dist-cli', '_manifest.json'), 'utf8'))
    
    function arg(flags, name) { const i = flags.indexOf(name); return i >= 0 ? flags[i + 1] : undefined }
    
    export async function runExport(flags) {
      const from = arg(flags, '--from') ?? './tostada.json'
      const out  = arg(flags, '--out')  ?? './src/design'
      if (!existsSync(from)) {
        console.error(`Couldn't find ${from}. Export your library from the app first, or pass --from <path>.`)
        process.exit(1)
      }
      let library
      try { library = JSON.parse(readFileSync(from, 'utf8')) }
      catch (e) { console.error(`Couldn't parse ${from}: ${e.message}`); process.exit(1) }
      const files = buildBundle(library, manifest)
      mkdirSync(resolve(out), { recursive: true })
      for (const [name, contents] of Object.entries(files)) writeFileSync(join(resolve(out), name), contents)
      const counts = `${Object.keys(library.tokens ?? {}).length} tokens · ${manifest.components.length} components · ${(library.shells?.length ?? 0) + (library.layouts?.length ?? 0)} principles`
      console.log(`✓ Wrote ${Object.keys(files).length} files to ${out} (${counts})`)
    }
    
  • Step 6: npm run test -- export/cli green (the beforeAll runs build:cli).

  • Step 7: npm run lint && npm run build && npm run test green; commit: feat(devexport): add tostada export CLI subcommand + Node bundle.


Task 10 — Developer page skeleton + route + Export button (DocumentationPage removed)

Files:

  • Create: src/components/pages/developer/DeveloperPage.tsx, .test.tsx
  • Modify: src/components/layout/AppShell.tsx, src/components/layout/TopNav.tsx, src/components/pages/LibraryPage.tsx
  • Delete: src/components/pages/documentation/DocumentationPage.tsx

Why: Land the new top-level surface with a working Export-bundle button (uses zipForUi + the store), before the browser/detail work.

  • Step 1: Write the smoke test DeveloperPage.test.tsx: renders the page (wrap in MemoryRouter), asserts an "Export bundle" button + the page heading exist.

  • Step 2: npm run test -- developer/DeveloperPage — fails.

  • Step 3: Implement DeveloperPage.tsx — header with title + Export bundle button. The button: reads the store, builds library: ExportLibrary (name + tokens + v2 slices + componentRules), imports _manifest.json (import manifest from '../../../../preview-host/src/shadcn/_manifest.json' — confirm relative path; Vite resolves JSON), calls buildBundle + exportLibrary() (from store) + zipForUi, and triggers download (<library-slug>-bundle.zip) reusing the downloadBlob helper pattern from the old DocumentationPage. Button states per §6 (idle/generating/downloaded/error). Below the header, render placeholders for the guide + browser (filled in Tasks 11–12).

  • Step 4: Wire the route. In AppShell.tsx add <Route path="/developer/*" element={<DeveloperPage />} />. In TopNav.tsx add a Developer link. In LibraryPage.tsx remove the documentation nav entry + <Route path="documentation" …> + the DocumentationPage import. Delete DocumentationPage.tsx.

  • Step 5: Manually verify /developer renders and Export bundle downloads a ZIP (browser). npm run test -- developer/DeveloperPage green.

  • Step 6: npm run lint && npm run build && npm run test green; commit: feat(devexport): add Developer page + Export bundle button; remove DocumentationPage.


Task 11 — HowToInstallGuide

Files:

  • Create: src/components/pages/developer/HowToInstallGuide.tsx, .test.tsx

  • Modify: DeveloperPage.tsx

  • Step 1: Test — renders the opener sentence + the four step headings; collapses/expands on click; persists open to localStorage (assert the key is written).

  • Step 2: fail.

  • Step 3: Implement the collapsible guide per §6 (opener + 4 sections, code blocks for the @import lines, localStorage key tostada:devguide-open, default expanded). Reuse the Disclosure pattern from _editorKit.tsx.

  • Step 4: wire into DeveloperPage above the browser placeholder.

  • Step 5: green; npm run build; commit: feat(devexport): add HowToInstallGuide.


Task 12 — useLibraryItems + LibraryBrowser (left list)

Files:

  • Create: useLibraryItems.ts, .test.ts, LibraryBrowser.tsx, .test.tsx
  • Modify: DeveloperPage.tsx

Why: The master list across four categories with search + filters.

  • Step 1: Write useLibraryItems.test.ts:

    import { describe, it, expect, beforeEach } from 'vitest'
    import { renderHook } from '@testing-library/react'
    import { useLibraryItems } from './useLibraryItems'
    import { useStore } from '@/store'
    import { DEFAULT_PRINCIPLES_STATE as P } from '@/principles/v2/defaults'
    
    beforeEach(() => useStore.setState({
      shells: structuredClone(P.shells), layouts: structuredClone(P.layouts),
      overlays: structuredClone(P.overlays), navigation: structuredClone(P.navigation),
    }))
    
    describe('useLibraryItems', () => {
      it('groups items into the four categories', () => {
        const { result } = renderHook(() => useLibraryItems())
        const cats = result.current.map((g) => g.category)
        expect(cats).toEqual(['Tokens — primitive','Tokens — semantic','Components','Principles'])
      })
      it('flags components that have rules / variants', () => {
        const { result } = renderHook(() => useLibraryItems())
        const comps = result.current.find((g) => g.category === 'Components')!.items
        expect(comps.length).toBeGreaterThan(0)
        expect(comps[0]).toHaveProperty('hasRules')
        expect(comps[0]).toHaveProperty('hasVariants')
      })
    })
    
  • Step 2: fail.

  • Step 3: Implement useLibraryItems() — returns { category, items: LibraryItem[] }[]. Tokens from useStore(s => s.tokens) split by tier; Components from the imported manifest joined with componentRules (reuse buildComponentDocs); Principles from the four v2 slices (kind:id style id + display name). LibraryItem = { id; name; kind: 'token'|'component'|'principle'; hasRules?; hasVariants?; … }.

  • Step 4: Write LibraryBrowser.test.tsx — renders four category headers; typing in search narrows items; toggling has rules chip filters components; clicking an item fires onSelect(item). (Mirror the PrincipleList test pattern.)

  • Step 5: Implement LibraryBrowser.tsx — search input, category-aware chips (has rules, has variants — shown only when Components are in scope), grouped list with the has-rules dot, onSelect. Search = substring + word-boundary score (≈30 lines, no dep). Hide empty categories.

  • Step 6: wire into DeveloperPage as the left rail; page tracks selected: LibraryItem | null; right pane is a placeholder until Tasks 13–15.

  • Step 7: green; npm run build; commit: feat(devexport): add LibraryBrowser + useLibraryItems.


Task 13 — TokenDetail

Files:

  • Create: detail/TokenDetail.tsx, .test.tsx

  • Modify: DeveloperPage.tsx (dispatch when selected.kind === 'token')

  • Step 1: Test — render for a seeded primitive (e.g. a color) → shows name, value; for a semantic token → shows the referenced primitive + resolved value; Copy button writes var(--<id>) to clipboard (mock navigator.clipboard).

  • Step 2: fail.

  • Step 3: Implement read-only catalog: name + tier, value (+ swatch for colors via existing Swatch from TokenRow.tsx where applicable), dark value when present, semantic → reference + resolved value, "Copy token name" → var(--<id>).

  • Step 4: wire dispatch in DeveloperPage; green; npm run build; commit: feat(devexport): add TokenDetail.


Task 14 — PrincipleDetail

Files:

  • Create: detail/PrincipleDetail.tsx, .test.tsx

  • Modify: DeveloperPage.tsx

  • Step 1: Test — render for layout-dashboard → shows kind layout, its blocks, and applicationTags; Copy writes the principle name/id.

  • Step 2: fail.

  • Step 3: Implement read-only catalog: kind, structure overview (layout blocks / overlay config / nav levels), applicationTags, Copy name. Look the principle up from the store by id across the four slices.

  • Step 4: wire dispatch; green; npm run build; commit: feat(devexport): add PrincipleDetail.


Task 15 — ComponentDetail (preview + rules + read-only snippet + Copy)

Files:

  • Create: detail/ComponentDetail.tsx, .test.tsx
  • Modify: DeveloperPage.tsx

Why: The playground core, read-only first (preview via existing variant adapter URL; freeform edit lands Task 17).

  • Step 1: Test — render for button: asserts the preview iframe (src #variant/button or #button), the application-rules panel (do/don't/notes), the snippet (install+imports+usage from getSnippet), and a Copy button that writes all three (joined with // separators) to clipboard.

  • Step 2: fail.

  • Step 3: Implement — header <name> · variant: <current> + light/dark toggle (hidden when the library has no darkValue tokens — check the store). Preview pane: <PrinciplePreview>-style iframe pointed at http://localhost:5179/#variant/<name>?… (adapter) or #<name> (demo) using useTokenBroadcast; min width/height per §6. Rules panel from resolveRules(componentRules[name], currentVariantKey). Snippet from getSnippet/fallbackSnippet. Copy → one block. (Reuse preview-host token broadcast exactly as the layout-principles PrinciplePreview does.)

  • Step 4: wire dispatch; green; npm run build; commit: feat(devexport): add ComponentDetail (read-only playground).


Task 16 — ComponentControls (variant axes drive preview + rules + snippet)

Files:

  • Create: ComponentControls.tsx, .test.tsx

  • Modify: detail/ComponentDetail.tsx

  • Step 1: Test — render controls for button (axes variant,size); selecting variant=outline fires onChange({ variant: 'outline' }); in ComponentDetail.test, after selecting a variant, assert the iframe src gains ?variant=outline, the rules panel shows the variant:outline override, and the snippet updates (via getSnippet(name, { variant:'outline' })).

  • Step 2: fail.

  • Step 3: Implement ComponentControls — a Disclosure (collapsed default) with one <select> per axis from the manifest. onChange(params) lifts to ComponentDetail, which recomputes iframe URL + resolveRules(…, 'variant:'+value) + getSnippet. Hide entirely for no-adapter components.

  • Step 4: green; npm run build; commit: feat(devexport): add ComponentControls (variant axes).


Task 17 — SnippetEditor freeform live-eval (sucrase) + Reset + states

Files:

  • Create: SnippetEditor.tsx, .test.tsx, preview-host/src/live/LiveSnippetRenderer.tsx, preview-host/src/live/scope.ts
  • Modify: detail/ComponentDetail.tsx, preview-host/src/App.tsx, package.json (add sucrase)

Why: The "tweak the snippet and see it" payload. Highest-risk task — the live compile runs in preview-host with the bundled components in scope.

  • Step 1: Add dep. npm i sucrase.

  • Step 2: Implement preview-host/src/live/scope.ts — build a name→export map from import.meta.glob('../shadcn/*.tsx', { eager: true }), plus React. Export buildScope() returning { React, ...allNamedExports }.

  • Step 3: Implement LiveSnippetRenderer.tsx (preview-host) — listens for tostada:snippet messages { usage: string }; transforms with sucrase (transform(wrapped, { transforms:['jsx','typescript'] }) where wrapped = "return (" + usage + ")"); evaluates via new Function(...Object.keys(scope), compiled)(...Object.values(scope)); renders the returned element. On throw → post tostada:snippet-error { message } to parent and keep the last good render. Add a #live route in preview-host/src/App.tsx that mounts it (tokens still broadcast as today).

  • Step 4: Write SnippetEditor.test.tsx — a <textarea> bound to the snippet; editing debounces and posts tostada:snippet (assert via a postMessage spy on the iframe ref); empty buffer disables Copy + shows the placeholder; Reset restores the default (prop callback).

  • Step 5: fail.

  • Step 6: Implement SnippetEditor.tsx<textarea> (no CodeMirror), debounced onChange → post { type:'tostada:snippet', usage } to the preview iframe; listen for tostada:snippet-error → surface inline error; empty → placeholder + Copy disabled; Reset → getSnippet/fallbackSnippet default. Wire into ComponentDetail: the read-only snippet area (Task 15) becomes the editor; the preview iframe switches to #live while editing, back to #variant/<name> when controls drive it. (Decide a single source of truth: editing the code is authoritative; toggling a control regenerates the code and re-posts.)

  • Step 7: Manual verify in-browser: edit <Button>Hi</Button>Button variant="outline" and watch the preview change; invalid code → inline error + last-good preview; Reset restores.

  • Step 8: npm run test -- developer/SnippetEditor developer/ComponentDetail green; npm run lint && npm run build && npm run test green; commit: feat(devexport): add freeform snippet live-eval (sucrase) + Reset.


Task 18 — E2E suite (Playwright)

Files:

  • Create: e2e/developer-export.spec.ts

Map §10 scenarios. Reuse the resetLibrary init-script + frameLocator patterns from e2e/layout-principles.spec.ts. The Playwright webServer list already starts :5173 + :5179.

  • Step 1: Stub the 17 test() blocks named "DEV N — …" with test.fixme() so the suite is discoverable but loud.

  • Step 2: npx playwright test developer-export --list — 17 discovered.

  • Steps 3–16: Implement each per §10 (Library browser: categories, search, chips, token copy, principle copy; Component playground: preview, variant-adapts-everything, copy, freeform edit, invalid, empty, reset, dark toggle, preview-host blocked; UI export ZIP contents; CLI covered by the node test in Task 9). For "UI export ZIP contents," intercept the download (page.waitForEvent('download')), read the file, unzip with fflate in-test, assert tostada.json + design/ six files.

  • Step 17: npx playwright test developer-export — all green (run from project root).

  • Step 18: Commit: test(devexport): add E2E suite.


Task 19 — Documentation

Files: REBUILD_PLAN.md, docs/changelog.md, CLAUDE.md, README.md, docs/library/documentation-export.md, new docs/ page, stub specs/developer-export-cloud.md + specs/rethemer-agent-notes.md.

  • Step 1: REBUILD_PLAN.md — add M5.9 — Developer export + Library browser (DONE …).
  • Step 2: docs/changelog.md — user-facing entry under today.
  • Step 3: CLAUDE.md — document src/export/, the CLI export subcommand, the Node bundle (dist-cli, build:cli), the Developer page modules, and the rule "generators in src/export/ must stay pure (no React/window) so they bundle for Node."
  • Step 4: README.md — quickstart (npx tostada-ui export + @import).
  • Step 5: docs/library/documentation-export.md — rewrite for the bundle + Developer page; note DocumentationPage removed.
  • Step 6: New docs page "Using Tostada in your codebase" — mirror the in-app guide.
  • Step 7: Stub the two follow-up specs (developer-export-cloud.md, rethemer-agent-notes.md) with a one-paragraph summary each.
  • Step 8: Commit: docs(devexport): plan + changelog + concept/library pages + follow-up spec stubs.

Task 20 — Final verification

  • Step 1: From project root: npm run lint && npm run build && npm run test && npx playwright test.
  • Step 2: Manual QA (per §10): boot npm run dev + npm run dev:preview-host; click Export bundle → unzip → confirm tostada.json + design/ six files; run npx tostada-ui export against that tostada.json → confirm the same six files; edit a snippet live; toggle a variant and confirm preview+rules+snippet move together; verify theme.css utilities in a scratch Tailwind v4 app; point a real coding agent at AGENTS.md.
  • Step 3: Open PR summarizing the bundle, the Developer page, the CLI, and the DocumentationPage removal.

Self-review checklist

  • Spec coverage. §5 deliverables map to tasks: export module (1), generators incl. rules+snippets+v2 principles (2–6), AGENTS.md 5 sections (5), bundle files (7), ZIP envelope (8), CLI auto-find/overrides (9), Developer page + Export button (10), install guide (11), Library browser 4 categories + search + filters (12), three detail views (13–15), variant controls adapting everything (16), freeform live-eval + Reset + states (17), DocumentationPage removal (10). §10 E2E → Task 18.
  • Open questions. All 8 are resolved in the spec; the plan encodes the resolutions (sucrase, esbuild, curated theme set, auto-find, top-level route, taxonomy-by-source).
  • Type consistency. ExportLibrary / ManifestShape / ComponentDoc / BundleFiles (Task 1) flow through buildComponentDocs (4) → generators (5–6) → buildBundle (7) → zipForUi (8) and the CLI (9); useLibraryItems (12) reuses buildComponentDocs.
  • No placeholders. Each code step shows real test or implementation; the few genuinely deferred items (exact copy strings, A11y order) are called out in the spec, not invented here.
  • Risk flag. Task 17 (in-iframe sucrase eval with the bundled component scope) is the highest-risk task; its read-only predecessor (Task 15) ships a working playground even if freeform editing needs iteration.