slidemind 0.3.13 → 0.3.14

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.
Files changed (49) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +86 -21
  4. package/README.md +14 -17
  5. package/bin/slidemind.mjs +65 -4969
  6. package/package.json +1 -1
  7. package/skills/create-slides/SKILL.md +113 -74
  8. package/skills/search/SKILL.md +25 -7
  9. package/src/bridge/build-and-push.mjs +108 -15
  10. package/src/bridge/bundle.mjs +69 -0
  11. package/src/bridge/capture.mjs +19 -0
  12. package/src/bridge/client/brands.mjs +622 -0
  13. package/src/bridge/client/engine-routing.mjs +237 -0
  14. package/src/bridge/client/engine-search.mjs +310 -0
  15. package/src/bridge/client/engine-slides.mjs +435 -0
  16. package/src/bridge/client/pane-ops.mjs +335 -0
  17. package/src/bridge/client/presentations.mjs +587 -0
  18. package/src/bridge/client/render.mjs +317 -0
  19. package/src/bridge/client/transport.mjs +695 -0
  20. package/src/bridge/client.mjs +80 -3452
  21. package/src/bridge/config.mjs +1 -1
  22. package/src/bridge/engine-download.mjs +15 -2
  23. package/src/builders/build.mjs +11 -804
  24. package/src/builders/draft-check.mjs +376 -0
  25. package/src/builders/lint.mjs +38 -2
  26. package/src/builders/preview.html +799 -0
  27. package/src/builders/review.mjs +23 -4
  28. package/src/builders/visual-families.mjs +6 -8
  29. package/src/cli/common.mjs +179 -0
  30. package/src/cli/concepts.mjs +310 -0
  31. package/src/cli/connection.mjs +147 -0
  32. package/src/cli/deck.mjs +608 -0
  33. package/src/cli/designs.mjs +113 -0
  34. package/src/cli/diagnose.mjs +141 -0
  35. package/src/cli/download-bundle.mjs +144 -0
  36. package/src/cli/engine-helpers.mjs +429 -0
  37. package/src/cli/engine-results.mjs +320 -0
  38. package/src/cli/help.mjs +201 -0
  39. package/src/cli/install.mjs +257 -0
  40. package/src/cli/read.mjs +120 -0
  41. package/src/cli/ready.mjs +805 -0
  42. package/src/cli/search.mjs +608 -0
  43. package/src/cli/session.mjs +177 -0
  44. package/src/cli/setup.mjs +282 -0
  45. package/src/cli/slide-reuse.mjs +334 -0
  46. package/src/cli/status.mjs +118 -0
  47. package/src/cli/workspace.mjs +302 -0
  48. package/src/installers/codex.mjs +1 -1
  49. package/src/update-check.mjs +231 -0
@@ -3,8 +3,8 @@
3
3
  // Pure Node stdlib — no external dependencies.
4
4
  //
5
5
  // Usage:
6
- // slidemind build <deck>
7
- // slidemind build <deck> --open
6
+ // slidemind view-draft <deck> (the CLI entry point; it builds, then captures)
7
+ // node src/builders/build.mjs --deck <deck> [--open] [--fix]
8
8
  //
9
9
  // Convention:
10
10
  // <deck>/
@@ -286,806 +286,13 @@ function collectSvgAssets(deckDir) {
286
286
  return map;
287
287
  }
288
288
 
289
- const HTML_TEMPLATE = `<!doctype html>
290
- <html lang="en">
291
- <head>
292
- <meta charset="utf-8" />
293
- <meta name="viewport" content="width=device-width, initial-scale=1" />
294
- <title>__TITLE__ — slidemind</title>
295
- ${FAVICON_LINK}
296
- <!-- Arimo: open-source Arial-metric-compatible font. We alias it to
297
- "Arial" so master text pinned to Arial gets a font with the same
298
- horizontal extents PowerPoint/LibreOffice use, instead of macOS
299
- Chrome's slightly wider system Arial that pushes long strings
300
- (addresses, single-line headers) past their bounding boxes. -->
301
- <link rel="preconnect" href="https://fonts.googleapis.com" />
302
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
303
- <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Arimo:ital,wght@0,400..700;1,400..700&display=swap" />
304
- <link rel="stylesheet" href="./corporate-design.css" />
305
- <link rel="stylesheet" href="./layouts.css" />
306
- <style>
307
- /* Compatibility aliases for decks/slides authored against the older
308
- statworx token names. New materialized bundles expose semantic tokens
309
- (--fg-*, --bg-*, --border-*); existing slide JSX may still refer
310
- to --sw-*. Keep those declarations valid in the HTML preview. */
311
- :root {
312
- --sw-black: var(--fg-1, #000000);
313
- --sw-white: var(--bg-1, #ffffff);
314
- --sw-ink: var(--fg-2, #283440);
315
- --sw-slate: var(--fg-3, #6C7D8C);
316
- --sw-gray: var(--fg-4, #B6BDCC);
317
- --sw-fog: var(--bg-2, #EBF0F2);
318
- --sw-ice-blue: var(--bg-3, #E6E6FF);
319
- --sw-tech-blue: var(--fg-accent, #0000FF);
320
- --sw-tech-blue-10: #E6E6FF;
321
- --sw-tech-blue-20: #CCCCFF;
322
- --sw-tech-blue-40: #9999FF;
323
- --sw-tech-blue-80: #3333FF;
324
- --sw-magenta: #E6007E;
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; }
332
- </style>
333
- <style>
334
- /* Substitute Arial with Arimo (a metric clone) so wrap behaviour
335
- matches PowerPoint's rendering. Local Arial wins only if Arimo
336
- hasn't loaded yet. */
337
- @font-face {
338
- font-family: "Arial";
339
- src: local("Arimo"), local("Arial");
340
- font-weight: 400;
341
- font-style: normal;
342
- }
343
- @font-face {
344
- font-family: "Arial";
345
- src: local("Arimo Bold"), local("Arial Bold");
346
- font-weight: 700;
347
- font-style: normal;
348
- }
349
- @font-face {
350
- font-family: "Arial";
351
- src: local("Arimo Italic"), local("Arial Italic");
352
- font-weight: 400;
353
- font-style: italic;
354
- }
355
- @font-face {
356
- font-family: "Arial";
357
- src: local("Arimo Bold Italic"), local("Arial Bold Italic");
358
- font-weight: 700;
359
- font-style: italic;
360
- }
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>
366
- <style>
367
- html, body { margin: 0; padding: 0; height: 100%; background: #0a0a0a; overflow: hidden; }
368
- :root {
369
- --deck-rail-w: 220px;
370
- --deck-chrome-h: 62px;
371
- --deck-chrome-bg: #111113;
372
- --deck-chrome-bg-2: #161618;
373
- --deck-chrome-border: #2b2b30;
374
- --deck-chrome-fg: #ededed;
375
- --deck-chrome-dim: #9a9aa0;
376
- }
377
- .deck {
378
- position: fixed;
379
- inset: 0 0 0 var(--deck-rail-w);
380
- display: flex;
381
- align-items: center;
382
- justify-content: center;
383
- }
384
- .deck__stage {
385
- width: 1280px;
386
- height: 720px;
387
- transform-origin: center center;
388
- background: var(--bg-1);
389
- box-shadow: 0 20px 60px rgba(0,0,0,0.35);
390
- }
391
- .deck__nav {
392
- position: fixed;
393
- bottom: 16px;
394
- right: 16px;
395
- padding: 6px 12px;
396
- border-radius: 999px;
397
- color: rgba(255,255,255,0.65);
398
- background: rgba(255,255,255,0.08);
399
- backdrop-filter: blur(6px);
400
- font-family: var(--font-sans, sans-serif);
401
- font-size: 12px;
402
- letter-spacing: 0.02em;
403
- user-select: none;
404
- }
405
- .deck__rail {
406
- position: fixed;
407
- top: 0;
408
- left: 0;
409
- bottom: 0;
410
- width: var(--deck-rail-w);
411
- z-index: 2147483590;
412
- display: flex;
413
- flex-direction: column;
414
- background: var(--deck-chrome-bg);
415
- border-right: 1px solid var(--deck-chrome-border);
416
- color: var(--deck-chrome-fg);
417
- font-family: var(--font-sans, sans-serif);
418
- }
419
- .deck__thumbs {
420
- flex: 1 1 auto;
421
- overflow-y: auto;
422
- padding: 12px 14px 18px;
423
- scrollbar-width: none;
424
- -ms-overflow-style: none;
425
- }
426
- .deck__thumbs::-webkit-scrollbar {
427
- width: 0;
428
- height: 0;
429
- display: none;
430
- }
431
- .deck__thumb {
432
- width: 100%;
433
- margin: 0 0 12px;
434
- padding: 8px;
435
- border: 1px solid var(--deck-chrome-border);
436
- border-radius: 8px;
437
- background: rgba(255,255,255,0.025);
438
- color: inherit;
439
- cursor: pointer;
440
- text-align: left;
441
- transition: border-color 120ms ease, background 120ms ease, box-shadow 120ms ease;
442
- }
443
- .deck__thumb:hover {
444
- background: rgba(255,255,255,0.06);
445
- border-color: #3a3a40;
446
- }
447
- .deck__thumb.is-active {
448
- border-color: rgba(237,237,237,0.7);
449
- background: rgba(255,255,255,0.085);
450
- box-shadow: 0 0 0 2px rgba(237,237,237,0.08);
451
- }
452
- .deck__thumb-preview {
453
- width: 176px;
454
- height: 99px;
455
- overflow: hidden;
456
- background: #fff;
457
- box-shadow: 0 4px 14px rgba(0,0,0,0.28);
458
- pointer-events: none;
459
- }
460
- .deck__thumb-frame {
461
- display: block;
462
- width: 176px;
463
- height: 99px;
464
- border: 0;
465
- background: #fff;
466
- pointer-events: none;
467
- }
468
- .deck__thumb-meta {
469
- display: flex;
470
- align-items: baseline;
471
- gap: 7px;
472
- margin-top: 7px;
473
- min-width: 0;
474
- font-size: 11px;
475
- line-height: 1.25;
476
- }
477
- .deck__thumb-number {
478
- flex: 0 0 auto;
479
- color: var(--deck-chrome-dim);
480
- font-variant-numeric: tabular-nums;
481
- }
482
- .deck__thumb-name {
483
- min-width: 0;
484
- overflow: hidden;
485
- text-overflow: ellipsis;
486
- white-space: nowrap;
487
- }
488
- .deck__error {
489
- padding: 32px;
490
- color: #ff6b6b;
491
- background: #1a0000;
492
- font-family: var(--font-mono, monospace);
493
- font-size: 14px;
494
- white-space: pre-wrap;
495
- }
496
- /* Export mode (?export=1) — used by the PPTX exporter's headless Chrome
497
- so it captures a clean 1280x720 stage with no letterbox/nav/scaling. */
498
- body.deck--export { background: #ffffff !important; overflow: hidden; }
499
- body.deck--export .deck { position: static; display: block; inset: auto; }
500
- body.deck--export .deck__stage {
501
- transform: none !important;
502
- box-shadow: none !important;
503
- margin: 0;
504
- position: static;
505
- }
506
- body.deck--export .deck__nav,
507
- body.deck--export .deck__rail { display: none !important; }
508
- /* Thumbnail mode runs inside the left rail's isolated iframes. It must
509
- render exactly one slide and no chrome, so per-slide <style> tags do not
510
- leak into the main review document. */
511
- body.deck--thumb { background: transparent !important; overflow: hidden; }
512
- body.deck--thumb .deck { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; }
513
- body.deck--thumb .deck__stage { box-shadow: none !important; }
514
- body.deck--thumb .deck__nav,
515
- body.deck--thumb .deck__rail { display: none !important; }
516
- /* Print mode (?print=1) — stacks every slide as its own page so the
517
- browser's Print / Cmd-P renders one slide per page (proofing). */
518
- body.deck--print {
519
- background: #ffffff !important;
520
- overflow: visible !important;
521
- height: auto !important;
522
- }
523
- body.deck--print .deck,
524
- body.deck--print .deck__nav,
525
- body.deck--print .deck__rail { display: none !important; }
526
- .print-deck { background: #ffffff; }
527
- .print-page {
528
- width: 1280px;
529
- height: 720px;
530
- position: relative;
531
- overflow: hidden;
532
- background: var(--bg-1);
533
- page-break-after: always;
534
- break-after: page;
535
- }
536
- .print-page:last-child {
537
- page-break-after: auto;
538
- break-after: auto;
539
- }
540
- .print-page__error {
541
- padding: 32px;
542
- color: #ff6b6b;
543
- background: #1a0000;
544
- font-family: var(--font-mono, monospace);
545
- font-size: 14px;
546
- white-space: pre-wrap;
547
- width: 100%;
548
- height: 100%;
549
- box-sizing: border-box;
550
- }
551
- @media print {
552
- @page { size: 1280px 720px; margin: 0; }
553
- html, body { background: #ffffff !important; }
554
- }
555
- </style>
556
- </head>
557
- <body>
558
- <aside class="deck__rail" id="slide-rail" aria-label="Slide thumbnails">
559
- <div class="deck__thumbs" id="slide-thumbs"></div>
560
- </aside>
561
- <div class="deck">
562
- <div class="deck__stage" id="stage"></div>
563
- </div>
564
- <div class="deck__nav" id="nav">loading…</div>
565
-
566
- <!-- Layout sources (inlined from skills/create-slides/layouts/*.jsx).
567
- Each one is evaluated once and exposed under its binding name
568
- (SlideCover, SlideAgendaBlack, ...) so slides can reference it. -->
569
- __LAYOUTS__
570
-
571
- <!-- Slide sources (inlined from this deck's slides/ folder, filename order). -->
572
- __SLIDES__
573
-
574
- <!-- Runtime: React + Babel-standalone from CDN. -->
575
- <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
576
- <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
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__
587
- <script>window.__SLIDE_EDITOR_OVERRIDES__ = __OVERRIDES__;</script>
588
- <script>window.SVG_ASSETS = __SVG_ASSETS__;</script>
589
- <script>
590
- __RUNTIME_APPLIER__
591
- </script>
592
-
593
- <script>
594
- (function () {
595
- // Map kebab-case filename stem -> PascalCase layout binding name.
596
- function layoutBindingName(stem) {
597
- return "Slide" + stem.split("-").map(function (p) {
598
- return p ? p[0].toUpperCase() + p.slice(1) : "";
599
- }).join("");
600
- }
601
-
602
- // Stamp every intrinsic JSX element with data-sm-loc="<prefix>:<line>:<col>"
603
- // so the preview-review overlay (review-overlay.js) can resolve a click back
604
- // to the exact slide source location. Runs as a Babel plugin BEFORE the react
605
- // preset (plugins run before presets) so it sees JSX nodes, not createElement
606
- // calls. Inert without the overlay. Mirrors the add-in's instrument-source-loc.
607
- function makeSourceLocPlugin(locPrefix) {
608
- return function (babel) {
609
- var t = babel.types;
610
- return {
611
- visitor: {
612
- JSXOpeningElement: function (path) {
613
- var node = path.node;
614
- var nameNode = node.name;
615
- // Intrinsic elements only: a lowercase JSXIdentifier. Skip
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.
620
- if (!nameNode || nameNode.type !== "JSXIdentifier") return;
621
- if (!/^[a-z]/.test(nameNode.name) && nameNode.name !== "Chart") return;
622
- for (var i = 0; i < node.attributes.length; i++) {
623
- var a = node.attributes[i];
624
- if (a.type === "JSXAttribute" && a.name &&
625
- a.name.name === "data-sm-loc") return;
626
- }
627
- var start = node.loc && node.loc.start;
628
- if (!start) return;
629
- var value = locPrefix + ":" + start.line + ":" + (start.column + 1);
630
- node.attributes.push(
631
- t.jsxAttribute(t.jsxIdentifier("data-sm-loc"),
632
- t.stringLiteral(value))
633
- );
634
- }
635
- }
636
- };
637
- };
638
- }
639
-
640
- function evalJsx(src, filename, extra) {
641
- // The inliner wraps each source as "\\n" + src + "\\n" inside the <script>
642
- // block; strip that one leading newline so Babel's line numbers (and thus
643
- // data-sm-loc) align with the author's pristine workspace file.
644
- if (src.charAt(0) === "\\n") src = src.slice(1);
645
- var result = Babel.transform(src, {
646
- presets: [["react", { runtime: "classic" }]],
647
- plugins: [makeSourceLocPlugin(filename)],
648
- sourceType: "module",
649
- filename: filename
650
- });
651
- var body = result.code.replace(
652
- /export\\s+default\\s+/g,
653
- "__module.default = "
654
- );
655
- var names = ["__module", "React"].concat(Object.keys(extra || {}));
656
- var values = [{}, React].concat(Object.keys(extra || {}).map(function (k) {
657
- return extra[k];
658
- }));
659
- var factory = new Function(
660
- names.join(","),
661
- body + "\\nreturn __module.default;"
662
- );
663
- return factory.apply(null, values);
664
- }
665
-
666
- // Preview parity: replace <img src="*.svg"> whose file we inlined into
667
- // window.SVG_ASSETS with the SVG markup itself, carrying the <img>'s box
668
- // and color onto a wrapper so fill="currentColor" resolves — the same
669
- // tint the .pptx renderer applies. Re-runs on every React render via a
670
- // MutationObserver so navigating slides keeps icons inlined.
671
- function smInlineSvgAssets() {
672
- var ASSETS = (typeof window !== "undefined" && window.SVG_ASSETS) || null;
673
- if (!ASSETS || !Object.keys(ASSETS).length) return;
674
-
675
- function lookup(src) {
676
- if (!src) return null;
677
- var s = src.split("?")[0].split("#")[0];
678
- var alt = s.indexOf("./") === 0 ? s.slice(2) : s;
679
- var base = s.split("/").pop();
680
- return ASSETS[s] || ASSETS[alt] || ASSETS[base] || null;
681
- }
682
-
683
- function inlineWithin(root) {
684
- if (!root || !root.querySelectorAll) return;
685
- var imgs = root.querySelectorAll("img");
686
- for (var i = 0; i < imgs.length; i++) {
687
- var img = imgs[i];
688
- if (img.getAttribute("data-sm-inlined") === "1") continue;
689
- var markup = lookup(img.getAttribute("src"));
690
- if (!markup) continue;
691
- var span = document.createElement("span");
692
- span.setAttribute("data-sm-inlined", "1");
693
- if (img.className) span.className = img.className;
694
- var styleAttr = img.getAttribute("style");
695
- if (styleAttr) span.setAttribute("style", styleAttr);
696
- // Pull the laid-out box + colour even when they came from CSS, not
697
- // an inline style, so sizing holds and currentColor resolves.
698
- var cs = window.getComputedStyle ? window.getComputedStyle(img) : null;
699
- var hasW = !!span.style.width;
700
- var hasH = !!span.style.height;
701
- if (cs) {
702
- if (!hasW && cs.width && cs.width !== "auto" && cs.width !== "0px") {
703
- span.style.width = cs.width;
704
- hasW = true;
705
- }
706
- if (!hasH && cs.height && cs.height !== "auto" && cs.height !== "0px") {
707
- span.style.height = cs.height;
708
- hasH = true;
709
- }
710
- if (!span.style.color && cs.color) span.style.color = cs.color;
711
- }
712
- if (!span.style.display || span.style.display === "inline") {
713
- span.style.display = "inline-flex";
714
- }
715
- if (!span.style.alignItems) span.style.alignItems = "center";
716
- if (!span.style.justifyContent) span.style.justifyContent = "center";
717
- span.innerHTML = markup;
718
- var svg = span.querySelector("svg");
719
- if (svg) {
720
- svg.style.display = "block";
721
- if (hasW || hasH) {
722
- svg.style.width = "100%";
723
- svg.style.height = "100%";
724
- svg.removeAttribute("width");
725
- svg.removeAttribute("height");
726
- }
727
- }
728
- if (img.parentNode) img.parentNode.replaceChild(span, img);
729
- }
730
- }
731
-
732
- var scheduled = false;
733
- var raf =
734
- window.requestAnimationFrame ||
735
- function (cb) { return setTimeout(cb, 16); };
736
- function schedule() {
737
- if (scheduled) return;
738
- scheduled = true;
739
- raf(function () {
740
- scheduled = false;
741
- inlineWithin(document.body);
742
- });
743
- }
744
- if (typeof MutationObserver !== "undefined") {
745
- new MutationObserver(schedule).observe(document.body, {
746
- childList: true,
747
- subtree: true,
748
- });
749
- }
750
- schedule();
751
- }
752
-
753
- var LAYOUTS = {};
754
- document.querySelectorAll('script[data-kind="layout"]').forEach(function (s) {
755
- var name = s.dataset.binding || layoutBindingName(s.dataset.name);
756
- try {
757
- LAYOUTS[name] = evalJsx(
758
- s.textContent,
759
- "layouts/" + s.dataset.name + ".jsx",
760
- {}
761
- );
762
- window[name] = LAYOUTS[name];
763
- } catch (err) {
764
- console.error("Layout " + name + " failed to load:", err);
765
- throw err;
766
- }
767
- });
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
-
777
- var slides = [];
778
- document.querySelectorAll('script[data-kind="slide"]').forEach(function (s) {
779
- var idx = parseInt(s.dataset.index, 10);
780
- var name = s.dataset.name;
781
- try {
782
- var Component = evalJsx(
783
- s.textContent,
784
- "slides/" + name + ".jsx",
785
- slideScope
786
- );
787
- slides[idx] = { name: name, Component: Component };
788
- } catch (err) {
789
- console.error("Slide " + name + " failed to load:", err);
790
- slides[idx] = { name: name, error: err };
791
- }
792
- });
793
-
794
- var stage = document.getElementById("stage");
795
- var nav = document.getElementById("nav");
796
- var rail = document.getElementById("slide-rail");
797
- var thumbs = document.getElementById("slide-thumbs");
798
- var thumbButtons = [];
799
- var params = new URLSearchParams(location.search);
800
- var exportMode = params.get("export") === "1";
801
- var printMode = params.get("print") === "1";
802
- var thumbMode = params.get("thumb") === "1";
803
- if (exportMode) document.body.classList.add("deck--export");
804
- if (thumbMode) document.body.classList.add("deck--thumb");
805
-
806
- smInlineSvgAssets();
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
-
932
- if (printMode) {
933
- document.body.classList.add("deck--print");
934
- var printDeck = document.createElement("div");
935
- printDeck.className = "print-deck";
936
- document.body.appendChild(printDeck);
937
- slides.forEach(function (slide) {
938
- var page = document.createElement("div");
939
- page.className = "print-page";
940
- printDeck.appendChild(page);
941
- if (slide.error) {
942
- var errEl = document.createElement("div");
943
- errEl.className = "print-page__error";
944
- errEl.textContent = slide.name + ".jsx failed:\\n" + slide.error.message;
945
- page.appendChild(errEl);
946
- return;
947
- }
948
- ReactDOM.createRoot(page).render(
949
- React.createElement(slide.Component, { assetsPath: "./assets" })
950
- );
951
- smScheduleConnectors(page);
952
- });
953
- return;
954
- }
955
-
956
- var startIndex = parseInt(params.get("slide") || "0", 10);
957
- if (isNaN(startIndex) || startIndex < 0) startIndex = 0;
958
- if (startIndex >= slides.length) startIndex = slides.length - 1;
959
- var current = startIndex;
960
- var root = ReactDOM.createRoot(stage);
961
-
962
- function buildThumbs() {
963
- if (!thumbs || printMode || exportMode || thumbMode) return;
964
- thumbs.innerHTML = "";
965
- thumbButtons = [];
966
- slides.forEach(function (slide, idx) {
967
- var btn = document.createElement("button");
968
- btn.type = "button";
969
- btn.className = "deck__thumb";
970
- btn.setAttribute("aria-label", "Go to slide " + (idx + 1) + ": " + slide.name);
971
-
972
- var preview = document.createElement("div");
973
- preview.className = "deck__thumb-preview";
974
- var frame = document.createElement("iframe");
975
- frame.className = "deck__thumb-frame";
976
- frame.loading = "lazy";
977
- frame.setAttribute("aria-hidden", "true");
978
- frame.tabIndex = -1;
979
- frame.src = location.pathname + "?slide=" + idx + "&thumb=1";
980
- preview.appendChild(frame);
981
-
982
- var meta = document.createElement("div");
983
- meta.className = "deck__thumb-meta";
984
- var num = document.createElement("span");
985
- num.className = "deck__thumb-number";
986
- num.textContent = String(idx + 1).padStart(2, "0");
987
- var name = document.createElement("span");
988
- name.className = "deck__thumb-name";
989
- name.textContent = slide.name;
990
- meta.appendChild(num);
991
- meta.appendChild(name);
992
-
993
- btn.appendChild(preview);
994
- btn.appendChild(meta);
995
- btn.addEventListener("click", function () {
996
- setCurrent(idx);
997
- stage.focus && stage.focus();
998
- });
999
- thumbs.appendChild(btn);
1000
- thumbButtons[idx] = btn;
1001
- });
1002
- }
1003
-
1004
- function updateThumbs() {
1005
- thumbButtons.forEach(function (btn, idx) {
1006
- if (!btn) return;
1007
- var active = idx === current;
1008
- btn.classList.toggle("is-active", active);
1009
- btn.setAttribute("aria-current", active ? "true" : "false");
1010
- if (active && thumbs) {
1011
- var top = btn.offsetTop;
1012
- var bottom = top + btn.offsetHeight;
1013
- if (top < thumbs.scrollTop) {
1014
- thumbs.scrollTop = Math.max(0, top - 12);
1015
- } else if (bottom > thumbs.scrollTop + thumbs.clientHeight) {
1016
- thumbs.scrollTop = bottom - thumbs.clientHeight + 12;
1017
- }
1018
- }
1019
- });
1020
- }
1021
-
1022
- function render() {
1023
- var slide = slides[current];
1024
- if (!slide) return;
1025
- nav.textContent =
1026
- (current + 1) + " / " + slides.length + " · " + slide.name;
1027
- if (slide.error) {
1028
- root.render(
1029
- React.createElement(
1030
- "div",
1031
- { className: "deck__error" },
1032
- slide.name + ".jsx failed:\\n" + slide.error.message
1033
- )
1034
- );
1035
- updateThumbs();
1036
- return;
1037
- }
1038
- root.render(
1039
- React.createElement(slide.Component, { assetsPath: "./assets" })
1040
- );
1041
- smScheduleConnectors(stage);
1042
- updateThumbs();
1043
- }
1044
-
1045
- function setCurrent(index) {
1046
- current = Math.max(0, Math.min(index, slides.length - 1));
1047
- render();
1048
- }
1049
-
1050
- function fit() {
1051
- if (exportMode) {
1052
- stage.style.transform = "none";
1053
- return;
1054
- }
1055
- var railW = rail && getComputedStyle(rail).display !== "none" ? rail.offsetWidth : 0;
1056
- var sx = Math.max(1, window.innerWidth - railW) / 1280;
1057
- var sy = window.innerHeight / 720;
1058
- var s = Math.min(sx, sy) * (thumbMode ? 1 : 0.95);
1059
- stage.style.transform = "scale(" + s + ")";
1060
- }
1061
-
1062
- window.addEventListener("resize", fit);
1063
- buildThumbs();
1064
- fit();
1065
- render();
1066
-
1067
- document.addEventListener("keydown", function (e) {
1068
- var k = e.key;
1069
- if (k === "ArrowRight" || k === "PageDown" || k === " ") {
1070
- setCurrent(current + 1);
1071
- e.preventDefault();
1072
- } else if (k === "ArrowLeft" || k === "PageUp") {
1073
- setCurrent(current - 1);
1074
- e.preventDefault();
1075
- } else if (k === "Home") {
1076
- setCurrent(0);
1077
- } else if (k === "End") {
1078
- setCurrent(slides.length - 1);
1079
- } else if (k === "f" || k === "F") {
1080
- if (document.fullscreenElement) document.exitFullscreen();
1081
- else document.documentElement.requestFullscreen();
1082
- }
1083
- });
1084
- })();
1085
- </script>
1086
- </body>
1087
- </html>
1088
- `;
289
+ // The preview page. Kept as a real .html file (syntax-highlighted, diffable)
290
+ // rather than an 800-line template string; the __PLACEHOLDER__ slots are
291
+ // filled by buildDeck below. Function replacer: a string replacement would
292
+ // expand `$&`-style patterns in the substituted value.
293
+ const HTML_TEMPLATE = fs
294
+ .readFileSync(path.join(__dirname, "preview.html"), "utf8")
295
+ .replace("__FAVICON_LINK__", () => FAVICON_LINK);
1089
296
 
1090
297
  export async function buildDeck(deckDir, opts = {}) {
1091
298
  const skill = resolveSkillForDeck(deckDir);
@@ -1316,8 +523,8 @@ function parseArgs(argv) {
1316
523
  args.fix = true;
1317
524
  } else if (a === "-h" || a === "--help") {
1318
525
  process.stdout.write(
1319
- "Usage: slidemind build <deck> [--open] [--fix]\n" +
1320
- " node src/builders/build.mjs --deck <deck> [--open] [--fix]\n" +
526
+ "Usage: node src/builders/build.mjs --deck <deck> [--open] [--fix]\n" +
527
+ " (the CLI entry point is `slidemind view-draft <deck> [--fix]`)\n" +
1321
528
  " --fix apply the deterministic lint autofixes to slides/*.jsx before building\n" +
1322
529
  " (pin inline-svg px size from its viewBox; letter-spacing em→px where the\n" +
1323
530
  " block's px font-size makes the conversion exact)\n",