pindeck 2.9.0 → 2.10.0

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/public/bridge.js CHANGED
@@ -4,7 +4,8 @@
4
4
  main the visible slide in the editor: navigation, selection, drag and resize, text editing, pins
5
5
  measure hidden 1280x720 copy used to measure any slide without moving the user's view
6
6
  shot headless screenshot of one slide, with pin markers
7
- dump headless measurement; writes the result into the DOM for --dump-dom */
7
+ dump headless measurement; writes the result into the DOM for --dump-dom
8
+ pdf print layout for the PDF export (decks made without reveal.js; reveal.js has its own) */
8
9
 
9
10
  (function () {
10
11
  const params = new URLSearchParams(location.search);
@@ -14,64 +15,69 @@
14
15
  const deckMeta = (n) => document.querySelector(`meta[name="pindeck:${n}"]`)?.content?.trim();
15
16
  const FOOTER_TOP = deckMeta("footer-top") ? +deckMeta("footer-top") : null;
16
17
  const LOCKED_SEL = deckMeta("locked") || ".slide-foot, .logo";
18
+ // A deck made without reveal.js names its slides with <meta name="pindeck:slides"> (a CSS selector)
19
+ const SLIDE_SEL = deckMeta("slides") || null;
20
+ const SLIDE = SLIDE_SEL ? `:is(${SLIDE_SEL})` : ".reveal .slides section"; // in CSS and closest()
21
+ const SLIDE_OF = SLIDE_SEL || "section"; // the slide an element is on
17
22
  const host = () => (window.parent !== window ? window.parent.__editorHost : null);
18
- let W = 1280, H = 720;
23
+ let W = 1280, H = 720, deck = null; // deck: reveal.js or a selector deck (see "Decks" below)
19
24
 
20
25
  /* ---- Styles for editor-only overlays ---------------------------------------- */
21
26
  const css = document.createElement("style");
22
- // --u = screen pixels per slide pixel, inverted: overlays keep a constant on-screen size at any zoom
27
+ // --pindeck-u = screen pixels per slide pixel, inverted: overlays keep a constant on-screen size at any
28
+ // zoom (a name of its own: decks use --u for their own units)
23
29
  css.textContent = `
24
- .__pin { position:absolute; z-index:1000; width:calc(26px*var(--u,1)); height:calc(26px*var(--u,1));
25
- margin:calc(-13px*var(--u,1)) 0 0 calc(-13px*var(--u,1)); border-radius:50%;
26
- background:#ED8C01; color:#fff; font:700 calc(14px*var(--u,1))/calc(26px*var(--u,1)) system-ui, sans-serif; text-align:center;
27
- box-shadow:0 0 0 calc(2px*var(--u,1)) #fff, 0 calc(2px*var(--u,1)) calc(6px*var(--u,1)) rgba(0,0,0,.45);
30
+ .__pin { position:absolute; z-index:1000; width:calc(26px*var(--pindeck-u,1)); height:calc(26px*var(--pindeck-u,1));
31
+ margin:calc(-13px*var(--pindeck-u,1)) 0 0 calc(-13px*var(--pindeck-u,1)); border-radius:50%;
32
+ background:#ED8C01; color:#fff; font:700 calc(14px*var(--pindeck-u,1))/calc(26px*var(--pindeck-u,1)) system-ui, sans-serif; text-align:center;
33
+ box-shadow:0 0 0 calc(2px*var(--pindeck-u,1)) #fff, 0 calc(2px*var(--pindeck-u,1)) calc(6px*var(--pindeck-u,1)) rgba(0,0,0,.45);
28
34
  cursor:pointer; pointer-events:auto; }
29
- .__hover { position:absolute; z-index:999; pointer-events:none; outline:calc(2px*var(--u,1)) dashed #ED8C01;
30
- outline-offset:calc(2px*var(--u,1)); background:rgba(237,140,1,.06); }
31
- .__hover span { position:absolute; left:calc(-2px*var(--u,1)); background:#ED8C01; color:#fff; white-space:nowrap;
32
- font:600 calc(11px*var(--u,1))/calc(17px*var(--u,1)) system-ui, sans-serif; padding:0 calc(5px*var(--u,1));
33
- border-radius:calc(3px*var(--u,1)); }
34
- .__hover span.above { top:calc(-21px*var(--u,1)); } .__hover span.below { bottom:calc(-21px*var(--u,1)); }
35
+ .__hover { position:absolute; z-index:999; pointer-events:none; outline:calc(2px*var(--pindeck-u,1)) dashed #ED8C01;
36
+ outline-offset:calc(2px*var(--pindeck-u,1)); background:rgba(237,140,1,.06); }
37
+ .__hover span { position:absolute; left:calc(-2px*var(--pindeck-u,1)); background:#ED8C01; color:#fff; white-space:nowrap;
38
+ font:600 calc(11px*var(--pindeck-u,1))/calc(17px*var(--pindeck-u,1)) system-ui, sans-serif; padding:0 calc(5px*var(--pindeck-u,1));
39
+ border-radius:calc(3px*var(--pindeck-u,1)); }
40
+ .__hover span.above { top:calc(-21px*var(--pindeck-u,1)); } .__hover span.below { bottom:calc(-21px*var(--pindeck-u,1)); }
35
41
  .__hover.locked { outline-color:#888; background:rgba(0,0,0,.04); }
36
42
  .__hover.locked span { background:#888; }
37
- .__flash { position:absolute; z-index:998; pointer-events:none; outline:calc(3px*var(--u,1)) solid #ED8C01;
38
- outline-offset:calc(3px*var(--u,1)); animation:__flash 1.4s ease-out forwards; }
43
+ .__flash { position:absolute; z-index:998; pointer-events:none; outline:calc(3px*var(--pindeck-u,1)) solid #ED8C01;
44
+ outline-offset:calc(3px*var(--pindeck-u,1)); animation:__flash 1.4s ease-out forwards; }
39
45
  @keyframes __flash { 0%,60% { opacity:1 } 100% { opacity:0 } }
40
- html.__pinmode .reveal .slides section, html.__pinmode .reveal .slides section * { cursor:crosshair !important; }
41
- html.__pinmode .reveal .slides section .__pin { cursor:pointer !important; }
42
- .__hover.sel { outline:calc(1px*var(--u,1)) solid #1a73e8; outline-offset:0; background:none; }
46
+ html.__pinmode ${SLIDE}, html.__pinmode ${SLIDE} * { cursor:crosshair !important; }
47
+ html.__pinmode ${SLIDE} .__pin { cursor:pointer !important; }
48
+ .__hover.sel { outline:calc(1px*var(--pindeck-u,1)) solid #1a73e8; outline-offset:0; background:none; }
43
49
  .__hover.sel span { background:#1a73e8; }
44
- .__sel { position:absolute; z-index:1001; pointer-events:none; outline:calc(1.5px*var(--u,1)) solid #1a73e8; }
45
- .__sel i { position:absolute; width:calc(9px*var(--u,1)); height:calc(9px*var(--u,1)); margin:calc(-5px*var(--u,1)) 0 0 calc(-5px*var(--u,1));
46
- background:#fff; border:calc(1.5px*var(--u,1)) solid #1a73e8; box-sizing:border-box; pointer-events:auto; }
50
+ .__sel { position:absolute; z-index:1001; pointer-events:none; outline:calc(1.5px*var(--pindeck-u,1)) solid #1a73e8; }
51
+ .__sel i { position:absolute; width:calc(9px*var(--pindeck-u,1)); height:calc(9px*var(--pindeck-u,1)); margin:calc(-5px*var(--pindeck-u,1)) 0 0 calc(-5px*var(--pindeck-u,1));
52
+ background:#fff; border:calc(1.5px*var(--pindeck-u,1)) solid #1a73e8; box-sizing:border-box; pointer-events:auto; }
47
53
  .__sel i[data-h="nw"] { left:0; top:0; cursor:nwse-resize; } .__sel i[data-h="n"] { left:50%; top:0; cursor:ns-resize; }
48
54
  .__sel i[data-h="ne"] { left:100%; top:0; cursor:nesw-resize; } .__sel i[data-h="e"] { left:100%; top:50%; cursor:ew-resize; }
49
55
  .__sel i[data-h="se"] { left:100%; top:100%; cursor:nwse-resize; } .__sel i[data-h="s"] { left:50%; top:100%; cursor:ns-resize; }
50
56
  .__sel i[data-h="sw"] { left:0; top:100%; cursor:nesw-resize; } .__sel i[data-h="w"] { left:0; top:50%; cursor:ew-resize; }
51
57
  .__sel.readonly i { display:none; }
52
- .__sel-label { position:absolute; left:calc(-1.5px*var(--u,1)); white-space:nowrap; background:#1a73e8; color:#fff;
53
- font:600 calc(11px*var(--u,1))/calc(17px*var(--u,1)) system-ui, sans-serif; padding:0 calc(5px*var(--u,1));
54
- border-radius:calc(3px*var(--u,1)); font-variant-numeric:tabular-nums; }
55
- .__sel-label.above { top:calc(-21px*var(--u,1)); } .__sel-label.below { bottom:calc(-21px*var(--u,1)); }
58
+ .__sel-label { position:absolute; left:calc(-1.5px*var(--pindeck-u,1)); white-space:nowrap; background:#1a73e8; color:#fff;
59
+ font:600 calc(11px*var(--pindeck-u,1))/calc(17px*var(--pindeck-u,1)) system-ui, sans-serif; padding:0 calc(5px*var(--pindeck-u,1));
60
+ border-radius:calc(3px*var(--pindeck-u,1)); font-variant-numeric:tabular-nums; }
61
+ .__sel-label.above { top:calc(-21px*var(--pindeck-u,1)); } .__sel-label.below { bottom:calc(-21px*var(--pindeck-u,1)); }
56
62
  .__guide { position:absolute; z-index:1002; pointer-events:none; background:#e5399d; }
57
- .__guide.v { top:0; height:100%; width:calc(1px*var(--u,1)); }
58
- .__guide.h { left:0; width:100%; height:calc(1px*var(--u,1)); }
59
- html.__selmode .reveal .slides section * { cursor:default; }
60
- html.__selmode .reveal .slides section .__selected, html.__selmode.__dragging * { cursor:move !important; }
61
- html.__selmode .reveal .slides section .__sel i { cursor:inherit; }
62
- .__editing { outline:calc(2px*var(--u,1)) solid #1a73e8 !important; outline-offset:calc(3px*var(--u,1));
63
+ .__guide.v { top:0; height:100%; width:calc(1px*var(--pindeck-u,1)); }
64
+ .__guide.h { left:0; width:100%; height:calc(1px*var(--pindeck-u,1)); }
65
+ html.__selmode ${SLIDE} * { cursor:default; }
66
+ html.__selmode ${SLIDE} .__selected, html.__selmode.__dragging * { cursor:move !important; }
67
+ html.__selmode ${SLIDE} .__sel i { cursor:inherit; }
68
+ .__editing { outline:calc(2px*var(--pindeck-u,1)) solid #1a73e8 !important; outline-offset:calc(3px*var(--pindeck-u,1));
63
69
  background:rgba(26,115,232,.04); cursor:text !important; caret-color:#1a73e8; }
64
70
  .__editing * { cursor:text !important; }
65
- .__hidden-banner { position:absolute; z-index:997; top:calc(26px*var(--u,1)); right:calc(26px*var(--u,1)); pointer-events:none;
66
- background:#ED8C01; color:#fff; font:600 calc(12px*var(--u,1))/calc(22px*var(--u,1)) system-ui, sans-serif;
67
- padding:0 calc(10px*var(--u,1)); border-radius:calc(11px*var(--u,1)); box-shadow:0 calc(2px*var(--u,1)) calc(8px*var(--u,1)) rgba(0,0,0,.25); }
71
+ .__hidden-banner { position:absolute; z-index:997; top:calc(26px*var(--pindeck-u,1)); right:calc(26px*var(--pindeck-u,1)); pointer-events:none;
72
+ background:#ED8C01; color:#fff; font:600 calc(12px*var(--pindeck-u,1))/calc(22px*var(--pindeck-u,1)) system-ui, sans-serif;
73
+ padding:0 calc(10px*var(--pindeck-u,1)); border-radius:calc(11px*var(--pindeck-u,1)); box-shadow:0 calc(2px*var(--pindeck-u,1)) calc(8px*var(--pindeck-u,1)) rgba(0,0,0,.25); }
68
74
  .__edit-hint { position:absolute; z-index:1003; pointer-events:none; white-space:nowrap; background:#1a73e8; color:#fff;
69
- font:600 calc(11px*var(--u,1))/calc(17px*var(--u,1)) system-ui, sans-serif; padding:0 calc(5px*var(--u,1));
70
- border-radius:calc(3px*var(--u,1)); }`;
75
+ font:600 calc(11px*var(--pindeck-u,1))/calc(17px*var(--pindeck-u,1)) system-ui, sans-serif; padding:0 calc(5px*var(--pindeck-u,1));
76
+ border-radius:calc(3px*var(--pindeck-u,1)); }`;
71
77
  document.head.appendChild(css);
72
78
 
73
79
  /* ---- Geometry ------------------------------------------------------------------ */
74
- const slides = () => Reveal.getSlides();
80
+ const slides = () => deck.slides();
75
81
  const indexOf = (sec) => slides().indexOf(sec);
76
82
  const r1 = (v) => Math.round(v);
77
83
  function frame(sec) {
@@ -122,7 +128,7 @@
122
128
  hasFooter: !!sec.querySelector(".slide-foot"), hidden };
123
129
  }
124
130
  function info() {
125
- return { slides: slides().map((s, i) => slideInfo(s, i)), current: indexOf(Reveal.getCurrentSlide()) };
131
+ return { slides: slides().map((s, i) => slideInfo(s, i)), current: indexOf(deck.current()) };
126
132
  }
127
133
 
128
134
  /* ---- Layout measurement ------------------------------------------------------------- */
@@ -138,7 +144,7 @@
138
144
  const hidden = cs.display === "none" || cs.visibility === "hidden" || +cs.opacity === 0 || (b.w === 0 && b.h === 0);
139
145
  const leaf = !el.querySelector("[data-eid]");
140
146
  const own = el === sec ? "" : textOf(el, !leaf);
141
- const tag = el.tagName.toLowerCase() + (el.classList.length ? "." + [...el.classList].filter((c) => !/^(fragment|visible|current-fragment|present|past|future)$/.test(c)).join(".") : "");
147
+ const tag = el.tagName.toLowerCase() + (el.classList.length ? "." + [...el.classList].filter((c) => !/^(fragment|visible|current-fragment|present|past|future)$/.test(c) && !c.startsWith("__")).join(".") : "");
142
148
  const e = { eid: el.dataset.eid || si.eid, tag: tag.replace(/\.$/, ""), depth, ...b };
143
149
  if (own && el.tagName !== "IMG" && el !== sec) e.font = fontOf(el);
144
150
  if (own) e.text = own;
@@ -218,13 +224,8 @@
218
224
  const nextFrame = () => Promise.resolve();
219
225
 
220
226
  /* ---- Navigation ------------------------------------------------------------------------ */
221
- function goto(index) {
222
- const sec = slides()[index];
223
- if (!sec) return;
224
- const ix = Reveal.getIndices(sec);
225
- Reveal.slide(ix.h, ix.v);
226
- }
227
- function indices(index) { const sec = slides()[index]; return sec ? Reveal.getIndices(sec) : { h: 0, v: 0 }; }
227
+ const goto = (index) => deck.goto(index);
228
+ const presentHash = (index) => deck.hash(index); // the slide's address in the deck opened on its own
228
229
 
229
230
  /* ---- Pins and hover ---------------------------------------------------------------------- */
230
231
  let mode = "select", pins = [], editable = true; // mode: "select" (drag, resize) or "pin"
@@ -249,12 +250,12 @@
249
250
  }
250
251
  function targetOf(t) {
251
252
  if (!t || !t.closest) return null;
252
- const sec = t.closest(".reveal .slides section");
253
+ const sec = t.closest(SLIDE);
253
254
  if (!sec || t.closest(".__pin, .__hover, .__sel, .__guide")) return null;
254
255
  const foot = t.closest(".slide-foot");
255
256
  const el = foot ? sec : t.closest("[data-eid]") || sec;
256
257
  const locked = foot ? "footer, set by brand.css (locked)" : el !== sec && el.matches(LOCKED_SEL) ? "positioned by brand.css (locked)" : null;
257
- return { sec: el === sec ? sec : el.closest("section"), el, locked, foot };
258
+ return { sec: el === sec ? sec : el.closest(SLIDE_OF), el, locked, foot };
258
259
  }
259
260
  function onPointerDown(e) {
260
261
  if (MODE !== "main" || e.button !== 0) return;
@@ -309,7 +310,7 @@
309
310
  document.querySelectorAll(".__pin").forEach((p) => p.remove());
310
311
  for (const p of pins) {
311
312
  const sec = byEid(p.slide.eid);
312
- if (!sec || sec.tagName !== "SECTION") continue;
313
+ if (!sec || !sec.matches(SLIDE_OF)) continue;
313
314
  let x = p.point.x, y = p.point.y;
314
315
  const el = p.eid !== p.slide.eid ? byEid(p.eid, sec) : null;
315
316
  if (el && el.getClientRects().length) {
@@ -328,8 +329,8 @@
328
329
  function highlight(eid) {
329
330
  const el = byEid(eid);
330
331
  if (!el) return;
331
- const sec = el.tagName === "SECTION" ? el : el.closest("section");
332
- if (sec !== Reveal.getCurrentSlide()) return;
332
+ const sec = el.closest(SLIDE_OF);
333
+ if (sec !== deck.current()) return;
333
334
  const b = boxOf(el, sec);
334
335
  const f = document.createElement("div");
335
336
  f.className = "__flash";
@@ -396,8 +397,8 @@
396
397
  }
397
398
  function selectEid(eid) {
398
399
  const el = eid && byEid(eid);
399
- const sec = el?.closest("section");
400
- if (!el || sec !== Reveal.getCurrentSlide()) return select(null, null);
400
+ const sec = el?.closest(SLIDE_OF);
401
+ if (!el || sec !== deck.current()) return select(null, null);
401
402
  select(el, sec);
402
403
  }
403
404
 
@@ -698,8 +699,8 @@
698
699
  const h = document.createElement("div");
699
700
  h.className = "__edit-hint";
700
701
  h.textContent = "Editing · Enter to save · Esc to cancel · Shift+Enter new line";
701
- Object.assign(h.style, b.y < 30 ? { left: b.x + "px", top: `calc(${b.y + b.h}px + 6px*var(--u,1))` }
702
- : { left: b.x + "px", top: `calc(${b.y}px - 22px*var(--u,1))` });
702
+ Object.assign(h.style, b.y < 30 ? { left: b.x + "px", top: `calc(${b.y + b.h}px + 6px*var(--pindeck-u,1))` }
703
+ : { left: b.x + "px", top: `calc(${b.y}px - 22px*var(--pindeck-u,1))` });
703
704
  editing.sec.appendChild(h);
704
705
  }
705
706
  function onEditKey(e) {
@@ -783,7 +784,7 @@
783
784
 
784
785
  /* A free id for a new bullet on the same slide ("summary/li-4"); the server double-checks. */
785
786
  function newBulletEid(el) {
786
- const prefix = el.closest("section").dataset.eid;
787
+ const prefix = el.closest(SLIDE_OF).dataset.eid;
787
788
  const used = new Set([...document.querySelectorAll("[data-eid]")].map((e) => e.dataset.eid));
788
789
  let id = `${prefix}/li`, k = 2;
789
790
  while (used.has(id)) id = `${prefix}/li-${k++}`;
@@ -843,18 +844,183 @@
843
844
  })));
844
845
  }
845
846
 
847
+ /* ---- Decks -----------------------------------------------------------------------------------------
848
+ What the editor needs from a deck: its slides, the current one, going to a slide, the slide size and
849
+ scale, and events when the slide or the scale changes. reveal.js provides all of it. */
850
+ function revealDeck() {
851
+ const ix = (index) => { const sec = Reveal.getSlides()[index]; return sec ? Reveal.getIndices(sec) : { h: 0, v: 0 }; };
852
+ return {
853
+ slides: () => Reveal.getSlides(),
854
+ current: () => Reveal.getCurrentSlide(),
855
+ goto(index) { if (Reveal.getSlides()[index]) { const i = ix(index); Reveal.slide(i.h, i.v); } },
856
+ hash(index) { const i = ix(index); return `#/${i.h}${i.v ? "/" + i.v : ""}`; },
857
+ size: () => ({ width: Reveal.getConfig().width, height: Reveal.getConfig().height }),
858
+ scale: () => Reveal.getScale() || 1,
859
+ on: (ev, fn) => Reveal.on(ev === "slide" ? "slidechanged" : ev, fn),
860
+ prepare() { // no transitions, every build step shown; the measure and headless copies fill their frame
861
+ const base = { transition: "none", backgroundTransition: "none", fragments: false };
862
+ if (MODE === "main") Reveal.configure({ ...base });
863
+ else Reveal.configure({ ...base, margin: 0, keyboard: false });
864
+ },
865
+ viewport(width, height) { // lay the deck out for this size whatever the window's
866
+ Object.assign(Reveal.getRevealElement().style, { width: width + "px", height: height + "px" });
867
+ Reveal.layout();
868
+ },
869
+ ready: (fn) => (Reveal.isReady() ? fn() : Reveal.on("ready", fn)),
870
+ keys() {}, // reveal.js's own keyboard handling stays
871
+ };
872
+ }
873
+
874
+ /* A deck made without reveal.js, whose slides <meta name="pindeck:slides"> names (and its size
875
+ <meta name="pindeck:size">). PinDeck shows the slides itself, one at a time and scaled to the frame,
876
+ over whatever the deck's own script does: the elements around the slides lose their transforms, a
877
+ backdrop covers the rest of the page, and the deck's script gets no keys while the editor has them.
878
+ The classes and styles added here live in the page only; the deck file is never written from the DOM. */
879
+ function selectorDeck() {
880
+ const root = document.documentElement;
881
+ const found = () => { const all = [...document.querySelectorAll(SLIDE_SEL)]; return all.filter((s) => !all.some((o) => o !== s && s.contains(o))); };
882
+ let list = [], cur = 0, scale = 1, sz = null, view = null;
883
+ const listeners = { slide: [], resize: [] };
884
+ const emit = (ev) => listeners[ev].forEach((fn) => fn());
885
+ function mark() { // the slides, and the elements between them and <body>
886
+ list = found();
887
+ for (const s of list) {
888
+ s.classList.add("__pindeck_slide");
889
+ for (let a = s.parentElement; a && a !== document.body && a !== root; a = a.parentElement) a.classList.add("__pindeck_anc");
890
+ }
891
+ }
892
+ function size() { // the first call also finds the slides
893
+ if (sz) return sz;
894
+ mark();
895
+ // the server refuses a deck without a readable size, so the fallback is only for a tag removed meanwhile
896
+ const m = /^(\d{2,5})\s*[x×*,\s]\s*(\d{2,5})$/i.exec(deckMeta("size") || "");
897
+ sz = m ? { width: +m[1], height: +m[2] } : { width: 1280, height: 720 };
898
+ return sz;
899
+ }
900
+ // what takes a slide and its containers out of the deck's own layout (scaling, stacking, page views)
901
+ const LOOSE = "transform:none !important; translate:none !important; scale:none !important; rotate:none !important; " +
902
+ "zoom:1 !important; filter:none !important; backdrop-filter:none !important; perspective:none !important; " +
903
+ "contain:none !important; will-change:auto !important; opacity:1 !important; isolation:auto !important; " +
904
+ "mix-blend-mode:normal !important; clip-path:none !important; transition:none !important;";
905
+ function addStyle(text) { const st = document.createElement("style"); st.textContent = text; document.head.appendChild(st); }
906
+ function fit() {
907
+ const { width, height } = size(), m = MODE === "main" ? 0.04 : 0; // the margin reveal.js leaves in the editor
908
+ const vw = view?.width ?? innerWidth, vh = view?.height ?? innerHeight;
909
+ scale = Math.min(vw * (1 - m) / width, vh * (1 - m) / height) || 1;
910
+ root.style.setProperty("--pindeck-s", String(scale));
911
+ root.style.setProperty("--pindeck-x", `${(vw - width * scale) / 2}px`);
912
+ root.style.setProperty("--pindeck-y", `${(vh - height * scale) / 2}px`);
913
+ }
914
+ // a slide the deck's own CSS hides with display: none is shown as a block
915
+ function display(s) { s.classList.remove("__pindeck_block"); if (getComputedStyle(s).display === "none") s.classList.add("__pindeck_block"); }
916
+ function show(i) {
917
+ const s = list[i];
918
+ if (!s) return false;
919
+ const changed = i !== cur;
920
+ list[cur]?.classList.remove("__pindeck_cur");
921
+ cur = i;
922
+ s.classList.add("__pindeck_cur");
923
+ s.removeAttribute("inert"); s.removeAttribute("hidden"); // the deck's script marks the slides it does not show
924
+ display(s);
925
+ return changed;
926
+ }
927
+ return {
928
+ slides: () => list,
929
+ current: () => list[cur],
930
+ goto(index) { if (show(index)) emit("slide"); },
931
+ hash(index) { const s = list[index]; return s?.id ? `#${encodeURIComponent(s.id)}` : `#${index + 1}`; },
932
+ size,
933
+ scale: () => scale,
934
+ on: (ev, fn) => listeners[ev]?.push(fn),
935
+ prepare() {
936
+ const { width, height } = size();
937
+ addStyle(`
938
+ html.__pindeck_deck, html.__pindeck_deck body { overflow:hidden !important; }
939
+ html.__pindeck_deck .__pindeck_anc { ${LOOSE} position:static !important; z-index:auto !important; }
940
+ html.__pindeck_deck .__pindeck_slide:not(.__pindeck_cur) { visibility:hidden !important; }
941
+ html.__pindeck_deck .__pindeck_slide.__pindeck_cur { ${LOOSE} position:fixed !important; inset:0 auto auto 0 !important;
942
+ margin:0 !important; width:${width}px !important; height:${height}px !important; min-width:0 !important; min-height:0 !important;
943
+ max-width:none !important; max-height:none !important; transform-origin:0 0 !important;
944
+ transform:translate(var(--pindeck-x, 0px), var(--pindeck-y, 0px)) scale(var(--pindeck-s, 1)) !important;
945
+ visibility:visible !important; z-index:2147483001 !important; }
946
+ html.__pindeck_deck .__pindeck_slide.__pindeck_block { display:block !important; }
947
+ html.__pindeck_deck .__pindeck_backdrop { position:fixed; inset:0; z-index:2147483000; background:var(--pindeck-bg, #fff); }
948
+ html.__pindeck_deck aside.notes { display:none !important; }`);
949
+ // the backdrop has the page's own background
950
+ const bg = [document.body, root].map((e) => getComputedStyle(e).backgroundColor).find((c) => !/^(transparent|rgba\(.*,\s*0\))$/.test(c));
951
+ if (bg) root.style.setProperty("--pindeck-bg", bg);
952
+ const backdrop = document.createElement("div");
953
+ backdrop.className = "__pindeck_backdrop";
954
+ document.body.appendChild(backdrop);
955
+ root.classList.add("__pindeck_deck");
956
+ fit();
957
+ show(cur);
958
+ addEventListener("resize", () => { fit(); emit("resize"); });
959
+ // the deck's script may hide the slide again (when it changes slides itself)
960
+ new MutationObserver((recs) => { if (recs.some((r) => r.target === list[cur])) { list[cur].removeAttribute("inert"); list[cur].removeAttribute("hidden"); display(list[cur]); } })
961
+ .observe(document.body, { subtree: true, attributes: true, attributeFilter: ["inert", "hidden", "style"] });
962
+ },
963
+ viewport(width, height) { view = { width, height }; fit(); }, // lay the slide out for this size whatever the window's
964
+ ready: (fn) => (document.readyState === "complete" ? fn() : addEventListener("load", () => fn(), { once: true })),
965
+ keys() { // after the editor's own key handler: slide keys change the slide here, and the deck's script gets none
966
+ const field = (t) => t.closest?.("input, textarea, select, [contenteditable]");
967
+ const STEP = { ArrowRight: 1, ArrowDown: 1, PageDown: 1, ArrowLeft: -1, ArrowUp: -1, PageUp: -1 };
968
+ addEventListener("keydown", (e) => {
969
+ if (field(e.target)) return;
970
+ if (!e.metaKey && !e.ctrlKey && !e.altKey) {
971
+ const k = e.key, step = k === " " ? (e.shiftKey ? -1 : 1) : STEP[k];
972
+ const to = step ? cur + step : k === "Home" ? 0 : k === "End" ? list.length - 1 : null;
973
+ if (to != null) { e.preventDefault(); this.goto(Math.max(0, Math.min(list.length - 1, to))); }
974
+ }
975
+ e.stopImmediatePropagation();
976
+ }, true);
977
+ for (const t of ["keyup", "keypress"]) addEventListener(t, (e) => { if (!field(e.target)) e.stopImmediatePropagation(); }, true);
978
+ },
979
+ /* ?editor=pdf: every slide as a page of its own size, for PinDeck's PDF export (lib/pdf.mjs prints
980
+ this page's DOM as it is once laid out, so the layout is all CSS) */
981
+ print() {
982
+ const { width, height } = size();
983
+ const all = params.get("showHiddenSlides") === "true";
984
+ const shown = list.filter((s) => all || s.getAttribute("data-visibility") !== "hidden");
985
+ for (const s of list) { s.classList.add("pdf-page"); if (!shown.includes(s)) s.classList.add("__pindeck_skip"); }
986
+ shown.at(-1)?.classList.add("__pindeck_last");
987
+ const OUT = ":not(.__pindeck_anc):not(.__pindeck_slide)";
988
+ addStyle(`
989
+ @page { size:${width}px ${height}px; margin:0; }
990
+ html.__pindeck_print, html.__pindeck_print body { margin:0 !important; padding:0 !important; width:auto !important;
991
+ height:auto !important; overflow:visible !important; }
992
+ html.__pindeck_print .__pindeck_anc { ${LOOSE} position:static !important; display:block !important; width:auto !important;
993
+ height:auto !important; margin:0 !important; padding:0 !important; border:0 !important; overflow:visible !important; }
994
+ html.__pindeck_print body > ${OUT}:not(svg), html.__pindeck_print .__pindeck_anc > ${OUT}:not(svg) { display:none !important; }
995
+ html.__pindeck_print body > svg${OUT}, html.__pindeck_print .__pindeck_anc > svg${OUT} { position:absolute !important;
996
+ width:0 !important; height:0 !important; overflow:hidden !important; }
997
+ html.__pindeck_print .__pindeck_slide { ${LOOSE} position:relative !important; inset:auto !important; margin:0 !important;
998
+ width:${width}px !important; height:${height}px !important; max-width:none !important; max-height:none !important;
999
+ visibility:visible !important; overflow:hidden !important; break-inside:avoid; break-after:page; page-break-after:always; }
1000
+ html.__pindeck_print .__pindeck_slide.__pindeck_last { break-after:auto; page-break-after:auto; }
1001
+ html.__pindeck_print .__pindeck_slide.__pindeck_block { display:block !important; }
1002
+ html.__pindeck_print .__pindeck_skip { display:none !important; }
1003
+ html.__pindeck_print aside.notes { display:none !important; }`);
1004
+ root.classList.add("__pindeck_print");
1005
+ shown.forEach(display);
1006
+ },
1007
+ };
1008
+ }
1009
+
846
1010
  /* ---- Start ------------------------------------------------------------------------------------- */
847
1011
  async function start() {
848
- W = Reveal.getConfig().width; H = Reveal.getConfig().height;
849
- const base = { transition: "none", backgroundTransition: "none", fragments: false };
850
- const setUnit = () => document.documentElement.style.setProperty("--u", String(1 / (Reveal.getScale() || 1)));
851
- Reveal.on("resize", setUnit);
852
- if (MODE === "main") Reveal.configure({ ...base });
853
- else Reveal.configure({ ...base, margin: 0, keyboard: false });
1012
+ ({ width: W, height: H } = deck.size());
1013
+ const setUnit = () => document.documentElement.style.setProperty("--pindeck-u", String(1 / (deck.scale() || 1)));
1014
+ deck.on("resize", setUnit);
1015
+ deck.prepare();
854
1016
  setUnit();
855
1017
  await document.fonts.ready;
856
1018
 
857
1019
  if (MODE === "shot" || MODE === "dump") {
1020
+ // the size of headless Chrome's window, which the page itself gets only once the screenshot is
1021
+ // taken: lay the deck out for it now rather than letterboxed into the shorter viewport
1022
+ const vw = +params.get("width"), vh = +params.get("height");
1023
+ if (vw > 0 && vh > 0) { deck.viewport(vw, vh); setUnit(); }
858
1024
  const sec = byEid(params.get("slide") || "") || slides()[0];
859
1025
  goto(indexOf(sec));
860
1026
  await nextFrame();
@@ -872,11 +1038,11 @@
872
1038
  }
873
1039
 
874
1040
  const api = {
875
- info, goto, indices, highlight, reloadCss,
1041
+ info, goto, presentHash, highlight, reloadCss,
876
1042
  setMode, selectEid, nudge, editSelection,
877
1043
  // a new slide: select its title and open it for typing
878
1044
  editTitle() {
879
- const sec = Reveal.getCurrentSlide();
1045
+ const sec = deck.current();
880
1046
  const h = sec?.querySelector("h1[data-eid], h2[data-eid], h3[data-eid]");
881
1047
  if (!h || mode !== "select") return;
882
1048
  select(h, sec);
@@ -942,22 +1108,35 @@
942
1108
  host()?.key(k, { meta, shift: e.shiftKey });
943
1109
  }
944
1110
  }, true);
945
- Reveal.on("slidechanged", () => { if (editing) finishEdit(true); hideHover(); if (sel) select(null, null); host()?.slideChanged(indexOf(Reveal.getCurrentSlide())); });
946
- Reveal.on("resize", () => { if (sel) drawSel(); });
1111
+ deck.on("slide", () => {
1112
+ if (editing) finishEdit(true); hideHover(); if (sel) select(null, null);
1113
+ // a deck made without reveal.js lays out only the current slide at the slide size: pins on the
1114
+ // others were placed in another layout
1115
+ if (SLIDE_SEL) renderPins(pins);
1116
+ host()?.slideChanged(indexOf(deck.current()));
1117
+ });
1118
+ deck.on("resize", () => { if (sel) drawSel(); });
947
1119
  setMode("select");
948
1120
  // a banner on hidden slides, so nobody edits one without noticing (only in the editor view)
949
1121
  for (const sec of slides()) {
950
1122
  if (sec.getAttribute("data-visibility") !== "hidden") continue;
951
1123
  const b = document.createElement("div");
952
1124
  b.className = "__hidden-banner";
953
- b.textContent = "Hidden slide · skipped when presenting and in PDFs";
1125
+ b.textContent = SLIDE_SEL ? "Hidden slide · skipped in PDFs" : "Hidden slide · skipped when presenting and in PDFs";
954
1126
  sec.appendChild(b);
955
1127
  }
956
1128
  const startAt = +params.get("start");
957
1129
  if (startAt) goto(startAt);
1130
+ deck.keys();
958
1131
  }
959
1132
  host()?.bridgeReady(MODE, api, window);
960
1133
  }
961
- if (typeof Reveal === "undefined") return;
962
- if (Reveal.isReady()) start(); else Reveal.on("ready", start);
1134
+ if (SLIDE_SEL) {
1135
+ deck = selectorDeck();
1136
+ deck.ready(() => (MODE === "pdf" ? deck.print() : start()));
1137
+ return;
1138
+ }
1139
+ if (typeof Reveal === "undefined" || MODE === "pdf") return; // reveal.js lays out its own print view
1140
+ deck = revealDeck();
1141
+ deck.ready(start);
963
1142
  })();
package/public/editor.js CHANGED
@@ -31,7 +31,8 @@ let measure = null; // { iframe, bridge }
31
31
  let measureReady = null, measureResolve = null;
32
32
 
33
33
  // showHiddenSlides: PinDeck shows hidden slides so they can be edited (reveal.js drops them otherwise)
34
- function deckSrc(mode, extra = "") { return `/deck/index.html?editor=${mode}&showHiddenSlides=true${extra}`; }
34
+ // /deck/ is the deck file the server was started with (index.html, or a named file such as talk.html)
35
+ function deckSrc(mode, extra = "") { return `/deck/?editor=${mode}&showHiddenSlides=true${extra}`; }
35
36
 
36
37
  function loadMain() {
37
38
  const f = document.createElement("iframe");
@@ -228,7 +229,8 @@ function renderSlides() {
228
229
  ${s.kind !== "content" ? `<span class="kind">${esc(s.kind.replace(/-slide$/, ""))}</span>` : ""}
229
230
  ${s.hidden ? '<span class="kind hid">hidden</span>' : ""}</span>
230
231
  ${counts[i] ? `<span class="badge" title="${counts[i]} pin(s)">${counts[i]}</span>` : "<span></span>"}
231
- <button class="eye" data-eye="${i}" title="${s.hidden ? "Show this slide again" : "Hide this slide (skipped when presenting)"}"
232
+ <button class="eye" data-eye="${i}" title="${s.hidden ? "Show this slide again" : state.status?.slideSelector ?
233
+ "Hide this slide (left out of PDFs)" : "Hide this slide (skipped when presenting)"}"
232
234
  aria-label="${s.hidden ? "Show" : "Hide"} slide ${i + 1}">${s.hidden ? EYE_ICON : EYE_OFF_ICON}</button>
233
235
  </li>`).join("");
234
236
  $("slide-list").querySelector("li.active")?.scrollIntoView({ block: "nearest" });
@@ -491,8 +493,7 @@ $("export-menu").addEventListener("click", async (e) => {
491
493
  });
492
494
 
493
495
  $("present").onclick = () => {
494
- const ix = main?.bridge.indices(state.current) || { h: 0, v: 0 };
495
- window.open(`/deck/index.html#/${ix.h}${ix.v ? "/" + ix.v : ""}`, "_blank");
496
+ window.open(`/deck/${main?.bridge.presentHash(state.current) || ""}`, "_blank");
496
497
  };
497
498
 
498
499
  /* ---- Pins --------------------------------------------------------------------------------------- */
@@ -593,13 +594,14 @@ function entryHtml(e) {
593
594
  if (e.error) parts.push(`<span class="warn">${esc(e.error)}</span>`);
594
595
  if (e.files?.length) {
595
596
  parts.push(`<span class="files ${t?.undone ? "undone" : ""}">Changed ${e.files.map((f) =>
596
- `${esc(f.file)} <span class="add">+${f.added}</span> <span class="del">−${f.removed}</span>`).join(", ")}</span>`);
597
+ `${esc(f.file)} ` + (f.binary ? `<span class="add">(binary)</span>`
598
+ : `<span class="add">+${f.added}</span> <span class="del">−${f.removed}</span>`)).join(", ")}</span>`);
597
599
  parts.push(`<button class="small" data-diff="${e.turnId}">Diff</button>`);
598
600
  if (t && !t.undone) parts.push(`<button class="small" data-undo="${e.turnId}">Undo</button>`);
599
601
  if (t?.undone) parts.push("<span>undone</span>");
600
602
  } else if (!e.error) parts.push("No file changes");
601
603
  if (e.restored?.length) parts.push(`<span class="warn">${esc(e.restored.join(", "))} is locked; the change was reverted.</span>`);
602
- if (e.denials?.length) parts.push(`<span class="warn" title="PinDeck lets Claude run only its own commands (layout, shot, slides, pdf) and edit index.html and theme.css">` +
604
+ if (e.denials?.length) parts.push(`<span class="warn" title="PinDeck lets Claude run only its own commands (layout, shot, slides, pdf) and edit ${esc(state.status?.deckFile || "index.html")} and theme.css">` +
603
605
  `Not allowed in PinDeck, so skipped: ${e.denials.map((d) => esc(`${d.name} ${d.detail}`)).join(", ")}</span>`);
604
606
  if (e.duration) parts.push(`<span class="meta">${(e.duration / 1000).toFixed(1)} s</span>`);
605
607
  return parts.join(" ");
@@ -737,14 +739,17 @@ document.addEventListener("keydown", (e) => {
737
739
  });
738
740
 
739
741
  /* ---- Server events ---------------------------------------------------------------------------------- */
742
+ let shownProblem = null; // the deck problem last shown, so a status update does not repeat it
740
743
  function applyStatus(s) {
741
744
  state.status = s;
742
- // the deck's name in the header ("ocean-example"), its full path on hover
745
+ // the deck's name in the header ("ocean-example", or "talk.html" for a named deck file), its full path on hover
743
746
  if (s.deck) {
744
747
  const folder = s.deck.split(/[\\/]/).filter(Boolean).pop() || s.deck;
745
- $("deck-name").textContent = folder;
748
+ const name = s.deckFile && s.deckFile !== "index.html" ? s.deckFile : folder;
749
+ $("deck-name").textContent = name;
746
750
  $("deck-name").title = `${s.deck}/${s.deckFile || "index.html"}`;
747
- document.title = `${folder} · PinDeck`;
751
+ if ($("deck-file")) $("deck-file").textContent = s.deckFile || "index.html";
752
+ document.title = `${name} · PinDeck`;
748
753
  }
749
754
  setBusy(!!s.busy);
750
755
  $("model").textContent = modelLabel(s);
@@ -773,6 +778,9 @@ function applyStatus(s) {
773
778
  $("server-note").textContent = caps.label ? `Server: ${caps.label}` : "";
774
779
  $("server-note").title = off.length ? `Not available on this server:\n${off.join("\n")}\n\nWhy:\n${reasons.join("\n")}` : "";
775
780
  $("add-slide-btn").disabled = !!s.busy;
781
+ // the deck's slide tags stopped working while PinDeck runs (at start, such a deck is refused)
782
+ if (s.deckProblem && s.deckProblem !== shownProblem) toast(s.deckProblem, { ms: 20000 });
783
+ shownProblem = s.deckProblem;
776
784
  renderToolbar();
777
785
  }
778
786
  function connect() {
package/public/index.html CHANGED
@@ -95,7 +95,7 @@
95
95
  double-click to change the text. A selected element is what Claude takes “this” to mean.</p>
96
96
  <p>Add figures, audio, a PDF or a screenshot with <strong>+</strong>, or drag and drop or paste a file; Claude can put it on a slide
97
97
  or, for a paper or report, take the details from it.</p>
98
- <p>Claude edits <code>index.html</code> and <code>theme.css</code>, checks the layout, and the slide updates here.
98
+ <p>Claude edits <code id="deck-file">index.html</code> and <code>theme.css</code>, checks the layout, and the slide updates here.
99
99
  <code>brand.css</code> is locked.</p>
100
100
  </div>
101
101
  </div>