Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions docs/indesign-to-react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ typed React components, design tokens, and assets — a print-first sibling to t
Figma/Canva/Screenshot pipelines.

> **Status:** in progress. This document tracks the epic and details the shipped
> stages — the **IDML parser + IR**, the **PDF parser** (same IR), and the
> **style & design-token mapper**.
> stages — the **IDML parser + IR**, the **PDF parser** (same IR), the **style &
> design-token mapper**, and the **React component generator**.
> See the epic for the full plan: _Add InDesign-to-React conversion pipeline_.

## Pipeline shape (target)
Expand Down Expand Up @@ -125,10 +125,33 @@ node packages/pipeline/dist/indesign/cli.js your-design.idml --emit-tokens ./src

Font fallbacks and out-of-gamut conversions are listed in the generator report.

## Stage 3 — React component generator (shipped)

Implemented in [`packages/pipeline/src/react`](../../packages/pipeline/src/react).
Turns the IR + tokens into importable React artifacts — one `.tsx` per spread:

- **Frames → JSX** — text frames become semantic tags (`h1`–`h6` / `p` /
`figcaption`) inferred from the paragraph-style role; image frames become
`<img>` or `next/image`; layout is a token-spaced flow (grid for multi-column).
- **Typed props** — explicit `…Props` for every extracted content field, with the
extracted text / image `src` as defaults, so consumers override content while
keeping layout.
- **Two styling modes** — Tailwind classes that resolve via the mapper's preset,
or CSS Modules referencing the `tokens.css` custom properties.
- **Storybook stories** per component, populated with the extracted content.
- **Generation report** (`indesign-pipeline-report.md`) listing produced files,
staged assets, unmapped IR nodes, and accessibility TODOs.

Generated components are deterministic and pass `tsc --noEmit` under strict React
JSX (verified in CI for Tailwind, CSS Modules, and Next.js targets).

```bash
node packages/pipeline/dist/indesign/cli.js brochure.idml \
--emit-components ./src/components --style tailwind --framework react
```

## Roadmap (remaining sub-issues)

- [ ] React component generator (TSX output, optional Tailwind/CSS Modules,
Storybook stories)
- [ ] `indesign-to-react` Claude Code agent + skill + end-to-end CLI command

## References
Expand Down
61 changes: 49 additions & 12 deletions packages/pipeline/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

Core library for the Aurelius design-to-code pipeline. It reads Adobe InDesign
sources — both `.idml` packages and exported `.pdf` files — into a single
normalized, typed intermediate representation (IR), and maps that IR to a
coherent **design-token set** (`tokens.ts`, `tokens.css`, a Tailwind preset, and
a Style Dictionary JSON).
normalized, typed intermediate representation (IR), maps that IR to a coherent
**design-token set** (`tokens.ts`, `tokens.css`, a Tailwind preset, and a Style
Dictionary JSON), and generates **typed React components** (`.tsx` + Storybook
stories) from it.

> Part of the [InDesign-to-React pipeline](../../docs/indesign-to-react/README.md)
> epic. Shipped so far: the IDML parser + IR, the PDF parser (same IR), and the
> design-token mapper. The React component generator is next.
> epic. Shipped so far: the IDML parser + IR, the PDF parser (same IR), the
> design-token mapper, and the React component generator. A Claude Code agent +
> skill that orchestrate the full pipeline are next.

## Designer hands you a PDF

Expand Down Expand Up @@ -137,6 +139,35 @@ The mapper:
`tokens.ts` is emitted self-contained (`as const`) so it type-checks anywhere;
the token shape is also published as the zod `DesignTokensSchema`.

## React components

Generate typed React components (one per spread) plus Storybook stories, a
barrel `index.ts`, and a generation report:

```ts
import { parseIdmlFile } from "@aurelius/pipeline/indesign";
import { mapDocumentToTokens } from "@aurelius/pipeline/tokens";
import { generateComponents } from "@aurelius/pipeline/react";

const { document } = parseIdmlFile("brochure.idml");
const tokens = mapDocumentToTokens(document);
const { files, report, a11y } = generateComponents(document, tokens, {
styleMode: "tailwind", // or "css-modules"
framework: "react", // or "next" (uses next/image)
});
// files: [{ path: "Spread1.tsx", contents }, { path: "Spread1.stories.tsx", … }, "index.ts", report]
```

- **Text frames** → semantic tags (`h1`–`h6` / `p` / `figcaption`) inferred from
the paragraph-style role; **image frames** → `<img>` or `next/image`.
- **Explicit prop types** for all extracted content, so consumers override
content while keeping layout; extracted text/`src` become the prop defaults.
- **Tailwind** classes resolve via the mapper's preset; **CSS Modules** emit a
co-located `.module.css` referencing the `tokens.css` custom properties.
- **Deterministic** — the same IR always produces byte-identical output.
- The report lists produced files, staged assets, unmapped IR nodes, and
accessibility TODOs (e.g. images missing alt text).

## CLI

The package ships an `aurelius-indesign` binary (built to `dist/indesign/cli.js`):
Expand All @@ -151,14 +182,20 @@ node dist/indesign/cli.js brochure.idml --json --pretty > ir.json
# Map the IR to design tokens and write the four artifacts into a directory
node dist/indesign/cli.js brochure.idml --emit-tokens ./src/tokens

# Generate React components (+ stories, barrel, report) from the IR
node dist/indesign/cli.js brochure.idml --emit-components ./src/components --style tailwind

# Options
# --json emit the IR as JSON on stdout
# --pretty pretty-print JSON
# --dpi <number> pixel conversion DPI (default 96)
# --no-validate skip zod validation of the produced IR
# --emit-tokens <dir> map the IR to tokens and write the four artifacts
# --no-tailwind with --emit-tokens, skip tailwind.preset.ts
# --font-map <file> JSON file of font fallback overrides
# --json emit the IR as JSON on stdout
# --pretty pretty-print JSON
# --dpi <number> pixel conversion DPI (default 96)
# --no-validate skip zod validation of the produced IR
# --emit-tokens <dir> map the IR to tokens and write the four artifacts
# --no-tailwind with --emit-tokens, skip tailwind.preset.ts
# --emit-components <dir> generate .tsx components, stories, and a report
# --style <mode> component styling: tailwind | css-modules
# --framework <fw> target framework: react | next
# --font-map <file> JSON file of font fallback overrides
```

Example report:
Expand Down
8 changes: 8 additions & 0 deletions packages/pipeline/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
"./pdf": {
"types": "./dist/pdf/index.d.ts",
"import": "./dist/pdf/index.js"
},
"./react": {
"types": "./dist/react/index.d.ts",
"import": "./dist/react/index.js"
}
},
"files": [
Expand All @@ -46,7 +50,11 @@
"devDependencies": {
"@types/node": "^20.17.0",
"@types/pngjs": "^6.0.5",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"pdf-lib": "^1.17.1",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"typescript": "^5.7.2",
"vitest": "^4.1.2"
}
Expand Down
78 changes: 78 additions & 0 deletions packages/pipeline/src/indesign/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { generateComponents } from "../react/index.js";
import type { Framework, GenerationResult, StyleMode } from "../react/model.js";
import { parseSourceFile, type SourcePriority } from "../source.js";
import { emitAll } from "../tokens/emit.js";
import type { FontMapOverrides } from "../tokens/font-map.js";
Expand Down Expand Up @@ -39,6 +41,9 @@ Options:
--emit-tokens <dir> Map the IR to design tokens and write tokens.ts,
tokens.css, tailwind.preset.ts, design-tokens.json
--no-tailwind With --emit-tokens, skip tailwind.preset.ts
--emit-components <dir> Generate .tsx components, stories, and a report
--style <mode> Component styling: tailwind | css-modules (default tailwind)
--framework <fw> Target framework: react | next (default react)
--font-map <file> JSON file of font fallback overrides
-h, --help Show this help
`;
Expand All @@ -52,6 +57,9 @@ interface ParsedArgs {
tailwind: boolean;
dpi?: number;
emitTokens?: string;
emitComponents?: string;
styleMode?: StyleMode;
framework?: Framework;
fontMap?: string;
sourcePriority?: SourcePriority;
assets?: string;
Expand Down Expand Up @@ -100,6 +108,25 @@ function parseArgs(argv: string[]): ParsedArgs {
else args.emitTokens = dir;
break;
}
case "--emit-components": {
const dir = argv[++i];
if (!dir) args.error = "--emit-components requires a directory";
else args.emitComponents = dir;
break;
}
case "--style": {
const value = argv[++i];
if (value === "tailwind" || value === "css-modules") args.styleMode = value;
else
args.error = `Invalid --style: ${value ?? "(missing)"} (expected tailwind|css-modules)`;
break;
}
case "--framework": {
const value = argv[++i];
if (value === "react" || value === "next") args.framework = value;
else args.error = `Invalid --framework: ${value ?? "(missing)"} (expected react|next)`;
break;
}
case "--font-map": {
const file = argv[++i];
if (!file) args.error = "--font-map requires a file path";
Expand Down Expand Up @@ -157,6 +184,10 @@ export async function runCli(argv: string[]): Promise<CliResult> {
return { code: 1, stdout: "", stderr: `${errorLabel(err)}: ${toMessage(err)}\n` };
}

if (args.emitComponents) {
return emitComponentsFlow(document, args);
}

if (args.emitTokens) {
return emitTokens(document, args);
}
Expand Down Expand Up @@ -210,6 +241,53 @@ function emitTokens(document: Document, args: ParsedArgs): CliResult {
};
}

function emitComponentsFlow(document: Document, args: ParsedArgs): CliResult {
const tokens = mapDocumentToTokens(document, { ...(args.dpi ? { dpi: args.dpi } : {}) });
const result = generateComponents(document, tokens, {
...(args.styleMode ? { styleMode: args.styleMode } : {}),
...(args.framework ? { framework: args.framework } : {}),
});

try {
mkdirSync(args.emitComponents!, { recursive: true });
for (const file of result.files) {
writeFileSync(join(args.emitComponents!, file.path), file.contents);
}
} catch (err) {
return {
code: 1,
stdout: "",
stderr: `Error: cannot write components to "${args.emitComponents}": ${toMessage(err)}\n`,
};
}

return {
code: 0,
stdout: formatComponentReport(result, args.emitComponents!, args.styleMode ?? "tailwind"),
stderr: formatWarningSummary(document.warnings),
};
}

function formatComponentReport(result: GenerationResult, dir: string, styleMode: string): string {
const lines: string[] = [];
lines.push("InDesign → React Components");
lines.push(` output: ${dir}`);
lines.push(` style mode: ${styleMode}`);
lines.push(
` components: ${result.componentNames.length} (${result.componentNames.join(", ") || "none"})`,
);
lines.push(` files: ${result.files.length}`);
lines.push(` assets: ${result.assets.length}`);
lines.push(` unmapped: ${result.unmapped.length}`);
lines.push(` a11y TODOs: ${result.a11y.length}`);
lines.push(` report: ${join(dir, "indesign-pipeline-report.md")}`);
if (result.a11y.length > 0) {
lines.push("", "Accessibility TODOs");
for (const item of result.a11y) lines.push(` ${item}`);
}
return `${lines.join("\n")}\n`;
}

interface FrameCounts {
text: number;
image: number;
Expand Down
1 change: 1 addition & 0 deletions packages/pipeline/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/
export * as indesign from "./indesign/index.js";
export * as pdf from "./pdf/index.js";
export * as react from "./react/index.js";
export * as tokens from "./tokens/index.js";
export {
parseSourceFile,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`deterministic JSX snapshots > image-heavy spread 1`] = `
"// AUTO-GENERATED by @aurelius/pipeline. Do not edit by hand.

export type Spread1Props = {
image?: string;
imageAlt?: string;
image2?: string;
image2Alt?: string;
body?: string;
};

export function Spread1({
image = "/indesign/spread1-image.png",
imageAlt = "",
image2 = "/indesign/spread1-image2.png",
image2Alt = "",
body = "Photography by the studio.",
}: Spread1Props) {
return (
<section className="flex flex-col gap-6">
<img src={image} alt={imageAlt} className="max-w-full h-auto" />
<img src={image2} alt={image2Alt} className="max-w-full h-auto" />
<p className="text-body font-helvetica">{body}</p>
</section>
);
}

export default Spread1;
"
`;

exports[`deterministic JSX snapshots > text-heavy spread 1`] = `
"// AUTO-GENERATED by @aurelius/pipeline. Do not edit by hand.

export type Spread1Props = {
heading?: string;
body?: string;
caption?: string;
};

export function Spread1({
heading = "Annual Report 2026",
body = "Body paragraph line 1 with enough text to read as flowing copy.\\nBody paragraph line 2 with enough text to read as flowing copy.\\nBody paragraph line 3 with enough text to read as flowing copy.\\nBody paragraph line 4 with enough text to read as flowing copy.\\nBody paragraph line 5 with enough text to read as flowing copy.\\nBody paragraph line 6 with enough text to read as flowing copy.\\nBody paragraph line 7 with enough text to read as flowing copy.\\nBody paragraph line 8 with enough text to read as flowing copy.\\nBody paragraph line 9 with enough text to read as flowing copy.\\nBody paragraph line 10 with enough text to read as flowing copy.\\nBody paragraph line 11 with enough text to read as flowing copy.\\nBody paragraph line 12 with enough text to read as flowing copy.\\nBody paragraph line 13 with enough text to read as flowing copy.\\nBody paragraph line 14 with enough text to read as flowing copy.\\nBody paragraph line 15 with enough text to read as flowing copy.\\nBody paragraph line 16 with enough text to read as flowing copy.\\nBody paragraph line 17 with enough text to read as flowing copy.\\nBody paragraph line 18 with enough text to read as flowing copy.\\nBody paragraph line 19 with enough text to read as flowing copy.\\nBody paragraph line 20 with enough text to read as flowing copy.\\nBody paragraph line 21 with enough text to read as flowing copy.\\nBody paragraph line 22 with enough text to read as flowing copy.\\nBody paragraph line 23 with enough text to read as flowing copy.\\nBody paragraph line 24 with enough text to read as flowing copy.",
caption = "Figure 1 — a small caption set in eight point type.",
}: Spread1Props) {
return (
<section className="flex flex-col gap-6">
<h1 className="text-heading font-helvetica-bold">{heading}</h1>
<p className="text-body font-helvetica">{body}</p>
<figcaption className="text-caption font-helvetica">{caption}</figcaption>
</section>
);
}

export default Spread1;
"
`;
Loading
Loading