slidemind 0.1.6 → 0.1.8
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 -7
- package/CLAUDE.md +21 -15
- package/README.md +21 -10
- package/bin/slidemind.mjs +836 -35
- package/package.json +2 -5
- package/skills/create-slides/SKILL.md +85 -60
- package/skills/search/SKILL.md +27 -27
- package/src/bridge/build-and-push.mjs +166 -55
- package/src/bridge/bundle.mjs +49 -7
- package/src/bridge/capture.mjs +2 -2
- package/src/bridge/client.mjs +374 -44
- package/src/bridge/engine-download.mjs +89 -0
- package/src/{mcp → bridge}/recolor.mjs +10 -9
- package/src/builders/build.mjs +261 -23
- package/src/builders/gate-server.mjs +223 -0
- package/src/builders/lint.mjs +2 -2
- package/src/builders/plan-md.mjs +216 -0
- package/src/builders/plan-overlay.js +747 -0
- package/src/builders/plan.mjs +575 -0
- package/src/builders/resolve-skill.mjs +40 -5
- package/src/builders/review-overlay.js +172 -16
- package/src/builders/review.mjs +139 -80
- package/src/installers/claude.mjs +10 -17
- package/src/installers/codex.mjs +14 -77
- package/src/installers/opencode.mjs +10 -18
- package/src/lib/reused-slides.mjs +2 -4
- package/src/mcp/server.mjs +0 -958
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Engine-result download logic behind the `slidemind download-result` CLI
|
|
2
|
+
// command (bin/slidemind.mjs). Fetches a chosen image/icon search result over
|
|
3
|
+
// the bridge and writes it into a deck's assets/, optionally baking in a
|
|
4
|
+
// colour, and classifies how it recolours.
|
|
5
|
+
//
|
|
6
|
+
// It takes an already-loaded bridge `client` rather than importing it, so the
|
|
7
|
+
// caller controls how the (heavier) client module is loaded.
|
|
8
|
+
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
import { describeRecolor } from "./recolor.mjs";
|
|
13
|
+
|
|
14
|
+
const MIME_EXT = {
|
|
15
|
+
"image/svg+xml": "svg",
|
|
16
|
+
"image/png": "png",
|
|
17
|
+
"image/jpeg": "jpg",
|
|
18
|
+
"image/jpg": "jpg",
|
|
19
|
+
"image/webp": "webp",
|
|
20
|
+
"image/gif": "gif",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Derive a usable file extension from an engine result's MIME type. The brand
|
|
24
|
+
// engine can return any image Content-Type (image/x-icon, image/bmp,
|
|
25
|
+
// image/avif, …) and may carry a `;charset=` suffix, so an exact-match table
|
|
26
|
+
// alone would fall back to a useless `.bin` that won't render in an <img>.
|
|
27
|
+
// Strip params, then fall back to the MIME subtype itself for any `image/*`.
|
|
28
|
+
export function extFromMime(mimeType) {
|
|
29
|
+
const mt = String(mimeType || "")
|
|
30
|
+
.split(";")[0]
|
|
31
|
+
.trim()
|
|
32
|
+
.toLowerCase();
|
|
33
|
+
if (MIME_EXT[mt]) return MIME_EXT[mt];
|
|
34
|
+
const m = /^image\/(?:x-)?([a-z0-9.+-]+)$/.exec(mt);
|
|
35
|
+
if (m) {
|
|
36
|
+
const sub = m[1].replace(/\+xml$/, "");
|
|
37
|
+
if (sub === "svg") return "svg";
|
|
38
|
+
if (sub === "jpeg") return "jpg";
|
|
39
|
+
if (sub === "icon") return "ico";
|
|
40
|
+
return sub; // png, gif, webp, bmp, avif, tiff, heic, …
|
|
41
|
+
}
|
|
42
|
+
return "bin";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Download one chosen engine result via the bridge and write it into assetsDir.
|
|
46
|
+
// Returns { selection, path, reference, mimeType, recolor }. Throws on failure
|
|
47
|
+
// so callers can Promise.allSettled a batch without one bad selection sinking
|
|
48
|
+
// the rest.
|
|
49
|
+
export async function downloadOneResult({
|
|
50
|
+
client,
|
|
51
|
+
presId,
|
|
52
|
+
backendURL = null,
|
|
53
|
+
searchId,
|
|
54
|
+
assetsDir,
|
|
55
|
+
selection,
|
|
56
|
+
iconColor,
|
|
57
|
+
}) {
|
|
58
|
+
const res = await client.engineDownload({
|
|
59
|
+
presId,
|
|
60
|
+
backendURL,
|
|
61
|
+
searchId,
|
|
62
|
+
selection,
|
|
63
|
+
iconColor: iconColor ?? null,
|
|
64
|
+
});
|
|
65
|
+
if (!res?.ok) {
|
|
66
|
+
throw new Error(res?.error ?? "download failed");
|
|
67
|
+
}
|
|
68
|
+
const ext = extFromMime(res.mimeType);
|
|
69
|
+
const filename = `engine-${searchId}-${selection}.${ext}`;
|
|
70
|
+
const target = path.join(assetsDir, filename);
|
|
71
|
+
if (res.svgMarkup && ext === "svg") {
|
|
72
|
+
// The (possibly recoloured) SVG markup is the canonical asset.
|
|
73
|
+
fs.writeFileSync(target, res.svgMarkup, "utf8");
|
|
74
|
+
} else if (typeof res.base64 === "string" && res.base64.length > 0) {
|
|
75
|
+
fs.writeFileSync(target, Buffer.from(res.base64, "base64"));
|
|
76
|
+
} else if (res.svgMarkup) {
|
|
77
|
+
// SVG bytes arrived only as markup (mimeType lacked an svg hint).
|
|
78
|
+
fs.writeFileSync(target, res.svgMarkup, "utf8");
|
|
79
|
+
} else {
|
|
80
|
+
throw new Error("engine result carried neither base64 bytes nor svgMarkup");
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
selection,
|
|
84
|
+
path: target,
|
|
85
|
+
reference: `./assets/${filename}`,
|
|
86
|
+
mimeType: res.mimeType,
|
|
87
|
+
recolor: describeRecolor(res.svgMarkup ?? null, res.mimeType, iconColor ?? null),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -2,22 +2,23 @@
|
|
|
2
2
|
// open the SVG to find out (the exact step that was wasting time: "let me
|
|
3
3
|
// inspect two SVGs to confirm how recolouring works").
|
|
4
4
|
//
|
|
5
|
-
// The
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// The download path already has the chosen result's bytes in hand (the bridge
|
|
6
|
+
// returns `svgMarkup` for SVG results), so we can classify it once and report
|
|
7
|
+
// the answer instead of leaving the agent to guess. Shared by the
|
|
8
|
+
// `slidemind download-result` CLI command (see src/bridge/engine-download.mjs).
|
|
8
9
|
//
|
|
9
10
|
// mode:
|
|
10
11
|
// "currentColor" — paints with fill/stroke="currentColor". A CSS `color`
|
|
11
12
|
// on the <img> tints it live in BOTH the HTML preview
|
|
12
13
|
// (which inlines the SVG, see src/builders/build.mjs) and
|
|
13
14
|
// the .pptx render (which substitutes currentColor). You
|
|
14
|
-
// can also bake a fixed colour in with
|
|
15
|
+
// can also bake a fixed colour in with --icon-color.
|
|
15
16
|
// "mono" — a single hard-coded paint colour (or the SVG default,
|
|
16
17
|
// black) and no currentColor. CSS `color` will NOT touch
|
|
17
|
-
// it — recolour by re-downloading with
|
|
18
|
+
// it — recolour by re-downloading with --icon-color, which
|
|
18
19
|
// makes the bridge rewrite the fill.
|
|
19
20
|
// "multicolor" — several distinct paint colours. Leave it as-is; passing
|
|
20
|
-
//
|
|
21
|
+
// --icon-color would flatten a deliberately multi-tone glyph.
|
|
21
22
|
// "raster" — PNG/JPEG/etc. Colour is not applicable.
|
|
22
23
|
|
|
23
24
|
function isSvg(mimeType, svgMarkup) {
|
|
@@ -92,13 +93,13 @@ export function describeRecolor(svgMarkup, mimeType, appliedColor = null) {
|
|
|
92
93
|
} else if (mode === "currentColor") {
|
|
93
94
|
note =
|
|
94
95
|
"Tintable: set CSS `color` on the <img> (honoured in both the HTML " +
|
|
95
|
-
"preview and the .pptx render), or pass
|
|
96
|
+
"preview and the .pptx render), or pass --icon-color to bake a fixed colour in.";
|
|
96
97
|
} else if (mode === "mono") {
|
|
97
98
|
note =
|
|
98
99
|
"Single-colour glyph: CSS `color` won't tint it — re-download with " +
|
|
99
|
-
"
|
|
100
|
+
"--icon-color (a hex resolved from a corporate-design.css token) to recolour.";
|
|
100
101
|
} else {
|
|
101
|
-
note = "Multi-colour glyph: leave as-is;
|
|
102
|
+
note = "Multi-colour glyph: leave as-is; --icon-color would flatten it.";
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
return {
|
package/src/builders/build.mjs
CHANGED
|
@@ -206,6 +206,27 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
206
206
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Arimo:ital,wght@0,400..700;1,400..700&display=swap" />
|
|
207
207
|
<link rel="stylesheet" href="./corporate-design.css" />
|
|
208
208
|
<link rel="stylesheet" href="./layouts.css" />
|
|
209
|
+
<style>
|
|
210
|
+
/* Compatibility aliases for decks/slides authored against the older
|
|
211
|
+
statworx token names. New materialized bundles expose semantic tokens
|
|
212
|
+
(--fg-*, --bg-*, --border-*); existing slide JSX may still refer
|
|
213
|
+
to --sw-*. Keep those declarations valid in the HTML preview. */
|
|
214
|
+
:root {
|
|
215
|
+
--sw-black: var(--fg-1, #000000);
|
|
216
|
+
--sw-white: var(--bg-1, #ffffff);
|
|
217
|
+
--sw-ink: var(--fg-2, #283440);
|
|
218
|
+
--sw-slate: var(--fg-3, #6C7D8C);
|
|
219
|
+
--sw-gray: var(--fg-4, #B6BDCC);
|
|
220
|
+
--sw-fog: var(--bg-2, #EBF0F2);
|
|
221
|
+
--sw-ice-blue: var(--bg-3, #E6E6FF);
|
|
222
|
+
--sw-tech-blue: var(--fg-accent, #0000FF);
|
|
223
|
+
--sw-tech-blue-10: #E6E6FF;
|
|
224
|
+
--sw-tech-blue-20: #CCCCFF;
|
|
225
|
+
--sw-tech-blue-40: #9999FF;
|
|
226
|
+
--sw-tech-blue-80: #3333FF;
|
|
227
|
+
--sw-magenta: #E6007E;
|
|
228
|
+
}
|
|
229
|
+
</style>
|
|
209
230
|
<style>
|
|
210
231
|
/* Substitute Arial with Arimo (a metric clone) so wrap behaviour
|
|
211
232
|
matches PowerPoint's rendering. Local Arial wins only if Arimo
|
|
@@ -237,9 +258,18 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
237
258
|
</style>
|
|
238
259
|
<style>
|
|
239
260
|
html, body { margin: 0; padding: 0; height: 100%; background: #0a0a0a; overflow: hidden; }
|
|
261
|
+
:root {
|
|
262
|
+
--deck-rail-w: 220px;
|
|
263
|
+
--deck-chrome-h: 62px;
|
|
264
|
+
--deck-chrome-bg: #111113;
|
|
265
|
+
--deck-chrome-bg-2: #161618;
|
|
266
|
+
--deck-chrome-border: #2b2b30;
|
|
267
|
+
--deck-chrome-fg: #ededed;
|
|
268
|
+
--deck-chrome-dim: #9a9aa0;
|
|
269
|
+
}
|
|
240
270
|
.deck {
|
|
241
271
|
position: fixed;
|
|
242
|
-
inset: 0;
|
|
272
|
+
inset: 0 0 0 var(--deck-rail-w);
|
|
243
273
|
display: flex;
|
|
244
274
|
align-items: center;
|
|
245
275
|
justify-content: center;
|
|
@@ -265,6 +295,89 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
265
295
|
letter-spacing: 0.02em;
|
|
266
296
|
user-select: none;
|
|
267
297
|
}
|
|
298
|
+
.deck__rail {
|
|
299
|
+
position: fixed;
|
|
300
|
+
top: 0;
|
|
301
|
+
left: 0;
|
|
302
|
+
bottom: 0;
|
|
303
|
+
width: var(--deck-rail-w);
|
|
304
|
+
z-index: 2147483590;
|
|
305
|
+
display: flex;
|
|
306
|
+
flex-direction: column;
|
|
307
|
+
background: var(--deck-chrome-bg);
|
|
308
|
+
border-right: 1px solid var(--deck-chrome-border);
|
|
309
|
+
color: var(--deck-chrome-fg);
|
|
310
|
+
font-family: var(--font-sans, sans-serif);
|
|
311
|
+
}
|
|
312
|
+
.deck__thumbs {
|
|
313
|
+
flex: 1 1 auto;
|
|
314
|
+
overflow-y: auto;
|
|
315
|
+
padding: 12px 14px 18px;
|
|
316
|
+
scrollbar-width: none;
|
|
317
|
+
-ms-overflow-style: none;
|
|
318
|
+
}
|
|
319
|
+
.deck__thumbs::-webkit-scrollbar {
|
|
320
|
+
width: 0;
|
|
321
|
+
height: 0;
|
|
322
|
+
display: none;
|
|
323
|
+
}
|
|
324
|
+
.deck__thumb {
|
|
325
|
+
width: 100%;
|
|
326
|
+
margin: 0 0 12px;
|
|
327
|
+
padding: 8px;
|
|
328
|
+
border: 1px solid var(--deck-chrome-border);
|
|
329
|
+
border-radius: 8px;
|
|
330
|
+
background: rgba(255,255,255,0.025);
|
|
331
|
+
color: inherit;
|
|
332
|
+
cursor: pointer;
|
|
333
|
+
text-align: left;
|
|
334
|
+
transition: border-color 120ms ease, background 120ms ease, box-shadow 120ms ease;
|
|
335
|
+
}
|
|
336
|
+
.deck__thumb:hover {
|
|
337
|
+
background: rgba(255,255,255,0.06);
|
|
338
|
+
border-color: #3a3a40;
|
|
339
|
+
}
|
|
340
|
+
.deck__thumb.is-active {
|
|
341
|
+
border-color: rgba(237,237,237,0.7);
|
|
342
|
+
background: rgba(255,255,255,0.085);
|
|
343
|
+
box-shadow: 0 0 0 2px rgba(237,237,237,0.08);
|
|
344
|
+
}
|
|
345
|
+
.deck__thumb-preview {
|
|
346
|
+
width: 176px;
|
|
347
|
+
height: 99px;
|
|
348
|
+
overflow: hidden;
|
|
349
|
+
background: #fff;
|
|
350
|
+
box-shadow: 0 4px 14px rgba(0,0,0,0.28);
|
|
351
|
+
pointer-events: none;
|
|
352
|
+
}
|
|
353
|
+
.deck__thumb-frame {
|
|
354
|
+
display: block;
|
|
355
|
+
width: 176px;
|
|
356
|
+
height: 99px;
|
|
357
|
+
border: 0;
|
|
358
|
+
background: #fff;
|
|
359
|
+
pointer-events: none;
|
|
360
|
+
}
|
|
361
|
+
.deck__thumb-meta {
|
|
362
|
+
display: flex;
|
|
363
|
+
align-items: baseline;
|
|
364
|
+
gap: 7px;
|
|
365
|
+
margin-top: 7px;
|
|
366
|
+
min-width: 0;
|
|
367
|
+
font-size: 11px;
|
|
368
|
+
line-height: 1.25;
|
|
369
|
+
}
|
|
370
|
+
.deck__thumb-number {
|
|
371
|
+
flex: 0 0 auto;
|
|
372
|
+
color: var(--deck-chrome-dim);
|
|
373
|
+
font-variant-numeric: tabular-nums;
|
|
374
|
+
}
|
|
375
|
+
.deck__thumb-name {
|
|
376
|
+
min-width: 0;
|
|
377
|
+
overflow: hidden;
|
|
378
|
+
text-overflow: ellipsis;
|
|
379
|
+
white-space: nowrap;
|
|
380
|
+
}
|
|
268
381
|
.deck__error {
|
|
269
382
|
padding: 32px;
|
|
270
383
|
color: #ff6b6b;
|
|
@@ -283,7 +396,16 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
283
396
|
margin: 0;
|
|
284
397
|
position: static;
|
|
285
398
|
}
|
|
286
|
-
body.deck--export .deck__nav
|
|
399
|
+
body.deck--export .deck__nav,
|
|
400
|
+
body.deck--export .deck__rail { display: none !important; }
|
|
401
|
+
/* Thumbnail mode runs inside the left rail's isolated iframes. It must
|
|
402
|
+
render exactly one slide and no chrome, so per-slide <style> tags do not
|
|
403
|
+
leak into the main review document. */
|
|
404
|
+
body.deck--thumb { background: transparent !important; overflow: hidden; }
|
|
405
|
+
body.deck--thumb .deck { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; }
|
|
406
|
+
body.deck--thumb .deck__stage { box-shadow: none !important; }
|
|
407
|
+
body.deck--thumb .deck__nav,
|
|
408
|
+
body.deck--thumb .deck__rail { display: none !important; }
|
|
287
409
|
/* Print mode (?print=1) — stacks every slide as its own page so the
|
|
288
410
|
browser's Print / Cmd-P renders one slide per page (proofing). */
|
|
289
411
|
body.deck--print {
|
|
@@ -292,7 +414,8 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
292
414
|
height: auto !important;
|
|
293
415
|
}
|
|
294
416
|
body.deck--print .deck,
|
|
295
|
-
body.deck--print .deck__nav
|
|
417
|
+
body.deck--print .deck__nav,
|
|
418
|
+
body.deck--print .deck__rail { display: none !important; }
|
|
296
419
|
.print-deck { background: #ffffff; }
|
|
297
420
|
.print-page {
|
|
298
421
|
width: 1280px;
|
|
@@ -325,6 +448,9 @@ const HTML_TEMPLATE = `<!doctype html>
|
|
|
325
448
|
</style>
|
|
326
449
|
</head>
|
|
327
450
|
<body>
|
|
451
|
+
<aside class="deck__rail" id="slide-rail" aria-label="Slide thumbnails">
|
|
452
|
+
<div class="deck__thumbs" id="slide-thumbs"></div>
|
|
453
|
+
</aside>
|
|
328
454
|
<div class="deck">
|
|
329
455
|
<div class="deck__stage" id="stage"></div>
|
|
330
456
|
</div>
|
|
@@ -540,10 +666,15 @@ __RUNTIME_APPLIER__
|
|
|
540
666
|
|
|
541
667
|
var stage = document.getElementById("stage");
|
|
542
668
|
var nav = document.getElementById("nav");
|
|
669
|
+
var rail = document.getElementById("slide-rail");
|
|
670
|
+
var thumbs = document.getElementById("slide-thumbs");
|
|
671
|
+
var thumbButtons = [];
|
|
543
672
|
var params = new URLSearchParams(location.search);
|
|
544
673
|
var exportMode = params.get("export") === "1";
|
|
545
674
|
var printMode = params.get("print") === "1";
|
|
675
|
+
var thumbMode = params.get("thumb") === "1";
|
|
546
676
|
if (exportMode) document.body.classList.add("deck--export");
|
|
677
|
+
if (thumbMode) document.body.classList.add("deck--thumb");
|
|
547
678
|
|
|
548
679
|
smInlineSvgAssets();
|
|
549
680
|
|
|
@@ -576,6 +707,66 @@ __RUNTIME_APPLIER__
|
|
|
576
707
|
var current = startIndex;
|
|
577
708
|
var root = ReactDOM.createRoot(stage);
|
|
578
709
|
|
|
710
|
+
function buildThumbs() {
|
|
711
|
+
if (!thumbs || printMode || exportMode || thumbMode) return;
|
|
712
|
+
thumbs.innerHTML = "";
|
|
713
|
+
thumbButtons = [];
|
|
714
|
+
slides.forEach(function (slide, idx) {
|
|
715
|
+
var btn = document.createElement("button");
|
|
716
|
+
btn.type = "button";
|
|
717
|
+
btn.className = "deck__thumb";
|
|
718
|
+
btn.setAttribute("aria-label", "Go to slide " + (idx + 1) + ": " + slide.name);
|
|
719
|
+
|
|
720
|
+
var preview = document.createElement("div");
|
|
721
|
+
preview.className = "deck__thumb-preview";
|
|
722
|
+
var frame = document.createElement("iframe");
|
|
723
|
+
frame.className = "deck__thumb-frame";
|
|
724
|
+
frame.loading = "lazy";
|
|
725
|
+
frame.setAttribute("aria-hidden", "true");
|
|
726
|
+
frame.tabIndex = -1;
|
|
727
|
+
frame.src = location.pathname + "?slide=" + idx + "&thumb=1";
|
|
728
|
+
preview.appendChild(frame);
|
|
729
|
+
|
|
730
|
+
var meta = document.createElement("div");
|
|
731
|
+
meta.className = "deck__thumb-meta";
|
|
732
|
+
var num = document.createElement("span");
|
|
733
|
+
num.className = "deck__thumb-number";
|
|
734
|
+
num.textContent = String(idx + 1).padStart(2, "0");
|
|
735
|
+
var name = document.createElement("span");
|
|
736
|
+
name.className = "deck__thumb-name";
|
|
737
|
+
name.textContent = slide.name;
|
|
738
|
+
meta.appendChild(num);
|
|
739
|
+
meta.appendChild(name);
|
|
740
|
+
|
|
741
|
+
btn.appendChild(preview);
|
|
742
|
+
btn.appendChild(meta);
|
|
743
|
+
btn.addEventListener("click", function () {
|
|
744
|
+
setCurrent(idx);
|
|
745
|
+
stage.focus && stage.focus();
|
|
746
|
+
});
|
|
747
|
+
thumbs.appendChild(btn);
|
|
748
|
+
thumbButtons[idx] = btn;
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function updateThumbs() {
|
|
753
|
+
thumbButtons.forEach(function (btn, idx) {
|
|
754
|
+
if (!btn) return;
|
|
755
|
+
var active = idx === current;
|
|
756
|
+
btn.classList.toggle("is-active", active);
|
|
757
|
+
btn.setAttribute("aria-current", active ? "true" : "false");
|
|
758
|
+
if (active && thumbs) {
|
|
759
|
+
var top = btn.offsetTop;
|
|
760
|
+
var bottom = top + btn.offsetHeight;
|
|
761
|
+
if (top < thumbs.scrollTop) {
|
|
762
|
+
thumbs.scrollTop = Math.max(0, top - 12);
|
|
763
|
+
} else if (bottom > thumbs.scrollTop + thumbs.clientHeight) {
|
|
764
|
+
thumbs.scrollTop = bottom - thumbs.clientHeight + 12;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
579
770
|
function render() {
|
|
580
771
|
var slide = slides[current];
|
|
581
772
|
if (!slide) return;
|
|
@@ -589,11 +780,18 @@ __RUNTIME_APPLIER__
|
|
|
589
780
|
slide.name + ".jsx failed:\\n" + slide.error.message
|
|
590
781
|
)
|
|
591
782
|
);
|
|
783
|
+
updateThumbs();
|
|
592
784
|
return;
|
|
593
785
|
}
|
|
594
786
|
root.render(
|
|
595
787
|
React.createElement(slide.Component, { assetsPath: "./assets" })
|
|
596
788
|
);
|
|
789
|
+
updateThumbs();
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function setCurrent(index) {
|
|
793
|
+
current = Math.max(0, Math.min(index, slides.length - 1));
|
|
794
|
+
render();
|
|
597
795
|
}
|
|
598
796
|
|
|
599
797
|
function fit() {
|
|
@@ -601,32 +799,30 @@ __RUNTIME_APPLIER__
|
|
|
601
799
|
stage.style.transform = "none";
|
|
602
800
|
return;
|
|
603
801
|
}
|
|
604
|
-
var
|
|
802
|
+
var railW = rail && getComputedStyle(rail).display !== "none" ? rail.offsetWidth : 0;
|
|
803
|
+
var sx = Math.max(1, window.innerWidth - railW) / 1280;
|
|
605
804
|
var sy = window.innerHeight / 720;
|
|
606
|
-
var s = Math.min(sx, sy) * 0.95;
|
|
805
|
+
var s = Math.min(sx, sy) * (thumbMode ? 1 : 0.95);
|
|
607
806
|
stage.style.transform = "scale(" + s + ")";
|
|
608
807
|
}
|
|
609
808
|
|
|
610
809
|
window.addEventListener("resize", fit);
|
|
810
|
+
buildThumbs();
|
|
611
811
|
fit();
|
|
612
812
|
render();
|
|
613
813
|
|
|
614
814
|
document.addEventListener("keydown", function (e) {
|
|
615
815
|
var k = e.key;
|
|
616
816
|
if (k === "ArrowRight" || k === "PageDown" || k === " ") {
|
|
617
|
-
|
|
618
|
-
render();
|
|
817
|
+
setCurrent(current + 1);
|
|
619
818
|
e.preventDefault();
|
|
620
819
|
} else if (k === "ArrowLeft" || k === "PageUp") {
|
|
621
|
-
|
|
622
|
-
render();
|
|
820
|
+
setCurrent(current - 1);
|
|
623
821
|
e.preventDefault();
|
|
624
822
|
} else if (k === "Home") {
|
|
625
|
-
|
|
626
|
-
render();
|
|
823
|
+
setCurrent(0);
|
|
627
824
|
} else if (k === "End") {
|
|
628
|
-
|
|
629
|
-
render();
|
|
825
|
+
setCurrent(slides.length - 1);
|
|
630
826
|
} else if (k === "f" || k === "F") {
|
|
631
827
|
if (document.fullscreenElement) document.exitFullscreen();
|
|
632
828
|
else document.documentElement.requestFullscreen();
|
|
@@ -721,17 +917,59 @@ export async function buildDeck(deckDir, opts = {}) {
|
|
|
721
917
|
return { out, slideCount: slideFiles.length };
|
|
722
918
|
}
|
|
723
919
|
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
//
|
|
920
|
+
// Detect whether this Linux box is really WSL, so we can hand the target to the
|
|
921
|
+
// Windows-side opener instead of a (usually absent) Linux one. Any read/parse
|
|
922
|
+
// failure just means "not WSL".
|
|
923
|
+
function isWsl() {
|
|
924
|
+
if (process.platform !== "linux") return false;
|
|
925
|
+
try {
|
|
926
|
+
return /microsoft/i.test(fs.readFileSync("/proc/version", "utf8"));
|
|
927
|
+
} catch {
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Ordered list of opener candidates to try for the current platform. The
|
|
933
|
+
// platform openers (open / start / xdg-open / wslview) accept both a local file
|
|
934
|
+
// path AND an http URL, so review.mjs reuses this to launch the local review
|
|
935
|
+
// server URL and view-draft passes png paths.
|
|
936
|
+
function openerChain(target) {
|
|
937
|
+
if (process.platform === "darwin") return [{ cmd: "open", args: [target] }];
|
|
938
|
+
if (process.platform === "win32")
|
|
939
|
+
return [{ cmd: "cmd", args: ["/c", "start", "", target] }];
|
|
940
|
+
if (isWsl())
|
|
941
|
+
return [
|
|
942
|
+
{ cmd: "wslview", args: [target] },
|
|
943
|
+
{ cmd: "powershell.exe", args: ["-NoProfile", "-Command", "start", target] },
|
|
944
|
+
];
|
|
945
|
+
return [{ cmd: "xdg-open", args: [target] }];
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Open a local file path OR an http URL in the user's default browser, trying
|
|
949
|
+
// each platform opener in turn. This is fire-and-forget and must NEVER crash:
|
|
950
|
+
// if the opener binary is missing (e.g. a headless Linux box with no xdg-open)
|
|
951
|
+
// the spawned child would otherwise emit an unhandled 'error' and take down the
|
|
952
|
+
// whole CLI. We degrade gracefully — on exhausting the chain we just print the
|
|
953
|
+
// target for the user to open manually.
|
|
727
954
|
export function openInBrowser(target) {
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
955
|
+
const chain = openerChain(target);
|
|
956
|
+
const tryAt = (i) => {
|
|
957
|
+
if (i >= chain.length) {
|
|
958
|
+
process.stderr.write(`couldn't open a browser — open ${target} manually\n`);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const { cmd, args } = chain[i];
|
|
962
|
+
let child;
|
|
963
|
+
try {
|
|
964
|
+
child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
965
|
+
} catch {
|
|
966
|
+
tryAt(i + 1);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
child.on("error", () => tryAt(i + 1));
|
|
970
|
+
child.unref();
|
|
971
|
+
};
|
|
972
|
+
tryAt(0);
|
|
735
973
|
}
|
|
736
974
|
|
|
737
975
|
function parseArgs(argv) {
|