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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidemind",
3
- "version": "0.3.12",
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.12",
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.12",
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": {
@@ -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
- async function withBrowser(fn) {
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 browser = await puppeteer.launch({
166
- headless: process.env.SLIDEMIND_CAPTURE_FULL_CHROME === "1" ? true : "shell",
167
- protocolTimeout: 600_000,
168
- args: LAUNCH_ARGS,
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");
@@ -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");