slidemind 0.0.27
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 +23 -0
- package/CLAUDE.md +137 -0
- package/README.md +100 -0
- package/bin/slidemind.mjs +603 -0
- package/package.json +62 -0
- package/skills/create-slides/SKILL.md +108 -0
- package/src/bridge/build-and-push.mjs +232 -0
- package/src/bridge/bundle.mjs +373 -0
- package/src/bridge/client.mjs +338 -0
- package/src/builders/build.mjs +724 -0
- package/src/builders/lint-slides.mjs +796 -0
- package/src/builders/primitives.json +19 -0
- package/src/builders/resolve-skill.mjs +205 -0
- package/src/builders/runtime-applier.js +275 -0
- package/src/installers/brand-theme.mjs +38 -0
- package/src/installers/claude.mjs +141 -0
- package/src/installers/codex.mjs +229 -0
- package/src/installers/lib.mjs +435 -0
- package/src/installers/opencode.mjs +181 -0
- package/src/installers/tui.mjs +125 -0
- package/src/mcp/recolor.mjs +111 -0
- package/src/mcp/server.mjs +476 -0
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Build a self-contained HTML presentation from per-slide .jsx files.
|
|
3
|
+
// Pure Node stdlib — no external dependencies.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// slidemind build <deck>
|
|
7
|
+
// slidemind build <deck> --open
|
|
8
|
+
//
|
|
9
|
+
// Convention:
|
|
10
|
+
// <deck>/
|
|
11
|
+
// slides/
|
|
12
|
+
// 01-cover.jsx
|
|
13
|
+
// 02-agenda.jsx
|
|
14
|
+
// ...
|
|
15
|
+
//
|
|
16
|
+
// Each slide is a thin JSX component that composes a layout from
|
|
17
|
+
// skills/create-slides/layouts/. Slide files do NOT import layouts; the
|
|
18
|
+
// build inlines all layouts once and exposes them as globals (SlideCover,
|
|
19
|
+
// SlideAgendaBlack, ...) inside the slide scope.
|
|
20
|
+
//
|
|
21
|
+
// The build:
|
|
22
|
+
// 1. Inlines every layout .jsx as a <script type="text/jsx-template"> block.
|
|
23
|
+
// 2. Inlines every slide .jsx the same way, in filename order.
|
|
24
|
+
// 3. Copies corporate-design.css, layouts.css, and the assets/ folder into
|
|
25
|
+
// the deck directory so the output is fully self-contained.
|
|
26
|
+
// 4. Emits presentation.html with React + Babel-standalone from CDN and a
|
|
27
|
+
// bootstrap that transpiles on load and renders a keyboard-navigable
|
|
28
|
+
// deck.
|
|
29
|
+
|
|
30
|
+
import { spawn } from "node:child_process";
|
|
31
|
+
import fs from "node:fs";
|
|
32
|
+
import path from "node:path";
|
|
33
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
34
|
+
|
|
35
|
+
import { lintSlidesOrThrow } from "./lint-slides.mjs";
|
|
36
|
+
import { resolveSkillForDeck } from "./resolve-skill.mjs";
|
|
37
|
+
|
|
38
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
39
|
+
const __dirname = path.dirname(__filename);
|
|
40
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
41
|
+
const RUNTIME_APPLIER = path.join(__dirname, "runtime-applier.js");
|
|
42
|
+
|
|
43
|
+
function safeInline(src) {
|
|
44
|
+
// Guard against stray </script> sequences breaking the inline block.
|
|
45
|
+
return src.replace(/<\/script>/g, "<\\/script>");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function safeInlineJson(data) {
|
|
49
|
+
return JSON.stringify(data).replace(/<\/script>/g, "<\\/script>");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function stripImports(src) {
|
|
53
|
+
// Remove `import ... from "..."` lines. Layouts live in global scope in
|
|
54
|
+
// the generated HTML, so slide files never need to import them explicitly.
|
|
55
|
+
return src.replace(
|
|
56
|
+
/^\s*import\s+[^;]+?from\s+["'][^"']+["']\s*;?\s*$/gm,
|
|
57
|
+
"",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function rewriteAssetsPath(src) {
|
|
62
|
+
// The layout defaults point at "../assets" (correct for in-repo authoring
|
|
63
|
+
// under skills/create-slides/layouts/). In a built deck the copied
|
|
64
|
+
// assets sit next to the HTML, so the default should be "./assets".
|
|
65
|
+
return src.replace(
|
|
66
|
+
/assetsPath = "\.\.\/assets"/g,
|
|
67
|
+
'assetsPath = "./assets"',
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function inlineLayout(file) {
|
|
72
|
+
const stem = path.basename(file, ".jsx");
|
|
73
|
+
const raw = rewriteAssetsPath(fs.readFileSync(file, "utf8"));
|
|
74
|
+
const binding = raw.match(/export\s+default\s+function\s+([A-Za-z_$][\w$]*)\s*\(/)?.[1] || layoutBindingNameForStem(stem);
|
|
75
|
+
const src = safeInline(raw);
|
|
76
|
+
return (
|
|
77
|
+
`<script type="text/jsx-template" data-kind="layout" ` +
|
|
78
|
+
`data-name="${stem}" data-binding="${binding}">\n${src}\n</script>`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function layoutBindingNameForStem(stem) {
|
|
83
|
+
return "Slide" + stem.split("-").map((p) => (
|
|
84
|
+
p ? p[0].toUpperCase() + p.slice(1) : ""
|
|
85
|
+
)).join("");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function inlineSlide(file, index) {
|
|
89
|
+
const stem = path.basename(file, ".jsx");
|
|
90
|
+
const src = safeInline(stripImports(fs.readFileSync(file, "utf8")));
|
|
91
|
+
return (
|
|
92
|
+
`<script type="text/jsx-template" data-kind="slide" ` +
|
|
93
|
+
`data-index="${index}" data-name="${stem}">\n${src}\n</script>`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function copyDesignSystem(deckDir, skill) {
|
|
98
|
+
// Copy CSS + assets into the deck directory so the HTML is portable.
|
|
99
|
+
// Layering: brand assets first (logos, shared decor from the design
|
|
100
|
+
// bundle), then the active skill's own assets on top — so a skill
|
|
101
|
+
// can override a brand file by shipping its own copy with the same name.
|
|
102
|
+
// For create-slides itself, skill.assetsDir === skill.brand.assetsDir, so
|
|
103
|
+
// this is a single copy pass.
|
|
104
|
+
const outAssets = path.join(deckDir, "assets");
|
|
105
|
+
fs.mkdirSync(outAssets, { recursive: true });
|
|
106
|
+
const assetSources =
|
|
107
|
+
skill.brand.assetsDir === skill.assetsDir
|
|
108
|
+
? [skill.assetsDir]
|
|
109
|
+
: [skill.brand.assetsDir, skill.assetsDir];
|
|
110
|
+
for (const src of assetSources) {
|
|
111
|
+
if (!fs.existsSync(src)) continue;
|
|
112
|
+
for (const entry of fs.readdirSync(src)) {
|
|
113
|
+
const p = path.join(src, entry);
|
|
114
|
+
if (fs.statSync(p).isFile()) {
|
|
115
|
+
fs.copyFileSync(p, path.join(outAssets, entry));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
fs.copyFileSync(skill.cssFile, path.join(deckDir, "corporate-design.css"));
|
|
120
|
+
|
|
121
|
+
// layouts.css references url("../assets/...") relative to the in-repo
|
|
122
|
+
// layouts dir. The build copies it flat into the deck next to assets/,
|
|
123
|
+
// so rewrite the URLs to "./assets/...".
|
|
124
|
+
//
|
|
125
|
+
// Layering: brand layouts.css first (lower in the cascade), then the
|
|
126
|
+
// active skill's layouts.css. Skill rules sharing a selector win.
|
|
127
|
+
const cssSources =
|
|
128
|
+
skill.brand.layoutsCssFile === path.join(skill.layoutsDir, "layouts.css")
|
|
129
|
+
? [path.join(skill.layoutsDir, "layouts.css")]
|
|
130
|
+
: [skill.brand.layoutsCssFile, path.join(skill.layoutsDir, "layouts.css")];
|
|
131
|
+
const cssChunks = [];
|
|
132
|
+
for (const cssPath of cssSources) {
|
|
133
|
+
if (!fs.existsSync(cssPath)) continue;
|
|
134
|
+
let css = fs.readFileSync(cssPath, "utf8");
|
|
135
|
+
css = css.replace(/url\("\.\.\/assets\//g, 'url("./assets/');
|
|
136
|
+
css = css.replace(/url\('\.\.\/assets\//g, "url('./assets/");
|
|
137
|
+
css = css.replace(/url\(\.\.\/assets\//g, "url(./assets/");
|
|
138
|
+
cssChunks.push(css);
|
|
139
|
+
}
|
|
140
|
+
fs.writeFileSync(path.join(deckDir, "layouts.css"), cssChunks.join("\n\n"));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function loadOverrides(deckDir) {
|
|
144
|
+
const file = path.join(deckDir, "overrides.json");
|
|
145
|
+
if (!fs.existsSync(file)) return { version: 1, slides: {} };
|
|
146
|
+
let data;
|
|
147
|
+
try {
|
|
148
|
+
data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
149
|
+
} catch (err) {
|
|
150
|
+
throw new Error(`Invalid overrides.json: ${err.message}`);
|
|
151
|
+
}
|
|
152
|
+
if (!data || typeof data !== "object") return { version: 1, slides: {} };
|
|
153
|
+
if (data.version === undefined) data.version = 1;
|
|
154
|
+
if (data.slides === undefined) data.slides = {};
|
|
155
|
+
return data;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Read the deck's SVG assets (after copyDesignSystem has populated assets/) so
|
|
159
|
+
// the preview can inline them. An <img src="x.svg"> can't inherit `color`, so a
|
|
160
|
+
// CSS-tinted icon looks wrong in the HTML preview yet right in the .pptx render
|
|
161
|
+
// (which substitutes currentColor) — the source-mismatch that sends agents off
|
|
162
|
+
// to inspect SVGs. Inlining the SVG at runtime makes the preview honour `color`
|
|
163
|
+
// exactly as the render does. Keyed three ways so any of the src spellings an
|
|
164
|
+
// author uses ("./assets/x.svg", "assets/x.svg", "x.svg") resolves.
|
|
165
|
+
function collectSvgAssets(deckDir) {
|
|
166
|
+
const assetsDir = path.join(deckDir, "assets");
|
|
167
|
+
const map = {};
|
|
168
|
+
let entries;
|
|
169
|
+
try {
|
|
170
|
+
entries = fs.readdirSync(assetsDir);
|
|
171
|
+
} catch {
|
|
172
|
+
return map;
|
|
173
|
+
}
|
|
174
|
+
for (const name of entries) {
|
|
175
|
+
if (!name.toLowerCase().endsWith(".svg")) continue;
|
|
176
|
+
let markup;
|
|
177
|
+
try {
|
|
178
|
+
markup = fs.readFileSync(path.join(assetsDir, name), "utf8");
|
|
179
|
+
} catch {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
map[`./assets/${name}`] = markup;
|
|
183
|
+
map[`assets/${name}`] = markup;
|
|
184
|
+
map[name] = markup;
|
|
185
|
+
}
|
|
186
|
+
return map;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const HTML_TEMPLATE = `<!doctype html>
|
|
190
|
+
<html lang="en">
|
|
191
|
+
<head>
|
|
192
|
+
<meta charset="utf-8" />
|
|
193
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
194
|
+
<title>__TITLE__</title>
|
|
195
|
+
<!-- Arimo: open-source Arial-metric-compatible font. We alias it to
|
|
196
|
+
"Arial" so master text pinned to Arial gets a font with the same
|
|
197
|
+
horizontal extents PowerPoint/LibreOffice use, instead of macOS
|
|
198
|
+
Chrome's slightly wider system Arial that pushes long strings
|
|
199
|
+
(addresses, single-line headers) past their bounding boxes. -->
|
|
200
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
201
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
202
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Arimo:ital,wght@0,400..700;1,400..700&display=swap" />
|
|
203
|
+
<link rel="stylesheet" href="./corporate-design.css" />
|
|
204
|
+
<link rel="stylesheet" href="./layouts.css" />
|
|
205
|
+
<style>
|
|
206
|
+
/* Substitute Arial with Arimo (a metric clone) so wrap behaviour
|
|
207
|
+
matches PowerPoint's rendering. Local Arial wins only if Arimo
|
|
208
|
+
hasn't loaded yet. */
|
|
209
|
+
@font-face {
|
|
210
|
+
font-family: "Arial";
|
|
211
|
+
src: local("Arimo"), local("Arial");
|
|
212
|
+
font-weight: 400;
|
|
213
|
+
font-style: normal;
|
|
214
|
+
}
|
|
215
|
+
@font-face {
|
|
216
|
+
font-family: "Arial";
|
|
217
|
+
src: local("Arimo Bold"), local("Arial Bold");
|
|
218
|
+
font-weight: 700;
|
|
219
|
+
font-style: normal;
|
|
220
|
+
}
|
|
221
|
+
@font-face {
|
|
222
|
+
font-family: "Arial";
|
|
223
|
+
src: local("Arimo Italic"), local("Arial Italic");
|
|
224
|
+
font-weight: 400;
|
|
225
|
+
font-style: italic;
|
|
226
|
+
}
|
|
227
|
+
@font-face {
|
|
228
|
+
font-family: "Arial";
|
|
229
|
+
src: local("Arimo Bold Italic"), local("Arial Bold Italic");
|
|
230
|
+
font-weight: 700;
|
|
231
|
+
font-style: italic;
|
|
232
|
+
}
|
|
233
|
+
</style>
|
|
234
|
+
<style>
|
|
235
|
+
html, body { margin: 0; padding: 0; height: 100%; background: #0a0a0a; overflow: hidden; }
|
|
236
|
+
.deck {
|
|
237
|
+
position: fixed;
|
|
238
|
+
inset: 0;
|
|
239
|
+
display: flex;
|
|
240
|
+
align-items: center;
|
|
241
|
+
justify-content: center;
|
|
242
|
+
}
|
|
243
|
+
.deck__stage {
|
|
244
|
+
width: 1280px;
|
|
245
|
+
height: 720px;
|
|
246
|
+
transform-origin: center center;
|
|
247
|
+
background: var(--bg-1);
|
|
248
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.35);
|
|
249
|
+
}
|
|
250
|
+
.deck__nav {
|
|
251
|
+
position: fixed;
|
|
252
|
+
bottom: 16px;
|
|
253
|
+
right: 16px;
|
|
254
|
+
padding: 6px 12px;
|
|
255
|
+
border-radius: 999px;
|
|
256
|
+
color: rgba(255,255,255,0.65);
|
|
257
|
+
background: rgba(255,255,255,0.08);
|
|
258
|
+
backdrop-filter: blur(6px);
|
|
259
|
+
font-family: var(--font-sans, sans-serif);
|
|
260
|
+
font-size: 12px;
|
|
261
|
+
letter-spacing: 0.02em;
|
|
262
|
+
user-select: none;
|
|
263
|
+
}
|
|
264
|
+
.deck__error {
|
|
265
|
+
padding: 32px;
|
|
266
|
+
color: #ff6b6b;
|
|
267
|
+
background: #1a0000;
|
|
268
|
+
font-family: var(--font-mono, monospace);
|
|
269
|
+
font-size: 14px;
|
|
270
|
+
white-space: pre-wrap;
|
|
271
|
+
}
|
|
272
|
+
/* Export mode (?export=1) — used by the PPTX exporter's headless Chrome
|
|
273
|
+
so it captures a clean 1280x720 stage with no letterbox/nav/scaling. */
|
|
274
|
+
body.deck--export { background: #ffffff !important; overflow: hidden; }
|
|
275
|
+
body.deck--export .deck { position: static; display: block; inset: auto; }
|
|
276
|
+
body.deck--export .deck__stage {
|
|
277
|
+
transform: none !important;
|
|
278
|
+
box-shadow: none !important;
|
|
279
|
+
margin: 0;
|
|
280
|
+
position: static;
|
|
281
|
+
}
|
|
282
|
+
body.deck--export .deck__nav { display: none !important; }
|
|
283
|
+
/* Print mode (?print=1) — stacks every slide as its own page so the
|
|
284
|
+
browser's Print / Cmd-P renders one slide per page (proofing). */
|
|
285
|
+
body.deck--print {
|
|
286
|
+
background: #ffffff !important;
|
|
287
|
+
overflow: visible !important;
|
|
288
|
+
height: auto !important;
|
|
289
|
+
}
|
|
290
|
+
body.deck--print .deck,
|
|
291
|
+
body.deck--print .deck__nav { display: none !important; }
|
|
292
|
+
.print-deck { background: #ffffff; }
|
|
293
|
+
.print-page {
|
|
294
|
+
width: 1280px;
|
|
295
|
+
height: 720px;
|
|
296
|
+
position: relative;
|
|
297
|
+
overflow: hidden;
|
|
298
|
+
background: var(--bg-1);
|
|
299
|
+
page-break-after: always;
|
|
300
|
+
break-after: page;
|
|
301
|
+
}
|
|
302
|
+
.print-page:last-child {
|
|
303
|
+
page-break-after: auto;
|
|
304
|
+
break-after: auto;
|
|
305
|
+
}
|
|
306
|
+
.print-page__error {
|
|
307
|
+
padding: 32px;
|
|
308
|
+
color: #ff6b6b;
|
|
309
|
+
background: #1a0000;
|
|
310
|
+
font-family: var(--font-mono, monospace);
|
|
311
|
+
font-size: 14px;
|
|
312
|
+
white-space: pre-wrap;
|
|
313
|
+
width: 100%;
|
|
314
|
+
height: 100%;
|
|
315
|
+
box-sizing: border-box;
|
|
316
|
+
}
|
|
317
|
+
@media print {
|
|
318
|
+
@page { size: 1280px 720px; margin: 0; }
|
|
319
|
+
html, body { background: #ffffff !important; }
|
|
320
|
+
}
|
|
321
|
+
</style>
|
|
322
|
+
</head>
|
|
323
|
+
<body>
|
|
324
|
+
<div class="deck">
|
|
325
|
+
<div class="deck__stage" id="stage"></div>
|
|
326
|
+
</div>
|
|
327
|
+
<div class="deck__nav" id="nav">loading…</div>
|
|
328
|
+
|
|
329
|
+
<!-- Layout sources (inlined from skills/create-slides/layouts/*.jsx).
|
|
330
|
+
Each one is evaluated once and exposed under its binding name
|
|
331
|
+
(SlideCover, SlideAgendaBlack, ...) so slides can reference it. -->
|
|
332
|
+
__LAYOUTS__
|
|
333
|
+
|
|
334
|
+
<!-- Slide sources (inlined from this deck's slides/ folder, filename order). -->
|
|
335
|
+
__SLIDES__
|
|
336
|
+
|
|
337
|
+
<!-- Runtime: React + Babel-standalone from CDN. -->
|
|
338
|
+
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
|
339
|
+
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
|
340
|
+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
341
|
+
<script>window.__SLIDE_EDITOR_OVERRIDES__ = __OVERRIDES__;</script>
|
|
342
|
+
<script>window.SVG_ASSETS = __SVG_ASSETS__;</script>
|
|
343
|
+
<script>
|
|
344
|
+
__RUNTIME_APPLIER__
|
|
345
|
+
</script>
|
|
346
|
+
|
|
347
|
+
<script>
|
|
348
|
+
(function () {
|
|
349
|
+
// Map kebab-case filename stem -> PascalCase layout binding name.
|
|
350
|
+
function layoutBindingName(stem) {
|
|
351
|
+
return "Slide" + stem.split("-").map(function (p) {
|
|
352
|
+
return p ? p[0].toUpperCase() + p.slice(1) : "";
|
|
353
|
+
}).join("");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function evalJsx(src, filename, extra) {
|
|
357
|
+
var result = Babel.transform(src, {
|
|
358
|
+
presets: [["react", { runtime: "classic" }]],
|
|
359
|
+
sourceType: "module",
|
|
360
|
+
filename: filename
|
|
361
|
+
});
|
|
362
|
+
var body = result.code.replace(
|
|
363
|
+
/export\\s+default\\s+/g,
|
|
364
|
+
"__module.default = "
|
|
365
|
+
);
|
|
366
|
+
var names = ["__module", "React"].concat(Object.keys(extra || {}));
|
|
367
|
+
var values = [{}, React].concat(Object.keys(extra || {}).map(function (k) {
|
|
368
|
+
return extra[k];
|
|
369
|
+
}));
|
|
370
|
+
var factory = new Function(
|
|
371
|
+
names.join(","),
|
|
372
|
+
body + "\\nreturn __module.default;"
|
|
373
|
+
);
|
|
374
|
+
return factory.apply(null, values);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Preview parity: replace <img src="*.svg"> whose file we inlined into
|
|
378
|
+
// window.SVG_ASSETS with the SVG markup itself, carrying the <img>'s box
|
|
379
|
+
// and color onto a wrapper so fill="currentColor" resolves — the same
|
|
380
|
+
// tint the .pptx renderer applies. Re-runs on every React render via a
|
|
381
|
+
// MutationObserver so navigating slides keeps icons inlined.
|
|
382
|
+
function smInlineSvgAssets() {
|
|
383
|
+
var ASSETS = (typeof window !== "undefined" && window.SVG_ASSETS) || null;
|
|
384
|
+
if (!ASSETS || !Object.keys(ASSETS).length) return;
|
|
385
|
+
|
|
386
|
+
function lookup(src) {
|
|
387
|
+
if (!src) return null;
|
|
388
|
+
var s = src.split("?")[0].split("#")[0];
|
|
389
|
+
var alt = s.indexOf("./") === 0 ? s.slice(2) : s;
|
|
390
|
+
var base = s.split("/").pop();
|
|
391
|
+
return ASSETS[s] || ASSETS[alt] || ASSETS[base] || null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function inlineWithin(root) {
|
|
395
|
+
if (!root || !root.querySelectorAll) return;
|
|
396
|
+
var imgs = root.querySelectorAll("img");
|
|
397
|
+
for (var i = 0; i < imgs.length; i++) {
|
|
398
|
+
var img = imgs[i];
|
|
399
|
+
if (img.getAttribute("data-sm-inlined") === "1") continue;
|
|
400
|
+
var markup = lookup(img.getAttribute("src"));
|
|
401
|
+
if (!markup) continue;
|
|
402
|
+
var span = document.createElement("span");
|
|
403
|
+
span.setAttribute("data-sm-inlined", "1");
|
|
404
|
+
if (img.className) span.className = img.className;
|
|
405
|
+
var styleAttr = img.getAttribute("style");
|
|
406
|
+
if (styleAttr) span.setAttribute("style", styleAttr);
|
|
407
|
+
// Pull the laid-out box + colour even when they came from CSS, not
|
|
408
|
+
// an inline style, so sizing holds and currentColor resolves.
|
|
409
|
+
var cs = window.getComputedStyle ? window.getComputedStyle(img) : null;
|
|
410
|
+
var hasW = !!span.style.width;
|
|
411
|
+
var hasH = !!span.style.height;
|
|
412
|
+
if (cs) {
|
|
413
|
+
if (!hasW && cs.width && cs.width !== "auto" && cs.width !== "0px") {
|
|
414
|
+
span.style.width = cs.width;
|
|
415
|
+
hasW = true;
|
|
416
|
+
}
|
|
417
|
+
if (!hasH && cs.height && cs.height !== "auto" && cs.height !== "0px") {
|
|
418
|
+
span.style.height = cs.height;
|
|
419
|
+
hasH = true;
|
|
420
|
+
}
|
|
421
|
+
if (!span.style.color && cs.color) span.style.color = cs.color;
|
|
422
|
+
}
|
|
423
|
+
if (!span.style.display || span.style.display === "inline") {
|
|
424
|
+
span.style.display = "inline-flex";
|
|
425
|
+
}
|
|
426
|
+
if (!span.style.alignItems) span.style.alignItems = "center";
|
|
427
|
+
if (!span.style.justifyContent) span.style.justifyContent = "center";
|
|
428
|
+
span.innerHTML = markup;
|
|
429
|
+
var svg = span.querySelector("svg");
|
|
430
|
+
if (svg) {
|
|
431
|
+
svg.style.display = "block";
|
|
432
|
+
if (hasW || hasH) {
|
|
433
|
+
svg.style.width = "100%";
|
|
434
|
+
svg.style.height = "100%";
|
|
435
|
+
svg.removeAttribute("width");
|
|
436
|
+
svg.removeAttribute("height");
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (img.parentNode) img.parentNode.replaceChild(span, img);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
var scheduled = false;
|
|
444
|
+
var raf =
|
|
445
|
+
window.requestAnimationFrame ||
|
|
446
|
+
function (cb) { return setTimeout(cb, 16); };
|
|
447
|
+
function schedule() {
|
|
448
|
+
if (scheduled) return;
|
|
449
|
+
scheduled = true;
|
|
450
|
+
raf(function () {
|
|
451
|
+
scheduled = false;
|
|
452
|
+
inlineWithin(document.body);
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
if (typeof MutationObserver !== "undefined") {
|
|
456
|
+
new MutationObserver(schedule).observe(document.body, {
|
|
457
|
+
childList: true,
|
|
458
|
+
subtree: true,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
schedule();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
var LAYOUTS = {};
|
|
465
|
+
document.querySelectorAll('script[data-kind="layout"]').forEach(function (s) {
|
|
466
|
+
var name = s.dataset.binding || layoutBindingName(s.dataset.name);
|
|
467
|
+
try {
|
|
468
|
+
LAYOUTS[name] = evalJsx(s.textContent, s.dataset.name + ".jsx", {});
|
|
469
|
+
window[name] = LAYOUTS[name];
|
|
470
|
+
} catch (err) {
|
|
471
|
+
console.error("Layout " + name + " failed to load:", err);
|
|
472
|
+
throw err;
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
var slides = [];
|
|
477
|
+
document.querySelectorAll('script[data-kind="slide"]').forEach(function (s) {
|
|
478
|
+
var idx = parseInt(s.dataset.index, 10);
|
|
479
|
+
var name = s.dataset.name;
|
|
480
|
+
try {
|
|
481
|
+
var Component = evalJsx(
|
|
482
|
+
s.textContent,
|
|
483
|
+
name + ".jsx",
|
|
484
|
+
LAYOUTS
|
|
485
|
+
);
|
|
486
|
+
slides[idx] = { name: name, Component: Component };
|
|
487
|
+
} catch (err) {
|
|
488
|
+
console.error("Slide " + name + " failed to load:", err);
|
|
489
|
+
slides[idx] = { name: name, error: err };
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
var stage = document.getElementById("stage");
|
|
494
|
+
var nav = document.getElementById("nav");
|
|
495
|
+
var params = new URLSearchParams(location.search);
|
|
496
|
+
var exportMode = params.get("export") === "1";
|
|
497
|
+
var printMode = params.get("print") === "1";
|
|
498
|
+
if (exportMode) document.body.classList.add("deck--export");
|
|
499
|
+
|
|
500
|
+
smInlineSvgAssets();
|
|
501
|
+
|
|
502
|
+
if (printMode) {
|
|
503
|
+
document.body.classList.add("deck--print");
|
|
504
|
+
var printDeck = document.createElement("div");
|
|
505
|
+
printDeck.className = "print-deck";
|
|
506
|
+
document.body.appendChild(printDeck);
|
|
507
|
+
slides.forEach(function (slide) {
|
|
508
|
+
var page = document.createElement("div");
|
|
509
|
+
page.className = "print-page";
|
|
510
|
+
printDeck.appendChild(page);
|
|
511
|
+
if (slide.error) {
|
|
512
|
+
var errEl = document.createElement("div");
|
|
513
|
+
errEl.className = "print-page__error";
|
|
514
|
+
errEl.textContent = slide.name + ".jsx failed:\\n" + slide.error.message;
|
|
515
|
+
page.appendChild(errEl);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
ReactDOM.createRoot(page).render(
|
|
519
|
+
React.createElement(slide.Component, { assetsPath: "./assets" })
|
|
520
|
+
);
|
|
521
|
+
});
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
var startIndex = parseInt(params.get("slide") || "0", 10);
|
|
526
|
+
if (isNaN(startIndex) || startIndex < 0) startIndex = 0;
|
|
527
|
+
if (startIndex >= slides.length) startIndex = slides.length - 1;
|
|
528
|
+
var current = startIndex;
|
|
529
|
+
var root = ReactDOM.createRoot(stage);
|
|
530
|
+
|
|
531
|
+
function render() {
|
|
532
|
+
var slide = slides[current];
|
|
533
|
+
if (!slide) return;
|
|
534
|
+
nav.textContent =
|
|
535
|
+
(current + 1) + " / " + slides.length + " · " + slide.name;
|
|
536
|
+
if (slide.error) {
|
|
537
|
+
root.render(
|
|
538
|
+
React.createElement(
|
|
539
|
+
"div",
|
|
540
|
+
{ className: "deck__error" },
|
|
541
|
+
slide.name + ".jsx failed:\\n" + slide.error.message
|
|
542
|
+
)
|
|
543
|
+
);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
root.render(
|
|
547
|
+
React.createElement(slide.Component, { assetsPath: "./assets" })
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function fit() {
|
|
552
|
+
if (exportMode) {
|
|
553
|
+
stage.style.transform = "none";
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
var sx = window.innerWidth / 1280;
|
|
557
|
+
var sy = window.innerHeight / 720;
|
|
558
|
+
var s = Math.min(sx, sy) * 0.95;
|
|
559
|
+
stage.style.transform = "scale(" + s + ")";
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
window.addEventListener("resize", fit);
|
|
563
|
+
fit();
|
|
564
|
+
render();
|
|
565
|
+
|
|
566
|
+
document.addEventListener("keydown", function (e) {
|
|
567
|
+
var k = e.key;
|
|
568
|
+
if (k === "ArrowRight" || k === "PageDown" || k === " ") {
|
|
569
|
+
current = Math.min(current + 1, slides.length - 1);
|
|
570
|
+
render();
|
|
571
|
+
e.preventDefault();
|
|
572
|
+
} else if (k === "ArrowLeft" || k === "PageUp") {
|
|
573
|
+
current = Math.max(current - 1, 0);
|
|
574
|
+
render();
|
|
575
|
+
e.preventDefault();
|
|
576
|
+
} else if (k === "Home") {
|
|
577
|
+
current = 0;
|
|
578
|
+
render();
|
|
579
|
+
} else if (k === "End") {
|
|
580
|
+
current = slides.length - 1;
|
|
581
|
+
render();
|
|
582
|
+
} else if (k === "f" || k === "F") {
|
|
583
|
+
if (document.fullscreenElement) document.exitFullscreen();
|
|
584
|
+
else document.documentElement.requestFullscreen();
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
})();
|
|
588
|
+
</script>
|
|
589
|
+
</body>
|
|
590
|
+
</html>
|
|
591
|
+
`;
|
|
592
|
+
|
|
593
|
+
export function buildDeck(deckDir, opts = {}) {
|
|
594
|
+
const skill = resolveSkillForDeck(deckDir);
|
|
595
|
+
const slidesDir = path.join(deckDir, "slides");
|
|
596
|
+
if (!fs.existsSync(slidesDir) || !fs.statSync(slidesDir).isDirectory()) {
|
|
597
|
+
throw new Error(`Expected a 'slides/' folder in ${deckDir}`);
|
|
598
|
+
}
|
|
599
|
+
if (skill.mode === "fill-template" && !opts.internalProposalFreeformRender) {
|
|
600
|
+
// Proposal decks are PPTX-native. The exporter may render freeform JSX
|
|
601
|
+
// inserts internally, but `slidemind build` should not create a lossy
|
|
602
|
+
// presentation.html artifact for proposal decks.
|
|
603
|
+
throw new Error(
|
|
604
|
+
`Deck ${path.basename(deckDir)} uses skill '${skill.skillName}' (mode ` +
|
|
605
|
+
`fill-template); it has no HTML preview. Run 'slidemind build-pptx ` +
|
|
606
|
+
`${path.basename(deckDir)}' to produce the editable .pptx.`,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
const slideFiles = fs
|
|
610
|
+
.readdirSync(slidesDir)
|
|
611
|
+
.filter((f) => f.endsWith(".jsx"))
|
|
612
|
+
.sort()
|
|
613
|
+
.map((f) => path.join(slidesDir, f));
|
|
614
|
+
if (slideFiles.length === 0) {
|
|
615
|
+
throw new Error(`No .jsx files in ${slidesDir}`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Layout discovery: brand layouts first, then the active skill's layouts
|
|
619
|
+
// override on basename collision — so a sibling skill could reach for the
|
|
620
|
+
// brand's freeform workhorse (SlideContentLightBlank) without duplicating the
|
|
621
|
+
// JSX. With only the brand skill present, brand === skill and the dedup pass
|
|
622
|
+
// is a no-op.
|
|
623
|
+
const layoutDirs =
|
|
624
|
+
skill.brand.layoutsDir === skill.layoutsDir
|
|
625
|
+
? [skill.layoutsDir]
|
|
626
|
+
: [skill.brand.layoutsDir, skill.layoutsDir];
|
|
627
|
+
const layoutByStem = new Map();
|
|
628
|
+
for (const dir of layoutDirs) {
|
|
629
|
+
if (!fs.existsSync(dir)) continue;
|
|
630
|
+
for (const f of fs.readdirSync(dir).filter((x) => x.endsWith(".jsx"))) {
|
|
631
|
+
layoutByStem.set(path.basename(f, ".jsx"), path.join(dir, f));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const layoutFiles = Array.from(layoutByStem.values()).sort();
|
|
635
|
+
if (layoutFiles.length === 0) {
|
|
636
|
+
throw new Error(`No layouts in ${skill.layoutsDir}`);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Merge the brand primitive registry with the active skill's registry so a
|
|
640
|
+
// sibling skill's aliases and the brand's freeform layouts can coexist in one
|
|
641
|
+
// deck. (With only the brand skill present this is a single registry.)
|
|
642
|
+
lintSlidesOrThrow(slideFiles, {
|
|
643
|
+
primitivesPath: skill.primitivesPath,
|
|
644
|
+
brandPrimitivesPath: skill.brand.primitivesPath,
|
|
645
|
+
// Derive valid layout component names from the materialized layouts/*.jsx so
|
|
646
|
+
// the lint recognizes the bundle's layouts without a primitives.json
|
|
647
|
+
// `layouts` list (the materialized bundle doesn't ship one).
|
|
648
|
+
layoutsDirs: layoutDirs,
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
copyDesignSystem(deckDir, skill);
|
|
652
|
+
|
|
653
|
+
const layoutsBlock = layoutFiles.map(inlineLayout).join("\n");
|
|
654
|
+
const slidesBlock = slideFiles
|
|
655
|
+
.map((file, i) => inlineSlide(file, i))
|
|
656
|
+
.join("\n");
|
|
657
|
+
|
|
658
|
+
const svgAssets = collectSvgAssets(deckDir);
|
|
659
|
+
|
|
660
|
+
const html = HTML_TEMPLATE.replace("__TITLE__", path.basename(deckDir))
|
|
661
|
+
.replace("__LAYOUTS__", layoutsBlock)
|
|
662
|
+
.replace("__SLIDES__", slidesBlock)
|
|
663
|
+
.replace("__OVERRIDES__", safeInlineJson(loadOverrides(deckDir)))
|
|
664
|
+
.replace("__SVG_ASSETS__", safeInlineJson(svgAssets))
|
|
665
|
+
.replace("__RUNTIME_APPLIER__", safeInline(fs.readFileSync(RUNTIME_APPLIER, "utf8")));
|
|
666
|
+
|
|
667
|
+
const out = path.join(deckDir, "presentation.html");
|
|
668
|
+
fs.writeFileSync(out, html);
|
|
669
|
+
return { out, slideCount: slideFiles.length };
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function openInBrowser(file) {
|
|
673
|
+
const opener =
|
|
674
|
+
process.platform === "darwin"
|
|
675
|
+
? { cmd: "open", args: [file] }
|
|
676
|
+
: process.platform === "win32"
|
|
677
|
+
? { cmd: "cmd", args: ["/c", "start", "", file] }
|
|
678
|
+
: { cmd: "xdg-open", args: [file] };
|
|
679
|
+
spawn(opener.cmd, opener.args, { stdio: "ignore", detached: true }).unref();
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function parseArgs(argv) {
|
|
683
|
+
const args = { deck: null, open: false };
|
|
684
|
+
for (let i = 0; i < argv.length; i++) {
|
|
685
|
+
const a = argv[i];
|
|
686
|
+
if (a === "--deck" && i + 1 < argv.length) {
|
|
687
|
+
args.deck = argv[++i];
|
|
688
|
+
} else if (a === "--open") {
|
|
689
|
+
args.open = true;
|
|
690
|
+
} else if (a === "-h" || a === "--help") {
|
|
691
|
+
process.stdout.write(
|
|
692
|
+
"Usage: slidemind build <deck> [--open]\n" +
|
|
693
|
+
" node src/builders/build.mjs --deck <deck> [--open]\n",
|
|
694
|
+
);
|
|
695
|
+
process.exit(0);
|
|
696
|
+
} else {
|
|
697
|
+
process.stderr.write(`build: unexpected argument '${a}'\n`);
|
|
698
|
+
process.exit(2);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (!args.deck) {
|
|
702
|
+
process.stderr.write("build: --deck is required\n");
|
|
703
|
+
process.exit(2);
|
|
704
|
+
}
|
|
705
|
+
return args;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function main() {
|
|
709
|
+
const args = parseArgs(process.argv.slice(2));
|
|
710
|
+
const deckDir = path.resolve(args.deck);
|
|
711
|
+
const { out, slideCount } = buildDeck(deckDir);
|
|
712
|
+
const rel = path.relative(process.cwd(), out) || out;
|
|
713
|
+
process.stdout.write(`Built ${rel} (${slideCount} slides)\n`);
|
|
714
|
+
if (args.open) openInBrowser(out);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
718
|
+
try {
|
|
719
|
+
main();
|
|
720
|
+
} catch (err) {
|
|
721
|
+
process.stderr.write(`build: ${err.message || err}\n`);
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
}
|