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 pattern | AST node type | Key properties |
|---|---|---|
<Text> | PaperText | content, paragraphs, style (TextStyle) |
<View> | PaperView | children, shapeType, style (FlexStyle) |
<Image> | PaperImage | src, svgSrc, crop, imageEffects |
<Chart> | PaperChart | chartData (ChartData) |
<Table> | PaperTable | tableData (TableData) |
<Group> | PaperGroup | children, locks |
<Connector> | PaperConnector | connectorType, start, end |
<Video> | PaperVideo | src, 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:
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:
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:
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 case | Recommended approach |
|---|---|
| Repeatable deck templates | Component functions |
| AI-generated content | Structured JSON (PaperDocument) |
| Brand-specific slide libraries | Component functions with ThemeConfig |
| One-off API integrations | Structured JSON via V2 API |
| Hybrid (AI content + brand frame) | PresentationSpec protocol with brand packs |
Rendering Component Output
Pass the assembled PaperDocument to the engine:
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:
curl -X POST https://paperjsx.com/api/v2/render \
-H "Authorization: Bearer $PAPERJSX_API_KEY" \
-H "Content-Type: application/json" \
-d @document.json