slidemind 0.3.5 → 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/AGENTS.md +5 -5
- package/bin/slidemind.mjs +520 -182
- package/package.json +1 -1
- package/skills/create-slides/SKILL.md +263 -104
- package/skills/search/SKILL.md +9 -5
- package/src/bridge/build-and-push.mjs +3 -3
- package/src/bridge/bundle.mjs +91 -36
- package/src/bridge/capture.mjs +86 -8
- package/src/bridge/client.mjs +59 -23
- package/src/bridge/config.mjs +21 -2
- package/src/bridge/engine-download.mjs +26 -7
- package/src/bridge/recolor.mjs +46 -0
- package/src/bridge/setup-claim.mjs +7 -1
- package/src/builders/build.mjs +319 -5
- package/src/builders/gate-server.mjs +25 -36
- package/src/builders/gate-session.js +28 -48
- package/src/builders/lint-fix.mjs +88 -0
- package/src/builders/plan-overlay.js +507 -136
- package/src/builders/plan.mjs +337 -60
- package/src/builders/review-overlay.js +126 -109
- package/src/builders/review.mjs +118 -64
- package/src/builders/storyboard-markdown.mjs +15 -0
- package/src/builders/storyboard-schema.mjs +23 -6
- package/src/builders/visual-families.mjs +20 -4
- package/src/lib/adopt.mjs +10 -6
- package/src/lib/conversions.mjs +26 -6
- package/src/lib/overwrite-guard.mjs +14 -1
- package/src/lib/reconcile.mjs +15 -4
- package/src/pptx/tags.mjs +49 -0
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
imageSizeFromBuffer,
|
|
15
15
|
svgSizeFromMarkup,
|
|
16
16
|
} from "./image-dimensions.mjs";
|
|
17
|
-
import { describeRecolor } from "./recolor.mjs";
|
|
17
|
+
import { applyLocalRecolor, describeRecolor } from "./recolor.mjs";
|
|
18
18
|
|
|
19
19
|
// Every image format an engine accepts, plus the alias spellings seen in the
|
|
20
20
|
// wild. Mirrors the add-in's `searchImage` set (lib/image-formats.ts there);
|
|
@@ -151,12 +151,18 @@ function describeRecolorWithRefusal(markup, mimeType, iconColor, refusedColor) {
|
|
|
151
151
|
refusedColor ? null : (iconColor ?? null),
|
|
152
152
|
);
|
|
153
153
|
if (!refusedColor) return described;
|
|
154
|
+
// With the local bake in place (applyLocalRecolor), a surviving refusal
|
|
155
|
+
// means the glyph itself declined the colour — say why instead of sending
|
|
156
|
+
// the agent on a pane hunt.
|
|
157
|
+
const why =
|
|
158
|
+
described.mode === "multicolor"
|
|
159
|
+
? "it is a multi-tone glyph and a single colour would flatten it — left as designed."
|
|
160
|
+
: described.mode === "raster"
|
|
161
|
+
? "raster images carry their own colours."
|
|
162
|
+
: "the glyph's paint structure could not be rewritten safely.";
|
|
154
163
|
return {
|
|
155
164
|
...described,
|
|
156
|
-
note:
|
|
157
|
-
`Saved in its ORIGINAL colours — recolouring to ${refusedColor} runs in the slidemind add-in. ` +
|
|
158
|
-
"Re-run this download with --to <presId> (a PowerPoint window with the slidemind pane open) " +
|
|
159
|
-
"to bake the colour in.",
|
|
165
|
+
note: `Saved in its ORIGINAL colours — recolouring to ${refusedColor} was not applied: ${why}`,
|
|
160
166
|
};
|
|
161
167
|
}
|
|
162
168
|
|
|
@@ -218,16 +224,29 @@ async function streamOneResult({
|
|
|
218
224
|
if (res?.svgMarkup) {
|
|
219
225
|
// SVG arrives inline, not as a stream.
|
|
220
226
|
fs.rmSync(tempPath, { force: true });
|
|
227
|
+
// Window-free tint fallback: the pane-side refusal is about WHERE the
|
|
228
|
+
// rewrite runs, not whether it's possible — for currentColor/mono SVGs
|
|
229
|
+
// the bake is a deterministic local string transform. Measured impact:
|
|
230
|
+
// one sim batch carried 235 "saved in ORIGINAL colours" notes, every
|
|
231
|
+
// one an off-brand icon on the rendered slide.
|
|
232
|
+
let markup = res.svgMarkup;
|
|
233
|
+
if (recolorRefused) {
|
|
234
|
+
const local = applyLocalRecolor(markup, recolorRefused);
|
|
235
|
+
if (local.applied) {
|
|
236
|
+
markup = local.markup;
|
|
237
|
+
recolorRefused = null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
221
240
|
const filename = `engine-${searchId}-${selection}.svg`;
|
|
222
241
|
const target = path.join(assetsDir, filename);
|
|
223
|
-
fs.writeFileSync(target,
|
|
242
|
+
fs.writeFileSync(target, markup, "utf8");
|
|
224
243
|
return {
|
|
225
244
|
selection,
|
|
226
245
|
path: target,
|
|
227
246
|
reference: `./assets/${filename}`,
|
|
228
247
|
mimeType: res.mimeType ?? "image/svg+xml",
|
|
229
248
|
recolor: describeRecolorWithRefusal(
|
|
230
|
-
|
|
249
|
+
markup,
|
|
231
250
|
res.mimeType,
|
|
232
251
|
iconColor,
|
|
233
252
|
recolorRefused,
|
package/src/bridge/recolor.mjs
CHANGED
|
@@ -110,3 +110,49 @@ export function describeRecolor(svgMarkup, mimeType, appliedColor = null) {
|
|
|
110
110
|
note,
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
|
+
|
|
114
|
+
// Bake a colour into an SVG LOCALLY — the fallback for the window-free path,
|
|
115
|
+
// where the add-in refuses the tint (`recolor_needs_host`) because its
|
|
116
|
+
// DOM-based classifier lives pane-side. For the two recolourable modes the
|
|
117
|
+
// rewrite is a deterministic string transform this module can do itself:
|
|
118
|
+
// currentColor — replace every `currentColor` paint with the hex;
|
|
119
|
+
// mono — replace every concrete fill/stroke paint with the hex.
|
|
120
|
+
// Multicolor and raster are refused (flattening a multi-tone glyph or touching
|
|
121
|
+
// raster bytes is never right). Returns { markup, applied }.
|
|
122
|
+
export function applyLocalRecolor(svgMarkup, hex) {
|
|
123
|
+
if (typeof svgMarkup !== "string" || !hex || !isSvg(null, svgMarkup)) {
|
|
124
|
+
return { markup: svgMarkup, applied: false };
|
|
125
|
+
}
|
|
126
|
+
if (hasCurrentColor(svgMarkup)) {
|
|
127
|
+
return {
|
|
128
|
+
markup: svgMarkup.replace(
|
|
129
|
+
/((?:fill|stroke)\s*[:=]\s*["']?\s*)currentColor/gi,
|
|
130
|
+
`$1${hex}`,
|
|
131
|
+
),
|
|
132
|
+
applied: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const paints = distinctPaints(svgMarkup);
|
|
136
|
+
if (paints.size >= 2 || paints.size === 0) {
|
|
137
|
+
// Multicolor: never flatten. Zero declared paints: the glyph renders in
|
|
138
|
+
// the SVG default (black) via inheritance — inject a root fill instead of
|
|
139
|
+
// rewriting nothing.
|
|
140
|
+
if (paints.size === 0) {
|
|
141
|
+
return {
|
|
142
|
+
markup: svgMarkup.replace(/<svg\b/i, `<svg fill="${hex}"`),
|
|
143
|
+
applied: true,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return { markup: svgMarkup, applied: false };
|
|
147
|
+
}
|
|
148
|
+
const markup = svgMarkup.replace(
|
|
149
|
+
/((?:fill|stroke)\s*[:=]\s*["']?\s*)(#[0-9a-fA-F]{3,8}|rgba?\([^)]*\)|hsla?\([^)]*\)|[a-zA-Z]+)/g,
|
|
150
|
+
(m, prefix, value) => {
|
|
151
|
+
const v = value.trim().toLowerCase();
|
|
152
|
+
if (v === "none" || v === "transparent") return m;
|
|
153
|
+
if (!/^#|^rgb|^hsl/.test(v) && !NAMED_COLORS.has(v)) return m;
|
|
154
|
+
return `${prefix}${hex}`;
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
return { markup, applied: true };
|
|
158
|
+
}
|
|
@@ -114,7 +114,13 @@ export async function redeemSetupClaim({ backendURL, secret, cliVersion }) {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
if (result.status === 200 && result.json?.sessionToken) {
|
|
117
|
-
|
|
117
|
+
// `oid` (the user's directory id) arrives from add-ins that report it; an
|
|
118
|
+
// older add-in leaves it null, which the session store treats as "unknown
|
|
119
|
+
// person" — i.e. never inherit the previous Microsoft 365 sign-in.
|
|
120
|
+
return {
|
|
121
|
+
sessionToken: String(result.json.sessionToken),
|
|
122
|
+
oid: typeof result.json.oid === "string" && result.json.oid ? result.json.oid : null,
|
|
123
|
+
};
|
|
118
124
|
}
|
|
119
125
|
// Discriminate on the error CODE, not the status. A deployment that predates
|
|
120
126
|
// one-command setup has no such route, so the request falls through to that
|
package/src/builders/build.mjs
CHANGED
|
@@ -29,9 +29,12 @@
|
|
|
29
29
|
|
|
30
30
|
import { spawn } from "node:child_process";
|
|
31
31
|
import fs from "node:fs";
|
|
32
|
+
import os from "node:os";
|
|
32
33
|
import path from "node:path";
|
|
33
34
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
34
35
|
|
|
36
|
+
import { resolveBundleDirForDeck } from "../bridge/bundle.mjs";
|
|
37
|
+
import { loadChartPreview, loadConnectorPreview } from "../bridge/capture.mjs";
|
|
35
38
|
import { FAVICON_LINK } from "./gate-server.mjs";
|
|
36
39
|
import { lintSlidesOrThrow } from "./lint.mjs";
|
|
37
40
|
import { resolveSkillForDeck } from "./resolve-skill.mjs";
|
|
@@ -145,6 +148,98 @@ function copyDesignSystem(deckDir, skill) {
|
|
|
145
148
|
fs.writeFileSync(path.join(deckDir, "layouts.css"), cssChunks.join("\n\n"));
|
|
146
149
|
}
|
|
147
150
|
|
|
151
|
+
// First concrete (non-generic) family of a CSS var's font stack, or null.
|
|
152
|
+
function firstConcreteFamily(cssText, varName) {
|
|
153
|
+
const match = cssText.match(new RegExp(`--${varName}\\s*:\\s*([^;]+);`));
|
|
154
|
+
if (!match) return null;
|
|
155
|
+
const generics = new Set([
|
|
156
|
+
"sans-serif",
|
|
157
|
+
"serif",
|
|
158
|
+
"monospace",
|
|
159
|
+
"system-ui",
|
|
160
|
+
"ui-sans-serif",
|
|
161
|
+
"ui-serif",
|
|
162
|
+
"ui-monospace",
|
|
163
|
+
"cursive",
|
|
164
|
+
"fantasy",
|
|
165
|
+
]);
|
|
166
|
+
for (const part of match[1].split(",")) {
|
|
167
|
+
const family = part.trim().replace(/^['"]|['"]$/g, "");
|
|
168
|
+
if (family && !generics.has(family.toLowerCase())) return family;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Copy the materialized bundle's brand fonts next to the preview and emit the
|
|
174
|
+
// same @font-face block the server's capture page uses: one rule per FACE
|
|
175
|
+
// under its canonical family plus the CSS-token alias (--font-sans/--font-mono),
|
|
176
|
+
// with real weight/style descriptors so bold/italic render (and later measure)
|
|
177
|
+
// with the real face instead of a synthesized one. Without this the preview
|
|
178
|
+
// showed system fallbacks while capture/export used the brand face.
|
|
179
|
+
function brandFontFaces(deckDir, skill) {
|
|
180
|
+
const bundleDir = skill.bundleDir;
|
|
181
|
+
if (!bundleDir || !fs.existsSync(bundleDir)) return "";
|
|
182
|
+
let meta = {};
|
|
183
|
+
try {
|
|
184
|
+
meta = JSON.parse(
|
|
185
|
+
fs.readFileSync(path.join(bundleDir, "bundle.json"), "utf8"),
|
|
186
|
+
);
|
|
187
|
+
} catch {
|
|
188
|
+
return "";
|
|
189
|
+
}
|
|
190
|
+
const fonts = Array.isArray(meta.fonts) ? meta.fonts : [];
|
|
191
|
+
if (fonts.length === 0) return "";
|
|
192
|
+
|
|
193
|
+
let css = "";
|
|
194
|
+
try {
|
|
195
|
+
css = fs.readFileSync(path.join(deckDir, "corporate-design.css"), "utf8");
|
|
196
|
+
} catch {
|
|
197
|
+
/* alias resolution degrades to canonical families only */
|
|
198
|
+
}
|
|
199
|
+
const aliasByRole = {
|
|
200
|
+
primary: firstConcreteFamily(css, "font-sans"),
|
|
201
|
+
mono: firstConcreteFamily(css, "font-mono"),
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const outFonts = path.join(deckDir, "fonts");
|
|
205
|
+
const rules = [];
|
|
206
|
+
for (const font of fonts) {
|
|
207
|
+
if (!font?.family || !font?.filename) continue;
|
|
208
|
+
const filename = path.basename(font.filename);
|
|
209
|
+
const src = path.join(bundleDir, "fonts", filename);
|
|
210
|
+
if (!fs.existsSync(src)) continue;
|
|
211
|
+
fs.mkdirSync(outFonts, { recursive: true });
|
|
212
|
+
fs.copyFileSync(src, path.join(outFonts, filename));
|
|
213
|
+
|
|
214
|
+
const weight =
|
|
215
|
+
font.variable &&
|
|
216
|
+
typeof font.variable.minWeight === "number" &&
|
|
217
|
+
typeof font.variable.maxWeight === "number"
|
|
218
|
+
? `${font.variable.minWeight} ${font.variable.maxWeight}`
|
|
219
|
+
: String(
|
|
220
|
+
typeof font.weight === "number" &&
|
|
221
|
+
font.weight >= 100 &&
|
|
222
|
+
font.weight <= 900
|
|
223
|
+
? font.weight
|
|
224
|
+
: 400,
|
|
225
|
+
);
|
|
226
|
+
const style = font.style === "italic" ? "italic" : "normal";
|
|
227
|
+
const format = filename.toLowerCase().endsWith(".otf")
|
|
228
|
+
? "opentype"
|
|
229
|
+
: "truetype";
|
|
230
|
+
const rule = (family) =>
|
|
231
|
+
`@font-face { font-family: ${JSON.stringify(family)}; ` +
|
|
232
|
+
`src: url(${JSON.stringify(`./fonts/${filename}`)}) format("${format}"); ` +
|
|
233
|
+
`font-weight: ${weight}; font-style: ${style}; font-display: block; }`;
|
|
234
|
+
rules.push(rule(font.family));
|
|
235
|
+
const alias = aliasByRole[font.role];
|
|
236
|
+
if (alias && alias.toLowerCase() !== font.family.toLowerCase()) {
|
|
237
|
+
rules.push(rule(alias));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return rules.join("\n");
|
|
241
|
+
}
|
|
242
|
+
|
|
148
243
|
function loadOverrides(deckDir) {
|
|
149
244
|
const file = path.join(deckDir, "overrides.json");
|
|
150
245
|
if (!fs.existsSync(file)) return { version: 1, slides: {} };
|
|
@@ -228,6 +323,12 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
228
323
|
--sw-tech-blue-80: #3333FF;
|
|
229
324
|
--sw-magenta: #E6007E;
|
|
230
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; }
|
|
231
332
|
</style>
|
|
232
333
|
<style>
|
|
233
334
|
/* Substitute Arial with Arimo (a metric clone) so wrap behaviour
|
|
@@ -258,6 +359,10 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
258
359
|
font-style: italic;
|
|
259
360
|
}
|
|
260
361
|
</style>
|
|
362
|
+
<style id="brand-font-faces">__BRAND_FONT_FACES__</style>
|
|
363
|
+
<!-- Chart palette for the <Chart> stand-in (from the bundle's capture engine;
|
|
364
|
+
empty when the bundle carries no chart runtime). -->
|
|
365
|
+
<style id="chart-theme-tokens">__CHART_THEME_TOKENS__</style>
|
|
261
366
|
<style>
|
|
262
367
|
html, body { margin: 0; padding: 0; height: 100%; background: #0a0a0a; overflow: hidden; }
|
|
263
368
|
:root {
|
|
@@ -470,6 +575,15 @@ __SLIDES__
|
|
|
470
575
|
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
|
471
576
|
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
|
472
577
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
578
|
+
<!-- The <Chart> stand-in runtime: the add-in's own vendored scripts, inlined
|
|
579
|
+
from the bundle's capture engine (this CLI ships no chart source). They
|
|
580
|
+
install window.SlidemindChart, which the bootstrap below binds as Chart
|
|
581
|
+
before any slide is evaluated. -->
|
|
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__
|
|
473
587
|
<script>window.__SLIDE_EDITOR_OVERRIDES__ = __OVERRIDES__;</script>
|
|
474
588
|
<script>window.SVG_ASSETS = __SVG_ASSETS__;</script>
|
|
475
589
|
<script>
|
|
@@ -500,8 +614,11 @@ __RUNTIME_APPLIER__
|
|
|
500
614
|
var nameNode = node.name;
|
|
501
615
|
// Intrinsic elements only: a lowercase JSXIdentifier. Skip
|
|
502
616
|
// components (<SlideCover>), member/namespaced names, fragments.
|
|
617
|
+
// <Chart> is the one component that opts in (as in the add-in's
|
|
618
|
+
// instrument-source-loc): it forwards the attribute onto its
|
|
619
|
+
// root div, so a review pin on a chart resolves to its tag.
|
|
503
620
|
if (!nameNode || nameNode.type !== "JSXIdentifier") return;
|
|
504
|
-
if (!/^[a-z]/.test(nameNode.name)) return;
|
|
621
|
+
if (!/^[a-z]/.test(nameNode.name) && nameNode.name !== "Chart") return;
|
|
505
622
|
for (var i = 0; i < node.attributes.length; i++) {
|
|
506
623
|
var a = node.attributes[i];
|
|
507
624
|
if (a.type === "JSXAttribute" && a.name &&
|
|
@@ -649,6 +766,14 @@ __RUNTIME_APPLIER__
|
|
|
649
766
|
}
|
|
650
767
|
});
|
|
651
768
|
|
|
769
|
+
// The Chart binding slide jsx is compiled against — installed by the
|
|
770
|
+
// bundle's chart-preview scripts above; without them a chart renders
|
|
771
|
+
// nothing, exactly as the add-in's page behaves.
|
|
772
|
+
var Chart = window.SlidemindChart
|
|
773
|
+
? window.SlidemindChart.createComponent(React)
|
|
774
|
+
: function () { return null; };
|
|
775
|
+
var slideScope = Object.assign({ Chart: Chart }, LAYOUTS);
|
|
776
|
+
|
|
652
777
|
var slides = [];
|
|
653
778
|
document.querySelectorAll('script[data-kind="slide"]').forEach(function (s) {
|
|
654
779
|
var idx = parseInt(s.dataset.index, 10);
|
|
@@ -657,7 +782,7 @@ __RUNTIME_APPLIER__
|
|
|
657
782
|
var Component = evalJsx(
|
|
658
783
|
s.textContent,
|
|
659
784
|
"slides/" + name + ".jsx",
|
|
660
|
-
|
|
785
|
+
slideScope
|
|
661
786
|
);
|
|
662
787
|
slides[idx] = { name: name, Component: Component };
|
|
663
788
|
} catch (err) {
|
|
@@ -680,6 +805,130 @@ __RUNTIME_APPLIER__
|
|
|
680
805
|
|
|
681
806
|
smInlineSvgAssets();
|
|
682
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
|
+
|
|
683
932
|
if (printMode) {
|
|
684
933
|
document.body.classList.add("deck--print");
|
|
685
934
|
var printDeck = document.createElement("div");
|
|
@@ -699,6 +948,7 @@ __RUNTIME_APPLIER__
|
|
|
699
948
|
ReactDOM.createRoot(page).render(
|
|
700
949
|
React.createElement(slide.Component, { assetsPath: "./assets" })
|
|
701
950
|
);
|
|
951
|
+
smScheduleConnectors(page);
|
|
702
952
|
});
|
|
703
953
|
return;
|
|
704
954
|
}
|
|
@@ -788,6 +1038,7 @@ __RUNTIME_APPLIER__
|
|
|
788
1038
|
root.render(
|
|
789
1039
|
React.createElement(slide.Component, { assetsPath: "./assets" })
|
|
790
1040
|
);
|
|
1041
|
+
smScheduleConnectors(stage);
|
|
791
1042
|
updateThumbs();
|
|
792
1043
|
}
|
|
793
1044
|
|
|
@@ -898,6 +1149,12 @@ export async function buildDeck(deckDir, opts = {}) {
|
|
|
898
1149
|
);
|
|
899
1150
|
await lintSlidesOrThrow({ deckDir, slideFiles, layoutSources });
|
|
900
1151
|
|
|
1152
|
+
// The <Chart> stand-in runtime rides in the same bundle cache (see
|
|
1153
|
+
// loadChartPreview) — resolved after lint, which already proved the bundle
|
|
1154
|
+
// is materialized.
|
|
1155
|
+
const chartPreview = await loadChartPreview(resolveBundleDirForDeck(deckDir));
|
|
1156
|
+
const connectorPreview = await loadConnectorPreview(resolveBundleDirForDeck(deckDir));
|
|
1157
|
+
|
|
901
1158
|
copyDesignSystem(deckDir, skill);
|
|
902
1159
|
|
|
903
1160
|
const layoutsBlock = layoutFiles.map(inlineLayout).join("\n");
|
|
@@ -908,11 +1165,25 @@ export async function buildDeck(deckDir, opts = {}) {
|
|
|
908
1165
|
const svgAssets = collectSvgAssets(deckDir);
|
|
909
1166
|
|
|
910
1167
|
const html = HTML_TEMPLATE.replace("__TITLE__", path.basename(deckDir))
|
|
1168
|
+
.replace("__BRAND_FONT_FACES__", brandFontFaces(deckDir, skill))
|
|
911
1169
|
.replace("__LAYOUTS__", layoutsBlock)
|
|
912
1170
|
.replace("__SLIDES__", slidesBlock)
|
|
913
1171
|
.replace("__OVERRIDES__", safeInlineJson(loadOverrides(deckDir)))
|
|
914
1172
|
.replace("__SVG_ASSETS__", safeInlineJson(svgAssets))
|
|
915
|
-
.replace("__RUNTIME_APPLIER__", safeInline(fs.readFileSync(RUNTIME_APPLIER, "utf8")))
|
|
1173
|
+
.replace("__RUNTIME_APPLIER__", safeInline(fs.readFileSync(RUNTIME_APPLIER, "utf8")))
|
|
1174
|
+
// Function replacers: script sources may contain `$&`-style patterns that a
|
|
1175
|
+
// string replacement would expand.
|
|
1176
|
+
.replace("__CHART_THEME_TOKENS__", () => (chartPreview ? chartPreview.tokensCss : ""))
|
|
1177
|
+
.replace("__CHART_PREVIEW_SCRIPTS__", () =>
|
|
1178
|
+
chartPreview
|
|
1179
|
+
? chartPreview.scripts
|
|
1180
|
+
.map((script) => ` <script>\n${safeInline(script.source)}\n </script>`)
|
|
1181
|
+
.join("\n")
|
|
1182
|
+
: "",
|
|
1183
|
+
)
|
|
1184
|
+
.replace("__CONNECTOR_PREVIEW_SCRIPT__", () =>
|
|
1185
|
+
connectorPreview ? ` <script>\n${safeInline(connectorPreview.source)}\n </script>` : "",
|
|
1186
|
+
);
|
|
916
1187
|
|
|
917
1188
|
const out = path.join(deckDir, "presentation.html");
|
|
918
1189
|
fs.writeFileSync(out, html);
|
|
@@ -965,6 +1236,35 @@ const OPENER_EXIT_GRACE_MS = 2000;
|
|
|
965
1236
|
// user to open manually unless `quiet` — callers with their own messaging (the
|
|
966
1237
|
// review/plan gates) pass quiet:true and speak for themselves.
|
|
967
1238
|
export function openInBrowser(target, { quiet = false } = {}) {
|
|
1239
|
+
// Machine-level browser kill-switch for headless simulation batches
|
|
1240
|
+
// (scripts/codex-sim.mjs writes it for the batch, removes it after): every
|
|
1241
|
+
// env/PATH/cwd channel for reaching a sim session proved lossy at least
|
|
1242
|
+
// once, so the LAST line of defense is a HOME-scoped marker no shell can
|
|
1243
|
+
// strip. Stale-guarded to 6h so a crashed batch can't silently disable the
|
|
1244
|
+
// browser for real use forever.
|
|
1245
|
+
// Two locations: the config dir, and THIS MODULE'S OWN DIRECTORY — session
|
|
1246
|
+
// shells have shown unreliable HOME/XDG resolution, but a session that runs
|
|
1247
|
+
// this code by definition resolves __dirname correctly.
|
|
1248
|
+
for (const kill of [
|
|
1249
|
+
path.join(
|
|
1250
|
+
process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"),
|
|
1251
|
+
"slidemind",
|
|
1252
|
+
"suppress-browser",
|
|
1253
|
+
),
|
|
1254
|
+
path.join(__dirname, ".sim-suppress-browser"),
|
|
1255
|
+
]) {
|
|
1256
|
+
try {
|
|
1257
|
+
const age = Date.now() - fs.statSync(kill).mtimeMs;
|
|
1258
|
+
if (age < 6 * 60 * 60 * 1000) {
|
|
1259
|
+
if (!quiet) {
|
|
1260
|
+
process.stderr.write(`browser suppressed (simulation) — open ${target} manually\n`);
|
|
1261
|
+
}
|
|
1262
|
+
return Promise.resolve(false);
|
|
1263
|
+
}
|
|
1264
|
+
} catch {
|
|
1265
|
+
// marker absent — keep checking / open normally
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
968
1268
|
const chain = openerChain(target);
|
|
969
1269
|
return new Promise((resolve) => {
|
|
970
1270
|
const tryAt = (i) => {
|
|
@@ -1012,10 +1312,15 @@ function parseArgs(argv) {
|
|
|
1012
1312
|
args.deck = argv[++i];
|
|
1013
1313
|
} else if (a === "--open") {
|
|
1014
1314
|
args.open = true;
|
|
1315
|
+
} else if (a === "--fix") {
|
|
1316
|
+
args.fix = true;
|
|
1015
1317
|
} else if (a === "-h" || a === "--help") {
|
|
1016
1318
|
process.stdout.write(
|
|
1017
|
-
"Usage: slidemind build <deck> [--open]\n" +
|
|
1018
|
-
" node src/builders/build.mjs --deck <deck> [--open]\n"
|
|
1319
|
+
"Usage: slidemind build <deck> [--open] [--fix]\n" +
|
|
1320
|
+
" node src/builders/build.mjs --deck <deck> [--open] [--fix]\n" +
|
|
1321
|
+
" --fix apply the deterministic lint autofixes to slides/*.jsx before building\n" +
|
|
1322
|
+
" (pin inline-svg px size from its viewBox; letter-spacing em→px where the\n" +
|
|
1323
|
+
" block's px font-size makes the conversion exact)\n",
|
|
1019
1324
|
);
|
|
1020
1325
|
process.exit(0);
|
|
1021
1326
|
} else {
|
|
@@ -1033,6 +1338,15 @@ function parseArgs(argv) {
|
|
|
1033
1338
|
async function main() {
|
|
1034
1339
|
const args = parseArgs(process.argv.slice(2));
|
|
1035
1340
|
const deckDir = path.resolve(args.deck);
|
|
1341
|
+
if (args.fix) {
|
|
1342
|
+
const { fixDeckSlides } = await import("./lint-fix.mjs");
|
|
1343
|
+
const fixed = fixDeckSlides(deckDir);
|
|
1344
|
+
if (fixed.fixes) {
|
|
1345
|
+
process.stdout.write(
|
|
1346
|
+
`Autofixed ${fixed.fixes} lint issue(s) in ${fixed.files} slide file(s) — re-read any file before patching it.\n`,
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1036
1350
|
const { out, slideCount } = await buildDeck(deckDir);
|
|
1037
1351
|
const rel = path.relative(process.cwd(), out) || out;
|
|
1038
1352
|
process.stdout.write(`Built ${rel} (${slideCount} slides)\n`);
|