htmlcraft 0.0.0-stage → 0.1.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.
@@ -0,0 +1,736 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Notes and transitions deck</title>
7
+ <style>
8
+ :root { color-scheme: light; }
9
+ html, body { margin: 0; padding: 0; background: #0b0d12; }
10
+ .slide {
11
+ width: 1600px;
12
+ height: 900px;
13
+ position: relative;
14
+ overflow: hidden;
15
+ background: #f7f5ef;
16
+ color: #17181c;
17
+ font-family: Inter, ui-sans-serif, system-ui, sans-serif;
18
+ }
19
+ @media screen {
20
+ [data-ppt-deck]:not([data-ppt-ready]) > [data-slide-id] ~ [data-slide-id] { display: none; }
21
+ }
22
+ @media print {
23
+ [data-ppt-deck] > [data-slide-id] { break-after: page; }
24
+ }
25
+ .kicker { margin: 0 0 18px; font-size: 22px; letter-spacing: .18em; text-transform: uppercase; color: #9a6b1f; }
26
+ .title { margin: 0; font-size: 72px; line-height: 1.1; }
27
+ .body { margin: 28px 0 0; font-size: 32px; line-height: 1.5; color: #4a4f59; }
28
+ .lab { display: inline-block; margin-top: 32px; padding: 12px 20px; border: 2px solid #9a6b1f; color: #9a6b1f; font-size: 24px; letter-spacing: .08em; text-transform: uppercase; }
29
+ </style>
30
+ </head>
31
+ <body>
32
+ <main data-ppt-deck>
33
+ <section class="slide" data-slide-id="s1" data-width="1600" data-height="900" data-ppt-transition="fade">
34
+ <div data-ppt-el="e1" style="position: absolute; left: 140px; top: 180px; width: 1200px;">
35
+ <p class="kicker" data-ppt-el="e1-kicker">Speaker support</p>
36
+ <h1 class="title" data-ppt-el="e1-title">Notes and transitions travel with the deck</h1>
37
+ <p class="body" data-ppt-el="e1-body">Notes stay hidden during presentation. Transitions are limited to none, fade, and slide.</p>
38
+ <span class="lab" data-ppt-el="e2">fade</span>
39
+ </div>
40
+ <aside data-ppt-notes hidden>This slide uses the fade transition. Read the note before advancing with the arrow keys.</aside>
41
+ </section>
42
+
43
+ <section class="slide" data-slide-id="s2" data-width="1600" data-height="900" data-ppt-transition="slide">
44
+ <h2 data-ppt-el="e3" style="position: absolute; left: 140px; top: 160px; margin: 0; font-size: 64px;">A slide transition</h2>
45
+ <p data-ppt-el="e4" style="position: absolute; left: 140px; top: 300px; width: 1180px; margin: 0; font-size: 32px; line-height: 1.5; color: #4a4f59;">Use <code>set_transition</code> or edit <code>data-ppt-transition</code>. An unknown value fails validation.</p>
46
+ <aside data-ppt-notes hidden>Demo the slide transition here. The note is stored in a direct-child aside with the hidden attribute.</aside>
47
+ </section>
48
+
49
+ <section class="slide" data-slide-id="s3" data-width="1600" data-height="900" data-ppt-transition="none">
50
+ <h2 data-ppt-el="e5" style="position: absolute; left: 140px; top: 160px; margin: 0; font-size: 64px;">No transition</h2>
51
+ <p data-ppt-el="e6" style="position: absolute; left: 140px; top: 300px; width: 1180px; margin: 0; font-size: 32px; line-height: 1.5; color: #4a4f59;">Use the Home and End keys to jump to the first and last slide. The URL hash tracks the position.</p>
52
+ <aside data-ppt-notes hidden>Close with a reminder: notes and transitions are ordinary attributes, so a round trip never loses them.</aside>
53
+ </section>
54
+ </main>
55
+ <script data-ppt-runtime="1">
56
+ (() => {
57
+ var slides = Array.prototype.slice.call(document.querySelectorAll('[data-slide-id]'));
58
+ if (!slides.length) return;
59
+ var index = 0;
60
+ var runtimeStyle = 'data-ppt-runtime-style';
61
+ var originalDisplay = slides.map(function (slide) { return slide.style.display; });
62
+ var sizes = slides.map(function (slide) { return measureSlide(slide); });
63
+ var controls = null;
64
+ var pageIndicator = null;
65
+ var previousButton = null;
66
+ var nextButton = null;
67
+ var sessionId = createSessionId();
68
+ var presentation = null;
69
+
70
+ var RUNTIME_STRINGS = {
71
+ en: {
72
+ group: 'Slide navigation',
73
+ previous: 'Previous slide',
74
+ next: 'Next slide',
75
+ page: '{index} / {total}',
76
+ presenterTitle: 'Presenter view',
77
+ waiting: 'Waiting for the main window…',
78
+ notConnected: 'Not connected yet.',
79
+ notes: 'Notes',
80
+ noNotes: 'No notes for this slide.',
81
+ nextSlide: 'Next slide',
82
+ nextPreview: 'Next slide preview',
83
+ lastSlidePlaceholder: '(Last slide)',
84
+ connected: 'Connected',
85
+ disconnected: 'Disconnected',
86
+ fullscreenUnsupported: 'This browser does not support fullscreen. Press F11.',
87
+ fullscreenDenied: 'Fullscreen was blocked. Press F11.',
88
+ presenterBlocked: 'The browser blocked the presenter window. Allow popups and press P.',
89
+ presenterUnsupported: 'This browser does not support presenter view.'
90
+ },
91
+ 'zh-Hant': {
92
+ group: '投影片導覽',
93
+ previous: '上一頁',
94
+ next: '下一頁',
95
+ page: '第 {index} / {total} 頁',
96
+ presenterTitle: '簡報者檢視',
97
+ waiting: '等待主視窗…',
98
+ notConnected: '尚未連線。',
99
+ notes: '備註',
100
+ noNotes: '此頁沒有備註。',
101
+ nextSlide: '下一頁',
102
+ nextPreview: '下一頁預覽',
103
+ lastSlidePlaceholder: '(最後一頁)',
104
+ connected: '已連線',
105
+ disconnected: '已中斷',
106
+ fullscreenUnsupported: '此瀏覽器不支援全螢幕;請按 F11 手動全螢幕。',
107
+ fullscreenDenied: '瀏覽器未允許全螢幕;請按 F11 手動全螢幕。',
108
+ presenterBlocked: '瀏覽器封鎖了簡報視窗。請允許此網站開啟彈出視窗,再按 P 重試。',
109
+ presenterUnsupported: '此瀏覽器不支援簡報者檢視。'
110
+ },
111
+ 'zh-Hans': {
112
+ group: '幻灯片导航',
113
+ previous: '上一页',
114
+ next: '下一页',
115
+ page: '第 {index} / {total} 页',
116
+ presenterTitle: '演讲者视图',
117
+ waiting: '等待主窗口…',
118
+ notConnected: '尚未连接。',
119
+ notes: '备注',
120
+ noNotes: '此页没有备注。',
121
+ nextSlide: '下一页',
122
+ nextPreview: '下一页预览',
123
+ lastSlidePlaceholder: '(最后一页)',
124
+ connected: '已连接',
125
+ disconnected: '已断开',
126
+ fullscreenUnsupported: '此浏览器不支持全屏;请按 F11 手动全屏。',
127
+ fullscreenDenied: '浏览器未允许全屏;请按 F11 手动全屏。',
128
+ presenterBlocked: '浏览器阻止了演讲者窗口。请允许此网站打开弹出窗口,再按 P 重试。',
129
+ presenterUnsupported: '此浏览器不支持演讲者视图。'
130
+ }
131
+ };
132
+
133
+ function runtimeLocale() {
134
+ var override = document.documentElement.getAttribute('data-ppt-locale');
135
+ var value = String(override || navigator.language || 'en').toLowerCase();
136
+ var subtags = value.split('-');
137
+ if (subtags[0] !== 'zh') return 'en';
138
+ if (subtags.indexOf('hant') !== -1) return 'zh-Hant';
139
+ if (subtags.indexOf('hans') !== -1) return 'zh-Hans';
140
+ if (value === 'zh-tw' || value === 'zh-hk' || value === 'zh-mo') {
141
+ return 'zh-Hant';
142
+ }
143
+ return 'zh-Hans';
144
+ }
145
+
146
+ var strings = RUNTIME_STRINGS[runtimeLocale()];
147
+
148
+ function pageLabel(pageIndex, total) {
149
+ return strings.page
150
+ .replace('{index}', String(pageIndex))
151
+ .replace('{total}', String(total));
152
+ }
153
+
154
+ Array.prototype.forEach.call(document.querySelectorAll('[data-ppt-deck]'), function (deck) {
155
+ deck.setAttribute('data-ppt-ready', '1');
156
+ });
157
+
158
+ document.documentElement.setAttribute('data-ppt-runtime-fit', '1');
159
+
160
+ function measureSlide(slide) {
161
+ return {
162
+ width: dimensionOf(slide, 'data-width', 'width', 1600),
163
+ height: dimensionOf(slide, 'data-height', 'height', 900)
164
+ };
165
+ }
166
+
167
+ function dimensionOf(slide, attribute, property, fallback) {
168
+ var value = Number(slide.getAttribute(attribute));
169
+ if (!(value > 0)) value = parseFloat(window.getComputedStyle(slide)[property]);
170
+ if (!(value > 0)) value = property === 'width' ? slide.offsetWidth : slide.offsetHeight;
171
+ return value > 0 ? value : fallback;
172
+ }
173
+
174
+ function layoutActiveSlide() {
175
+ var slide = slides[index];
176
+ if (!slide) return;
177
+ var size = sizes[index];
178
+ var viewportWidth = window.innerWidth || document.documentElement.clientWidth || size.width;
179
+ var viewportHeight = window.innerHeight || document.documentElement.clientHeight || size.height;
180
+ var scale = Math.min(viewportWidth / size.width, viewportHeight / size.height);
181
+ if (!(scale > 0) || !isFinite(scale)) scale = 1;
182
+ slide.style.setProperty('--ppt-fit-scale', String(scale));
183
+ slide.style.setProperty('--ppt-fit-left', (viewportWidth - size.width * scale) / 2 + 'px');
184
+ slide.style.setProperty('--ppt-fit-top', (viewportHeight - size.height * scale) / 2 + 'px');
185
+ }
186
+
187
+ function buildControls() {
188
+ if (slides.length < 2) return;
189
+ controls = document.createElement('div');
190
+ controls.setAttribute('data-ppt-runtime-controls', '1');
191
+ controls.setAttribute('data-testid', 'runtime-controls');
192
+ controls.setAttribute('role', 'group');
193
+ controls.setAttribute('aria-label', strings.group);
194
+ previousButton = document.createElement('button');
195
+ previousButton.type = 'button';
196
+ previousButton.setAttribute('data-testid', 'runtime-prev');
197
+ previousButton.setAttribute('aria-label', strings.previous);
198
+ previousButton.textContent = strings.previous;
199
+ pageIndicator = document.createElement('span');
200
+ pageIndicator.setAttribute('data-testid', 'runtime-page');
201
+ pageIndicator.setAttribute('aria-live', 'polite');
202
+ nextButton = document.createElement('button');
203
+ nextButton.type = 'button';
204
+ nextButton.setAttribute('data-testid', 'runtime-next');
205
+ nextButton.setAttribute('aria-label', strings.next);
206
+ nextButton.textContent = strings.next;
207
+ previousButton.addEventListener('click', function () { show(index - 1, true); });
208
+ nextButton.addEventListener('click', function () { show(index + 1, true); });
209
+ controls.appendChild(previousButton);
210
+ controls.appendChild(pageIndicator);
211
+ controls.appendChild(nextButton);
212
+ document.body.appendChild(controls);
213
+ }
214
+
215
+ function renderControls() {
216
+ if (!controls) return;
217
+ pageIndicator.textContent = pageLabel(index + 1, slides.length);
218
+ previousButton.disabled = index <= 0;
219
+ nextButton.disabled = index >= slides.length - 1;
220
+ }
221
+
222
+ function createSessionId() {
223
+ if (window.crypto && window.crypto.randomUUID) return window.crypto.randomUUID();
224
+ var values = new Uint8Array(16);
225
+ if (window.crypto && window.crypto.getRandomValues) window.crypto.getRandomValues(values);
226
+ else for (var i = 0; i < 16; i += 1) values[i] = Math.floor(Math.random() * 256);
227
+ var text = '';
228
+ for (var j = 0; j < values.length; j += 1) text += ('0' + values[j].toString(16)).slice(-2);
229
+ return text;
230
+ }
231
+
232
+ function isEditing(target) {
233
+ var element = target;
234
+ while (element) {
235
+ var tag = element.tagName;
236
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || element.isContentEditable) return true;
237
+ if (element === document.body) return false;
238
+ element = element.parentElement;
239
+ }
240
+ return false;
241
+ }
242
+
243
+ function isInteractive(target) {
244
+ var element = target;
245
+ while (element && element !== document.body) {
246
+ var tag = element.tagName;
247
+ if (tag === 'A' || tag === 'a' || tag === 'BUTTON' || tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA' || tag === 'LABEL' || tag === 'SUMMARY' || tag === 'DETAILS') return true;
248
+ if ((tag === 'VIDEO' || tag === 'AUDIO') && element.hasAttribute('controls')) return true;
249
+ var role = element.getAttribute('role');
250
+ if (role === 'button' || role === 'link') return true;
251
+ if (element.isContentEditable) return true;
252
+ element = element.parentElement;
253
+ }
254
+ return false;
255
+ }
256
+
257
+ function isControl(target) {
258
+ var element = target;
259
+ while (element && element !== document.body) {
260
+ if (element.hasAttribute && element.hasAttribute('data-ppt-runtime-controls')) return true;
261
+ element = element.parentElement;
262
+ }
263
+ return false;
264
+ }
265
+
266
+ function transitionOf(slide) {
267
+ var value = slide.getAttribute('data-ppt-transition');
268
+ return value === 'fade' || value === 'slide' ? value : 'none';
269
+ }
270
+
271
+ function animateSlide(slide) {
272
+ if (!window.matchMedia || !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
273
+ var kind = transitionOf(slide);
274
+ if (kind !== 'none') {
275
+ slide.classList.remove('htmlppt-runtime-fade', 'htmlppt-runtime-slide');
276
+ void slide.offsetWidth;
277
+ slide.classList.add('htmlppt-runtime-' + kind);
278
+ window.setTimeout(function () {
279
+ slide.classList.remove('htmlppt-runtime-' + kind);
280
+ }, 500);
281
+ }
282
+ }
283
+ }
284
+
285
+ function postState() {
286
+ if (!presentation) return;
287
+ presentation.channel.postMessage({
288
+ sessionId: presentation.sessionId,
289
+ type: 'state',
290
+ index: index,
291
+ total: slides.length
292
+ });
293
+ }
294
+
295
+ function show(next, animate) {
296
+ var clamped = Math.max(0, Math.min(slides.length - 1, next));
297
+ var changed = clamped !== index;
298
+ index = clamped;
299
+ slides.forEach(function (slide, i) {
300
+ slide.style.display = i === index ? originalDisplay[i] : 'none';
301
+ });
302
+ layoutActiveSlide();
303
+ renderControls();
304
+ if (changed && animate !== false) animateSlide(slides[index]);
305
+ history.replaceState(null, '', '#/' + (index + 1));
306
+ postState();
307
+ }
308
+
309
+ function notice(message) {
310
+ var previous = document.querySelector('[data-ppt-runtime-notice]');
311
+ if (previous) previous.remove();
312
+ var element = document.createElement('div');
313
+ element.setAttribute('data-ppt-runtime-notice', '1');
314
+ element.setAttribute('data-testid', 'runtime-notice');
315
+ element.setAttribute('role', 'status');
316
+ element.textContent = message;
317
+ element.style.cssText = 'position:fixed;left:16px;top:16px;z-index:2147483647;max-width:380px;padding:8px 12px;border:1px solid rgba(245,158,11,.5);background:rgba(69,26,3,.92);color:#fef3c7;font:12px/1.5 system-ui,sans-serif;border-radius:6px';
318
+ document.body.appendChild(element);
319
+ window.setTimeout(function () {
320
+ element.remove();
321
+ }, 4000);
322
+ }
323
+
324
+ function toggleFullscreen() {
325
+ if (document.fullscreenElement) {
326
+ var exit = document.exitFullscreen();
327
+ if (exit && exit.catch) exit.catch(function () {});
328
+ return;
329
+ }
330
+ var request = document.documentElement.requestFullscreen;
331
+ if (!request) {
332
+ notice(strings.fullscreenUnsupported);
333
+ return;
334
+ }
335
+ var result;
336
+ try {
337
+ result = request.call(document.documentElement);
338
+ } catch (error) {
339
+ notice(strings.fullscreenDenied);
340
+ return;
341
+ }
342
+ if (result && result.catch) {
343
+ result.catch(function () {
344
+ notice(strings.fullscreenDenied);
345
+ });
346
+ }
347
+ }
348
+
349
+ var PRESENTER_CSS = 'html,body{margin:0;background:#0b0d12;color:#e4e4e7;font:14px/1.5 system-ui,sans-serif}' +
350
+ 'body{padding:20px;display:flex;flex-direction:column;gap:16px;min-height:100vh;box-sizing:border-box}' +
351
+ 'header{display:flex;justify-content:space-between;align-items:flex-end;border-bottom:1px solid #27272a;padding-bottom:12px}' +
352
+ 'h1{font-size:14px;margin:0;letter-spacing:.05em}' +
353
+ 'h2{font-size:11px;text-transform:uppercase;letter-spacing:.14em;color:#71717a;margin:0 0 6px}' +
354
+ '.presenter-meta{display:flex;gap:16px;font-size:12px;color:#a1a1aa;margin:4px 0 0}' +
355
+ '[data-testid="presenter-status"]{color:#fcd34d}' +
356
+ '[data-testid="presenter-notes"]{min-height:96px;white-space:pre-wrap;border:1px solid #27272a;background:#18181b;padding:12px;border-radius:6px;margin:0}' +
357
+ '[data-testid="presenter-next-preview"]{display:flex;gap:16px;align-items:center;border:1px solid #27272a;background:#18181b;padding:12px;border-radius:6px;min-height:204px;box-sizing:border-box}' +
358
+ '.preview-holder{width:320px;height:180px;overflow:hidden;border-radius:4px;background:#fff;flex:none}' +
359
+ '.preview-holder iframe{width:1600px;height:900px;border:0;transform:scale(.2);transform-origin:top left;pointer-events:none;background:#fff}' +
360
+ '.presenter-actions{display:flex;gap:8px}' +
361
+ '.presenter-actions button{border:1px solid #3f3f46;background:transparent;color:#e4e4e7;border-radius:4px;padding:6px 12px;font-size:12px;cursor:pointer}' +
362
+ '.presenter-actions button:disabled{opacity:.4;cursor:default}';
363
+
364
+ function element(doc, tag, attributes, text) {
365
+ var node = doc.createElement(tag);
366
+ if (attributes) {
367
+ Object.keys(attributes).forEach(function (name) {
368
+ node.setAttribute(name, attributes[name]);
369
+ });
370
+ }
371
+ if (text !== undefined) node.textContent = text;
372
+ return node;
373
+ }
374
+
375
+ function buildPresenterWindow(win) {
376
+ var doc = win.document;
377
+ doc.documentElement.lang = runtimeLocale();
378
+ doc.title = strings.presenterTitle;
379
+ while (doc.body.firstChild) doc.body.removeChild(doc.body.firstChild);
380
+ doc.body.setAttribute('data-ppt-strings', JSON.stringify(strings));
381
+ doc.head.appendChild(element(doc, 'style', null, PRESENTER_CSS));
382
+ var root = element(doc, 'div', { 'data-testid': 'presenter-view' });
383
+ var header = element(doc, 'header');
384
+ var titleBlock = element(doc, 'div');
385
+ titleBlock.appendChild(element(doc, 'h1', null, strings.presenterTitle));
386
+ var pageMeta = element(doc, 'p', { class: 'presenter-meta' });
387
+ pageMeta.appendChild(element(doc, 'span', { 'data-testid': 'presenter-page' }, pageLabel(0, 0)));
388
+ titleBlock.appendChild(pageMeta);
389
+ header.appendChild(titleBlock);
390
+ var meta = element(doc, 'div', { class: 'presenter-meta' });
391
+ meta.appendChild(element(doc, 'span', { 'data-testid': 'presenter-status' }, strings.waiting));
392
+ meta.appendChild(element(doc, 'span', { 'data-testid': 'presenter-timer' }, '00:00'));
393
+ header.appendChild(meta);
394
+
395
+ var notesSection = element(doc, 'section');
396
+ notesSection.appendChild(element(doc, 'h2', null, strings.notes));
397
+ notesSection.appendChild(element(doc, 'p', { 'data-testid': 'presenter-notes' }, strings.notConnected));
398
+
399
+ var nextSection = element(doc, 'section');
400
+ nextSection.appendChild(element(doc, 'h2', null, strings.nextSlide));
401
+ var nextPreview = element(doc, 'div', { 'data-testid': 'presenter-next-preview' });
402
+ nextPreview.appendChild(element(doc, 'span', null, strings.lastSlidePlaceholder));
403
+ nextSection.appendChild(nextPreview);
404
+
405
+ var actions = element(doc, 'div', { class: 'presenter-actions' });
406
+ var previous = element(doc, 'button', { type: 'button', 'data-testid': 'presenter-prev' }, strings.previous);
407
+ previous.disabled = true;
408
+ var next = element(doc, 'button', { type: 'button', 'data-testid': 'presenter-next' }, strings.next);
409
+ next.disabled = true;
410
+ actions.appendChild(previous);
411
+ actions.appendChild(next);
412
+
413
+ root.appendChild(header);
414
+ root.appendChild(notesSection);
415
+ root.appendChild(nextSection);
416
+ root.appendChild(actions);
417
+ doc.body.appendChild(root);
418
+ }
419
+
420
+ function PRESENTER_CLIENT() {
421
+ var strings = {};
422
+ try {
423
+ var parsed = JSON.parse(document.body.getAttribute('data-ppt-strings') || '{}');
424
+ if (parsed && typeof parsed === 'object') strings = parsed;
425
+ } catch (error) {
426
+ strings = {};
427
+ }
428
+ function label(key, fallback) {
429
+ return typeof strings[key] === 'string' && strings[key] ? strings[key] : fallback;
430
+ }
431
+ function pageText(pageIndex, total) {
432
+ var format = label('page', '');
433
+ if (!format) return String(pageIndex) + ' / ' + String(total);
434
+ return format
435
+ .replace('{index}', String(pageIndex))
436
+ .replace('{total}', String(total));
437
+ }
438
+ var session = document.body.getAttribute('data-session-id');
439
+ if (!session) return;
440
+ var status = document.querySelector('[data-testid="presenter-status"]');
441
+ var timer = document.querySelector('[data-testid="presenter-timer"]');
442
+ var page = document.querySelector('[data-testid="presenter-page"]');
443
+ var notes = document.querySelector('[data-testid="presenter-notes"]');
444
+ var preview = document.querySelector('[data-testid="presenter-next-preview"]');
445
+ var prevButton = document.querySelector('[data-testid="presenter-prev"]');
446
+ var nextButton = document.querySelector('[data-testid="presenter-next"]');
447
+ var slides = [];
448
+ var headHtml = '';
449
+ var bodyFallback = false;
450
+ var previewKey = '';
451
+ var index = 0;
452
+ var total = 0;
453
+ var lastState = 0;
454
+ var started = Date.now();
455
+ var channel = new BroadcastChannel('htmlppt-presenter-' + session);
456
+
457
+ function setStatus(text) {
458
+ if (status) status.textContent = text;
459
+ }
460
+ function request() {
461
+ channel.postMessage({ sessionId: session, type: 'request' });
462
+ }
463
+ function notesOf(slide) {
464
+ if (!slide) return '';
465
+ var aside = slide.querySelector('[data-ppt-notes]');
466
+ return aside ? (aside.textContent || '').trim() : '';
467
+ }
468
+ function titleOf(slide) {
469
+ if (!slide) return '';
470
+ var heading = slide.querySelector('h1, h2, h3, [data-ppt-title]');
471
+ return heading ? (heading.textContent || '').replace(/\s+/g, ' ').trim() : '';
472
+ }
473
+ function renderPreview() {
474
+ if (!preview) return;
475
+ var next = slides[index + 1];
476
+ var key = index + '|' + bodyFallback + '|' + headHtml + '|' + (next ? next.outerHTML : '');
477
+ if (key === previewKey) return;
478
+ previewKey = key;
479
+ while (preview.firstChild) preview.removeChild(preview.firstChild);
480
+ if (!next) {
481
+ preview.appendChild(document.createTextNode(label('lastSlidePlaceholder', '(Last slide)')));
482
+ return;
483
+ }
484
+ var holder = document.createElement('div');
485
+ holder.className = 'preview-holder';
486
+ var width = Number(next.getAttribute('data-width')) || 1600;
487
+ var height = Number(next.getAttribute('data-height')) || 900;
488
+ var scale = 320 / width;
489
+ holder.style.height = Math.round(height * scale) + 'px';
490
+ var frame = document.createElement('iframe');
491
+ frame.setAttribute('sandbox', '');
492
+ frame.setAttribute('title', label('nextPreview', 'Next slide preview'));
493
+ frame.style.width = width + 'px';
494
+ frame.style.height = height + 'px';
495
+ frame.style.transform = 'scale(' + scale + ')';
496
+ frame.srcdoc = bodyFallback
497
+ ? '<!doctype html><html><head>' + headHtml + '</head><body>' + next.innerHTML + '</body></html>'
498
+ : '<!doctype html><html><head>' + headHtml + '</head><body><main data-ppt-deck>' + next.outerHTML + '</main></body></html>';
499
+ holder.appendChild(frame);
500
+ var caption = document.createElement('span');
501
+ caption.textContent = (index + 2) + '. ' + titleOf(next);
502
+ preview.appendChild(holder);
503
+ preview.appendChild(caption);
504
+ }
505
+ function render() {
506
+ if (notes) notes.textContent = notesOf(slides[index]) || label('noNotes', 'No notes for this slide.');
507
+ if (page) page.textContent = pageText(total === 0 ? 0 : index + 1, total);
508
+ renderPreview();
509
+ if (prevButton) prevButton.disabled = index <= 0;
510
+ if (nextButton) nextButton.disabled = total === 0 || index >= total - 1;
511
+ }
512
+ channel.onmessage = function (event) {
513
+ var message = event.data;
514
+ if (!message || message.sessionId !== session) return;
515
+ if (message.type === 'snapshot') {
516
+ var parsed = new DOMParser().parseFromString(message.html, 'text/html');
517
+ var found = parsed.querySelectorAll('[data-slide-id]');
518
+ slides = Array.prototype.slice.call(found.length ? found : [parsed.body]);
519
+ bodyFallback = found.length === 0;
520
+ headHtml = parsed.head ? parsed.head.innerHTML : '';
521
+ index = message.index;
522
+ total = message.total;
523
+ lastState = Date.now();
524
+ setStatus(label('connected', 'Connected'));
525
+ render();
526
+ } else if (message.type === 'state') {
527
+ index = message.index;
528
+ total = message.total;
529
+ lastState = Date.now();
530
+ setStatus(label('connected', 'Connected'));
531
+ render();
532
+ } else if (message.type === 'bye') {
533
+ setStatus(label('disconnected', 'Disconnected'));
534
+ }
535
+ };
536
+ function sendSlide(next) {
537
+ if (next < 0 || next >= total) return;
538
+ channel.postMessage({ sessionId: session, type: 'slide', index: next });
539
+ }
540
+ if (prevButton) prevButton.onclick = function () { sendSlide(index - 1); };
541
+ if (nextButton) nextButton.onclick = function () { sendSlide(index + 1); };
542
+ var poll = window.setInterval(request, 2000);
543
+ var clock = window.setInterval(function () {
544
+ var elapsed = Math.floor((Date.now() - started) / 1000);
545
+ var minutes = Math.floor(elapsed / 60);
546
+ var seconds = elapsed % 60;
547
+ if (timer) timer.textContent = ('0' + minutes).slice(-2) + ':' + ('0' + seconds).slice(-2);
548
+ if (lastState !== 0 && Date.now() - lastState > 6000) setStatus(label('disconnected', 'Disconnected'));
549
+ }, 1000);
550
+ window.addEventListener('pagehide', function () {
551
+ try {
552
+ channel.postMessage({ sessionId: session, type: 'bye' });
553
+ } catch (error) {}
554
+ channel.close();
555
+ window.clearInterval(poll);
556
+ window.clearInterval(clock);
557
+ });
558
+ request();
559
+ }
560
+
561
+ function snapshotHtml() {
562
+ var clone = document.documentElement.cloneNode(true);
563
+ clone.removeAttribute('data-ppt-runtime-fit');
564
+ var copied = clone.querySelectorAll('[data-slide-id]');
565
+ for (var i = 0; i < copied.length && i < originalDisplay.length; i += 1) {
566
+ copied[i].style.display = originalDisplay[i];
567
+ }
568
+ return '<!doctype html>' + clone.outerHTML;
569
+ }
570
+
571
+ function sendSnapshot() {
572
+ if (!presentation) return;
573
+ presentation.channel.postMessage({
574
+ sessionId: presentation.sessionId,
575
+ type: 'snapshot',
576
+ html: snapshotHtml(),
577
+ index: index,
578
+ total: slides.length
579
+ });
580
+ }
581
+
582
+ function closePresenter(notify) {
583
+ if (!presentation) return;
584
+ var current = presentation;
585
+ presentation = null;
586
+ window.clearInterval(current.closedTimer);
587
+ try {
588
+ if (notify) current.channel.postMessage({ sessionId: current.sessionId, type: 'bye' });
589
+ } catch (error) {}
590
+ try {
591
+ current.channel.close();
592
+ } catch (error) {}
593
+ if (notify && current.win && !current.win.closed) {
594
+ try {
595
+ current.win.close();
596
+ } catch (error) {}
597
+ }
598
+ }
599
+
600
+ function openPresenter() {
601
+ if (presentation && presentation.win && !presentation.win.closed) {
602
+ presentation.win.focus();
603
+ return;
604
+ }
605
+ if (presentation) closePresenter(false);
606
+ var win = window.open('', 'htmlppt-presenter-' + sessionId, 'popup=yes,width=960,height=720');
607
+ if (!win) {
608
+ notice(strings.presenterBlocked);
609
+ return;
610
+ }
611
+ var channel;
612
+ try {
613
+ channel = new BroadcastChannel('htmlppt-presenter-' + sessionId);
614
+ } catch (error) {
615
+ win.close();
616
+ notice(strings.presenterUnsupported);
617
+ return;
618
+ }
619
+ presentation = { sessionId: sessionId, channel: channel, win: win, closedTimer: null };
620
+ buildPresenterWindow(win);
621
+ win.document.body.setAttribute('data-session-id', sessionId);
622
+ var client = win.document.createElement('script');
623
+ client.textContent = '(' + PRESENTER_CLIENT.toString() + ')();';
624
+ win.document.body.appendChild(client);
625
+ var sentSnapshot = false;
626
+ channel.onmessage = function (event) {
627
+ var message = event.data;
628
+ if (!message || message.sessionId !== sessionId) return;
629
+ if (message.type === 'request') {
630
+ if (!sentSnapshot) {
631
+ sentSnapshot = true;
632
+ sendSnapshot();
633
+ }
634
+ postState();
635
+ } else if (message.type === 'slide') {
636
+ var requested = message.index;
637
+ if (typeof requested === 'number' && Math.floor(requested) === requested && requested >= 0 && requested < slides.length) {
638
+ show(requested, true);
639
+ }
640
+ } else if (message.type === 'bye') {
641
+ closePresenter(true);
642
+ }
643
+ };
644
+ presentation.closedTimer = window.setInterval(function () {
645
+ if (win.closed) closePresenter(false);
646
+ }, 1000);
647
+ sentSnapshot = true;
648
+ sendSnapshot();
649
+ postState();
650
+ }
651
+
652
+ document.addEventListener('keydown', function (event) {
653
+ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || isEditing(event.target)) return;
654
+ if (event.key === ' ' && isInteractive(event.target)) return;
655
+ if (event.key === 'ArrowRight' || event.key === ' ' || event.key === 'PageDown') {
656
+ event.preventDefault();
657
+ show(index + 1, true);
658
+ } else if (event.key === 'ArrowLeft' || event.key === 'PageUp') {
659
+ event.preventDefault();
660
+ show(index - 1, true);
661
+ } else if (event.key === 'Home') {
662
+ event.preventDefault();
663
+ show(0, true);
664
+ } else if (event.key === 'End') {
665
+ event.preventDefault();
666
+ show(slides.length - 1, true);
667
+ } else if (event.key === 'f' || event.key === 'F') {
668
+ event.preventDefault();
669
+ toggleFullscreen();
670
+ } else if (event.key === 'p' || event.key === 'P') {
671
+ event.preventDefault();
672
+ openPresenter();
673
+ } else if (event.key === 'Escape' && document.fullscreenElement) {
674
+ var exit = document.exitFullscreen();
675
+ if (exit && exit.catch) exit.catch(function () {});
676
+ }
677
+ });
678
+
679
+ document.addEventListener('click', function (event) {
680
+ if (event.defaultPrevented || isInteractive(event.target) || isControl(event.target)) return;
681
+ show(index + 1, true);
682
+ });
683
+
684
+ window.addEventListener('resize', layoutActiveSlide);
685
+
686
+ window.addEventListener('hashchange', function () {
687
+ var match = /#\/(\d+)/.exec(location.hash);
688
+ if (match) show(Number(match[1]) - 1, true);
689
+ });
690
+
691
+ if (!document.querySelector('[' + runtimeStyle + ']')) {
692
+ var style = document.createElement('style');
693
+ style.setAttribute(runtimeStyle, '1');
694
+ style.textContent = '@media print{[data-slide-id]{break-after:page;page-break-after:always}}' +
695
+ '[data-ppt-notes]{display:none!important}' +
696
+ '@media screen{' +
697
+ 'html[data-ppt-runtime-fit="1"],html[data-ppt-runtime-fit="1"] body{overflow:hidden}' +
698
+ 'html[data-ppt-runtime-fit="1"] [data-slide-id]{position:fixed!important;left:var(--ppt-fit-left,0);top:var(--ppt-fit-top,0);margin:0!important;transform:scale(var(--ppt-fit-scale,1));transform-origin:top left}' +
699
+ '}' +
700
+ '[data-ppt-runtime-controls]{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);z-index:2147483646;display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:999px;border:1px solid rgba(255,255,255,.22);background:rgba(12,14,20,.72);color:#f4f4f5;font:13px/1.2 system-ui,sans-serif;backdrop-filter:blur(8px)}' +
701
+ '[data-ppt-runtime-controls] button{border:1px solid rgba(255,255,255,.28);background:transparent;color:inherit;font:inherit;padding:5px 12px;border-radius:999px;cursor:pointer}' +
702
+ '[data-ppt-runtime-controls] button:hover:not(:disabled){border-color:rgba(255,255,255,.6);background:rgba(255,255,255,.1)}' +
703
+ '[data-ppt-runtime-controls] button:disabled{opacity:.35;cursor:default}' +
704
+ '[data-ppt-runtime-controls] [data-testid="runtime-page"]{min-width:48px;text-align:center;font-variant-numeric:tabular-nums;color:rgba(244,244,245,.85)}' +
705
+ '@media print{[data-ppt-runtime-controls]{display:none!important}}' +
706
+ '@keyframes htmlppt-runtime-fade{from{opacity:0}to{opacity:1}}' +
707
+ '@keyframes htmlppt-runtime-slide{from{opacity:.4;translate:6% 0}to{opacity:1;translate:0 0}}' +
708
+ '.htmlppt-runtime-fade{animation:htmlppt-runtime-fade 450ms ease-out}' +
709
+ '.htmlppt-runtime-slide{animation:htmlppt-runtime-slide 450ms ease-out}' +
710
+ '@media (prefers-reduced-motion: reduce){.htmlppt-runtime-fade,.htmlppt-runtime-slide{animation:none}}';
711
+ document.head.appendChild(style);
712
+ }
713
+
714
+ var printState = null;
715
+ window.addEventListener('beforeprint', function () {
716
+ if (printState) return;
717
+ printState = slides.map(function (slide) { return slide.style.display; });
718
+ slides.forEach(function (slide, i) { slide.style.display = originalDisplay[i]; });
719
+ });
720
+ window.addEventListener('afterprint', function () {
721
+ printState = null;
722
+ show(index, false);
723
+ });
724
+
725
+ window.addEventListener('pagehide', function () {
726
+ closePresenter(true);
727
+ });
728
+
729
+ buildControls();
730
+
731
+ var initial = /#\/(\d+)/.exec(location.hash);
732
+ show(initial ? Number(initial[1]) - 1 : 0, false);
733
+ })();
734
+ </script>
735
+ </body>
736
+ </html>