slidemind 0.3.16 → 0.3.17
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/create-slides/SKILL.md +3 -1
- package/src/bridge/build-and-push.mjs +4 -1
- package/src/builders/build.mjs +37 -2
- package/src/builders/draft-check.mjs +47 -3
- package/src/lib/reconcile.mjs +27 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slidemind",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.17",
|
|
4
4
|
"description": "Connect Claude Code to the slidemind PowerPoint add-in — build and edit native, on-brand PowerPoint decks against the active corporate design, from the terminal.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "statworx",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slidemind",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.17",
|
|
4
4
|
"description": "Connect Codex to the slidemind PowerPoint add-in — build and edit native, on-brand PowerPoint decks against the active corporate design. Requires the slidemind CLI (npm install -g slidemind); the skills explain the install when it is missing.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "statworx",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slidemind",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.17",
|
|
4
4
|
"description": "Connect Claude Code, Codex & opencode to the slidemind PowerPoint add-in — let your coding agent build and edit native, on-brand PowerPoint decks against your active corporate design, from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -652,7 +652,9 @@ From here:
|
|
|
652
652
|
the gate will open). Two things come back, and both bind:
|
|
653
653
|
- **Measured lines** under each slide — `[fill:warn]` (a stretched region whose organisms pool
|
|
654
654
|
at its top, with the region and the dead band) and `[inventory:warn]` (connectors listed in
|
|
655
|
-
`asset-plan.md` but not drawn
|
|
655
|
+
`asset-plan.md` but not drawn — counted per route kind too, so five `curved` rows drawn as
|
|
656
|
+
elbows warn even when the total matches; an empty inventory, a fetched asset no slide
|
|
657
|
+
references) —
|
|
656
658
|
and a closing `DRAFT CHECK: N measured defects` line. Fix every one in the JSX and re-run;
|
|
657
659
|
the review gate refuses to open while they stand for an unchanged slide. A fill band is
|
|
658
660
|
fixed by distributing the organisms (`grid-template-rows` with one track per organism,
|
|
@@ -621,7 +621,10 @@ export async function viewDraft(
|
|
|
621
621
|
|
|
622
622
|
let captured;
|
|
623
623
|
try {
|
|
624
|
-
|
|
624
|
+
// Converted slides never reach the local engine — it would lint each one
|
|
625
|
+
// as an unregistered layout and the noise would ride the translate gate.
|
|
626
|
+
const capturable = all.filter((f) => f.endsWith(".jsx") && !conversions[f]);
|
|
627
|
+
captured = await captureDeckLocally({ deckDir, bundleDir, onProgress, only: capturable });
|
|
625
628
|
} catch (err) {
|
|
626
629
|
throw new Error(
|
|
627
630
|
`local draft capture failed (${err instanceof Error ? err.message : String(err)}). ` +
|
package/src/builders/build.mjs
CHANGED
|
@@ -38,6 +38,7 @@ import { loadChartPreview, loadConnectorPreview } from "../bridge/capture.mjs";
|
|
|
38
38
|
import { FAVICON_LINK } from "./gate-server.mjs";
|
|
39
39
|
import { lintSlidesOrThrow } from "./lint.mjs";
|
|
40
40
|
import { resolveSkillForDeck } from "./resolve-skill.mjs";
|
|
41
|
+
import { loadConversions } from "../lib/conversions.mjs";
|
|
41
42
|
import { slideOrderWarning, sortSlideFilenames } from "../lib/slide-order.mjs";
|
|
42
43
|
|
|
43
44
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -87,6 +88,29 @@ function inlineLayout(file) {
|
|
|
87
88
|
);
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
// Preview stand-ins for the ConvertedHost layouts converted slides invoke. The
|
|
92
|
+
// host's name carries its canvas (`ConvertedHost<cx>x<cy>`, EMU), so the stub
|
|
93
|
+
// sizes itself the way the minted host does (EMU / 9525, 3 decimals) instead of
|
|
94
|
+
// assuming 16:9 — a 4:3 source previews at 960x720, not stretched to 1280.
|
|
95
|
+
// Real geometry still comes from the server render.
|
|
96
|
+
const HOST_NAME_RE = /<(ConvertedHost(\d+)x(\d+))/;
|
|
97
|
+
const emuToCssPx = (emu) => `${Math.round((Number(emu) / 9525) * 1000) / 1000}px`;
|
|
98
|
+
|
|
99
|
+
function convertedHostStubs(slideFiles, conversions) {
|
|
100
|
+
const hosts = new Map();
|
|
101
|
+
for (const file of slideFiles) {
|
|
102
|
+
if (!conversions[path.basename(file)]) continue;
|
|
103
|
+
const m = HOST_NAME_RE.exec(fs.readFileSync(file, "utf8"));
|
|
104
|
+
if (m) hosts.set(m[1], { w: emuToCssPx(m[2]), h: emuToCssPx(m[3]) });
|
|
105
|
+
}
|
|
106
|
+
return [...hosts].map(
|
|
107
|
+
([name, { w, h }]) =>
|
|
108
|
+
`<script type="text/jsx-template" data-kind="layout" data-name="${name}" data-binding="${name}">\n` +
|
|
109
|
+
`function ${name}({ background, children }) { return <div style={{ position: "relative", width: "${w}", height: "${h}", overflow: "hidden", background: background || "#FFFFFF" }}>{children}</div>; }\n` +
|
|
110
|
+
`</script>`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
90
114
|
function layoutBindingNameForStem(stem) {
|
|
91
115
|
return "Slide" + stem.split("-").map((p) => (
|
|
92
116
|
p ? p[0].toUpperCase() + p.slice(1) : ""
|
|
@@ -354,7 +378,15 @@ export async function buildDeck(deckDir, opts = {}) {
|
|
|
354
378
|
fs.readFileSync(file, "utf8"),
|
|
355
379
|
]),
|
|
356
380
|
);
|
|
357
|
-
|
|
381
|
+
// Converted slides (`slidemind adopt`) invoke a synthetic ConvertedHost
|
|
382
|
+
// layout the server mints per source canvas; no bundle carries it, so the
|
|
383
|
+
// local engine would fail every one of them as an unregistered layout. They
|
|
384
|
+
// are linted server-side on render — skip them here, lint the authored rest.
|
|
385
|
+
const conversions = loadConversions(deckDir, { includeSource: false });
|
|
386
|
+
const authoredFiles = slideFiles.filter((f) => !conversions[path.basename(f)]);
|
|
387
|
+
if (authoredFiles.length > 0) {
|
|
388
|
+
await lintSlidesOrThrow({ deckDir, slideFiles: authoredFiles, layoutSources });
|
|
389
|
+
}
|
|
358
390
|
|
|
359
391
|
// The <Chart> stand-in runtime rides in the same bundle cache (see
|
|
360
392
|
// loadChartPreview) — resolved after lint, which already proved the bundle
|
|
@@ -364,7 +396,10 @@ export async function buildDeck(deckDir, opts = {}) {
|
|
|
364
396
|
|
|
365
397
|
copyDesignSystem(deckDir, skill);
|
|
366
398
|
|
|
367
|
-
const layoutsBlock = layoutFiles
|
|
399
|
+
const layoutsBlock = layoutFiles
|
|
400
|
+
.map(inlineLayout)
|
|
401
|
+
.concat(convertedHostStubs(slideFiles, conversions))
|
|
402
|
+
.join("\n");
|
|
368
403
|
const slidesBlock = slideFiles
|
|
369
404
|
.map((file, i) => inlineSlide(file, i))
|
|
370
405
|
.join("\n");
|
|
@@ -30,6 +30,20 @@ const NONE_RE = /^\(?\s*(none|n\/a|no\b)/i;
|
|
|
30
30
|
|
|
31
31
|
const pad2 = (n) => String(n).padStart(2, "0");
|
|
32
32
|
|
|
33
|
+
// Route kind per connector row / directive — part of the mockup contract: a
|
|
34
|
+
// loop the concept draws as curves that lands as elbows is a different figure,
|
|
35
|
+
// and the count alone (5 = 5) never fires. Port of inventory.ts (gotcha 84).
|
|
36
|
+
const ROUTE_KINDS = ["straight", "elbow", "curved"];
|
|
37
|
+
const emptyRoutes = () => ({ straight: 0, elbow: 0, curved: 0 });
|
|
38
|
+
function routeKindOf(text) {
|
|
39
|
+
for (const token of text.split(",")) {
|
|
40
|
+
const t = token.trim().toLowerCase();
|
|
41
|
+
if (ROUTE_KINDS.includes(t)) return t;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const describeRoutes = (r) => ROUTE_KINDS.map((k) => `${r[k]} ${k}`).join(", ");
|
|
46
|
+
|
|
33
47
|
/** Seed the inventory form once (never overwrites a filled one). `images`: [{slideNumber,label}]. */
|
|
34
48
|
export function seedAssetPlan(deckDir, images) {
|
|
35
49
|
const file = path.join(deckDir, ASSET_PLAN_FILE);
|
|
@@ -91,7 +105,7 @@ function parseAssetPlan(text) {
|
|
|
91
105
|
const line = raw.trim();
|
|
92
106
|
const heading = SLIDE_HEADING_RE.exec(line);
|
|
93
107
|
if (heading) {
|
|
94
|
-
current = { filled: false, connectors: 0, organisms: 0, icons: 0, iconsResolved: 0, verdict: null, ticked: 0, unticked: 0 };
|
|
108
|
+
current = { filled: false, connectors: 0, routes: emptyRoutes(), organisms: 0, icons: 0, iconsResolved: 0, verdict: null, ticked: 0, unticked: 0 };
|
|
95
109
|
out.set(Number(heading[1]), current);
|
|
96
110
|
section = "";
|
|
97
111
|
continue;
|
|
@@ -119,7 +133,12 @@ function parseAssetPlan(text) {
|
|
|
119
133
|
if (inline && !NONE_RE.test(inline)) {
|
|
120
134
|
if (section === "organisms" || section === "regions") current.filled = true;
|
|
121
135
|
if (section === "connectors") {
|
|
122
|
-
|
|
136
|
+
for (const row of inline.split(";")) {
|
|
137
|
+
if (!row.trim()) continue;
|
|
138
|
+
current.connectors += 1;
|
|
139
|
+
const kind = routeKindOf(row);
|
|
140
|
+
if (kind) current.routes[kind] += 1;
|
|
141
|
+
}
|
|
123
142
|
}
|
|
124
143
|
}
|
|
125
144
|
continue;
|
|
@@ -129,7 +148,11 @@ function parseAssetPlan(text) {
|
|
|
129
148
|
if (!body || NONE_RE.test(body)) continue;
|
|
130
149
|
if (section === "organisms" || section === "regions") current.filled = true;
|
|
131
150
|
if (section === "organisms") current.organisms += 1;
|
|
132
|
-
if (section === "connectors")
|
|
151
|
+
if (section === "connectors") {
|
|
152
|
+
current.connectors += 1;
|
|
153
|
+
const kind = routeKindOf(body);
|
|
154
|
+
if (kind) current.routes[kind] += 1;
|
|
155
|
+
}
|
|
133
156
|
if (section === "icons") {
|
|
134
157
|
current.icons += 1;
|
|
135
158
|
// Resolved = a fetched file appended, or an explicit "(no result)".
|
|
@@ -167,6 +190,19 @@ function countConnectors(jsx) {
|
|
|
167
190
|
return attached + markers;
|
|
168
191
|
}
|
|
169
192
|
|
|
193
|
+
// Route kind per `data-connector` directive; a missing data-route is straight.
|
|
194
|
+
// Imported native connectors carry their own preset and are not authored
|
|
195
|
+
// against the mockup, so they are left out of the kind tally.
|
|
196
|
+
function countRoutes(jsx) {
|
|
197
|
+
const routes = emptyRoutes();
|
|
198
|
+
for (const tag of jsx.match(/<[^<>]*\bdata-connector\b[^<>]*>/g) ?? []) {
|
|
199
|
+
if (/\bdata-native-connector\b/.test(tag)) continue;
|
|
200
|
+
const kind = /\bdata-route\s*=\s*["']([a-z]+)["']/.exec(tag)?.[1] ?? "straight";
|
|
201
|
+
if (ROUTE_KINDS.includes(kind)) routes[kind] += 1;
|
|
202
|
+
}
|
|
203
|
+
return routes;
|
|
204
|
+
}
|
|
205
|
+
|
|
170
206
|
/**
|
|
171
207
|
* `[inventory:warn]` lines for the deck: per authored slide (inventory empty,
|
|
172
208
|
* connectors listed > drawn) plus fetched assets no slide references. Empty
|
|
@@ -212,6 +248,14 @@ export function inventoryWarnings(deckDir, jsxFiles = null) {
|
|
|
212
248
|
`[inventory:warn] ${jsxPath} — asset-plan.md lists ${inv.connectors} connector${inv.connectors === 1 ? "" : "s"} for this slide, the JSX draws ${drawn} (data-connector / marker-end). Draw the missing ones per workflow/authoring.md → Arrow contract; a mockup connector is never dropped.`,
|
|
213
249
|
);
|
|
214
250
|
}
|
|
251
|
+
const drawnRoutes = countRoutes(jsx);
|
|
252
|
+
for (const kind of ROUTE_KINDS) {
|
|
253
|
+
if (inv.routes[kind] > drawnRoutes[kind]) {
|
|
254
|
+
lines.push(
|
|
255
|
+
`[inventory:warn] ${jsxPath} — asset-plan.md lists ${inv.routes[kind]} ${kind} connector${inv.routes[kind] === 1 ? "" : "s"} for this slide, the JSX draws ${drawnRoutes[kind]} (${describeRoutes(drawnRoutes)}). The mockup's route shape is the contract: keep data-route="${kind}" and fix the endpoint sides or node positions instead of changing the route kind.`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
215
259
|
}
|
|
216
260
|
// Fetched assets nobody references: the mockup staged them, the deck lost them.
|
|
217
261
|
const referenced = allJsx
|
package/src/lib/reconcile.mjs
CHANGED
|
@@ -166,8 +166,34 @@ export async function collectLiveReconcileInputs({
|
|
|
166
166
|
const before = Object.keys(exported).length;
|
|
167
167
|
const ack = await exportRound(exportSlides, pending);
|
|
168
168
|
deck ??= ack?.deck ?? null;
|
|
169
|
+
// PowerPoint web (package fallback on) may hand back the SAVED file's bytes
|
|
170
|
+
// (`provenance.source` graph/saved) when the host export fails. Saved bytes
|
|
171
|
+
// cannot show unsaved manual edits, so projecting them would read "no
|
|
172
|
+
// drift" for a slide the user is editing right now — treat them as not
|
|
173
|
+
// exported, same failure mode as a slide that never arrived.
|
|
174
|
+
const stale = [];
|
|
169
175
|
for (const [guid, slide] of Object.entries(ack?.slides ?? {})) {
|
|
170
|
-
if (slide?.base64
|
|
176
|
+
if (!slide?.base64 || !slide?.tags) continue;
|
|
177
|
+
if (slide.provenance && slide.provenance.source !== "host") stale.push(guid);
|
|
178
|
+
else exported[guid] = slide;
|
|
179
|
+
}
|
|
180
|
+
// `failed` names slides neither the host nor the stored file could supply
|
|
181
|
+
// (web fallback); they never come back, so they fail closed at once.
|
|
182
|
+
const failed = Object.keys(ack?.failed ?? {}).filter(
|
|
183
|
+
(guid) => !exported[guid] && requested.includes(guid),
|
|
184
|
+
);
|
|
185
|
+
const unverifiable = [...new Set([...failed, ...stale])];
|
|
186
|
+
if (unverifiable.length > 0) {
|
|
187
|
+
const err = new Error(
|
|
188
|
+
`the pane could not export ${unverifiable.length} live slide(s) from the ` +
|
|
189
|
+
`open presentation (${failed.length} unavailable, ${stale.length} only as ` +
|
|
190
|
+
`the saved file), so their manual edits cannot be checked. Save the ` +
|
|
191
|
+
`presentation and retry, or re-run with --force to push anyway and ` +
|
|
192
|
+
`discard any manual edits.`,
|
|
193
|
+
);
|
|
194
|
+
err.code = "export_incomplete";
|
|
195
|
+
err.failed = ack?.failed ?? {};
|
|
196
|
+
throw err;
|
|
171
197
|
}
|
|
172
198
|
const next = (Array.isArray(ack?.truncated) ? ack.truncated : []).filter(
|
|
173
199
|
(guid) => !exported[guid] && requested.includes(guid),
|