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 +33 -9
- package/SKILL.md +31 -4
- package/bin/cli.mjs +3 -2
- package/examples/cache-miss.sequence.json +23 -0
- package/index.mjs +8 -1
- package/lib/layout.mjs +23 -0
- package/lib/routes.mjs +16 -0
- package/lib/shapes.mjs +9 -0
- package/lib/validate.mjs +40 -1
- package/package.json +4 -3
- package/renderers/architecture/render-architecture.mjs +2 -0
- package/renderers/sequence/render-sequence.mjs +67 -0
- package/renderers/workflow/render-workflow.mjs +2 -0
- package/schemas/sequence.schema.json +68 -0
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
|
-
- **
|
|
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
|
-
##
|
|
15
|
+
## Use it in an agent (Claude Code, Codex, Cursor, ...)
|
|
14
16
|
|
|
15
|
-
|
|
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
|
-
|
|
23
|
+
Then just ask in chat:
|
|
22
24
|
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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 [`
|
|
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
|
|
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.
|
|
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
|
-
- `
|
|
173
|
-
`
|
|
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 {
|
|
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.
|
|
4
|
-
"description": "Generate native, editable PowerPoint (PPTX) architecture and
|
|
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
|
+
}
|