glideshow 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. glideshow/__init__.py +15 -0
  2. glideshow/__main__.py +3 -0
  3. glideshow/agent.py +681 -0
  4. glideshow/build.py +54 -0
  5. glideshow/cli.py +320 -0
  6. glideshow/comments.py +35 -0
  7. glideshow/decks.py +230 -0
  8. glideshow/docs/agents.md +223 -0
  9. glideshow/docs/deck-format.md +125 -0
  10. glideshow/editor/.assetsignore +5 -0
  11. glideshow/editor/apple-touch-icon.png +0 -0
  12. glideshow/editor/data/font-metrics.json +1 -0
  13. glideshow/editor/data/fonts.json +1 -0
  14. glideshow/editor/data/icons.json +1 -0
  15. glideshow/editor/data/symbols.js +42 -0
  16. glideshow/editor/data/themes.js +15 -0
  17. glideshow/editor/deck/base.css +92 -0
  18. glideshow/editor/deck/present.js +222 -0
  19. glideshow/editor/deck/shapes.js +377 -0
  20. glideshow/editor/deck/template.html +37 -0
  21. glideshow/editor/editor.css +657 -0
  22. glideshow/editor/glideshow-1024.png +0 -0
  23. glideshow/editor/glideshow.svg +10 -0
  24. glideshow/editor/globals.d.ts +21 -0
  25. glideshow/editor/home.html +358 -0
  26. glideshow/editor/index.html +386 -0
  27. glideshow/editor/js/arrange.js +122 -0
  28. glideshow/editor/js/boot.js +66 -0
  29. glideshow/editor/js/boxes.js +25 -0
  30. glideshow/editor/js/comments.js +85 -0
  31. glideshow/editor/js/context-menus.js +93 -0
  32. glideshow/editor/js/dialogs.js +168 -0
  33. glideshow/editor/js/dock.js +22 -0
  34. glideshow/editor/js/draw.js +107 -0
  35. glideshow/editor/js/file.js +318 -0
  36. glideshow/editor/js/find.js +72 -0
  37. glideshow/editor/js/format-panel.js +101 -0
  38. glideshow/editor/js/geometry.js +153 -0
  39. glideshow/editor/js/guides.js +37 -0
  40. glideshow/editor/js/insert.js +73 -0
  41. glideshow/editor/js/keyboard.js +256 -0
  42. glideshow/editor/js/live.js +48 -0
  43. glideshow/editor/js/menubar.js +290 -0
  44. glideshow/editor/js/menus.js +93 -0
  45. glideshow/editor/js/motion.js +148 -0
  46. glideshow/editor/js/notes.js +63 -0
  47. glideshow/editor/js/objects.js +202 -0
  48. glideshow/editor/js/people.js +73 -0
  49. glideshow/editor/js/phone-comments.js +43 -0
  50. glideshow/editor/js/phone-edit.js +253 -0
  51. glideshow/editor/js/phone-home.js +51 -0
  52. glideshow/editor/js/phone-mode.js +73 -0
  53. glideshow/editor/js/phone-sheets.js +153 -0
  54. glideshow/editor/js/phone-touch.js +145 -0
  55. glideshow/editor/js/phone-view.js +150 -0
  56. glideshow/editor/js/pointer.js +407 -0
  57. glideshow/editor/js/render.js +353 -0
  58. glideshow/editor/js/shortcuts.js +67 -0
  59. glideshow/editor/js/slides.js +120 -0
  60. glideshow/editor/js/spacing.js +23 -0
  61. glideshow/editor/js/state.js +58 -0
  62. glideshow/editor/js/storage.js +250 -0
  63. glideshow/editor/js/text.js +366 -0
  64. glideshow/editor/js/theme-toggle.js +41 -0
  65. glideshow/editor/js/theme.js +173 -0
  66. glideshow/editor/js/toolbar-groups.js +109 -0
  67. glideshow/editor/js/toolbar.js +372 -0
  68. glideshow/editor/lib/browser-db.js +47 -0
  69. glideshow/editor/lib/library.js +612 -0
  70. glideshow/editor/lib/merge.js +174 -0
  71. glideshow/editor/lib/move-local.js +51 -0
  72. glideshow/editor/lib/onefile.js +108 -0
  73. glideshow/editor/lib/store-browser.js +75 -0
  74. glideshow/editor/lib/store-folder.js +109 -0
  75. glideshow/editor/local/sw.js +66 -0
  76. glideshow/editor/local/tsconfig.json +5 -0
  77. glideshow/editor/looks/glider.svg +9 -0
  78. glideshow/editor/looks/thermal-phone.css +144 -0
  79. glideshow/editor/looks/thermal.css +268 -0
  80. glideshow/editor/manifest.json +23 -0
  81. glideshow/editor/phone.css +345 -0
  82. glideshow/editor/share/share.css +124 -0
  83. glideshow/editor/share/share.js +256 -0
  84. glideshow/editor/share/speaker.html +88 -0
  85. glideshow/editor/tsconfig.json +6 -0
  86. glideshow/figure.py +33 -0
  87. glideshow/fonts.py +63 -0
  88. glideshow/gslides.py +1065 -0
  89. glideshow/history.py +210 -0
  90. glideshow/merge.py +210 -0
  91. glideshow/osutil.py +44 -0
  92. glideshow/pptx_export.py +207 -0
  93. glideshow/pptx_import.py +115 -0
  94. glideshow/publish.py +106 -0
  95. glideshow/render.py +288 -0
  96. glideshow/server.py +549 -0
  97. glideshow/service.py +65 -0
  98. glideshow/skills/glideshow/SKILL.md +85 -0
  99. glideshow-0.1.2.dist-info/METADATA +69 -0
  100. glideshow-0.1.2.dist-info/RECORD +103 -0
  101. glideshow-0.1.2.dist-info/WHEEL +4 -0
  102. glideshow-0.1.2.dist-info/entry_points.txt +2 -0
  103. glideshow-0.1.2.dist-info/licenses/LICENSE +662 -0
@@ -0,0 +1,92 @@
1
+ /* Box model shared by the editor and every deck. The editor copies this file into each deck's
2
+ <style id="deck-base"> on save, so edit it here, not in a deck. */
3
+ .reveal .slides > section { width: var(--slide-w, 1920px); height: var(--slide-h, 1080px); padding: 0; top: 0; left: 0; } /* page size: data-size on .slides (shapes.js) */
4
+ /* The box centres its text vertically; .content itself is ordinary flowing text, so inline formatting
5
+ (bold words, links, highlights) stays on one line. */
6
+ .box { position: absolute; box-sizing: border-box; display: flex; flex-direction: column; justify-content: center; }
7
+ .box:not([data-shape]) { background: var(--fill, transparent); border: var(--sw, 0px) solid var(--stroke, transparent); }
8
+ .box > svg.shape { position: absolute; left: 0; top: 0; width: 100%; height: 100%; min-width: 1px; min-height: 1px; overflow: visible; pointer-events: none; }
9
+ .box > svg.shape .f { fill: var(--fill, #4a86e8); stroke: var(--stroke, none); stroke-width: var(--sw, 0px); }
10
+ .box > svg.shape .l { fill: none; stroke: var(--stroke, #333); stroke-width: var(--sw, 4px); stroke-linecap: round; }
11
+ .box > svg.shape .h { fill: var(--stroke, #333); }
12
+ .box:not([data-shape])[data-dash="dot"] { border-style: dotted; }
13
+ .box:not([data-shape])[data-dash]:not([data-dash="dot"]) { border-style: dashed; }
14
+ .box > svg.shape .hit { fill: none; stroke: transparent; stroke-width: 24px; }
15
+ .box > .content {
16
+ position: relative; display: block; flex: none;
17
+ width: 100%; box-sizing: border-box; padding: 8px 16px;
18
+ font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
19
+ font-size: 40px; line-height: 1.3; color: #222; text-align: left; overflow-wrap: break-word; outline: none;
20
+ }
21
+ .box[data-shape] > .content { text-align: center; }
22
+ .box[data-shape] > .content { width: auto; align-self: stretch; margin: var(--tx-t, 0px) var(--tx-r, 0px) var(--tx-b, 0px) var(--tx-l, 0px); } /* the preset's text rectangle (--tx-* set by shapes.js), as Slides */
23
+ .box[data-shape="wordart"] > .content { opacity: 0; } /* the text is drawn by the SVG; .content keeps it for editing and search */
24
+ .box[data-shape="wordart"] > svg.shape .f { fill: var(--fill, #000); }
25
+ .box.group { display: block; } /* members are positioned inside it, relative to its top-left */
26
+ .box[data-valign="top"] { justify-content: flex-start; }
27
+ .box[data-valign="bottom"] { justify-content: flex-end; }
28
+ .box > .content > * { margin: var(--space-before, 0px) 0 var(--space-after, 0px); } /* paragraph spacing, set on .content */
29
+ /* A superscript or subscript doesn't make its line taller, as in Slides (and Word, Docs): it rides on the line. */
30
+ .box > .content sup, .box > .content sub { line-height: 0; }
31
+ .box > .content ul, .box > .content ol { display: block; margin: 0; padding-left: 1.2em; text-align: left; }
32
+ .box > .content ul { list-style: disc; }
33
+ .box > .content ol { list-style: decimal; }
34
+ /* Nested levels as Slides' defaults: ● ○ ■ and 1. a. i. */
35
+ .box > .content ul ul { list-style-type: circle; }
36
+ .box > .content ul ul ul { list-style-type: square; }
37
+ .box > .content ol ol { list-style-type: lower-alpha; }
38
+ .box > .content ol ol ol { list-style-type: lower-roman; }
39
+ .box > .content li { margin: var(--space-before, 0px) 0 var(--space-after, 0px); }
40
+ /* Lists from Google Slides (data-mk): the marker ends at the paragraph's first-line indent and the text starts at its
41
+ indent, as Slides draws them. --mk: the gap between the two; --glyph: a bullet's own character; --num, --sfx: a
42
+ number's style and suffix; --bl: the font's baseline shift (as its runs get). The marker's box is 3em, right-aligned,
43
+ and takes no room on the line */
44
+ .box > .content [data-mk] { list-style: none; }
45
+ .box > .content [data-mk] > li::before { display: inline-block; width: 3em; text-align: right; margin-left: calc(-3em - var(--mk, 0px));
46
+ margin-right: var(--mk, 0px); content: var(--glyph); position: relative; top: var(--bl, 0px); }
47
+ .box > .content ol[data-mk] > li::before { content: counter(list-item, var(--num, decimal)) var(--sfx, "."); }
48
+ .box > .content a { color: #1a73e8; text-decoration: underline; }
49
+ /* Tables (.box.table): a plain <table> filling the box, 1pt black rules, 0.1in cell padding, as Slides' default */
50
+ .box.table > .content { align-self: stretch; height: 100%; }
51
+ .box > .content table { width: 100%; height: 100%; border-collapse: collapse; table-layout: fixed; }
52
+ .box > .content td, .box > .content th { border: 2.667px solid #000000; padding: 9.6px 19.2px; vertical-align: top; text-align: left; overflow-wrap: anywhere; }
53
+ .box > .content pre {
54
+ font-size: 0.7em; background: #f4f4f4; padding: 0.6em 0.8em; border-radius: 8px;
55
+ overflow: auto; white-space: pre; text-align: left; line-height: 1.4;
56
+ }
57
+ .box > .content pre code { font-family: Menlo, "SF Mono", Consolas, monospace; background: none; padding: 0; }
58
+ /* highlight.js token colours (light, GitHub-like) for <pre><code class="language-…"> */
59
+ .hljs-keyword, .hljs-selector-tag, .hljs-type, .hljs-doctag { color: #cf222e; }
60
+ .hljs-string, .hljs-regexp, .hljs-template-tag { color: #0a3069; }
61
+ .hljs-comment, .hljs-quote { color: #6e7781; font-style: italic; }
62
+ .hljs-number, .hljs-literal, .hljs-attr, .hljs-attribute, .hljs-variable, .hljs-template-variable { color: #0550ae; }
63
+ .hljs-title, .hljs-title.function_, .hljs-section { color: #8250df; }
64
+ .hljs-built_in, .hljs-symbol, .hljs-meta { color: #953800; }
65
+ .hljs-params, .hljs-subst { color: inherit; }
66
+ .box > .content code { font-family: ui-monospace, "SF Mono", Menlo, monospace; }
67
+ .box.image > .content, .box.video > .content, .box.embed > .content { padding: 0; height: 100%; }
68
+ .box.embed iframe { display: block; width: 100%; height: 100%; border: 0; background: #fff; }
69
+ .box.live > .content { padding: 0; height: 100%; } .box.live iframe { display: block; width: 100%; height: 100%; border: 0; background: transparent; }
70
+ .box.video video { display: block; width: 100%; height: 100%; object-fit: contain; background: #000; }
71
+ .box[data-autofit="shrink"] > .content { zoom: var(--fit, 1); } /* --fit set by shapes.js */
72
+ .box[data-crop] > .content { position: relative; overflow: hidden; }
73
+ .box[data-crop] > .content > img { position: absolute; object-fit: fill;
74
+ width: calc(100% / (1 - var(--cl, 0) - var(--cr, 0))); height: calc(100% / (1 - var(--ct, 0) - var(--cb, 0)));
75
+ left: calc(-100% * var(--cl, 0) / (1 - var(--cl, 0) - var(--cr, 0))); top: calc(-100% * var(--ct, 0) / (1 - var(--ct, 0) - var(--cb, 0))); }
76
+ .box.image img { display: block; width: 100%; height: 100%; object-fit: contain; margin: 0; max-width: none; max-height: none; border: 0; box-shadow: none; }
77
+ .box[data-shape="line"] > .content, .box[data-shape="arrowline"] > .content, .box[data-shape="elbow"] > .content, .box[data-shape="curve"] > .content, .box[data-shape="path"] > .content { display: none; }
78
+ /* Flip (data-flip="h|v|hv") composes with the inline rotate transform; text stays readable. */
79
+ .box[data-flip="h"], .box[data-flip="h"] > .content { scale: -1 1; }
80
+ .box[data-flip="v"], .box[data-flip="v"] > .content { scale: 1 -1; }
81
+ .box[data-flip="hv"], .box[data-flip="hv"] > .content { scale: -1 -1; }
82
+ /* --shx/--shy/--shb/--shc are set by shapes.js from data-shadow-*, so the shadow keeps its page direction. */
83
+ .box[data-shadow] { filter: drop-shadow(var(--shx, 0px) var(--shy, 6px) var(--shb, 8px) var(--shc, rgba(0, 0, 0, .3))); }
84
+ /* "Slide from left" (data-transition="sl-left"): reveal's slide transition, entering from the other side. */
85
+ .reveal .slides > section[data-transition="sl-left"].past { transform: translate(150%, 0); }
86
+ .reveal .slides > section[data-transition="sl-left"].future { transform: translate(-150%, 0); }
87
+ /* Object animation "Appear" (class fragment appear): reveal fragment without the fade. */
88
+ .reveal .slides section .fragment.appear { transition: none; }
89
+ .box[data-placeholder]:has(> .content:empty) { display: none; } /* an unfilled layout placeholder: the editor shows "Click to add …", decks show nothing */
90
+
91
+ /* Notes for the AI (AGENTS.md): sticky notes on a slide that only the editor shows */
92
+ .box.note { display: none; }
@@ -0,0 +1,222 @@
1
+ /* Presenting keys as Google Slides, for every deck (copied into <script id="deck-present"> on save, like
2
+ shapes.js). B / W black or white screen, L laser pointer, a slide number then ⏎ to jump, ⏎ next, Esc to
3
+ leave (back to the editor when presenting from it), M a note for the AI (from the editor), F fullscreen and S speaker notes from reveal.js.
4
+ Turns off reveal.js defaults Slides doesn't have: O / Esc overview, V pause, ? help. */
5
+ (function () {
6
+ if (!window.Reveal) return;
7
+
8
+ /* Morph (data-auto-animate on a slide): reveal's Auto-Animate from the slide before, which needs the attribute too.
9
+ Boxes pair by data-morph (Duplicate slide sets it to the original's id), else by their own id.
10
+ Builds by paragraph (data-build="paragraph" on an animated text box): each paragraph or list item is its own step,
11
+ with the box's effect, and the slide's later steps move back to make room. */
12
+ const prepare = () => {
13
+ const all = Reveal.getSlides();
14
+ all.forEach((sl, i) => { if (sl.hasAttribute('data-auto-animate') && all[i - 1] && !all[i - 1].hasAttribute('data-auto-animate')) all[i - 1].setAttribute('data-auto-animate', ''); });
15
+ document.querySelectorAll('.reveal .slides .box').forEach(b => { if (!b.dataset.id) b.dataset.id = b.dataset.morph || b.id; });
16
+ let changed = false;
17
+ all.forEach(sl => {
18
+ const frags = [...sl.querySelectorAll('.box.fragment[data-build="paragraph"]')].sort((a, b) => (+a.dataset.fragmentIndex || 0) - (+b.dataset.fragmentIndex || 0));
19
+ frags.reverse().forEach(box => { // from the last, so each one's room doesn't move the ones before it
20
+ const parts = [...box.querySelectorAll(':scope > .content > :is(p, div, h1, h2, h3, h4, h5, h6), :scope > .content > :is(ul, ol) > li')];
21
+ if (parts.length < 2) return;
22
+ const at = +box.dataset.fragmentIndex || 0, cls = [...box.classList].filter(c => /^(appear|fade-[\w-]+|zoom-in|grow|shrink|strike|semi-fade-out|current-visible|highlight-[\w-]+)$/.test(c)); // its effect
23
+ sl.querySelectorAll('.fragment').forEach(f => { if (f !== box && +f.dataset.fragmentIndex > at) f.dataset.fragmentIndex = String(+f.dataset.fragmentIndex + parts.length - 1); });
24
+ box.classList.remove('fragment', ...cls); delete box.dataset.fragmentIndex;
25
+ parts.forEach((el, i) => { el.classList.add('fragment', ...cls); el.dataset.fragmentIndex = String(at + i); });
26
+ changed = true;
27
+ });
28
+ });
29
+ // a slide's own style (<style class="slide-css" media="not all"> in the slide): applied to that slide only
30
+ let n = 0;
31
+ all.forEach(sl => sl.querySelectorAll(':scope > style').forEach(st => {
32
+ st.media = 'not all'; sl.dataset.slideStyle = String(++n);
33
+ document.head.append(Object.assign(document.createElement('style'), { textContent: `.reveal .slides section[data-slide-style="${n}"] { ${st.textContent} }` }));
34
+ }));
35
+ if (changed) Reveal.sync();
36
+ if (new URLSearchParams(location.search).get('view') === 'scroll') Reveal.configure({ view: 'scroll' }); // read as a page (reveal's scroll view)
37
+ };
38
+ Reveal.isReady() ? prepare() : Reveal.on('ready', prepare);
39
+ const embedded = new URLSearchParams(location.search).has('embedded');
40
+ let typed = '', screen = null, laser = null;
41
+
42
+ const cover = color => {
43
+ if (screen) { screen.remove(); const was = screen.dataset.c; screen = null; if (was === color) return; }
44
+ screen = document.createElement('div'); screen.dataset.c = color;
45
+ screen.style.cssText = `position:fixed;inset:0;z-index:100;background:${color};cursor:none`;
46
+ document.body.append(screen);
47
+ };
48
+ const toggleLaser = () => {
49
+ if (laser) { laser.remove(); laser = null; document.documentElement.style.cursor = ''; return; }
50
+ laser = document.createElement('div');
51
+ laser.style.cssText = 'position:fixed;z-index:101;width:8px;height:8px;margin:-4px 0 0 -4px;border-radius:50%;background:#e8173a;box-shadow:0 0 3px 1px rgba(232,23,58,.5);pointer-events:none;left:-20px;top:-20px';
52
+ document.body.append(laser); document.documentElement.style.cursor = 'none';
53
+ };
54
+ let ptr = { x: innerWidth / 2, y: innerHeight / 2 };
55
+ addEventListener('pointermove', e => { ptr = { x: e.clientX, y: e.clientY }; if (laser) { laser.style.left = e.clientX + 'px'; laser.style.top = e.clientY + 'px'; } });
56
+
57
+ /* M (presenting from the editor): a note for the AI at the pointer; ⏎ adds it to this slide as a .box.note, Esc drops it. */
58
+ let memo = null;
59
+ const openMemo = () => {
60
+ if (memo || !embedded) return;
61
+ memo = document.createElement('textarea'); memo.placeholder = 'Note for AI (⏎ to keep, Esc to drop)';
62
+ memo.style.cssText = `position:fixed;z-index:103;left:${Math.min(ptr.x, innerWidth - 300)}px;top:${Math.min(ptr.y, innerHeight - 140)}px;width:280px;height:120px;padding:10px;border:0;border-radius:4px;background:#fff3a8;color:#3c4043;font:15px Roboto,Arial,sans-serif;box-shadow:0 2px 8px rgba(0,0,0,.35);resize:none;outline:none`;
63
+ document.body.append(memo); memo.focus();
64
+ memo.addEventListener('keydown', e => {
65
+ e.stopPropagation();
66
+ if (e.key === 'Escape') { e.preventDefault(); memo.remove(); memo = null; }
67
+ else if (e.key === 'Enter' && !e.shiftKey) {
68
+ e.preventDefault();
69
+ const text = memo.value.trim(), r = document.querySelector('.reveal .slides').getBoundingClientRect(), k = Reveal.getScale();
70
+ const { h, v } = Reveal.getIndices();
71
+ if (text) parent.postMessage({ slides: 'note', h, v, x: (parseFloat(memo.style.left) - r.left) / k, y: (parseFloat(memo.style.top) - r.top) / k, text }, '*');
72
+ memo.remove(); memo = null;
73
+ }
74
+ });
75
+ };
76
+
77
+ const exit = () => {
78
+ if (document.fullscreenElement) { document.exitFullscreen(); return; }
79
+ if (embedded) parent.postMessage({ slides: 'exit', h: Reveal.getIndices().h, v: Reveal.getIndices().v }, '*');
80
+ };
81
+ addEventListener('keydown', e => {
82
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
83
+ if (/^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName) || e.target.isContentEditable) return; // typing in a field on the page (a comment box) is not a slideshow key
84
+ const k = e.key.toLowerCase();
85
+ if (screen && k !== 'b' && k !== 'w') { screen.remove(); screen = null; if (k !== 'escape') { e.preventDefault(); e.stopImmediatePropagation(); return; } }
86
+ if (memo) return; // typing a note
87
+ if (/^[0-9]$/.test(e.key)) { typed += e.key; return; }
88
+ if (e.key === 'Enter') {
89
+ e.preventDefault(); e.stopImmediatePropagation();
90
+ if (typed) Reveal.slide(Math.max(0, +typed - 1)); else Reveal.next();
91
+ typed = ''; return;
92
+ }
93
+ typed = '';
94
+ if (k === 'b' || k === 'w') { e.preventDefault(); e.stopImmediatePropagation(); cover(k === 'b' ? '#000' : '#fff'); }
95
+ else if (k === 'l') { e.preventDefault(); e.stopImmediatePropagation(); toggleLaser(); }
96
+ else if ((k === 'n' || k === 'p') && !e.shiftKey) { e.preventDefault(); e.stopImmediatePropagation(); k === 'n' ? Reveal.right() : Reveal.left(); } // Slides: N / P = → / ←, past a stack
97
+ else if (k === 's' && Reveal.hasPlugin('notes')) { e.preventDefault(); e.stopImmediatePropagation(); Reveal.getPlugin('notes').open(); }
98
+
99
+ else if (k === 'm' && embedded) { e.preventDefault(); e.stopImmediatePropagation(); openMemo(); }
100
+ else if (e.key === 'Escape') { e.preventDefault(); e.stopImmediatePropagation(); exit(); }
101
+ }, true); // capture: before reveal.js's own handler
102
+
103
+ Reveal.configure({ keyboard: { 79: null, 86: null, 191: null, 190: null, 27: null } }); // O, V, ?, ., Esc
104
+ if (embedded) Reveal.on('ready', () => window.focus());
105
+ if (/print-pdf/.test(location.search)) { // PDF, as Slides prints it: one page per slide, edge to edge, no numbers, colours kept
106
+ Reveal.configure({ slideNumber: false, margin: 0, pdfSeparateFragments: false, pdfMaxPagesPerSlide: 1, showNotes: false });
107
+ const css = document.createElement('style');
108
+ css.textContent = `* { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; } /* without the print dialog's "Background graphics" */
109
+ @page { margin: 0; } .reveal .slide-number, .sl-bar { display: none !important; }`;
110
+ document.head.append(css);
111
+ return;
112
+ }
113
+
114
+
115
+ /* Look: black letterbox, Slides' own hover toolbar instead of reveal's controls / progress / number. */
116
+ /* reveal.js pauses a slide's videos when it is left and, coming back, plays only those marked data-autoplay themselves;
117
+ a Glideshow deck marks their box (.box[data-autoplay]), so they came back still (2026-10-21). Play them again whenever
118
+ their slide or fragment is shown. */
119
+ const playShown = () => Reveal.getCurrentSlide()?.querySelectorAll('.box[data-autoplay] video, video[autoplay]').forEach(v => {
120
+ if (v.closest('.fragment:not(.visible)') || !v.paused) return;
121
+ v.play().catch(() => {});
122
+ });
123
+ Reveal.on('slidechanged', playShown); Reveal.on('fragmentshown', playShown);
124
+
125
+ Reveal.configure({ controls: false, progress: false, slideNumber: false, margin: 0 }); // edge to edge, as Slides
126
+ const css = document.createElement('style');
127
+ css.textContent = `
128
+ html, body, .reveal { background: #000 !important; }
129
+ .reveal .backgrounds { display: none; }
130
+ .sl-bar { position: fixed; left: 20px; bottom: 20px; z-index: 102; display: flex; align-items: center; height: 36px; padding: 0 4px;
131
+ background: #f8fafd; border-radius: 18px; color: #444746; font: 14px Roboto, -apple-system, Arial, sans-serif;
132
+ box-shadow: 0 1px 3px rgba(60, 64, 67, .3), 0 4px 8px 3px rgba(60, 64, 67, .15); transition: opacity .3s; }
133
+ .sl-bar.idle { opacity: 0; pointer-events: none; }
134
+ .sl-bar button { display: inline-flex; align-items: center; justify-content: center; min-width: 28px; height: 28px; padding: 0;
135
+ border: 0; border-radius: 14px; background: none; color: inherit; font: inherit; cursor: pointer; }
136
+ .sl-bar button:hover { background: rgba(68, 71, 70, .08); }
137
+ .sl-bar .num { min-width: 40px; margin: 0 2px; background: #eef1f5; color: #1f1f1f; }
138
+ .sl-bar svg, .sl-pop svg { width: 20px; height: 20px; fill: currentColor; }
139
+ .sl-pop { position: fixed; z-index: 103; min-width: 220px; padding: 8px 0; background: #f8fafd; color: #1f1f1f; border-radius: 8px;
140
+ font: 14px Roboto, -apple-system, Arial, sans-serif; box-shadow: 0 1px 3px rgba(60, 64, 67, .3), 0 4px 8px 3px rgba(60, 64, 67, .15); }
141
+ .sl-pop button { display: flex; width: 100%; height: 35px; align-items: center; gap: 12px; padding: 0 16px 0 18px; border: 0; background: none;
142
+ font: inherit; color: inherit; text-align: left; cursor: pointer; white-space: nowrap; }
143
+ .sl-pop button:hover, .sl-pop button.open { background: #e9eef6; }
144
+ .sl-pop .ic { display: inline-flex; width: 20px; color: #444746; }
145
+ .sl-pop .on { font-weight: 600; }
146
+ .sl-pop button.indent { position: relative; padding-left: 40px; }
147
+ .sl-pop button.indent::before { content: ""; position: absolute; left: 27px; top: 0; bottom: 0; width: 2px; background: #c4c7c5; }
148
+
149
+
150
+ .sl-pop .k { margin-left: auto; padding-left: 24px; color: #444746; }
151
+ .sl-pop hr { margin: 8px 0 8px 40px; border: 0; border-top: 1px solid #c7c7c7; }`;
152
+ document.head.append(css);
153
+ // Each slide paints its own background colour, so the area around it stays black.
154
+ const paint = () => document.querySelectorAll('.reveal .slides section').forEach(s => {
155
+ s.style.background = s.dataset.backgroundColor || '#fff';
156
+ if (s.dataset.backgroundImage) Object.assign(s.style, { backgroundImage: `url("${s.dataset.backgroundImage}")`, backgroundSize: 'cover', backgroundPosition: 'center' });
157
+ });
158
+
159
+ const icon = d => `<svg viewBox="0 0 24 24"><path d="${d}"/></svg>`;
160
+ const I = { prev: 'M15.4 7.4 14 6l-6 6 6 6 1.4-1.4L10.8 12z', next: 'M8.6 16.6 10 18l6-6-6-6-1.4 1.4 4.6 4.6z',
161
+ play: 'M8 5v14l11-7z', pause: 'M6 19h4V5H6zm8-14v14h4V5z', check: 'M9 16.2 4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4z',
162
+ notes: 'M3 6h18v2H3zm0 5h18v2H3zm0 5h12v2H3z', laser: 'M7 2v11h3v9l7-12h-4l4-8z',
163
+ full: 'M5 16h3v3h2v-5H5zm3-8H5v2h5V5H8zm6 11h2v-3h3v-2h-5zm2-11V5h-2v5h5V8z', exit: 'M19 6.4 17.6 5 12 10.6 6.4 5 5 6.4 10.6 12 5 17.6 6.4 19 12 13.4 17.6 19 19 17.6 13.4 12z',
164
+ hidden: 'M12 7a5 5 0 0 1 4.6 7l2.9 2.9A11.8 11.8 0 0 0 23 12c-1.7-4.4-6-7.5-11-7.5-1.4 0-2.7.3-4 .7l2.2 2.2A5 5 0 0 1 12 7zM2 4.3l2.7 2.7A11.8 11.8 0 0 0 1 12c1.7 4.4 6 7.5 11 7.5 1.5 0 3-.3 4.4-.8l3.3 3.3 1.3-1.3L3.3 3z',
165
+ more: 'M12 8a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 2a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm0 6a2 2 0 1 0 0 4 2 2 0 0 0 0-4z', sub: 'M10 17l5-5-5-5z' };
166
+ const bar = document.createElement('div'); bar.className = 'sl-bar';
167
+ bar.innerHTML = `<button data-a="prev" title="Previous">${icon(I.prev)}</button><button class="num" data-a="num" title="Go to slide"></button>
168
+ <button data-a="next" title="Next">${icon(I.next)}</button><button data-a="more" title="Options">${icon(I.more)}</button>`;
169
+ const num = bar.querySelector('.num');
170
+ const showNum = () => { const { h, v } = Reveal.getIndices(); num.textContent = v ? `${h + 1}.${v}` : h + 1; }; // main slide 3, its sub-slides 3.1, 3.2
171
+ // Menus as Slides': [icon, label, run, {on, key, sub}] rows, '-' for a divider; a sub opens a second menu to the right.
172
+ let pop = null, sub = null, subTimer = 0;
173
+ const closePop = () => { pop?.remove(); sub?.remove(); pop = sub = null; };
174
+ const menu = (items, place) => {
175
+ const m = document.createElement('div'); m.className = 'sl-pop';
176
+ for (const it of items) {
177
+ if (it === '-') { m.append(document.createElement('hr')); continue; }
178
+ const [ic, label, run, o = {}] = it, b = document.createElement('button');
179
+ b.innerHTML = `<span class="ic">${o.on ? icon(I.check) : ic ? icon(ic) : ''}</span><span></span>${o.key ? `<span class="k">${o.key}</span>` : o.sub ? `<span class="k">${icon(I.sub)}</span>` : ''}`;
180
+ b.children[1].textContent = label; if (o.on) b.classList.add('on'); if (o.indent) b.classList.add('indent');
181
+ if (o.sub) b.onmouseenter = b.onclick = () => { sub?.remove(); m.querySelector('.open')?.classList.remove('open'); b.classList.add('open'); sub = menu(o.sub, mm => { const r = b.getBoundingClientRect(); mm.style.left = r.right + 'px'; mm.style.top = Math.max(8, r.bottom - mm.offsetHeight + 8) + 'px'; }); };
182
+ else { b.onmouseenter = () => { if (m === pop) subTimer = setTimeout(() => { sub?.remove(); sub = null; m.querySelector('.open')?.classList.remove('open'); }, 300); };
183
+ b.onmouseleave = () => clearTimeout(subTimer); b.onclick = () => { closePop(); run(); }; }
184
+ m.append(b);
185
+ }
186
+ m.onmouseenter = () => { if (m !== pop) clearTimeout(subTimer); }; // the pointer crossed the parent on its way into the submenu
187
+ document.body.append(m); place(m); return m;
188
+ };
189
+ const openPop = (btn, items) => { closePop(); pop = menu(items, m => { const r = btn.getBoundingClientRect(); m.style.left = Math.max(8, r.right - m.offsetWidth) + 'px'; m.style.top = r.top - m.offsetHeight - 4 + 'px'; }); };
190
+ let autoMs = 0, loop = false;
191
+ const setAuto = (ms, lp = loop) => { autoMs = ms; loop = lp; Reveal.configure({ autoSlide: ms, loop: lp }); };
192
+ const isFull = () => !!document.fullscreenElement;
193
+ bar.addEventListener('mousedown', e => e.preventDefault()); // keep keyboard focus on the slides
194
+ bar.onclick = e => {
195
+ const btn = e.target.closest('button'), a = btn?.dataset.a;
196
+ if (a === 'prev') Reveal.prev(); if (a === 'next') Reveal.next();
197
+ if (a === 'num') { const at = Reveal.getIndices(); openPop(num, Reveal.getHorizontalSlides().flatMap((hs, h) => { // the stacks as a tree, as the filmstrip
198
+ const leaves = hs.querySelector(':scope > section') ? [...hs.querySelectorAll(':scope > section')] : [hs];
199
+ return leaves.map((s, v) => [s.dataset.visibility === 'hidden' ? I.hidden : null, v ? `${h + 1}.${v}` : `Slide ${h + 1}`, () => Reveal.slide(h, v),
200
+ { on: h === at.h && v === (at.v || 0), indent: v > 0 }]);
201
+ })); }
202
+ if (a === 'more') openPop(btn, [
203
+ [I.notes, 'Open speaker notes', () => Reveal.getPlugin?.('notes')?.open(), { key: 'S' }],
204
+ [I.laser, laser ? 'Turn off the laser pointer' : 'Turn on the laser pointer', toggleLaser, { key: 'L' }],
205
+ [I.full, isFull() ? 'Exit full screen' : 'Full screen', () => isFull() ? document.exitFullscreen() : document.documentElement.requestFullscreen?.(), { key: 'F' }],
206
+ [I.exit, 'Exit slideshow', exit, { key: 'Esc' }], '-',
207
+ [I.play, 'Auto-play', null, { sub: [
208
+ [autoMs ? I.pause : I.play, autoMs ? 'Stop' : 'Play', () => setAuto(autoMs ? 0 : 2000)], '-',
209
+ ...[1, 2, 3, 5, 10, 15, 30, 60].map(sec => [null, sec === 1 ? 'Every second' : sec < 60 ? `Every ${sec} seconds` : 'Every minute', () => setAuto(sec * 1000), { on: autoMs === sec * 1000 }]), '-',
210
+ [null, 'Loop', () => setAuto(autoMs, !loop), { on: loop }]] }]]);
211
+ };
212
+ // Toolbar and cursor fade after 3 s without the mouse moving, as Slides.
213
+ let idleTimer = 0;
214
+ const wake = () => {
215
+ bar.classList.remove('idle'); if (!laser) document.documentElement.style.cursor = '';
216
+ clearTimeout(idleTimer);
217
+ idleTimer = setTimeout(() => { if (pop) return; bar.classList.add('idle'); if (!laser) document.documentElement.style.cursor = 'none'; }, 3000);
218
+ };
219
+ addEventListener('pointermove', wake); addEventListener('pointerdown', e => { if (pop && !e.target.closest?.('.sl-pop')) closePop(); });
220
+ Reveal.on('ready', () => { document.body.append(bar); paint(); showNum(); wake(); });
221
+ Reveal.on('slidechanged', showNum);
222
+ })();