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/README.md +123 -20
- package/cli.mjs +2 -1
- package/lib/capabilities.mjs +13 -7
- package/lib/deck-files.mjs +38 -0
- package/lib/eids.mjs +101 -8
- package/lib/frameworks.mjs +59 -0
- package/lib/instructions.mjs +74 -0
- package/lib/pptx-render.mjs +78 -9
- package/lib/system-prompt.md +5 -3
- package/lib/waveform.mjs +9 -3
- package/package.json +1 -1
- package/public/bridge.js +248 -69
- package/public/editor.js +17 -9
- package/public/index.html +1 -1
- package/server.mjs +207 -61
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
|
|
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
|
|
41
|
-
html.__pinmode
|
|
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
|
|
60
|
-
html.__selmode
|
|
61
|
-
html.__selmode
|
|
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 = () =>
|
|
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(
|
|
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
|
-
|
|
222
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
332
|
-
if (sec !==
|
|
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(
|
|
400
|
-
if (!el || sec !==
|
|
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(
|
|
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
|
|
849
|
-
const
|
|
850
|
-
|
|
851
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
946
|
-
|
|
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 (
|
|
962
|
-
|
|
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
|
-
|
|
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" :
|
|
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
|
-
|
|
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)}
|
|
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
|
-
|
|
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
|
-
|
|
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>
|