pindeck 3.0.0-beta.1 → 3.0.0-beta.3

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.css CHANGED
@@ -65,6 +65,7 @@ code { font: 12.5px ui-monospace, SFMono-Regular, Menlo, monospace; background:
65
65
  }
66
66
  .pos { font-variant-numeric: tabular-nums; color: var(--muted); min-width: 52px; text-align: center; }
67
67
  .deck-name { min-width: 0; margin-left: 8px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
68
+ .server-note { flex: none; margin-left: 8px; padding: 1px 6px; border-radius: 4px; font-size: 12px; color: #6b4a12; background: #fbf0dc; cursor: help; }
68
69
  .toggle { display: inline-flex; align-items: center; gap: 6px; }
69
70
  .toggle .dot { display: inline-block; width: 18px; height: 18px; border-radius: 50%; background: var(--orange); color: #fff;
70
71
  font-size: 11px; font-weight: 700; line-height: 18px; text-align: center; }