slidemind 0.3.12 → 0.3.13
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/src/bridge/capture.mjs +52 -8
- package/src/builders/build.mjs +141 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slidemind",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
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.13",
|
|
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.13",
|
|
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": {
|
package/src/bridge/capture.mjs
CHANGED
|
@@ -145,8 +145,9 @@ export function buildCaptureInput(deckDir, bundleDir) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
// Launch a headless Chrome, run `fn`, then tear it down — mirrors the add-in's
|
|
148
|
-
// withBrowser (waits for the process to fully exit before cleanup).
|
|
149
|
-
|
|
148
|
+
// withBrowser (waits for the process to fully exit before cleanup). Exported
|
|
149
|
+
// for the draft-check grid overlay, which reuses the same pinned Chrome.
|
|
150
|
+
export async function withBrowser(fn) {
|
|
150
151
|
// Test hook: run the engine with no real browser. Only safe with a fake
|
|
151
152
|
// engine that ignores its `browser` argument — never set in production.
|
|
152
153
|
if (process.env.SLIDEMIND_CAPTURE_BROWSERLESS === "1") {
|
|
@@ -162,11 +163,25 @@ async function withBrowser(fn) {
|
|
|
162
163
|
// the bundle-shipped fonts are identical at the same pinned version, so text
|
|
163
164
|
// measurement parity with the add-in's renderer is preserved.
|
|
164
165
|
// SLIDEMIND_CAPTURE_FULL_CHROME=1 forces the full binary for A/B debugging.
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
const launch = (args) =>
|
|
167
|
+
puppeteer.launch({
|
|
168
|
+
headless: process.env.SLIDEMIND_CAPTURE_FULL_CHROME === "1" ? true : "shell",
|
|
169
|
+
protocolTimeout: 600_000,
|
|
170
|
+
args,
|
|
171
|
+
});
|
|
172
|
+
let browser;
|
|
173
|
+
try {
|
|
174
|
+
browser = await launch(LAUNCH_ARGS);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
// Codex's workspace-write seatbelt denies the mach-port rendezvous the
|
|
177
|
+
// browser process uses to hand off to its renderer ("bootstrap_check_in
|
|
178
|
+
// … Permission denied"), so a multi-process launch dies before the first
|
|
179
|
+
// page. Single-process keeps the renderer in-process — no rendezvous —
|
|
180
|
+
// and paints identically (same Blink/Skia, same fonts). Retried only on a
|
|
181
|
+
// launch failure, so every other environment keeps the stock launch.
|
|
182
|
+
if (!/Failed to launch the browser process/i.test(String(err?.message ?? err))) throw err;
|
|
183
|
+
browser = await launch([...LAUNCH_ARGS, "--single-process", "--no-zygote"]);
|
|
184
|
+
}
|
|
170
185
|
try {
|
|
171
186
|
return await fn(browser);
|
|
172
187
|
} finally {
|
|
@@ -199,7 +214,7 @@ async function withBrowser(fn) {
|
|
|
199
214
|
* Throws if the engine is missing/corrupt or the local browser can't launch —
|
|
200
215
|
* the caller decides whether to fall back to the server render path.
|
|
201
216
|
*/
|
|
202
|
-
export async function captureDeckLocally({ deckDir, bundleDir, onProgress }) {
|
|
217
|
+
export async function captureDeckLocally({ deckDir, bundleDir, onProgress, only = null }) {
|
|
203
218
|
const enginePath = path.join(bundleDir, "capture-engine.mjs");
|
|
204
219
|
if (!fs.existsSync(enginePath)) {
|
|
205
220
|
throw new Error(`no capture engine in bundle ${bundleDir}`);
|
|
@@ -218,6 +233,15 @@ export async function captureDeckLocally({ deckDir, bundleDir, onProgress }) {
|
|
|
218
233
|
}
|
|
219
234
|
|
|
220
235
|
const input = buildCaptureInput(deckDir, bundleDir);
|
|
236
|
+
// `only`: capture a subset of the authored slides (a per-slide builder
|
|
237
|
+
// re-checking its own file) — the engine renders each slide on its own, so
|
|
238
|
+
// dropping the others from the input is exact, not an approximation.
|
|
239
|
+
if (Array.isArray(only) && only.length > 0) {
|
|
240
|
+
const keep = new Set(only);
|
|
241
|
+
for (const name of Object.keys(input.slides)) {
|
|
242
|
+
if (!keep.has(name)) delete input.slides[name];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
221
245
|
return withBrowser((browser) => engine.captureDeck(browser, input, { onProgress }));
|
|
222
246
|
}
|
|
223
247
|
|
|
@@ -232,6 +256,26 @@ export async function captureDeckLocally({ deckDir, bundleDir, onProgress }) {
|
|
|
232
256
|
* never a silent degradation: such a bundle's lint engine (same deployment)
|
|
233
257
|
* rejects `<Chart>` as an unknown component before a page is ever built.
|
|
234
258
|
*/
|
|
259
|
+
/**
|
|
260
|
+
* The exporter's connector geometry (`connector-derive`) as a browser script
|
|
261
|
+
* exported from the same artifact (`connectorPreview: { globalName, source }`).
|
|
262
|
+
* The HTML preview's connector overlay calls it on the measured DOM rects, so
|
|
263
|
+
* the preview draws the route and arrowhead the .pptx will have. The CLI ships
|
|
264
|
+
* no connector geometry of its own. null when the bundle has no engine or the
|
|
265
|
+
* engine predates the export (the overlay then draws nothing and says so).
|
|
266
|
+
*/
|
|
267
|
+
export async function loadConnectorPreview(bundleDir) {
|
|
268
|
+
if (!bundleDir || !bundleHasCaptureEngine(bundleDir)) return null;
|
|
269
|
+
const enginePath = path.join(bundleDir, "capture-engine.mjs");
|
|
270
|
+
const version = readBundleMeta(bundleDir).captureEngineVersion ?? "0";
|
|
271
|
+
const engine = await import(
|
|
272
|
+
`${pathToFileURL(enginePath).href}?v=${encodeURIComponent(version)}`
|
|
273
|
+
);
|
|
274
|
+
const preview = engine.connectorPreview;
|
|
275
|
+
if (!preview || typeof preview.source !== "string") return null;
|
|
276
|
+
return { globalName: String(preview.globalName || "SlidemindConnectorDerive"), source: preview.source };
|
|
277
|
+
}
|
|
278
|
+
|
|
235
279
|
export async function loadChartPreview(bundleDir) {
|
|
236
280
|
if (!bundleDir || !bundleHasCaptureEngine(bundleDir)) return null;
|
|
237
281
|
const enginePath = path.join(bundleDir, "capture-engine.mjs");
|
package/src/builders/build.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import path from "node:path";
|
|
|
34
34
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
35
35
|
|
|
36
36
|
import { resolveBundleDirForDeck } from "../bridge/bundle.mjs";
|
|
37
|
-
import { loadChartPreview } from "../bridge/capture.mjs";
|
|
37
|
+
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";
|
|
@@ -323,6 +323,12 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
323
323
|
--sw-tech-blue-80: #3333FF;
|
|
324
324
|
--sw-magenta: #E6007E;
|
|
325
325
|
}
|
|
326
|
+
/* A data-connector div is a directive, not a painted box (the render
|
|
327
|
+
derives the line between its two referenced nodes). Same rule the
|
|
328
|
+
bundle's corporate-design.css carries; repeated here so a preview
|
|
329
|
+
against an older bundle lays out like the render will. */
|
|
330
|
+
[data-connector] { position: absolute; width: 0; height: 0; padding: 0; margin: 0; border-color: transparent; pointer-events: none; }
|
|
331
|
+
[data-sm-connector-overlay] { position: absolute; left: 0; top: 0; width: 1280px; height: 720px; overflow: visible; pointer-events: none; z-index: 2147483000; }
|
|
326
332
|
</style>
|
|
327
333
|
<style>
|
|
328
334
|
/* Substitute Arial with Arimo (a metric clone) so wrap behaviour
|
|
@@ -574,6 +580,10 @@ __SLIDES__
|
|
|
574
580
|
install window.SlidemindChart, which the bootstrap below binds as Chart
|
|
575
581
|
before any slide is evaluated. -->
|
|
576
582
|
__CHART_PREVIEW_SCRIPTS__
|
|
583
|
+
<!-- The exporter's connector geometry (connector-derive), inlined from the
|
|
584
|
+
bundle's capture engine: the overlay below derives every data-connector
|
|
585
|
+
route and arrowhead exactly as the .pptx export does. -->
|
|
586
|
+
__CONNECTOR_PREVIEW_SCRIPT__
|
|
577
587
|
<script>window.__SLIDE_EDITOR_OVERRIDES__ = __OVERRIDES__;</script>
|
|
578
588
|
<script>window.SVG_ASSETS = __SVG_ASSETS__;</script>
|
|
579
589
|
<script>
|
|
@@ -795,6 +805,130 @@ __RUNTIME_APPLIER__
|
|
|
795
805
|
|
|
796
806
|
smInlineSvgAssets();
|
|
797
807
|
|
|
808
|
+
// Connector overlay — the preview's stand-in for the native connectors
|
|
809
|
+
// the render derives from every data-connector directive (the capture
|
|
810
|
+
// engine injects an equivalent SVG before its screenshot). Straight,
|
|
811
|
+
// elbow and arc routes, side pins, arrowheads, dash and colour follow
|
|
812
|
+
// the same rules as the exporter's connector-derive; the exporter's
|
|
813
|
+
// measured output at view-draft / in the .pptx stays the truth.
|
|
814
|
+
function smDrawConnectors(container) {
|
|
815
|
+
if (!container) return;
|
|
816
|
+
var old = container.querySelector(":scope > svg[data-sm-connector-overlay]");
|
|
817
|
+
if (old) old.remove();
|
|
818
|
+
var dirs = container.querySelectorAll("[data-connector]");
|
|
819
|
+
if (!dirs.length) return;
|
|
820
|
+
var derive = window.SlidemindConnectorDerive;
|
|
821
|
+
if (!derive || typeof derive.resolveConnector !== "function") {
|
|
822
|
+
console.warn("connector overlay skipped: the bundle's capture engine exports no connector geometry (re-fetch the bundle)");
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
var cRect = container.getBoundingClientRect();
|
|
826
|
+
var scale = cRect.width / 1280 || 1;
|
|
827
|
+
var NS = "http://www.w3.org/2000/svg";
|
|
828
|
+
var svg = document.createElementNS(NS, "svg");
|
|
829
|
+
svg.setAttribute("data-sm-connector-overlay", "1");
|
|
830
|
+
svg.setAttribute("viewBox", "0 0 1280 720");
|
|
831
|
+
var defs = document.createElementNS(NS, "defs");
|
|
832
|
+
svg.appendChild(defs);
|
|
833
|
+
var rectOf = function (el) {
|
|
834
|
+
var r = el.getBoundingClientRect();
|
|
835
|
+
return { x: (r.left - cRect.left) / scale, y: (r.top - cRect.top) / scale, w: r.width / scale, h: r.height / scale };
|
|
836
|
+
};
|
|
837
|
+
var resolveRef = function (ref) {
|
|
838
|
+
if (!ref) return null;
|
|
839
|
+
try { return container.querySelector(ref.charAt(0) === "#" ? ref : "#" + ref); } catch (e) { return null; }
|
|
840
|
+
};
|
|
841
|
+
var sideOrNull = function (raw) {
|
|
842
|
+
var s = (raw || "").toLowerCase();
|
|
843
|
+
return (s === "top" || s === "bottom" || s === "left" || s === "right") ? s : null;
|
|
844
|
+
};
|
|
845
|
+
// Computed CSS colour ("rgb(a)(...)") → { hex, alpha } as the serialiser reports it.
|
|
846
|
+
var colorOf = function (css) {
|
|
847
|
+
var m = /rgba?\(([^)]+)\)/.exec(css || "");
|
|
848
|
+
if (!m) return { hex: "000000", alpha: 1 };
|
|
849
|
+
var p = m[1].split(",").map(function (v) { return parseFloat(v); });
|
|
850
|
+
var hex = p.slice(0, 3).map(function (v) {
|
|
851
|
+
var h = Math.max(0, Math.min(255, Math.round(v))).toString(16);
|
|
852
|
+
return h.length < 2 ? "0" + h : h;
|
|
853
|
+
}).join("").toUpperCase();
|
|
854
|
+
return { hex: hex, alpha: p.length > 3 && isFinite(p[3]) ? p[3] : 1 };
|
|
855
|
+
};
|
|
856
|
+
dirs.forEach(function (el, idx) {
|
|
857
|
+
var fromEl = resolveRef(el.getAttribute("data-from"));
|
|
858
|
+
var toEl = resolveRef(el.getAttribute("data-to"));
|
|
859
|
+
var route = (el.getAttribute("data-route") || "straight").toLowerCase();
|
|
860
|
+
var centerEl = route === "arc" ? resolveRef(el.getAttribute("data-center")) : null;
|
|
861
|
+
var arrowRaw = (el.getAttribute("data-arrow") || "none").toLowerCase();
|
|
862
|
+
var cs = getComputedStyle(el);
|
|
863
|
+
var stroke = colorOf(cs.color);
|
|
864
|
+
// The same payload the serialiser hands the exporter; resolveConnector
|
|
865
|
+
// derives endpoints, route, bends, arc, and arrowhead size from it.
|
|
866
|
+
var r = derive.resolveConnector({
|
|
867
|
+
from: el.getAttribute("data-from") || "",
|
|
868
|
+
to: el.getAttribute("data-to") || "",
|
|
869
|
+
arrow: (arrowRaw === "start" || arrowRaw === "end" || arrowRaw === "both") ? arrowRaw : "none",
|
|
870
|
+
route: route,
|
|
871
|
+
fromSide: sideOrNull(el.getAttribute("data-from-side")),
|
|
872
|
+
toSide: sideOrNull(el.getAttribute("data-to-side")),
|
|
873
|
+
center: el.getAttribute("data-center") || null,
|
|
874
|
+
fromRect: fromEl ? rectOf(fromEl) : null,
|
|
875
|
+
toRect: toEl ? rectOf(toEl) : null,
|
|
876
|
+
centerRect: centerEl ? rectOf(centerEl) : null,
|
|
877
|
+
strokeHex: stroke.hex,
|
|
878
|
+
strokeAlpha: stroke.alpha,
|
|
879
|
+
strokeWidthPx: parseFloat(cs.borderTopWidth) || 0,
|
|
880
|
+
dashed: /dashed|dotted/.test(cs.borderTopStyle || ""),
|
|
881
|
+
missing: [],
|
|
882
|
+
});
|
|
883
|
+
if (!r) return;
|
|
884
|
+
var color = "#" + r.strokeHex;
|
|
885
|
+
var d;
|
|
886
|
+
if (r.arc) {
|
|
887
|
+
var large = Math.abs(r.arc.sweepDeg) > 180 ? 1 : 0;
|
|
888
|
+
var sweep = r.arc.sweepDeg > 0 ? 1 : 0;
|
|
889
|
+
d = "M" + r.start.x + " " + r.start.y + " A" + r.arc.r + " " + r.arc.r + " 0 " + large + " " + sweep + " " + r.end.x + " " + r.end.y;
|
|
890
|
+
} else {
|
|
891
|
+
d = r.points.map(function (p, i) { return (i ? "L" : "M") + p.x + " " + p.y; }).join(" ");
|
|
892
|
+
}
|
|
893
|
+
var path = document.createElementNS(NS, "path");
|
|
894
|
+
path.setAttribute("d", d);
|
|
895
|
+
path.setAttribute("fill", "none");
|
|
896
|
+
path.setAttribute("stroke", color);
|
|
897
|
+
path.setAttribute("stroke-opacity", String(r.strokeAlpha));
|
|
898
|
+
path.setAttribute("stroke-width", String(r.strokeWidthPx));
|
|
899
|
+
if (r.dashed) {
|
|
900
|
+
var dash = Math.max(4, r.strokeWidthPx * 3);
|
|
901
|
+
path.setAttribute("stroke-dasharray", dash + " " + dash);
|
|
902
|
+
}
|
|
903
|
+
if (r.arrowStart || r.arrowEnd) {
|
|
904
|
+
// Marker sized to the head PowerPoint renders (r.arrowheadPx).
|
|
905
|
+
var markerId = "sm-conn-arrow-" + idx;
|
|
906
|
+
var marker = document.createElementNS(NS, "marker");
|
|
907
|
+
marker.setAttribute("id", markerId);
|
|
908
|
+
marker.setAttribute("viewBox", "0 0 10 10");
|
|
909
|
+
marker.setAttribute("refX", "9");
|
|
910
|
+
marker.setAttribute("refY", "5");
|
|
911
|
+
marker.setAttribute("markerWidth", String(r.arrowheadPx));
|
|
912
|
+
marker.setAttribute("markerHeight", String(r.arrowheadPx));
|
|
913
|
+
marker.setAttribute("orient", "auto-start-reverse");
|
|
914
|
+
marker.setAttribute("markerUnits", "userSpaceOnUse");
|
|
915
|
+
var tri = document.createElementNS(NS, "path");
|
|
916
|
+
tri.setAttribute("d", "M0,0 L10,5 L0,10 z");
|
|
917
|
+
tri.setAttribute("fill", color);
|
|
918
|
+
tri.setAttribute("fill-opacity", String(r.strokeAlpha));
|
|
919
|
+
marker.appendChild(tri);
|
|
920
|
+
defs.appendChild(marker);
|
|
921
|
+
if (r.arrowStart) path.setAttribute("marker-start", "url(#" + markerId + ")");
|
|
922
|
+
if (r.arrowEnd) path.setAttribute("marker-end", "url(#" + markerId + ")");
|
|
923
|
+
}
|
|
924
|
+
svg.appendChild(path);
|
|
925
|
+
});
|
|
926
|
+
if (svg.childNodes.length > 1) container.appendChild(svg);
|
|
927
|
+
}
|
|
928
|
+
function smScheduleConnectors(container) {
|
|
929
|
+
requestAnimationFrame(function () { requestAnimationFrame(function () { smDrawConnectors(container); }); });
|
|
930
|
+
}
|
|
931
|
+
|
|
798
932
|
if (printMode) {
|
|
799
933
|
document.body.classList.add("deck--print");
|
|
800
934
|
var printDeck = document.createElement("div");
|
|
@@ -814,6 +948,7 @@ __RUNTIME_APPLIER__
|
|
|
814
948
|
ReactDOM.createRoot(page).render(
|
|
815
949
|
React.createElement(slide.Component, { assetsPath: "./assets" })
|
|
816
950
|
);
|
|
951
|
+
smScheduleConnectors(page);
|
|
817
952
|
});
|
|
818
953
|
return;
|
|
819
954
|
}
|
|
@@ -903,6 +1038,7 @@ __RUNTIME_APPLIER__
|
|
|
903
1038
|
root.render(
|
|
904
1039
|
React.createElement(slide.Component, { assetsPath: "./assets" })
|
|
905
1040
|
);
|
|
1041
|
+
smScheduleConnectors(stage);
|
|
906
1042
|
updateThumbs();
|
|
907
1043
|
}
|
|
908
1044
|
|
|
@@ -1017,6 +1153,7 @@ export async function buildDeck(deckDir, opts = {}) {
|
|
|
1017
1153
|
// loadChartPreview) — resolved after lint, which already proved the bundle
|
|
1018
1154
|
// is materialized.
|
|
1019
1155
|
const chartPreview = await loadChartPreview(resolveBundleDirForDeck(deckDir));
|
|
1156
|
+
const connectorPreview = await loadConnectorPreview(resolveBundleDirForDeck(deckDir));
|
|
1020
1157
|
|
|
1021
1158
|
copyDesignSystem(deckDir, skill);
|
|
1022
1159
|
|
|
@@ -1043,6 +1180,9 @@ export async function buildDeck(deckDir, opts = {}) {
|
|
|
1043
1180
|
.map((script) => ` <script>\n${safeInline(script.source)}\n </script>`)
|
|
1044
1181
|
.join("\n")
|
|
1045
1182
|
: "",
|
|
1183
|
+
)
|
|
1184
|
+
.replace("__CONNECTOR_PREVIEW_SCRIPT__", () =>
|
|
1185
|
+
connectorPreview ? ` <script>\n${safeInline(connectorPreview.source)}\n </script>` : "",
|
|
1046
1186
|
);
|
|
1047
1187
|
|
|
1048
1188
|
const out = path.join(deckDir, "presentation.html");
|