ptxiagram 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,39 +2,63 @@
2
2
 
3
3
  **Generate architecture and workflow diagrams as native, editable PowerPoint shapes — not an embedded image.**
4
4
 
5
+ > Built with [Claude Code](https://claude.com/claude-code) — from the initial prototype through the pptxgenjs/Hancom Office compatibility investigation, the JSON schema design, the layout validator, and this package's release automation. See the [commit history](https://github.com/hiio420official/ptxiagram/commits/main) for the build process.
6
+
5
7
  ptxiagram is an agent skill for Claude (and a standalone CLI/library) that turns a plain-English description of a system or process into a `.pptx` slide built from real PowerPoint shapes: rectangles, cylinders, clouds, connectors, text boxes. Anyone can open the result in PowerPoint — or Hancom 한쇼 — and click a box to change its color, text, or position, because it *is* a box, not a picture of one.
6
8
 
7
9
  - **Native shapes, not a picture** — every box and arrow is a real PPTX shape, editable after generation
8
- - **Two diagram types today** — `workflow` (swimlanes, happy-path flows) and `architecture` (free-placed components, trust boundaries, hub-and-spoke)
10
+ - **Three diagram types today** — `workflow` (swimlanes, happy-path flows), `architecture` (free-placed components, trust boundaries, hub-and-spoke), and `sequence` (participants + lifelines, API call chains)
9
11
  - **A validator that catches real layout bugs before rendering** — node overlap, labels wider than their shape, and connectors that cut through an unrelated node all fail loudly with a specific fix, instead of shipping a broken-looking slide
10
12
  - **Ships a Hancom Office compatibility fix** — every generated file is post-processed to strip a documented PptxGenJS defect that PowerPoint silently repairs but Hancom's 한쇼 flags as a corrupted document (see below)
11
13
  - **Route presets instead of hand-computed coordinates** — `straight`, `drop`, `elbow-right`, `arc-over-top` cover swimlane transitions, hub-and-spoke fan-out, and routing around a boundary box
12
14
 
13
- ## Install
15
+ ## Use it in an agent (Claude Code, Codex, Cursor, ...)
14
16
 
15
- As a Claude Skill:
17
+ Install with the [`skills` CLI](https://github.com/vercel-labs/skills) — it detects every supported agent on your machine and wires the skill into each one:
16
18
 
17
19
  ```bash
18
20
  npx skills add hiio420official/ptxiagram -g
19
21
  ```
20
22
 
21
- As a CLI / library:
23
+ Then just ask in chat:
22
24
 
23
- ```bash
24
- npm install -g ptxiagram
25
- ptxiagram doctor
25
+ ```text
26
+ Use ptxiagram to draw an architecture diagram for this repo's runtime,
27
+ as a PPTX I can drop into a slide deck.
26
28
  ```
27
29
 
28
- ## Quick start
30
+ ```text
31
+ Use ptxiagram to make a workflow diagram of our incident response process,
32
+ with a lane per team, so I can paste it into the postmortem deck.
33
+ ```
34
+
35
+ The agent reads `SKILL.md`, writes a small JSON file describing your nodes and
36
+ edges, and runs the CLI to produce a `.pptx` with real, editable shapes.
37
+
38
+ **Per-agent notes:**
39
+ - **Claude Code**: works out of the box after `npx skills add`.
40
+ - **Codex CLI / opencode**: install with an explicit `--agent` flag if you don't want it installed everywhere, e.g. `npx skills use hiio420official/ptxiagram@ptxiagram --agent codex`.
41
+ - **Any other `skills`-supported agent** (Cursor, Windsurf, Continue, Antigravity, Gemini CLI, ...): the same `npx skills add` command installs to whichever of these are detected on your machine.
42
+
43
+ ## Use it standalone (CLI / library)
29
44
 
30
45
  ```bash
46
+ npm install -g ptxiagram
47
+ ptxiagram doctor
31
48
  ptxiagram render workflow examples/onboarding.workflow.json out.pptx
32
49
  ptxiagram check out.pptx
33
50
  ```
34
51
 
52
+ Or as a library:
53
+
54
+ ```js
55
+ import { renderWorkflow } from "ptxiagram";
56
+ await renderWorkflow("my-diagram.workflow.json", "out.pptx");
57
+ ```
58
+
35
59
  `render` validates the input (schema + layout) before writing anything, and always repairs the output for Hancom Office compatibility. `check` confirms the repair took, dumps every embedded text run for a spelling/content spot-check, and — if LibreOffice is installed — writes a PNG preview for a quick visual check.
36
60
 
37
- See [`ptxiagram/SKILL.md`](ptxiagram/SKILL.md) for the full JSON shape of both diagram types, the node-type → shape/color mapping, and the route presets.
61
+ See [`SKILL.md`](SKILL.md) for the full JSON shape of both diagram types, the node-type → shape/color mapping, and the route presets.
38
62
 
39
63
  ## Why every file gets repaired
40
64
 
package/SKILL.md CHANGED
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: ptxiagram
3
- description: Create architecture and workflow diagrams as native, editable PowerPoint shapes (real rectangles, connectors, and text boxes a user can click and restyle in PowerPoint or Hancom 한쇼) instead of an embedded image. Accepts a plain-language description, turns it into a small JSON file, and renders it with pptxgenjs. Use when the user asks for a diagram, architecture drawing, or process flow that needs to end up inside a .pptx deck or a Korean-office-suite-compatible presentation - not when they just want a standalone image or an HTML/web diagram (use an SVG-based diagram tool for that instead).
3
+ description: Create architecture, workflow, and sequence diagrams as native, editable PowerPoint shapes (real rectangles, connectors, and text boxes a user can click and restyle in PowerPoint or Hancom 한쇼) instead of an embedded image. Accepts a plain-language description, turns it into a small JSON file, and renders it with pptxgenjs. Use when the user asks for a diagram, architecture drawing, process flow, or API call sequence that needs to end up inside a .pptx deck or a Korean-office-suite-compatible presentation - not when they just want a standalone image or an HTML/web diagram (use an SVG-based diagram tool for that instead).
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.1.0"
6
+ version: "0.2.0"
7
7
  status: prototype
8
8
  based_on: "gitbrent/PptxGenJS (MIT)"
9
9
  ---
@@ -51,6 +51,7 @@ validation and the embedded-text dump, just no rendered preview image).
51
51
  |------|---------|--------|
52
52
  | `workflow` | Cross-team processes, approval chains, anything with swimlanes and a happy path | `schemas/workflow.schema.json` |
53
53
  | `architecture` | System components, services, datastores, trust boundaries, hub-and-spoke integrations | `schemas/architecture.schema.json` |
54
+ | `sequence` | API call chains, request/response traces, cache-fallback flows — up to 6 participants, up to 9 messages | `schemas/sequence.schema.json` |
54
55
 
55
56
  ## The loop
56
57
 
@@ -58,6 +59,7 @@ validation and the embedded-text dump, just no rendered preview image).
58
59
  shape, don't guess the field names:
59
60
  - `examples/onboarding.workflow.json`
60
61
  - `examples/order-processing.architecture.json`
62
+ - `examples/cache-miss.sequence.json`
61
63
  2. Write `<name>.<type>.json`.
62
64
  3. Render:
63
65
  ```bash
@@ -127,6 +129,30 @@ order. Default node size is 1.8×0.95in.
127
129
  `boundaries[].wraps` takes component ids, not coordinates — the renderer
128
130
  computes a padded frame around them, so never hand-compute a boundary box.
129
131
 
132
+ ## Sequence JSON shape
133
+
134
+ ```json
135
+ {
136
+ "schema_version": 1,
137
+ "diagram_type": "sequence",
138
+ "meta": { "title": "...", "output": "out.pptx" },
139
+ "participants": [
140
+ { "id": "api", "type": "backend", "label": "API", "sublabel": "handler" }
141
+ ],
142
+ "messages": [
143
+ { "from": "client", "to": "api", "label": "GET /orders/123", "variant": "emphasis" }
144
+ ],
145
+ "cards": []
146
+ }
147
+ ```
148
+
149
+ **Layout budget**: up to 6 participants in a row (auto-spaced, 2.2in apart),
150
+ each with a vertical dashed lifeline. Messages are drawn in JSON array order
151
+ at auto-incrementing y positions (0.55in apart) — order matters, there's no
152
+ explicit `y` field. Up to 9 messages without `cards`, ~6 with (cards need
153
+ room at the bottom, so the lifeline gets shorter). Self-messages (same
154
+ `from`/`to`) aren't supported yet.
155
+
130
156
  ## Node types
131
157
 
132
158
  `client` `frontend` `neutral` `backend` `database` `cache` `queue`
@@ -169,7 +195,8 @@ this automatically; `check` verifies it actually happened. Source:
169
195
 
170
196
  ## What's not built yet
171
197
 
172
- - `sequence`, `dataflow`, `lifecycle` diagram types (only `workflow` and
173
- `architecture` exist so far)
198
+ - `dataflow`, `lifecycle` diagram types (`workflow`, `architecture`, and
199
+ `sequence` exist so far)
174
200
  - No CJK exact-metrics font measurement — the label-width check is a
175
201
  conservative estimate, not real glyph widths
202
+ - Sequence self-messages (a participant messaging itself)
package/bin/cli.mjs CHANGED
@@ -7,11 +7,12 @@ import { promisify } from "node:util";
7
7
  import JSZip from "jszip";
8
8
  import { renderWorkflow } from "../renderers/workflow/render-workflow.mjs";
9
9
  import { renderArchitecture } from "../renderers/architecture/render-architecture.mjs";
10
+ import { renderSequence } from "../renderers/sequence/render-sequence.mjs";
10
11
  import { validateAll } from "../lib/validate.mjs";
11
12
 
12
13
  const execFileAsync = promisify(execFile);
13
14
 
14
- const RENDERERS = { workflow: renderWorkflow, architecture: renderArchitecture };
15
+ const RENDERERS = { workflow: renderWorkflow, architecture: renderArchitecture, sequence: renderSequence };
15
16
 
16
17
  function usage() {
17
18
  console.log(`Usage:
@@ -20,7 +21,7 @@ function usage() {
20
21
  cli check <output.pptx>
21
22
  cli doctor
22
23
 
23
- Types: workflow, architecture`);
24
+ Types: workflow, architecture, sequence`);
24
25
  }
25
26
 
26
27
  async function cmdRender(type, inputPath, outputPath) {
@@ -0,0 +1,23 @@
1
+ {
2
+ "schema_version": 1,
3
+ "diagram_type": "sequence",
4
+ "meta": {
5
+ "title": "캐시 미스 요청 흐름",
6
+ "subtitle": "Client → API → Cache → DB",
7
+ "output": "test/output/cache-miss.schema-driven.pptx"
8
+ },
9
+ "participants": [
10
+ { "id": "client", "type": "client", "label": "Client", "sublabel": "브라우저" },
11
+ { "id": "api", "type": "backend", "label": "API", "sublabel": "handler" },
12
+ { "id": "cache", "type": "cache", "label": "Cache", "sublabel": "Redis" },
13
+ { "id": "db", "type": "database", "label": "DB", "sublabel": "PostgreSQL" }
14
+ ],
15
+ "messages": [
16
+ { "from": "client", "to": "api", "label": "GET /orders/123", "variant": "emphasis" },
17
+ { "from": "api", "to": "cache", "label": "조회", "variant": "default" },
18
+ { "from": "cache", "to": "api", "label": "캐시 미스", "variant": "dashed" },
19
+ { "from": "api", "to": "db", "label": "쿼리 실행", "variant": "emphasis" },
20
+ { "from": "db", "to": "api", "label": "결과 반환", "variant": "dashed" },
21
+ { "from": "api", "to": "client", "label": "200 OK", "variant": "emphasis" }
22
+ ]
23
+ }
package/index.mjs CHANGED
@@ -1,4 +1,11 @@
1
1
  export { renderWorkflow } from "./renderers/workflow/render-workflow.mjs";
2
2
  export { renderArchitecture } from "./renderers/architecture/render-architecture.mjs";
3
- export { validateAll, validateSchema, validateWorkflowLayout, validateArchitectureLayout } from "./lib/validate.mjs";
3
+ export { renderSequence } from "./renderers/sequence/render-sequence.mjs";
4
+ export {
5
+ validateAll,
6
+ validateSchema,
7
+ validateWorkflowLayout,
8
+ validateArchitectureLayout,
9
+ validateSequenceLayout,
10
+ } from "./lib/validate.mjs";
4
11
  export { repairPptx } from "./lib/repair-pptx.mjs";
package/lib/layout.mjs CHANGED
@@ -47,6 +47,29 @@ export function computeWorkflowLayout(raw) {
47
47
  return { laneBoxes, nodeBoxes, nodeMeta, cardsY };
48
48
  }
49
49
 
50
+ export const SEQUENCE_LAYOUT = {
51
+ X0: 0.7, GAP: 2.2, BOX_W: 1.5, BOX_H: 0.7,
52
+ TOP_Y: 1.1, LIFELINE_BOTTOM: 7.0,
53
+ MSG_START_Y: 2.3, MSG_STEP: 0.55,
54
+ };
55
+
56
+ export function computeSequenceLayout(raw) {
57
+ const L = SEQUENCE_LAYOUT;
58
+ const participantBoxes = {};
59
+ const centerX = {};
60
+ raw.participants.forEach((p, i) => {
61
+ const x = L.X0 + i * L.GAP;
62
+ participantBoxes[p.id] = { x, y: L.TOP_Y, w: L.BOX_W, h: L.BOX_H };
63
+ centerX[p.id] = x + L.BOX_W / 2;
64
+ });
65
+ const messageYs = raw.messages.map((_, i) => L.MSG_START_Y + i * L.MSG_STEP);
66
+ // cards need ~1.75in of vertical room, so diagrams that use them get a
67
+ // shorter lifeline (fewer messages fit) instead of overflowing the slide
68
+ const lifelineBottom = raw.cards && raw.cards.length ? 5.3 : L.LIFELINE_BOTTOM;
69
+ const cardsY = lifelineBottom + 0.15;
70
+ return { participantBoxes, centerX, messageYs, lifelineBottom, cardsY };
71
+ }
72
+
50
73
  const BOUNDARY_PAD = 0.3;
51
74
 
52
75
  export function computeArchitectureLayout(raw) {
package/lib/routes.mjs CHANGED
@@ -110,3 +110,19 @@ export function connector(slide, from, to, opts = {}) {
110
110
  });
111
111
  }
112
112
  }
113
+
114
+ /** Draws a single point-to-point arrow at an explicit y (sequence
115
+ * messages, self-loops) instead of anchoring to node box sides. Direction
116
+ * matters: the arrowhead lands at (x2,y). opts: { color, dashed, label } */
117
+ export function drawArrow(slide, { x1, x2, y, color = "94A3B8", dashed = false, label }) {
118
+ slide.addShape("line", {
119
+ x: Math.min(x1, x2), y, w: Math.abs(x2 - x1), h: 0,
120
+ line: { color, width: 2, dashType: dashed ? "dash" : "solid", endArrowType: "triangle" },
121
+ });
122
+ if (label) {
123
+ slide.addText(label, {
124
+ x: Math.min(x1, x2), y: y - 0.26, w: Math.abs(x2 - x1), h: 0.24,
125
+ align: "center", fontFace: FONT, fontSize: 8.5, color: "475569",
126
+ });
127
+ }
128
+ }
package/lib/shapes.mjs CHANGED
@@ -92,6 +92,15 @@ export function drawCards(slide, cards, y) {
92
92
  });
93
93
  }
94
94
 
95
+ // Draws a participant's vertical lifeline (a sequence-diagram convention:
96
+ // a thin dashed reference line a participant's messages attach to).
97
+ export function drawLifeline(slide, { x, fromY, toY }) {
98
+ slide.addShape("line", {
99
+ x, y: fromY, w: 0, h: toY - fromY,
100
+ line: { color: "CBD5E1", width: 1, dashType: "dash" },
101
+ });
102
+ }
103
+
95
104
  // Draws a swimlane frame with a title strip. Returns the lane's box.
96
105
  export function drawLane(slide, { x, y, w, h, label }) {
97
106
  slide.addShape("rect", {
package/lib/validate.mjs CHANGED
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import Ajv2020 from "ajv/dist/2020.js";
5
- import { computeWorkflowLayout, computeArchitectureLayout, SLIDE_W, SLIDE_H } from "./layout.mjs";
5
+ import { computeWorkflowLayout, computeArchitectureLayout, computeSequenceLayout, SLIDE_W, SLIDE_H } from "./layout.mjs";
6
6
  import { computeSegments } from "./routes.mjs";
7
7
 
8
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -133,11 +133,50 @@ export function validateArchitectureLayout(raw) {
133
133
  });
134
134
  }
135
135
 
136
+ export function validateSequenceLayout(raw) {
137
+ const problems = [];
138
+ const { participantBoxes, messageYs, lifelineBottom } = computeSequenceLayout(raw);
139
+ const ids = Object.keys(participantBoxes);
140
+
141
+ for (const p of raw.participants) {
142
+ const box = participantBoxes[p.id];
143
+ const w = estimateTextWidth(p.label, 12.5);
144
+ if (w > box.w - 0.1) {
145
+ problems.push(`Participant "${p.id}" label "${p.label}" (~${w.toFixed(2)}in) is wider than its shape (${box.w.toFixed(2)}in) — shorten the label or move detail to sublabel.`);
146
+ }
147
+ if (offSlide(box)) {
148
+ problems.push(`Participant "${p.id}" is off-slide (slide is ${SLIDE_W}x${SLIDE_H}in).`);
149
+ }
150
+ }
151
+
152
+ for (let i = 0; i < ids.length; i++) {
153
+ for (let j = i + 1; j < ids.length; j++) {
154
+ if (rectsOverlap(participantBoxes[ids[i]], participantBoxes[ids[j]])) {
155
+ problems.push(`Participants "${ids[i]}" and "${ids[j]}" overlap.`);
156
+ }
157
+ }
158
+ }
159
+
160
+ raw.messages.forEach((m, i) => {
161
+ if (m.from === m.to) {
162
+ problems.push(`Message ${i} ("${m.label}") has the same participant "${m.from}" as from and to — self-messages aren't supported yet.`);
163
+ }
164
+ if (!participantBoxes[m.from]) problems.push(`Message ${i} references unknown participant "${m.from}".`);
165
+ if (!participantBoxes[m.to]) problems.push(`Message ${i} references unknown participant "${m.to}".`);
166
+ if (messageYs[i] > lifelineBottom - 0.3) {
167
+ problems.push(`Message ${i} ("${m.label}") falls below the lifeline (too many messages for the available space) — split into fewer messages or drop the cards to free up room.`);
168
+ }
169
+ });
170
+
171
+ return problems;
172
+ }
173
+
136
174
  /** Runs schema validation, then layout collision checks. Returns the list
137
175
  * of layout problems (empty = clean); throws if schema validation fails. */
138
176
  export async function validateAll(type, raw) {
139
177
  await validateSchema(type, raw);
140
178
  if (type === "workflow") return validateWorkflowLayout(raw);
141
179
  if (type === "architecture") return validateArchitectureLayout(raw);
180
+ if (type === "sequence") return validateSequenceLayout(raw);
142
181
  throw new Error(`Unknown diagram type: ${type}`);
143
182
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ptxiagram",
3
- "version": "0.1.0",
4
- "description": "Generate native, editable PowerPoint (PPTX) architecture and workflow diagrams from a small JSON file — real shapes and connectors, not an embedded image. Ships with a Claude Skill (SKILL.md) and a CLI.",
3
+ "version": "0.2.0",
4
+ "description": "Generate native, editable PowerPoint (PPTX) architecture, workflow, and sequence diagrams from a small JSON file — real shapes and connectors, not an embedded image. Ships with a Claude Skill (SKILL.md) and a CLI.",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
7
7
  "bin": {
@@ -18,7 +18,7 @@
18
18
  ],
19
19
  "scripts": {
20
20
  "doctor": "node bin/cli.mjs doctor",
21
- "test": "node test/render-workflow.test.mjs && node test/render-architecture.test.mjs && node test/validate.test.mjs && node test/workflow-lib-smoke.test.mjs"
21
+ "test": "node test/render-workflow.test.mjs && node test/render-architecture.test.mjs && node test/render-sequence.test.mjs && node test/validate.test.mjs && node test/workflow-lib-smoke.test.mjs"
22
22
  },
23
23
  "keywords": [
24
24
  "pptx",
@@ -27,6 +27,7 @@
27
27
  "claude-skill",
28
28
  "architecture-diagram",
29
29
  "workflow-diagram",
30
+ "sequence-diagram",
30
31
  "hancom",
31
32
  "hanshow",
32
33
  "office-automation"
@@ -1,5 +1,6 @@
1
1
  import pptxgen from "pptxgenjs";
2
2
  import fs from "node:fs/promises";
3
+ import path from "node:path";
3
4
  import { drawNode, drawBoundary, drawCards, FONT, EDGE_VARIANTS } from "../../lib/shapes.mjs";
4
5
  import { connector } from "../../lib/routes.mjs";
5
6
  import { repairPptx } from "../../lib/repair-pptx.mjs";
@@ -62,6 +63,7 @@ export async function renderArchitecture(inputPath, outputPath) {
62
63
  });
63
64
 
64
65
  const outFile = outputPath || raw.meta.output || "output.pptx";
66
+ await fs.mkdir(path.dirname(outFile), { recursive: true });
65
67
  await pptx.writeFile({ fileName: outFile });
66
68
  await repairPptx(outFile);
67
69
  return outFile;
@@ -0,0 +1,67 @@
1
+ import pptxgen from "pptxgenjs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { drawNode, drawLifeline, drawCards, FONT, EDGE_VARIANTS } from "../../lib/shapes.mjs";
5
+ import { drawArrow } from "../../lib/routes.mjs";
6
+ import { repairPptx } from "../../lib/repair-pptx.mjs";
7
+ import { validateSchema, validateSequenceLayout } from "../../lib/validate.mjs";
8
+ import { computeSequenceLayout } from "../../lib/layout.mjs";
9
+
10
+ export async function renderSequence(inputPath, outputPath) {
11
+ const raw = JSON.parse(await fs.readFile(inputPath, "utf8"));
12
+ await validateSchema("sequence", raw);
13
+
14
+ const { participantBoxes, centerX, messageYs, lifelineBottom, cardsY } = computeSequenceLayout(raw);
15
+ const problems = validateSequenceLayout(raw);
16
+ if (problems.length) {
17
+ throw new Error(`Sequence layout validation failed:\n- ${problems.join("\n- ")}`);
18
+ }
19
+
20
+ const pptx = new pptxgen();
21
+ pptx.defineLayout({ name: "WIDE", width: 13.33, height: 7.5 });
22
+ pptx.layout = "WIDE";
23
+ const slide = pptx.addSlide();
24
+
25
+ slide.addText(raw.meta.title, {
26
+ x: 0.4, y: 0.2, w: 12.5, h: 0.5,
27
+ fontFace: FONT, fontSize: 20, bold: true, color: "0F172A",
28
+ });
29
+ if (raw.meta.subtitle) {
30
+ slide.addText(raw.meta.subtitle, {
31
+ x: 0.4, y: 0.65, w: 12.5, h: 0.3,
32
+ fontFace: FONT, fontSize: 11, color: "64748B",
33
+ });
34
+ }
35
+
36
+ for (const p of raw.participants) {
37
+ drawNode(slide, { ...participantBoxes[p.id], label: p.label, sublabel: p.sublabel, type: p.type });
38
+ drawLifeline(slide, {
39
+ x: centerX[p.id],
40
+ fromY: participantBoxes[p.id].y + participantBoxes[p.id].h,
41
+ toY: lifelineBottom,
42
+ });
43
+ }
44
+
45
+ raw.messages.forEach((m, i) => {
46
+ const variant = EDGE_VARIANTS[m.variant || "default"];
47
+ drawArrow(slide, {
48
+ x1: centerX[m.from], x2: centerX[m.to], y: messageYs[i],
49
+ color: variant.color, dashed: variant.dashed, label: m.label,
50
+ });
51
+ });
52
+
53
+ if (raw.cards && raw.cards.length) {
54
+ drawCards(slide, raw.cards, cardsY);
55
+ }
56
+
57
+ slide.addText("Made with a Claude Skill · native PPTX shapes, editable in PowerPoint", {
58
+ x: 0.4, y: 7.15, w: 12.5, h: 0.3,
59
+ fontFace: FONT, fontSize: 8, color: "94A3B8", italic: true,
60
+ });
61
+
62
+ const outFile = outputPath || raw.meta.output || "output.pptx";
63
+ await fs.mkdir(path.dirname(outFile), { recursive: true });
64
+ await pptx.writeFile({ fileName: outFile });
65
+ await repairPptx(outFile);
66
+ return outFile;
67
+ }
@@ -1,5 +1,6 @@
1
1
  import pptxgen from "pptxgenjs";
2
2
  import fs from "node:fs/promises";
3
+ import path from "node:path";
3
4
  import { drawNode, drawLane, drawCards, FONT, EDGE_VARIANTS } from "../../lib/shapes.mjs";
4
5
  import { connector } from "../../lib/routes.mjs";
5
6
  import { repairPptx } from "../../lib/repair-pptx.mjs";
@@ -61,6 +62,7 @@ export async function renderWorkflow(inputPath, outputPath) {
61
62
  });
62
63
 
63
64
  const outFile = outputPath || raw.meta.output || "output.pptx";
65
+ await fs.mkdir(path.dirname(outFile), { recursive: true });
64
66
  await pptx.writeFile({ fileName: outFile });
65
67
  await repairPptx(outFile);
66
68
  return outFile;
@@ -0,0 +1,68 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://claude-skills/pptx-diagram/schemas/sequence.schema.json",
4
+ "title": "Native PPTX Sequence Diagram",
5
+ "description": "Participants sit in a row (max 6, auto-spaced) with a vertical lifeline; messages are drawn in JSON order at auto-incrementing y positions. See renderers/sequence for the layout budget.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schema_version", "diagram_type", "meta", "participants", "messages"],
9
+ "properties": {
10
+ "schema_version": { "const": 1 },
11
+ "diagram_type": { "const": "sequence" },
12
+ "meta": {
13
+ "type": "object",
14
+ "additionalProperties": false,
15
+ "required": ["title"],
16
+ "properties": {
17
+ "title": { "type": "string", "minLength": 1 },
18
+ "subtitle": { "type": "string" },
19
+ "output": { "type": "string" }
20
+ }
21
+ },
22
+ "participants": {
23
+ "type": "array",
24
+ "minItems": 2,
25
+ "maxItems": 6,
26
+ "items": {
27
+ "type": "object",
28
+ "additionalProperties": false,
29
+ "required": ["id", "type", "label"],
30
+ "properties": {
31
+ "id": { "$ref": "common.schema.json#/$defs/id" },
32
+ "type": { "$ref": "common.schema.json#/$defs/nodeType" },
33
+ "label": { "type": "string", "minLength": 1 },
34
+ "sublabel": { "type": "string" }
35
+ }
36
+ }
37
+ },
38
+ "messages": {
39
+ "type": "array",
40
+ "minItems": 1,
41
+ "maxItems": 9,
42
+ "items": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "required": ["from", "to", "label"],
46
+ "properties": {
47
+ "from": { "$ref": "common.schema.json#/$defs/id" },
48
+ "to": { "$ref": "common.schema.json#/$defs/id" },
49
+ "label": { "type": "string", "minLength": 1 },
50
+ "variant": { "enum": ["default", "emphasis", "security", "dashed"] }
51
+ }
52
+ }
53
+ },
54
+ "cards": {
55
+ "type": "array",
56
+ "items": {
57
+ "type": "object",
58
+ "additionalProperties": false,
59
+ "required": ["title", "items"],
60
+ "properties": {
61
+ "title": { "type": "string", "minLength": 1 },
62
+ "color": { "$ref": "common.schema.json#/$defs/hexColor" },
63
+ "items": { "type": "array", "items": { "type": "string" } }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }