How JSX Maps to PaperDocument

CmdCal does not ship a JSX runtime. Instead, the term "JSX components" refers to the pattern of building typed helper functions that return PaperNode and PaperSlide objects. Each function acts like a component: it accepts props, applies layout logic, and returns a subtree of the AST.

The mapping between conceptual JSX and AST node types is direct:

Component patternAST node typeKey properties
<Text>PaperTextcontent, paragraphs, style (TextStyle)
<View>PaperViewchildren, shapeType, style (FlexStyle)
<Image>PaperImagesrc, svgSrc, crop, imageEffects
<Chart>PaperChartchartData (ChartData)
<Table>PaperTabletableData (TableData)
<Group>PaperGroupchildren, locks
<Connector>PaperConnectorconnectorType, start, end
<Video>PaperVideosrc, poster, mimeType, playback

Every node accepts FlexStyle for layout (position, flex, padding, gap) and optional animations, morphId, altText, and decorative fields.

Building a Reusable Slide Component

A component is just a function that returns a PaperSlide:

TYPESCRIPT
import type { PaperSlide, PaperNode, ColorValue } from "@paperjsx/core/engine";

interface TitleSlideProps {
  title: string;
  subtitle?: string;
  accentColor: ColorValue;
}

function TitleSlide({ title, subtitle, accentColor }: TitleSlideProps): PaperSlide {
  const children: PaperNode[] = [
    {
      type: "View",
      style: {
        position: "absolute",
        top: 0, left: 0,
        width: 960, height: 8,
        backgroundColor: accentColor,
      },
    },
    {
      type: "Text",
      style: {
        position: "absolute",
        top: 180, left: 80, width: 800,
        fontSize: 36, fontWeight: "bold",
        color: "#0F172A",
        textAlign: "center",
      },
      content: title,
    },
  ];

  if (subtitle) {
    children.push({
      type: "Text",
      style: {
        position: "absolute",
        top: 240, left: 80, width: 800,
        fontSize: 16, color: "#475569",
        textAlign: "center",
      },
      content: subtitle,
    });
  }

  return { type: "Slide", background: { type: "solid", color: "#F8FAFC" }, children };
}

Building a KPI Card Component

Reusable node-level components compose into slide-level ones:

TYPESCRIPT
import type { PaperNode } from "@paperjsx/core/engine";

interface KpiCardProps {
  label: string;
  value: string;
  trend?: "up" | "down" | "flat";
  x: number;
  y: number;
  width: number;
  accentColor: string;
}

function KpiCard({ label, value, trend, x, y, width, accentColor }: KpiCardProps): PaperNode {
  return {
    type: "View",
    style: {
      position: "absolute",
      left: x, top: y,
      width, height: 90,
      backgroundColor: "#FFFFFF",
      borderColor: "#CBD5E1",
      borderWidth: 1,
      padding: 16,
      flexDirection: "column",
      gap: 4,
    },
    shapeType: "roundRect",
    children: [
      {
        type: "Text",
        style: { fontSize: 11, color: "#475569" },
        content: label,
      },
      {
        type: "Text",
        style: { fontSize: 28, fontWeight: "bold", color: accentColor },
        content: value,
      },
    ],
  };
}

Use it inside a slide builder:

TYPESCRIPT
function DashboardSlide(title: string, kpis: KpiCardProps[]): PaperSlide {
  return {
    type: "Slide",
    children: [
      { type: "Text", style: { position: "absolute", top: 28, left: 60, fontSize: 24, fontWeight: "bold" }, content: title },
      ...kpis.map((kpi) => KpiCard(kpi)),
    ],
  };
}

Component Props Match AST Types

Every prop you pass to a component should map directly to the AST type definition. The style prop on each node uses FlexStyle (for layout nodes) or TextStyle (which extends FlexStyle with font properties). This means IDE autocomplete works out of the box when you import the types from @paperjsx/core/engine.

Key style properties shared across all nodes:

  • Layout: width, height, position, top, left, flexDirection, gap, justifyContent, alignItems
  • Visual: backgroundColor, fill, borderWidth, borderColor, opacity, rotation
  • Flex: flexGrow, flexShrink, flexBasis, flexWrap, minWidth, maxWidth

When to Use Components vs Structured JSON

Use caseRecommended approach
Repeatable deck templatesComponent functions
AI-generated contentStructured JSON (PaperDocument)
Brand-specific slide librariesComponent functions with ThemeConfig
One-off API integrationsStructured JSON via V2 API
Hybrid (AI content + brand frame)PresentationSpec protocol with brand packs
Component functions are a TypeScript-only authoring pattern. If you are using the hosted V2 API with `PresentationSpec` JSON, the protocol compiler handles layout internally. See [Structured JSON](/docs/sdk/structured-json) for the JSON-first approach.

Rendering Component Output

Pass the assembled PaperDocument to the engine:

TYPESCRIPT
import { PaperEngine } from "@paperjsx/core/engine";
import { writeFileSync } from "fs";

const doc = {
  type: "Document" as const,
  meta: { title: "Q4 Review" },
  slides: [
    TitleSlide({ title: "Q4 Review", subtitle: "December 2025", accentColor: "#2563EB" }),
    DashboardSlide("Key Metrics", [ /* ...kpis */ ]),
  ],
};

const buffer = await PaperEngine.render(doc);
writeFileSync("q4-review.pptx", buffer);

Or submit it to the hosted API as JSON:

Terminal
curl -X POST https://paperjsx.com/api/v2/render \
  -H "Authorization: Bearer $PAPERJSX_API_KEY" \
  -H "Content-Type: application/json" \
  -d @document.json