@thanh01.pmt/presentation-kit 0.2.10 → 0.2.11

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,3464 @@
1
+ 'use strict';
2
+
3
+ var yaml = require('js-yaml');
4
+
5
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
6
+
7
+ var yaml__default = /*#__PURE__*/_interopDefault(yaml);
8
+
9
+ // src/core/layout/serializer.ts
10
+ function serializeLayoutToComment(layout) {
11
+ if (!layout.boxes || layout.boxes.length === 0) return "";
12
+ const cleanData = {
13
+ boxes: layout.boxes.map((box) => {
14
+ const b = {
15
+ id: box.id,
16
+ target: box.target || `h1`,
17
+ pos: [
18
+ Math.round(box.pos[0]),
19
+ Math.round(box.pos[1]),
20
+ Math.round(box.pos[2]),
21
+ Math.round(box.pos[3])
22
+ ]
23
+ };
24
+ if (box.fontSize) b.fontSize = box.fontSize;
25
+ if (box.color) b.color = box.color;
26
+ if (box.textAlign) b.textAlign = box.textAlign;
27
+ if (box.style) b.style = box.style;
28
+ return b;
29
+ })
30
+ };
31
+ const yamlStr = yaml__default.default.dump(cleanData, {
32
+ indent: 2,
33
+ lineWidth: -1,
34
+ noRefs: true
35
+ }).trim();
36
+ return `<!-- layout:
37
+ ${yamlStr.split("\n").join("\n ")}
38
+ -->`;
39
+ }
40
+ function splitMarkdownSlides(markdown) {
41
+ const lines = markdown.split("\n");
42
+ let frontmatter = null;
43
+ const slideBuffers = [];
44
+ let currentBuffer = [];
45
+ let inCodeFence = false;
46
+ let lineIdx = 0;
47
+ if (lines.length > 0 && lines[0].trim() === "---") {
48
+ const fmBuffer = [lines[0]];
49
+ lineIdx = 1;
50
+ while (lineIdx < lines.length) {
51
+ const line = lines[lineIdx];
52
+ fmBuffer.push(line);
53
+ if (line.trim() === "---") {
54
+ frontmatter = fmBuffer.join("\n");
55
+ lineIdx++;
56
+ break;
57
+ }
58
+ lineIdx++;
59
+ }
60
+ }
61
+ for (let i = lineIdx; i < lines.length; i++) {
62
+ const line = lines[i];
63
+ if (/^```/.test(line.trim())) {
64
+ inCodeFence = !inCodeFence;
65
+ currentBuffer.push(line);
66
+ continue;
67
+ }
68
+ if (!inCodeFence && line.trim() === "---") {
69
+ slideBuffers.push(currentBuffer);
70
+ currentBuffer = [];
71
+ continue;
72
+ }
73
+ currentBuffer.push(line);
74
+ }
75
+ slideBuffers.push(currentBuffer);
76
+ const slides = slideBuffers.map((b) => b.join("\n"));
77
+ return { frontmatter, slides };
78
+ }
79
+ function updateSlideLayoutInMarkdown(markdown, slideIndex, layout) {
80
+ const { frontmatter, slides } = splitMarkdownSlides(markdown);
81
+ if (slideIndex < 0 || slideIndex >= slides.length) {
82
+ return markdown;
83
+ }
84
+ let slideContent = slides[slideIndex];
85
+ const layoutCommentRegex = /<!--\s*layout:\s*[\s\S]*?-->/i;
86
+ const newComment = layout && layout.boxes && layout.boxes.length > 0 ? serializeLayoutToComment(layout) : "";
87
+ if (layoutCommentRegex.test(slideContent)) {
88
+ if (newComment) {
89
+ slideContent = slideContent.replace(layoutCommentRegex, newComment);
90
+ } else {
91
+ slideContent = slideContent.replace(layoutCommentRegex, "").trim();
92
+ }
93
+ } else if (newComment) {
94
+ const trimmed = slideContent.trimStart();
95
+ slideContent = `${newComment}
96
+
97
+ ${trimmed}`;
98
+ }
99
+ slides[slideIndex] = slideContent;
100
+ const joinedSlides = slides.join("\n---\n");
101
+ if (frontmatter) {
102
+ return `${frontmatter}
103
+
104
+ ${joinedSlides}`;
105
+ }
106
+ return joinedSlides;
107
+ }
108
+ function updateSlideTextInMarkdown(markdown, slideIndex, oldText, newText) {
109
+ const { frontmatter, slides } = splitMarkdownSlides(markdown);
110
+ if (slideIndex < 0 || slideIndex >= slides.length) {
111
+ return markdown;
112
+ }
113
+ const cleanOld = oldText.trim();
114
+ const cleanNew = newText.trim();
115
+ if (!cleanOld || cleanOld === cleanNew) return markdown;
116
+ let slideContent = slides[slideIndex];
117
+ if (slideContent.includes(cleanOld)) {
118
+ slideContent = slideContent.replace(cleanOld, cleanNew);
119
+ } else {
120
+ const lines = slideContent.split("\n");
121
+ let replaced = false;
122
+ for (let i = 0; i < lines.length; i++) {
123
+ const line = lines[i];
124
+ const stripped = line.replace(/^[#\s\-\*\>]+/, "").replace(/[*_`]/g, "").trim();
125
+ if (stripped === cleanOld) {
126
+ const prefixMatch = line.match(/^([#\s\-\*\>]+)/);
127
+ const prefix = prefixMatch ? prefixMatch[1] : "";
128
+ lines[i] = `${prefix}${cleanNew}`;
129
+ replaced = true;
130
+ break;
131
+ }
132
+ }
133
+ if (replaced) {
134
+ slideContent = lines.join("\n");
135
+ } else {
136
+ const escaped = cleanOld.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
137
+ const regex = new RegExp(escaped, "i");
138
+ if (regex.test(slideContent)) {
139
+ slideContent = slideContent.replace(regex, cleanNew);
140
+ } else {
141
+ const words = cleanOld.split(/\s+/).filter((w) => w.length > 2);
142
+ if (words.length > 0) {
143
+ const pattern = words.map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\s\\S]*?");
144
+ const fuzzyRegex = new RegExp(pattern, "i");
145
+ if (fuzzyRegex.test(slideContent)) {
146
+ slideContent = slideContent.replace(fuzzyRegex, cleanNew);
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ slides[slideIndex] = slideContent;
153
+ const joinedSlides = slides.join("\n---\n");
154
+ if (frontmatter) {
155
+ return `${frontmatter}
156
+
157
+ ${joinedSlides}`;
158
+ }
159
+ return joinedSlides;
160
+ }
161
+ function removeSlideElementFromMarkdown(markdown, slideIndex, options) {
162
+ const { frontmatter, slides } = splitMarkdownSlides(markdown);
163
+ if (slideIndex < 0 || slideIndex >= slides.length) {
164
+ return markdown;
165
+ }
166
+ let slideContent = slides[slideIndex];
167
+ const cleanContent = options.content?.trim();
168
+ if (cleanContent) {
169
+ const lines = slideContent.split("\n");
170
+ let removed = false;
171
+ const updatedLines = lines.filter((line) => {
172
+ if (removed) return true;
173
+ const stripped = line.replace(/^[#\s\-\*\>]+/, "").replace(/[*_`]/g, "").trim();
174
+ if (stripped === cleanContent || line.includes(cleanContent)) {
175
+ removed = true;
176
+ return false;
177
+ }
178
+ return true;
179
+ });
180
+ if (removed) {
181
+ slideContent = updatedLines.join("\n");
182
+ }
183
+ }
184
+ const layoutCommentRegex = /<!--\s*layout:\s*([\s\S]*?)-->/i;
185
+ const match = slideContent.match(layoutCommentRegex);
186
+ if (match && match[1]) {
187
+ try {
188
+ const parsed = yaml__default.default.load(match[1]);
189
+ if (parsed && Array.isArray(parsed.boxes)) {
190
+ const remainingBoxes = parsed.boxes.filter((b) => {
191
+ if (options.boxId && b.id === options.boxId) return false;
192
+ if (options.target && b.target === options.target) return false;
193
+ return true;
194
+ });
195
+ if (remainingBoxes.length > 0) {
196
+ const yamlStr = yaml__default.default.dump({ boxes: remainingBoxes }, { indent: 2, lineWidth: -1, noRefs: true }).trim();
197
+ slideContent = slideContent.replace(layoutCommentRegex, `<!-- layout:
198
+ ${yamlStr.split("\n").join("\n ")}
199
+ -->`);
200
+ } else {
201
+ slideContent = slideContent.replace(layoutCommentRegex, "").trim();
202
+ }
203
+ }
204
+ } catch {
205
+ }
206
+ }
207
+ slides[slideIndex] = slideContent;
208
+ const joinedSlides = slides.join("\n---\n");
209
+ if (frontmatter) {
210
+ return `${frontmatter}
211
+
212
+ ${joinedSlides}`;
213
+ }
214
+ return joinedSlides;
215
+ }
216
+
217
+ // src/core/layout/presets.ts
218
+ var SLIDE_LAYOUT_PRESETS = [
219
+ {
220
+ id: "title-hero",
221
+ name: "Title & Speaker Cover",
222
+ category: "cover",
223
+ description: "High-impact opening slide with large title, subtitle, and speaker credentials.",
224
+ layout: {
225
+ boxes: [
226
+ {
227
+ id: "title",
228
+ target: "h1",
229
+ pos: [100, 180, 1080, 140],
230
+ fontSize: "3rem",
231
+ textAlign: "center"
232
+ },
233
+ {
234
+ id: "subtitle",
235
+ target: "h3",
236
+ pos: [100, 350, 1080, 70],
237
+ fontSize: "1.5rem",
238
+ textAlign: "center"
239
+ },
240
+ {
241
+ id: "speaker-meta",
242
+ target: "p",
243
+ pos: [100, 460, 1080, 80],
244
+ fontSize: "1.1rem",
245
+ textAlign: "center"
246
+ }
247
+ ]
248
+ },
249
+ markdownSnippet: `<!-- layout:
250
+ boxes:
251
+ - id: title
252
+ target: h1
253
+ pos: [100, 180, 1080, 140]
254
+ fontSize: 3rem
255
+ textAlign: center
256
+ - id: subtitle
257
+ target: h3
258
+ pos: [100, 350, 1080, 70]
259
+ fontSize: 1.5rem
260
+ textAlign: center
261
+ - id: speaker-meta
262
+ target: p
263
+ pos: [100, 460, 1080, 80]
264
+ fontSize: 1.1rem
265
+ textAlign: center
266
+ -->
267
+
268
+ # \u{1F680} Engineering Scalable Cloud Systems
269
+ ### Principles, Bottlenecks, and Real-World Architecture Patterns
270
+
271
+ **Presenter:** Senior Systems Architect | **Duration:** 45 mins | **Level:** Intermediate
272
+
273
+ <!--
274
+ Presenter Notes:
275
+ - Welcome the audience and outline today's primary engineering goals.
276
+ - Spark curiosity with an opening hook about scaling under high concurrency.
277
+ -->`
278
+ },
279
+ {
280
+ id: "two-column-split",
281
+ name: "Two-Column Comparison",
282
+ category: "split",
283
+ description: "Balanced side-by-side comparison for legacy vs modern patterns or pros vs cons.",
284
+ layout: {
285
+ boxes: [
286
+ {
287
+ id: "heading",
288
+ target: "h2",
289
+ pos: [80, 50, 1120, 70],
290
+ fontSize: "2rem"
291
+ },
292
+ {
293
+ id: "col-left",
294
+ target: ".columns-2 > div:first-child",
295
+ pos: [80, 150, 530, 510]
296
+ },
297
+ {
298
+ id: "col-right",
299
+ target: ".columns-2 > div:last-child",
300
+ pos: [670, 150, 530, 510]
301
+ }
302
+ ]
303
+ },
304
+ markdownSnippet: `<!-- layout:
305
+ boxes:
306
+ - id: heading
307
+ target: h2
308
+ pos: [80, 50, 1120, 70]
309
+ fontSize: 2rem
310
+ - id: col-left
311
+ target: '.columns-2 > div:first-child'
312
+ pos: [80, 150, 530, 510]
313
+ - id: col-right
314
+ target: '.columns-2 > div:last-child'
315
+ pos: [670, 150, 530, 510]
316
+ -->
317
+
318
+ ## \u2696\uFE0F Monolithic Polling vs Event-Driven Architecture
319
+
320
+ <div class="columns-2">
321
+ <div>
322
+
323
+ ### \u{1F534} Legacy Polling
324
+ - Heavy database CPU overhead on idle cycles
325
+ - High network latency between updates
326
+ - Prone to cascading timeouts during spikes
327
+
328
+ </div>
329
+ <div>
330
+
331
+ ### \u{1F7E2} Event-Driven Streams
332
+ - Real-time event propagation via webhooks
333
+ - Zero idle compute waste with serverless consumers
334
+ - Automatic backpressure and queue isolation
335
+
336
+ </div>
337
+ </div>
338
+
339
+ <!--
340
+ Presenter Notes:
341
+ - Emphasize the core failure mode of periodic polling under sudden load spikes.
342
+ - Ask the room if anyone has experienced database lockups caused by polling loops.
343
+ -->`
344
+ },
345
+ {
346
+ id: "code-explainer",
347
+ name: "Code Walkthrough & Analysis",
348
+ category: "code",
349
+ description: "Syntax-highlighted code block on the left with step-by-step key annotations on the right.",
350
+ layout: {
351
+ boxes: [
352
+ {
353
+ id: "heading",
354
+ target: "h2",
355
+ pos: [80, 45, 1120, 65],
356
+ fontSize: "1.9rem"
357
+ },
358
+ {
359
+ id: "code-box",
360
+ target: ".columns-2 > div:first-child",
361
+ pos: [80, 135, 630, 535]
362
+ },
363
+ {
364
+ id: "notes-box",
365
+ target: ".columns-2 > div:last-child",
366
+ pos: [740, 135, 460, 535]
367
+ }
368
+ ]
369
+ },
370
+ markdownSnippet: `<!-- layout:
371
+ boxes:
372
+ - id: heading
373
+ target: h2
374
+ pos: [80, 45, 1120, 65]
375
+ fontSize: 1.9rem
376
+ - id: code-box
377
+ target: '.columns-2 > div:first-child'
378
+ pos: [80, 135, 630, 535]
379
+ - id: notes-box
380
+ target: '.columns-2 > div:last-child'
381
+ pos: [740, 135, 460, 535]
382
+ -->
383
+
384
+ ## \u{1F4BB} Robust Async Transaction Pipeline
385
+
386
+ <div class="columns-2">
387
+ <div>
388
+
389
+ \`\`\`ts
390
+ // Process payment with idempotency guarantee
391
+ export async function processPayment(order: Order) {
392
+ const isValid = await validateOrder(order);
393
+ if (!isValid) throw new Error("ValidationFailed");
394
+
395
+ const receipt = await chargeCard(order);
396
+ return sendConfirmation(receipt);
397
+ }
398
+ \`\`\`
399
+
400
+ </div>
401
+ <div>
402
+
403
+ ### \u{1F50D} Key Engineering Invariants:
404
+ 1. **Early Guard Clause:** Validates payload immediately at line 3 before triggering external calls.
405
+ 2. **Deterministic Sequence:** Enforces strict order of operations using native \`await\`.
406
+ 3. **Audit Trail:** Returns immutable payment receipt upon successful execution.
407
+
408
+ </div>
409
+ </div>
410
+
411
+ <!--
412
+ Presenter Notes:
413
+ - Walk through lines 1 to 7 sequentially.
414
+ - Cold-call check: "What happens if line 6 throws a network timeout?"
415
+ - Cognitive scaffolding: Highlight the importance of idempotency keys in payment APIs.
416
+ -->`
417
+ },
418
+ {
419
+ id: "metrics-3-card",
420
+ name: "3-Metric Key Results Grid",
421
+ category: "metrics",
422
+ description: "Three high-visibility KPI stat cards for benchmarks, impact, and success criteria.",
423
+ layout: {
424
+ boxes: [
425
+ {
426
+ id: "heading",
427
+ target: "h2",
428
+ pos: [80, 50, 1120, 70],
429
+ fontSize: "2rem",
430
+ textAlign: "center"
431
+ },
432
+ {
433
+ id: "metric-1",
434
+ target: ".columns-3 > div:nth-child(1)",
435
+ pos: [80, 165, 340, 485]
436
+ },
437
+ {
438
+ id: "metric-2",
439
+ target: ".columns-3 > div:nth-child(2)",
440
+ pos: [470, 165, 340, 485]
441
+ },
442
+ {
443
+ id: "metric-3",
444
+ target: ".columns-3 > div:nth-child(3)",
445
+ pos: [860, 165, 340, 485]
446
+ }
447
+ ]
448
+ },
449
+ markdownSnippet: `<!-- layout:
450
+ boxes:
451
+ - id: heading
452
+ target: h2
453
+ pos: [80, 50, 1120, 70]
454
+ fontSize: 2rem
455
+ textAlign: center
456
+ - id: metric-1
457
+ target: '.columns-3 > div:nth-child(1)'
458
+ pos: [80, 165, 340, 485]
459
+ - id: metric-2
460
+ target: '.columns-3 > div:nth-child(2)'
461
+ pos: [470, 165, 340, 485]
462
+ - id: metric-3
463
+ target: '.columns-3 > div:nth-child(3)'
464
+ pos: [860, 165, 340, 485]
465
+ -->
466
+
467
+ ## \u{1F4CA} Performance & Reliability Benchmarks
468
+
469
+ <div class="columns-3">
470
+ <div>
471
+
472
+ # \u26A1 99.99%
473
+ ### SLA Availability
474
+ High-availability multi-region cluster with automated failover routing.
475
+
476
+ </div>
477
+ <div>
478
+
479
+ # \u{1F680} 15ms
480
+ ### P99 Latency
481
+ Sub-20ms roundtrip response time via distributed edge caching.
482
+
483
+ </div>
484
+ <div>
485
+
486
+ # \u{1F6E1}\uFE0F Zero
487
+ ### Unhandled Breaches
488
+ Full SOC2 Type II compliance and end-to-end payload encryption.
489
+
490
+ </div>
491
+ </div>
492
+
493
+ <!--
494
+ Presenter Notes:
495
+ - Highlight the 15ms P99 latency target as the primary technical milestone.
496
+ - Connect these metrics directly to the architecture decisions discussed next.
497
+ -->`
498
+ },
499
+ {
500
+ id: "process-flow",
501
+ name: "Architecture & Process Flow",
502
+ category: "diagram",
503
+ description: "Multi-stage workflow with responsive Mermaid diagram and phase summaries.",
504
+ layout: {
505
+ boxes: [
506
+ {
507
+ id: "heading",
508
+ target: "h2",
509
+ pos: [80, 45, 1120, 65],
510
+ fontSize: "1.9rem"
511
+ },
512
+ {
513
+ id: "diagram-box",
514
+ target: "pre.mermaid, .mermaid",
515
+ pos: [80, 130, 1120, 310]
516
+ },
517
+ {
518
+ id: "summary-box",
519
+ target: "ul, p",
520
+ pos: [80, 465, 1120, 205]
521
+ }
522
+ ]
523
+ },
524
+ markdownSnippet: `<!-- layout:
525
+ boxes:
526
+ - id: heading
527
+ target: h2
528
+ pos: [80, 45, 1120, 65]
529
+ fontSize: 1.9rem
530
+ - id: diagram-box
531
+ target: 'pre.mermaid, .mermaid'
532
+ pos: [80, 130, 1120, 310]
533
+ - id: summary-box
534
+ target: 'ul, p'
535
+ pos: [80, 465, 1120, 205]
536
+ -->
537
+
538
+ ## \u{1F504} End-to-End Data Ingestion Pipeline
539
+
540
+ \`\`\`mermaid
541
+ graph LR
542
+ A[1. Client Ingress] --> B[2. API Gateway]
543
+ B --> C[3. Auth Verifier]
544
+ C --> D[4. Distributed DB]
545
+ style A fill:#0284c7,stroke:#38bdf8,stroke-width:2px,color:#fff
546
+ style B fill:#1e293b,stroke:#64748b,stroke-width:2px,color:#fff
547
+ style C fill:#059669,stroke:#34d399,stroke-width:2px,color:#fff
548
+ style D fill:#7c3aed,stroke:#a78bfa,stroke-width:2px,color:#fff
549
+ \`\`\`
550
+
551
+ - **Ingress & Gateway:** Rate-limits traffic and applies reverse-proxy load balancing.
552
+ - **Verification & Storage:** Decodes cryptographic JWT tokens before transactional write commit.
553
+
554
+ <!--
555
+ Presenter Notes:
556
+ - Trace each hop across the diagram from left to right.
557
+ - Ask: "Where should rate limiting be applied to prevent DDoS attacks?"
558
+ -->`
559
+ },
560
+ {
561
+ id: "quote-highlight",
562
+ name: "Core Principle & Quote Highlight",
563
+ category: "quote",
564
+ description: "Prominent quote callout for fundamental engineering rules or memorable mantras.",
565
+ layout: {
566
+ boxes: [
567
+ {
568
+ id: "heading",
569
+ target: "h2",
570
+ pos: [100, 100, 1080, 80],
571
+ fontSize: "2.2rem",
572
+ textAlign: "center"
573
+ },
574
+ {
575
+ id: "quote-box",
576
+ target: "blockquote",
577
+ pos: [140, 230, 1e3, 360],
578
+ fontSize: "1.4rem"
579
+ }
580
+ ]
581
+ },
582
+ markdownSnippet: `<!-- layout:
583
+ boxes:
584
+ - id: heading
585
+ target: h2
586
+ pos: [100, 100, 1080, 80]
587
+ fontSize: 2.2rem
588
+ textAlign: center
589
+ - id: quote-box
590
+ target: blockquote
591
+ pos: [140, 230, 1000, 360]
592
+ fontSize: 1.4rem
593
+ -->
594
+
595
+ ## \u{1F4A1} Foundational Architecture Principle
596
+
597
+ > "Simplicity is prerequisite for reliability. A well-engineered distributed system is one where a new engineer can understand the request flow within their first hour."
598
+ >
599
+ > \u2014 **Edsger W. Dijkstra**
600
+
601
+ <!--
602
+ Presenter Notes:
603
+ - Pause for 5 seconds of silence to let the quote sink in.
604
+ - Ask: "How do our current architectural choices reflect this principle?"
605
+ -->`
606
+ },
607
+ {
608
+ id: "quiz-checkpoint",
609
+ name: "Interactive Formative Checkpoint",
610
+ category: "assessment",
611
+ description: "Interactive multiple-choice diagnostic checkpoint with A/B/C/D voting options.",
612
+ layout: {
613
+ boxes: [
614
+ {
615
+ id: "heading",
616
+ target: "h2",
617
+ pos: [80, 45, 1120, 65],
618
+ fontSize: "1.9rem"
619
+ },
620
+ {
621
+ id: "question-callout",
622
+ target: "blockquote",
623
+ pos: [80, 130, 1120, 110],
624
+ fontSize: "1.2rem"
625
+ },
626
+ {
627
+ id: "options-list",
628
+ target: "ul",
629
+ pos: [80, 260, 1120, 390],
630
+ fontSize: "1.15rem"
631
+ }
632
+ ]
633
+ },
634
+ markdownSnippet: `<!-- layout:
635
+ boxes:
636
+ - id: heading
637
+ target: h2
638
+ pos: [80, 45, 1120, 65]
639
+ fontSize: 1.9rem
640
+ - id: question-callout
641
+ target: blockquote
642
+ pos: [80, 130, 1120, 110]
643
+ fontSize: 1.2rem
644
+ - id: options-list
645
+ target: ul
646
+ pos: [80, 260, 1120, 390]
647
+ fontSize: 1.15rem
648
+ -->
649
+
650
+ ## \u{1F3AF} Formative Checkpoint: Verify Your Understanding
651
+
652
+ > **Question:** In an event-driven architecture, which pattern guarantees that messages are never permanently lost if a downstream consumer crashes?
653
+
654
+ - **A.** Direct HTTP POST call with a short 2-second timeout
655
+ - **B.** Persistent Message Broker with Dead-Letter Queue (DLQ) & retry policy
656
+ - **C.** In-memory client RAM cache without disk persistence
657
+ - **D.** Discarding failed packets and prompting the user to re-submit
658
+
659
+ <!--
660
+ Presenter Notes:
661
+ - Give students 30 seconds to vote on option A, B, C, or D.
662
+ - Correct answer: B.
663
+ - Explain why options A and C introduce catastrophic data loss in production.
664
+ -->`
665
+ },
666
+ {
667
+ id: "tiered-practice-3cards",
668
+ name: "Tiered Differentiation (Bronze / Silver / Gold)",
669
+ category: "content",
670
+ description: "Three progressive competency tiers allowing self-paced learning acceleration.",
671
+ layout: {
672
+ boxes: [
673
+ {
674
+ id: "heading",
675
+ target: "h2",
676
+ pos: [80, 45, 1120, 65],
677
+ fontSize: "1.9rem"
678
+ },
679
+ {
680
+ id: "tier-bronze",
681
+ target: ".columns-3 > div:nth-child(1)",
682
+ pos: [80, 140, 350, 520]
683
+ },
684
+ {
685
+ id: "tier-silver",
686
+ target: ".columns-3 > div:nth-child(2)",
687
+ pos: [465, 140, 350, 520]
688
+ },
689
+ {
690
+ id: "tier-gold",
691
+ target: ".columns-3 > div:nth-child(3)",
692
+ pos: [850, 140, 350, 520]
693
+ }
694
+ ]
695
+ },
696
+ markdownSnippet: `<!-- layout:
697
+ boxes:
698
+ - id: heading
699
+ target: h2
700
+ pos: [80, 45, 1120, 65]
701
+ fontSize: 1.9rem
702
+ - id: tier-bronze
703
+ target: '.columns-3 > div:nth-child(1)'
704
+ pos: [80, 140, 350, 520]
705
+ - id: tier-silver
706
+ target: '.columns-3 > div:nth-child(2)'
707
+ pos: [465, 140, 350, 520]
708
+ - id: tier-gold
709
+ target: '.columns-3 > div:nth-child(3)'
710
+ pos: [850, 140, 350, 520]
711
+ -->
712
+
713
+ ## \u{1F6E0}\uFE0F Hands-On Challenge: 3-Tier Differentiation
714
+
715
+ <div class="columns-3">
716
+ <div>
717
+
718
+ ### \u{1F949} Bronze Tier
719
+ - Implement basic handler logic
720
+ - Pass 3 baseline unit tests
721
+ - *Goal: Foundational mastery*
722
+
723
+ </div>
724
+ <div>
725
+
726
+ ### \u{1F948} Silver Tier
727
+ - Add boundary error handling
728
+ - Enforce O(N) memory complexity
729
+ - *Goal: Production-ready code*
730
+
731
+ </div>
732
+ <div>
733
+
734
+ ### \u{1F947} Gold Tier
735
+ - Design distributed retry queue
736
+ - Add 95% automated test coverage
737
+ - *Goal: Architectural leadership*
738
+
739
+ </div>
740
+ </div>
741
+
742
+ <!--
743
+ Presenter Notes:
744
+ - All learners begin at Bronze to establish baseline competency.
745
+ - Accelerate self-directed learners to Silver and Gold as they complete each tier.
746
+ -->`
747
+ },
748
+ {
749
+ id: "agenda-timeline",
750
+ name: "Session Agenda & Milestones",
751
+ category: "content",
752
+ description: "Structured time-budgeted agenda mapping stages to measurable deliverables.",
753
+ layout: {
754
+ boxes: [
755
+ {
756
+ id: "heading",
757
+ target: "h2",
758
+ pos: [80, 45, 1120, 65],
759
+ fontSize: "1.9rem"
760
+ },
761
+ {
762
+ id: "table-box",
763
+ target: "table",
764
+ pos: [80, 140, 1120, 520]
765
+ }
766
+ ]
767
+ },
768
+ markdownSnippet: `<!-- layout:
769
+ boxes:
770
+ - id: heading
771
+ target: h2
772
+ pos: [80, 45, 1120, 65]
773
+ fontSize: 1.9rem
774
+ - id: table-box
775
+ target: table
776
+ pos: [80, 140, 1120, 520]
777
+ -->
778
+
779
+ ## \u{1F5FA}\uFE0F Milestone Roadmap & Time Budget
780
+
781
+ | Allocated Time | Session Stage | Core Learning Focus | Deliverable Milestone |
782
+ |:---:|---|---|---|
783
+ | **05 mins** | Warm-Up | Real-World Scenario & Context | Identify system bottleneck |
784
+ | **15 mins** | Concept Discovery | Core Architectural Patterns | Deconstruct message queue models |
785
+ | **20 mins** | Hands-On Lab | Tiered Code Implementation | Complete Bronze + Silver tasks |
786
+ | **05 mins** | Wrap-Up | Formative Checkpoint & Debrief | Consolidate key principles |
787
+
788
+ <!--
789
+ Presenter Notes:
790
+ - Walk through the time budget so students understand expectations and pacing.
791
+ -->`
792
+ },
793
+ {
794
+ id: "takeaways-summary",
795
+ name: "Key Takeaways & Wrap-up",
796
+ category: "content",
797
+ description: "Three memorable synthesis takeaways with actionable next steps.",
798
+ layout: {
799
+ boxes: [
800
+ {
801
+ id: "heading",
802
+ target: "h2",
803
+ pos: [80, 50, 1120, 70],
804
+ fontSize: "2rem"
805
+ },
806
+ {
807
+ id: "list-box",
808
+ target: "ol",
809
+ pos: [80, 150, 1120, 340],
810
+ fontSize: "1.25rem"
811
+ },
812
+ {
813
+ id: "callout-next",
814
+ target: "blockquote",
815
+ pos: [80, 520, 1120, 140],
816
+ fontSize: "1.1rem"
817
+ }
818
+ ]
819
+ },
820
+ markdownSnippet: `<!-- layout:
821
+ boxes:
822
+ - id: heading
823
+ target: h2
824
+ pos: [80, 50, 1120, 70]
825
+ fontSize: 2rem
826
+ - id: list-box
827
+ target: ol
828
+ pos: [80, 150, 1120, 340]
829
+ fontSize: 1.25rem
830
+ - id: callout-next
831
+ target: blockquote
832
+ pos: [80, 520, 1120, 140]
833
+ fontSize: 1.1rem
834
+ -->
835
+
836
+ ## \u{1F3C1} Key Takeaways & Action Items
837
+
838
+ 1. **Decouple Before Scaling:** Separate producer rate from consumer processing capacity.
839
+ 2. **Design for Failure:** Always configure retries, backoff intervals, and dead-letter queues.
840
+ 3. **Measure End-to-End:** Track P99 latency and error rates across all distributed boundaries.
841
+
842
+ > \u{1F4DD} **Next Action Item:** Complete the Silver Tier refactoring challenge before our next peer-review session.
843
+
844
+ <!--
845
+ Presenter Notes:
846
+ - Deliver a crisp 2-minute wrap-up reinforcing the 3 core takeaways.
847
+ - Acknowledge strong peer collaboration during the hands-on lab.
848
+ -->`
849
+ }
850
+ ];
851
+ function getSlideLayoutPresetById(id) {
852
+ return SLIDE_LAYOUT_PRESETS.find((p) => p.id === id);
853
+ }
854
+ function getSlideLayoutPresetsByCategory(category) {
855
+ return SLIDE_LAYOUT_PRESETS.filter((p) => p.category === category);
856
+ }
857
+
858
+ // src/html-engine/themes.ts
859
+ var HTML_THEMES = {
860
+ "blue-professional": {
861
+ id: "blue-professional",
862
+ name: "Blue Professional",
863
+ scheme: "light",
864
+ palette: {
865
+ bg: "#FDFAE7",
866
+ bgSecondary: "#F4EFCF",
867
+ primary: "#1E2BFA",
868
+ accent: "#0D18B9",
869
+ text: "#111111",
870
+ textMuted: "#6B6B6B",
871
+ border: "#E2DCB9"
872
+ },
873
+ typography: {
874
+ display: "'Space Grotesk', system-ui, -apple-system, sans-serif",
875
+ body: "'Inter', system-ui, -apple-system, sans-serif",
876
+ code: "'JetBrains Mono', 'Fira Code', monospace"
877
+ }
878
+ },
879
+ "editorial-forest": {
880
+ id: "editorial-forest",
881
+ name: "Editorial Forest",
882
+ scheme: "dark",
883
+ palette: {
884
+ bg: "#0F1F17",
885
+ bgSecondary: "#183024",
886
+ primary: "#D9A74A",
887
+ accent: "#8EA696",
888
+ text: "#F4F1EA",
889
+ textMuted: "#A0B4A8",
890
+ border: "rgba(244, 241, 234, 0.12)"
891
+ },
892
+ typography: {
893
+ display: "'Playfair Display', Georgia, serif",
894
+ body: "'Inter', system-ui, -apple-system, sans-serif",
895
+ code: "'JetBrains Mono', monospace"
896
+ }
897
+ },
898
+ "cobalt-grid": {
899
+ id: "cobalt-grid",
900
+ name: "Cobalt Grid (Tech Modern)",
901
+ scheme: "dark",
902
+ palette: {
903
+ bg: "#090D1E",
904
+ bgSecondary: "#111836",
905
+ primary: "#00E5FF",
906
+ accent: "#3B82F6",
907
+ text: "#F8FAFC",
908
+ textMuted: "#94A3B8",
909
+ border: "rgba(255, 255, 255, 0.1)"
910
+ },
911
+ typography: {
912
+ display: "'Plus Jakarta Sans', system-ui, sans-serif",
913
+ body: "'Inter', system-ui, sans-serif",
914
+ code: "'Fira Code', 'JetBrains Mono', monospace"
915
+ },
916
+ customCss: `
917
+ .slide-canvas {
918
+ background-image: radial-gradient(rgba(0, 229, 255, 0.05) 1px, transparent 0);
919
+ background-size: 32px 32px;
920
+ }
921
+ `
922
+ },
923
+ studio: {
924
+ id: "studio",
925
+ name: "Studio Minimalist",
926
+ scheme: "light",
927
+ palette: {
928
+ bg: "#F8F9FA",
929
+ bgSecondary: "#EDEFF2",
930
+ primary: "#0F172A",
931
+ accent: "#2563EB",
932
+ text: "#0F172A",
933
+ textMuted: "#64748B",
934
+ border: "#E2E8F0"
935
+ },
936
+ typography: {
937
+ display: "'Inter', system-ui, -apple-system, sans-serif",
938
+ body: "'Inter', system-ui, -apple-system, sans-serif",
939
+ code: "'JetBrains Mono', monospace"
940
+ }
941
+ },
942
+ monochrome: {
943
+ id: "monochrome",
944
+ name: "Monochrome Dark",
945
+ scheme: "dark",
946
+ palette: {
947
+ bg: "#09090B",
948
+ bgSecondary: "#141417",
949
+ primary: "#FAFAFA",
950
+ accent: "#38BDF8",
951
+ text: "#FAFAFA",
952
+ textMuted: "#A1A1AA",
953
+ border: "rgba(255, 255, 255, 0.12)"
954
+ },
955
+ typography: {
956
+ display: "'Inter', system-ui, sans-serif",
957
+ body: "'Inter', system-ui, sans-serif",
958
+ code: "'JetBrains Mono', monospace"
959
+ }
960
+ }
961
+ };
962
+ function resolveHtmlTheme(themeName) {
963
+ if (themeName && HTML_THEMES[themeName]) {
964
+ return HTML_THEMES[themeName];
965
+ }
966
+ return HTML_THEMES["blue-professional"];
967
+ }
968
+
969
+ // src/html-engine/presets.ts
970
+ function escapeHtml(str) {
971
+ if (str === void 0 || str === null) return "";
972
+ return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
973
+ }
974
+ var HTML_SLIDE_PRESETS = {
975
+ "hero-cover": {
976
+ id: "hero-cover",
977
+ name: "Hero Cover",
978
+ category: "cover",
979
+ description: "Title slide with topic badge, large display heading, subtitle, and author/date info",
980
+ slots: [
981
+ { name: "tag", label: "Badge / Tag", type: "badge", description: "e.g. Unit 1 \xB7 Lesson 3" },
982
+ { name: "title", label: "Main Title", type: "text", description: "Main title of the lesson or deck", required: true },
983
+ { name: "subtitle", label: "Subtitle", type: "multiline", description: "Brief description or learning goal" },
984
+ { name: "author", label: "Instructor / Author", type: "text", description: "Instructor name or school" },
985
+ { name: "date", label: "Date / Period", type: "text", description: "Date or semester" }
986
+ ],
987
+ template: (slots, theme) => `
988
+ <div class="h-full w-full flex flex-col justify-between p-20 relative overflow-hidden">
989
+ <div class="space-y-6 max-w-4xl z-10 my-auto">
990
+ ${slots.tag ? `<div class="inline-flex items-center px-4 py-1.5 rounded-full text-sm font-bold tracking-widest uppercase border border-current opacity-90" style="color: var(--primary);">${escapeHtml(slots.tag)}</div>` : ""}
991
+ <h1 class="text-6xl font-extrabold leading-tight tracking-tight" style="font-family: var(--font-display); color: var(--text);">
992
+ ${escapeHtml(slots.title || "Slide Title")}
993
+ </h1>
994
+ ${slots.subtitle ? `<p class="text-2xl leading-relaxed opacity-85 max-w-3xl" style="color: var(--text-muted);">${escapeHtml(slots.subtitle)}</p>` : ""}
995
+ </div>
996
+ <div class="flex items-center justify-between border-t pt-6 text-base opacity-75 z-10" style="border-color: var(--border); color: var(--text-muted);">
997
+ <span class="font-medium">${escapeHtml(slots.author || "")}</span>
998
+ <span class="font-mono">${escapeHtml(slots.date || "")}</span>
999
+ </div>
1000
+ </div>
1001
+ `
1002
+ },
1003
+ "split-concept-code": {
1004
+ id: "split-concept-code",
1005
+ name: "Split Concept & Code",
1006
+ category: "split",
1007
+ description: "Left column for theoretical concept & bullet points, right column for code walkthrough",
1008
+ slots: [
1009
+ { name: "tag", label: "Section Tag", type: "badge", description: "e.g. Syntax & Example" },
1010
+ { name: "title", label: "Slide Title", type: "text", description: "Heading for this topic", required: true },
1011
+ { name: "points", label: "Key Points", type: "list", description: "Array of key takeaways or bullet points" },
1012
+ { name: "code", label: "Code Snippet", type: "code", description: "Source code", required: true },
1013
+ { name: "language", label: "Programming Language", type: "text", description: "e.g. python, typescript, swift" },
1014
+ { name: "codeNote", label: "Code Footnote / Explanation", type: "text", description: "Small tip under the code box" }
1015
+ ],
1016
+ template: (slots, theme) => {
1017
+ const points = Array.isArray(slots.points) ? slots.points : [];
1018
+ return `
1019
+ <div class="h-full w-full flex flex-col p-16 gap-6 justify-between">
1020
+ <div>
1021
+ ${slots.tag ? `<span class="text-xs uppercase tracking-widest font-bold opacity-80" style="color: var(--primary);">${escapeHtml(slots.tag)}</span>` : ""}
1022
+ <h2 class="text-4xl font-bold mt-1.5" style="font-family: var(--font-display); color: var(--text);">${escapeHtml(slots.title || "")}</h2>
1023
+ </div>
1024
+ <div class="flex-1 grid grid-cols-12 gap-8 items-center min-h-0">
1025
+ <div class="col-span-5 flex flex-col justify-center space-y-4">
1026
+ <ul class="space-y-4">
1027
+ ${points.map((pt) => `
1028
+ <li class="flex items-start gap-3 text-lg leading-relaxed" style="color: var(--text);">
1029
+ <span class="inline-block mt-2 w-2 h-2 rounded-full flex-shrink-0" style="background-color: var(--primary);"></span>
1030
+ <span>${escapeHtml(pt)}</span>
1031
+ </li>
1032
+ `).join("")}
1033
+ </ul>
1034
+ </div>
1035
+ <div class="col-span-7 flex flex-col rounded-xl overflow-hidden border shadow-lg" style="background-color: var(--bg-secondary); border-color: var(--border);">
1036
+ <div class="flex items-center justify-between px-5 py-2.5 border-b text-xs font-mono opacity-80" style="border-color: var(--border); color: var(--text-muted);">
1037
+ <div class="flex items-center gap-1.5">
1038
+ <span class="w-3 h-3 rounded-full bg-red-500/70 inline-block"></span>
1039
+ <span class="w-3 h-3 rounded-full bg-yellow-500/70 inline-block"></span>
1040
+ <span class="w-3 h-3 rounded-full bg-green-500/70 inline-block"></span>
1041
+ </div>
1042
+ <span class="uppercase tracking-wider font-semibold">${escapeHtml(slots.language || "code")}</span>
1043
+ </div>
1044
+ <pre class="flex-1 p-6 text-sm font-mono overflow-hidden leading-relaxed" style="color: var(--text);"><code>${escapeHtml(slots.code || "")}</code></pre>
1045
+ ${slots.codeNote ? `<div class="px-5 py-2.5 border-t text-xs font-mono opacity-80" style="border-color: var(--border); color: var(--text-muted);">${escapeHtml(slots.codeNote)}</div>` : ""}
1046
+ </div>
1047
+ </div>
1048
+ </div>
1049
+ `;
1050
+ }
1051
+ },
1052
+ "two-columns-compare": {
1053
+ id: "two-columns-compare",
1054
+ name: "Two Columns Compare",
1055
+ category: "split",
1056
+ description: "Side-by-side comparison for Pros/Cons, Approaches, or Before/After",
1057
+ slots: [
1058
+ { name: "tag", label: "Section Tag", type: "badge", description: "e.g. Comparison" },
1059
+ { name: "title", label: "Slide Title", type: "text", description: "Comparison headline", required: true },
1060
+ { name: "col1Title", label: "Left Column Title", type: "text", description: "e.g. Approach A / Pros" },
1061
+ { name: "col1Items", label: "Left Column Items", type: "list", description: "Array of points" },
1062
+ { name: "col2Title", label: "Right Column Title", type: "text", description: "e.g. Approach B / Cons" },
1063
+ { name: "col2Items", label: "Right Column Items", type: "list", description: "Array of points" }
1064
+ ],
1065
+ template: (slots, theme) => {
1066
+ const col1 = Array.isArray(slots.col1Items) ? slots.col1Items : [];
1067
+ const col2 = Array.isArray(slots.col2Items) ? slots.col2Items : [];
1068
+ return `
1069
+ <div class="h-full w-full flex flex-col p-16 gap-6 justify-between">
1070
+ <div>
1071
+ ${slots.tag ? `<span class="text-xs uppercase tracking-widest font-bold opacity-80" style="color: var(--primary);">${escapeHtml(slots.tag)}</span>` : ""}
1072
+ <h2 class="text-4xl font-bold mt-1.5" style="font-family: var(--font-display); color: var(--text);">${escapeHtml(slots.title || "")}</h2>
1073
+ </div>
1074
+ <div class="flex-1 grid grid-cols-2 gap-8 items-stretch min-h-0 my-auto">
1075
+ <div class="p-8 rounded-xl border flex flex-col space-y-4 shadow-sm justify-center" style="background-color: var(--bg-secondary); border-color: var(--border);">
1076
+ <h3 class="text-2xl font-bold" style="color: var(--primary);">${escapeHtml(slots.col1Title || "Column 1")}</h3>
1077
+ <ul class="space-y-3">
1078
+ ${col1.map((item) => `
1079
+ <li class="flex items-start gap-3 text-lg leading-relaxed" style="color: var(--text);">
1080
+ <span class="opacity-60 mt-0.5 font-bold">\u2022</span>
1081
+ <span>${escapeHtml(item)}</span>
1082
+ </li>
1083
+ `).join("")}
1084
+ </ul>
1085
+ </div>
1086
+ <div class="p-8 rounded-xl border flex flex-col space-y-4 shadow-sm justify-center" style="background-color: var(--bg-secondary); border-color: var(--border);">
1087
+ <h3 class="text-2xl font-bold" style="color: var(--text);">${escapeHtml(slots.col2Title || "Column 2")}</h3>
1088
+ <ul class="space-y-3">
1089
+ ${col2.map((item) => `
1090
+ <li class="flex items-start gap-3 text-lg leading-relaxed" style="color: var(--text);">
1091
+ <span class="opacity-60 mt-0.5 font-bold">\u2022</span>
1092
+ <span>${escapeHtml(item)}</span>
1093
+ </li>
1094
+ `).join("")}
1095
+ </ul>
1096
+ </div>
1097
+ </div>
1098
+ </div>
1099
+ `;
1100
+ }
1101
+ },
1102
+ "three-cards-grid": {
1103
+ id: "three-cards-grid",
1104
+ name: "Three Cards Grid",
1105
+ category: "grid",
1106
+ description: "3 cards showcasing 3 key pillars, concepts, or architectural layers",
1107
+ slots: [
1108
+ { name: "tag", label: "Section Tag", type: "badge", description: "e.g. Core Pillars" },
1109
+ { name: "title", label: "Slide Title", type: "text", description: "Overview title", required: true },
1110
+ { name: "cards", label: "Cards List", type: "options", description: "Array of 3 cards: {badge, title, desc}" }
1111
+ ],
1112
+ template: (slots, theme) => {
1113
+ const cards = Array.isArray(slots.cards) ? slots.cards : [];
1114
+ return `
1115
+ <div class="h-full w-full flex flex-col p-16 gap-6 justify-between">
1116
+ <div>
1117
+ ${slots.tag ? `<span class="text-xs uppercase tracking-widest font-bold opacity-80" style="color: var(--primary);">${escapeHtml(slots.tag)}</span>` : ""}
1118
+ <h2 class="text-4xl font-bold mt-1.5" style="font-family: var(--font-display); color: var(--text);">${escapeHtml(slots.title || "")}</h2>
1119
+ </div>
1120
+ <div class="flex-1 grid grid-cols-3 gap-6 items-stretch min-h-0 my-auto">
1121
+ ${cards.map((c, idx) => `
1122
+ <div class="p-8 rounded-xl border flex flex-col justify-between space-y-4 shadow-sm" style="background-color: var(--bg-secondary); border-color: var(--border);">
1123
+ <div class="space-y-3">
1124
+ <span class="text-xs font-mono font-bold uppercase tracking-wider px-3 py-1 rounded border inline-block" style="border-color: var(--border); color: var(--primary);">
1125
+ ${escapeHtml(c.badge || `0${idx + 1}`)}
1126
+ </span>
1127
+ <h3 class="text-2xl font-bold mt-2" style="color: var(--text);">${escapeHtml(c.title || "")}</h3>
1128
+ <p class="text-base leading-relaxed opacity-85" style="color: var(--text-muted);">${escapeHtml(c.desc || "")}</p>
1129
+ </div>
1130
+ ${c.footer ? `<div class="text-xs font-mono opacity-65 border-t pt-3" style="border-color: var(--border); color: var(--text-muted);">${escapeHtml(c.footer)}</div>` : ""}
1131
+ </div>
1132
+ `).join("")}
1133
+ </div>
1134
+ </div>
1135
+ `;
1136
+ }
1137
+ },
1138
+ "timeline-steps": {
1139
+ id: "timeline-steps",
1140
+ name: "Timeline / Workflow Steps",
1141
+ category: "timeline",
1142
+ description: "Step-by-step workflow, pipeline stages, or algorithm execution order",
1143
+ slots: [
1144
+ { name: "tag", label: "Section Tag", type: "badge", description: "e.g. Execution Flow" },
1145
+ { name: "title", label: "Slide Title", type: "text", description: "Workflow title", required: true },
1146
+ { name: "steps", label: "Steps List", type: "options", description: "Array of {stepNum, title, desc}" }
1147
+ ],
1148
+ template: (slots, theme) => {
1149
+ const steps = Array.isArray(slots.steps) ? slots.steps : [];
1150
+ return `
1151
+ <div class="h-full w-full flex flex-col p-16 gap-6 justify-between">
1152
+ <div>
1153
+ ${slots.tag ? `<span class="text-xs uppercase tracking-widest font-bold opacity-80" style="color: var(--primary);">${escapeHtml(slots.tag)}</span>` : ""}
1154
+ <h2 class="text-4xl font-bold mt-1.5" style="font-family: var(--font-display); color: var(--text);">${escapeHtml(slots.title || "")}</h2>
1155
+ </div>
1156
+ <div class="flex-1 flex flex-col justify-center my-auto">
1157
+ <div class="grid grid-cols-${steps.length || 4} gap-5 relative">
1158
+ ${steps.map((st, idx) => `
1159
+ <div class="flex flex-col space-y-3 p-6 rounded-xl border relative shadow-sm" style="background-color: var(--bg-secondary); border-color: var(--border);">
1160
+ <div class="w-10 h-10 rounded-xl flex items-center justify-center font-bold text-lg shadow" style="background-color: var(--primary); color: #FFFFFF;">
1161
+ ${escapeHtml(st.stepNum || idx + 1)}
1162
+ </div>
1163
+ <h4 class="text-xl font-bold" style="color: var(--text);">${escapeHtml(st.title || "")}</h4>
1164
+ <p class="text-sm opacity-85 leading-relaxed" style="color: var(--text-muted);">${escapeHtml(st.desc || "")}</p>
1165
+ </div>
1166
+ `).join("")}
1167
+ </div>
1168
+ </div>
1169
+ </div>
1170
+ `;
1171
+ }
1172
+ },
1173
+ "metric-callout": {
1174
+ id: "metric-callout",
1175
+ name: "Metric / Key Callout",
1176
+ category: "callout",
1177
+ description: "High-impact slide emphasizing a critical benchmark, theorem, or takeaway statistic",
1178
+ slots: [
1179
+ { name: "tag", label: "Category Tag", type: "badge", description: "e.g. Performance Benchmark" },
1180
+ { name: "metric", label: "Big Stat / Number", type: "metric", description: "e.g. 10x, 99.9%, O(1)" },
1181
+ { name: "metricLabel", label: "Metric Label", type: "text", description: "Short metric description" },
1182
+ { name: "title", label: "Callout Headline", type: "text", description: "Main point of this stat", required: true },
1183
+ { name: "desc", label: "Context Explanation", type: "multiline", description: "Deep context or impact" }
1184
+ ],
1185
+ template: (slots, theme) => `
1186
+ <div class="h-full w-full flex flex-col justify-center items-center p-16 text-center space-y-6 my-auto">
1187
+ ${slots.tag ? `<div class="px-5 py-1.5 rounded-full text-xs font-bold uppercase tracking-widest border" style="border-color: var(--border); color: var(--primary);">${escapeHtml(slots.tag)}</div>` : ""}
1188
+ <div class="text-8xl font-black tracking-tighter" style="font-family: var(--font-display); color: var(--primary);">
1189
+ ${escapeHtml(slots.metric || "100%")}
1190
+ </div>
1191
+ ${slots.metricLabel ? `<div class="text-lg font-bold tracking-widest uppercase opacity-75" style="color: var(--text-muted);">${escapeHtml(slots.metricLabel)}</div>` : ""}
1192
+ <h2 class="text-4xl font-extrabold max-w-3xl leading-tight" style="font-family: var(--font-display); color: var(--text);">${escapeHtml(slots.title || "")}</h2>
1193
+ ${slots.desc ? `<p class="text-lg max-w-2xl opacity-85 leading-relaxed" style="color: var(--text-muted);">${escapeHtml(slots.desc)}</p>` : ""}
1194
+ </div>
1195
+ `
1196
+ },
1197
+ "checkpoint-quiz": {
1198
+ id: "checkpoint-quiz",
1199
+ name: "Interactive Checkpoint Quiz",
1200
+ category: "assessment",
1201
+ description: "Interactive mid-lesson comprehension check with option cards and revealable answer",
1202
+ slots: [
1203
+ { name: "tag", label: "Badge", type: "badge", description: "e.g. Knowledge Check" },
1204
+ { name: "question", label: "Question Text", type: "multiline", description: "The question to answer", required: true },
1205
+ { name: "options", label: "Options List", type: "options", description: "Array of {label, text, isCorrect}" },
1206
+ { name: "explanation", label: "Explanation", type: "multiline", description: "Explanation revealed on answer" }
1207
+ ],
1208
+ template: (slots, theme) => {
1209
+ const opts = Array.isArray(slots.options) ? slots.options : [];
1210
+ return `
1211
+ <div class="h-full w-full flex flex-col p-16 gap-6 justify-between">
1212
+ <div>
1213
+ ${slots.tag ? `<span class="text-xs uppercase tracking-widest font-bold opacity-80" style="color: var(--primary);">${escapeHtml(slots.tag)}</span>` : ""}
1214
+ <h2 class="text-3xl font-extrabold mt-1.5 leading-snug max-w-4xl" style="font-family: var(--font-display); color: var(--text);">${escapeHtml(slots.question || "Quick Check")}</h2>
1215
+ </div>
1216
+ <div class="w-full grid grid-cols-2 gap-5 items-stretch min-h-0 my-auto">
1217
+ ${opts.map((opt, idx) => {
1218
+ const letter = opt.label || String.fromCharCode(65 + idx);
1219
+ return `
1220
+ <div class="quiz-option p-6 rounded-xl border flex items-center gap-4 shadow-sm" style="background-color: var(--bg-secondary); border-color: var(--border);">
1221
+ <span class="opt-badge w-12 h-12 rounded-xl flex items-center justify-center font-bold text-lg border shrink-0" style="border-color: var(--border); color: var(--primary);">
1222
+ ${escapeHtml(letter)}
1223
+ </span>
1224
+ <span class="text-lg font-medium leading-relaxed" style="color: var(--text);">${escapeHtml(opt.text || "")}</span>
1225
+ </div>
1226
+ `;
1227
+ }).join("")}
1228
+ </div>
1229
+ ${slots.explanation ? `
1230
+ <div class="quiz-explanation p-4 rounded-xl border text-sm flex items-center gap-3 opacity-90 shadow-sm" style="background-color: var(--bg-secondary); border-color: var(--border); color: var(--text);">
1231
+ <span class="text-xl">\u{1F4A1}</span>
1232
+ <div><strong class="text-primary font-bold">Hint / Explanation:</strong> ${escapeHtml(slots.explanation)}</div>
1233
+ </div>
1234
+ ` : ""}
1235
+ </div>
1236
+ `;
1237
+ }
1238
+ },
1239
+ "summary-takeaways": {
1240
+ id: "summary-takeaways",
1241
+ name: "Summary & Takeaways",
1242
+ category: "content",
1243
+ description: "End-of-lesson wrap-up slide with bulleted takeaways and next lesson teaser",
1244
+ slots: [
1245
+ { name: "tag", label: "Tag", type: "badge", description: "e.g. Recap" },
1246
+ { name: "title", label: "Slide Title", type: "text", description: "Wrap up headline", required: true },
1247
+ { name: "takeaways", label: "Takeaways List", type: "list", description: "Key points students should remember" },
1248
+ { name: "nextStep", label: "Next Lesson / Challenge", type: "text", description: "Preview of what comes next" }
1249
+ ],
1250
+ template: (slots, theme) => {
1251
+ const takeaways = Array.isArray(slots.takeaways) ? slots.takeaways : [];
1252
+ return `
1253
+ <div class="h-full w-full flex flex-col p-16 gap-6 justify-between">
1254
+ <div>
1255
+ ${slots.tag ? `<span class="text-xs uppercase tracking-widest font-bold opacity-80" style="color: var(--primary);">${escapeHtml(slots.tag)}</span>` : ""}
1256
+ <h2 class="text-4xl font-bold mt-1.5" style="font-family: var(--font-display); color: var(--text);">${escapeHtml(slots.title || "Summary")}</h2>
1257
+ </div>
1258
+ <div class="flex-1 flex flex-col justify-center space-y-6 min-h-0 my-auto">
1259
+ <div class="space-y-4">
1260
+ ${takeaways.map((t) => `
1261
+ <div class="p-5 rounded-xl border flex items-start gap-4 shadow-sm" style="background-color: var(--bg-secondary); border-color: var(--border);">
1262
+ <span class="text-xs font-bold w-7 h-7 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5 shadow-sm" style="background-color: var(--primary); color: #FFF;">\u2713</span>
1263
+ <span class="text-lg font-medium leading-relaxed" style="color: var(--text);">${escapeHtml(t)}</span>
1264
+ </div>
1265
+ `).join("")}
1266
+ </div>
1267
+ ${slots.nextStep ? `
1268
+ <div class="p-6 rounded-xl border flex items-center justify-between shadow-sm" style="border-color: var(--primary); background-color: var(--bg-secondary);">
1269
+ <div>
1270
+ <div class="text-xs uppercase font-bold tracking-widest opacity-80" style="color: var(--primary);">Next Session</div>
1271
+ <div class="text-lg font-bold mt-1" style="color: var(--text);">${escapeHtml(slots.nextStep)}</div>
1272
+ </div>
1273
+ <span class="text-2xl" style="color: var(--primary);">\u2794</span>
1274
+ </div>
1275
+ ` : ""}
1276
+ </div>
1277
+ </div>
1278
+ `;
1279
+ }
1280
+ }
1281
+ };
1282
+
1283
+ // src/html-engine/shell.ts
1284
+ function generateHtmlShell(options) {
1285
+ const { title, theme, slidesHtml, slideCount, customHeadTags = "" } = options;
1286
+ return `<!DOCTYPE html>
1287
+ <html lang="en" class="${theme.scheme}">
1288
+ <head>
1289
+ <meta charset="UTF-8">
1290
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
1291
+ <title>${title}</title>
1292
+
1293
+ <!-- Fonts -->
1294
+ <link rel="preconnect" href="https://fonts.googleapis.com">
1295
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1296
+ <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Plus+Jakarta+Sans:wght@500;700;800&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet">
1297
+
1298
+ <!-- Core Presentation Styling & 1920x1080 Scaled Tailwind Configuration -->
1299
+ <script src="https://cdn.tailwindcss.com"></script>
1300
+ <script>
1301
+ tailwind.config = {
1302
+ darkMode: 'class',
1303
+ theme: {
1304
+ extend: {
1305
+ fontSize: {
1306
+ 'xs': ['var(--font-size-xs, 14px)', { lineHeight: '1.4' }],
1307
+ 'sm': ['var(--font-size-sm, 16px)', { lineHeight: '1.45' }],
1308
+ 'base': ['var(--font-size-base, 18px)', { lineHeight: '1.5' }],
1309
+ 'lg': ['var(--font-size-lg, 20px)', { lineHeight: '1.5' }],
1310
+ 'xl': ['var(--font-size-xl, 22px)', { lineHeight: '1.5' }],
1311
+ '2xl': ['var(--font-size-2xl, 26px)', { lineHeight: '1.4' }],
1312
+ '3xl': ['var(--font-size-3xl, 32px)', { lineHeight: '1.3' }],
1313
+ '4xl': ['var(--font-size-4xl, 38px)', { lineHeight: '1.25' }],
1314
+ '5xl': ['var(--font-size-5xl, 46px)', { lineHeight: '1.2' }],
1315
+ '6xl': ['var(--font-size-6xl, 58px)', { lineHeight: '1.15' }],
1316
+ '7xl': ['var(--font-size-7xl, 72px)', { lineHeight: '1.1' }],
1317
+ '8xl': ['var(--font-size-8xl, 96px)', { lineHeight: '1.05' }],
1318
+ },
1319
+ spacing: {
1320
+ '2': 'calc(0.5rem * var(--scale-spacing-eff, 1))',
1321
+ '3': 'calc(0.75rem * var(--scale-spacing-eff, 1))',
1322
+ '4': 'calc(1rem * var(--scale-spacing-eff, 1))',
1323
+ '5': 'calc(1.25rem * var(--scale-spacing-eff, 1))',
1324
+ '6': 'calc(1.5rem * var(--scale-spacing-eff, 1))',
1325
+ '8': 'calc(2rem * var(--scale-spacing-eff, 1))',
1326
+ '10': 'calc(2.5rem * var(--scale-spacing-eff, 1))',
1327
+ '12': 'calc(3rem * var(--scale-spacing-eff, 1))',
1328
+ '16': 'calc(4rem * var(--scale-spacing-eff, 1))',
1329
+ '20': 'calc(5rem * var(--scale-spacing-eff, 1))',
1330
+ '24': 'calc(6rem * var(--scale-spacing-eff, 1))',
1331
+ },
1332
+ colors: {
1333
+ themeBg: 'var(--bg)',
1334
+ themeBgSecondary: 'var(--bg-secondary)',
1335
+ themePrimary: 'var(--primary)',
1336
+ themeText: 'var(--text)',
1337
+ themeMuted: 'var(--text-muted)',
1338
+ themeBorder: 'var(--border)',
1339
+ }
1340
+ }
1341
+ }
1342
+ }
1343
+ </script>
1344
+
1345
+ <style>
1346
+ :root {
1347
+ /* Scaling Multiplier Architecture: Unified vs Independent */
1348
+ --scale-mode: unified;
1349
+ --scale-factor: 1.0;
1350
+ --scale-headings: 1.0;
1351
+ --scale-body: 1.0;
1352
+ --scale-code: 1.0;
1353
+ --scale-spacing: 1.0;
1354
+
1355
+ /* Effective Multipliers */
1356
+ --scale-headings-eff: 1.0;
1357
+ --scale-body-eff: 1.0;
1358
+ --scale-code-eff: 1.0;
1359
+ --scale-spacing-eff: 1.0;
1360
+
1361
+ --bg: ${theme.palette.bg};
1362
+ --bg-secondary: ${theme.palette.bgSecondary || theme.palette.bg};
1363
+ --primary: ${theme.palette.primary};
1364
+ --accent: ${theme.palette.accent || theme.palette.primary};
1365
+ --text: ${theme.palette.text};
1366
+ --text-muted: ${theme.palette.textMuted};
1367
+ --border: ${theme.palette.border || "rgba(0, 0, 0, 0.12)"};
1368
+ --font-display: ${theme.typography.display};
1369
+ --font-body: ${theme.typography.body};
1370
+ --font-code: ${theme.typography.code || "monospace"};
1371
+
1372
+ /* Dynamic Typography calculated from effective multipliers */
1373
+ --font-size-xs: calc(14px * var(--scale-code-eff));
1374
+ --font-size-sm: calc(16px * var(--scale-code-eff));
1375
+ --font-size-base: calc(18px * var(--scale-body-eff));
1376
+ --font-size-lg: calc(20px * var(--scale-body-eff));
1377
+ --font-size-xl: calc(22px * var(--scale-body-eff));
1378
+ --font-size-2xl: calc(26px * var(--scale-body-eff));
1379
+ --font-size-3xl: calc(32px * var(--scale-headings-eff));
1380
+ --font-size-4xl: calc(38px * var(--scale-headings-eff));
1381
+ --font-size-5xl: calc(46px * var(--scale-headings-eff));
1382
+ --font-size-6xl: calc(58px * var(--scale-headings-eff));
1383
+ --font-size-7xl: calc(72px * var(--scale-headings-eff));
1384
+ --font-size-8xl: calc(96px * var(--scale-headings-eff));
1385
+ }
1386
+
1387
+ * {
1388
+ box-sizing: border-box;
1389
+ margin: 0;
1390
+ padding: 0;
1391
+ }
1392
+
1393
+ html, body {
1394
+ width: 100%;
1395
+ height: 100%;
1396
+ overflow: hidden;
1397
+ background-color: #050507;
1398
+ font-family: var(--font-body);
1399
+ color: var(--text);
1400
+ user-select: none;
1401
+ -webkit-font-smoothing: antialiased;
1402
+ -moz-osx-font-smoothing: grayscale;
1403
+ }
1404
+
1405
+ #viewport {
1406
+ position: absolute;
1407
+ top: 0;
1408
+ left: 0;
1409
+ width: 100vw;
1410
+ height: 100vh;
1411
+ display: flex;
1412
+ align-items: center;
1413
+ justify-content: center;
1414
+ background-color: #050507;
1415
+ overflow: hidden;
1416
+ }
1417
+
1418
+ .slide-canvas {
1419
+ position: absolute;
1420
+ top: 50%;
1421
+ left: 50%;
1422
+ width: 1920px;
1423
+ height: 1080px;
1424
+ transform-origin: center center;
1425
+ background-color: var(--bg);
1426
+ overflow: hidden;
1427
+ box-shadow: 0 30px 90px rgba(0, 0, 0, 0.6);
1428
+ border-radius: 4px;
1429
+ }
1430
+
1431
+ .slide-page {
1432
+ position: absolute;
1433
+ top: 0;
1434
+ left: 0;
1435
+ width: 100%;
1436
+ height: 100%;
1437
+ display: none;
1438
+ opacity: 0;
1439
+ transition: opacity 0.2s cubic-bezier(0.16, 1, 0.3, 1);
1440
+ }
1441
+
1442
+ .slide-page.active {
1443
+ display: block;
1444
+ opacity: 1;
1445
+ }
1446
+
1447
+ /* Top Progress Bar */
1448
+ #progress-bar {
1449
+ position: absolute;
1450
+ top: 0;
1451
+ left: 0;
1452
+ height: 5px;
1453
+ background-color: var(--primary);
1454
+ transition: width 0.25s cubic-bezier(0.16, 1, 0.3, 1);
1455
+ z-index: 50;
1456
+ }
1457
+
1458
+ /* Floating Navigation Controls */
1459
+ #controls {
1460
+ position: absolute;
1461
+ bottom: 28px;
1462
+ right: 28px;
1463
+ display: flex;
1464
+ align-items: center;
1465
+ gap: 12px;
1466
+ background: rgba(15, 15, 20, 0.75);
1467
+ backdrop-filter: blur(16px);
1468
+ border: 1px solid rgba(255, 255, 255, 0.15);
1469
+ border-radius: 9999px;
1470
+ padding: 8px 20px;
1471
+ z-index: 100;
1472
+ opacity: 0;
1473
+ transition: opacity 0.25s ease;
1474
+ }
1475
+
1476
+ body:hover #controls {
1477
+ opacity: 1;
1478
+ }
1479
+
1480
+ .control-btn {
1481
+ background: transparent;
1482
+ border: none;
1483
+ color: #ffffff;
1484
+ cursor: pointer;
1485
+ font-size: 16px;
1486
+ display: flex;
1487
+ align-items: center;
1488
+ justify-content: center;
1489
+ padding: 6px;
1490
+ border-radius: 8px;
1491
+ transition: background-color 0.15s, transform 0.1s;
1492
+ }
1493
+
1494
+ .control-btn:hover {
1495
+ background: rgba(255, 255, 255, 0.2);
1496
+ transform: scale(1.08);
1497
+ }
1498
+
1499
+ #slide-counter {
1500
+ color: #ffffff;
1501
+ font-size: 15px;
1502
+ font-family: var(--font-code);
1503
+ font-weight: 600;
1504
+ letter-spacing: 0.08em;
1505
+ padding: 0 4px;
1506
+ }
1507
+
1508
+ /* Interactive Quiz Styling */
1509
+ .quiz-option {
1510
+ cursor: pointer;
1511
+ user-select: none;
1512
+ transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
1513
+ }
1514
+
1515
+ .quiz-option:hover {
1516
+ transform: translateY(-2px);
1517
+ border-color: var(--primary) !important;
1518
+ box-shadow: 0 12px 30px -8px rgba(0, 0, 0, 0.15);
1519
+ }
1520
+
1521
+ .quiz-option.selected {
1522
+ border-color: var(--primary) !important;
1523
+ box-shadow: 0 0 0 3px var(--primary);
1524
+ }
1525
+
1526
+ .quiz-option.selected .opt-badge {
1527
+ background-color: var(--primary) !important;
1528
+ color: #ffffff !important;
1529
+ border-color: var(--primary) !important;
1530
+ }
1531
+
1532
+ .quiz-explanation {
1533
+ transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
1534
+ }
1535
+
1536
+ /* \u2500\u2500 WYSIWYG Inline Editing (Level 1) Styles \u2500\u2500 */
1537
+ .edit-hotzone {
1538
+ position: fixed;
1539
+ top: 0;
1540
+ left: 0;
1541
+ width: 90px;
1542
+ height: 90px;
1543
+ z-index: 9999;
1544
+ cursor: pointer;
1545
+ }
1546
+
1547
+ .edit-toggle {
1548
+ position: fixed;
1549
+ top: 24px;
1550
+ left: 24px;
1551
+ z-index: 10000;
1552
+ background: rgba(15, 15, 20, 0.85);
1553
+ backdrop-filter: blur(14px);
1554
+ border: 1px solid rgba(255, 255, 255, 0.2);
1555
+ border-radius: 9999px;
1556
+ color: #ffffff;
1557
+ padding: 7px 16px;
1558
+ font-size: 14px;
1559
+ font-family: var(--font-body);
1560
+ font-weight: 600;
1561
+ display: flex;
1562
+ align-items: center;
1563
+ gap: 8px;
1564
+ cursor: pointer;
1565
+ opacity: 0;
1566
+ pointer-events: none;
1567
+ transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1), transform 0.15s;
1568
+ }
1569
+
1570
+ .edit-toggle:hover {
1571
+ background: rgba(255, 255, 255, 0.2);
1572
+ transform: scale(1.04);
1573
+ }
1574
+
1575
+ .edit-toggle.show,
1576
+ .edit-toggle.active {
1577
+ opacity: 1;
1578
+ pointer-events: auto;
1579
+ }
1580
+
1581
+ .edit-toggle.active {
1582
+ background: var(--primary);
1583
+ border-color: var(--primary);
1584
+ color: #ffffff;
1585
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.4);
1586
+ }
1587
+
1588
+ #edit-bar {
1589
+ position: fixed;
1590
+ bottom: 28px;
1591
+ left: 50%;
1592
+ transform: translateX(-50%);
1593
+ background: rgba(15, 15, 20, 0.92);
1594
+ backdrop-filter: blur(16px);
1595
+ border: 1px solid rgba(255, 255, 255, 0.22);
1596
+ border-radius: 9999px;
1597
+ padding: 6px 18px;
1598
+ display: flex;
1599
+ align-items: center;
1600
+ gap: 10px;
1601
+ z-index: 10000;
1602
+ box-shadow: 0 20px 45px rgba(0, 0, 0, 0.5);
1603
+ animation: editBarSlideUp 0.2s cubic-bezier(0.16, 1, 0.3, 1);
1604
+ }
1605
+
1606
+ @keyframes editBarSlideUp {
1607
+ from { opacity: 0; transform: translate(-50%, 15px); }
1608
+ to { opacity: 1; transform: translate(-50%, 0); }
1609
+ }
1610
+
1611
+ .edit-bar-btn {
1612
+ background: transparent;
1613
+ border: none;
1614
+ color: #ffffff;
1615
+ font-size: 14px;
1616
+ font-weight: 600;
1617
+ padding: 6px 12px;
1618
+ border-radius: 8px;
1619
+ cursor: pointer;
1620
+ display: flex;
1621
+ align-items: center;
1622
+ gap: 6px;
1623
+ transition: background 0.15s, transform 0.1s;
1624
+ }
1625
+
1626
+ .edit-bar-btn:hover {
1627
+ background: rgba(255, 255, 255, 0.2);
1628
+ transform: translateY(-1px);
1629
+ }
1630
+
1631
+ .edit-bar-btn.save-btn {
1632
+ background: var(--primary);
1633
+ color: #ffffff;
1634
+ }
1635
+ .edit-bar-btn.save-btn:hover {
1636
+ filter: brightness(1.12);
1637
+ }
1638
+
1639
+ .edit-bar-divider {
1640
+ width: 1px;
1641
+ height: 20px;
1642
+ background: rgba(255, 255, 255, 0.2);
1643
+ }
1644
+
1645
+ /* Editable active styles */
1646
+ body.edit-mode-active [contenteditable="true"] {
1647
+ outline: 1px dashed rgba(100, 149, 237, 0.35);
1648
+ outline-offset: 3px;
1649
+ cursor: text;
1650
+ border-radius: 4px;
1651
+ }
1652
+
1653
+ body.edit-mode-active [contenteditable="true"]:hover {
1654
+ outline: 2px dashed var(--primary);
1655
+ }
1656
+
1657
+ body.edit-mode-active [contenteditable="true"]:focus {
1658
+ outline: 2px solid var(--primary);
1659
+ background: rgba(100, 149, 237, 0.08);
1660
+ }
1661
+
1662
+ /* \u2500\u2500\u2500 Floating Scale Tuning Panel (Unified vs Independent) \u2500\u2500\u2500 */
1663
+ #scale-panel {
1664
+ position: fixed;
1665
+ bottom: 82px;
1666
+ left: 50%;
1667
+ transform: translateX(-50%);
1668
+ width: 390px;
1669
+ background: rgba(18, 18, 26, 0.94);
1670
+ backdrop-filter: blur(20px);
1671
+ -webkit-backdrop-filter: blur(20px);
1672
+ border: 1px solid rgba(255, 255, 255, 0.2);
1673
+ border-radius: 16px;
1674
+ padding: 16px 20px;
1675
+ z-index: 10002;
1676
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.65), 0 0 0 1px rgba(255, 255, 255, 0.08);
1677
+ color: #f4f4f5;
1678
+ font-family: var(--font-body);
1679
+ animation: scalePanelPop 0.2s cubic-bezier(0.16, 1, 0.3, 1);
1680
+ }
1681
+
1682
+ @keyframes scalePanelPop {
1683
+ from { opacity: 0; transform: translate(-50%, 10px) scale(0.97); }
1684
+ to { opacity: 1; transform: translate(-50%, 0) scale(1); }
1685
+ }
1686
+
1687
+ .scale-panel-header {
1688
+ display: flex;
1689
+ align-items: center;
1690
+ justify-content: space-between;
1691
+ gap: 12px;
1692
+ margin-bottom: 14px;
1693
+ padding-bottom: 10px;
1694
+ border-bottom: 1px solid rgba(255, 255, 255, 0.12);
1695
+ }
1696
+
1697
+ .scale-panel-title {
1698
+ display: flex;
1699
+ align-items: center;
1700
+ gap: 8px;
1701
+ font-size: 13px;
1702
+ font-weight: 700;
1703
+ letter-spacing: -0.01em;
1704
+ color: #ffffff;
1705
+ text-transform: uppercase;
1706
+ }
1707
+
1708
+ .scale-mode-switch {
1709
+ display: flex;
1710
+ background: rgba(0, 0, 0, 0.35);
1711
+ border: 1px solid rgba(255, 255, 255, 0.12);
1712
+ border-radius: 9999px;
1713
+ padding: 2px;
1714
+ gap: 2px;
1715
+ }
1716
+
1717
+ .mode-tab {
1718
+ background: transparent;
1719
+ border: none;
1720
+ color: #a1a1aa;
1721
+ font-size: 11px;
1722
+ font-weight: 600;
1723
+ padding: 3px 10px;
1724
+ border-radius: 9999px;
1725
+ cursor: pointer;
1726
+ transition: all 0.15s ease;
1727
+ }
1728
+
1729
+ .mode-tab.active {
1730
+ background: var(--primary);
1731
+ color: #ffffff;
1732
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
1733
+ }
1734
+
1735
+ .scale-panel-close {
1736
+ background: transparent;
1737
+ border: none;
1738
+ color: #71717a;
1739
+ cursor: pointer;
1740
+ font-size: 14px;
1741
+ padding: 4px;
1742
+ display: flex;
1743
+ align-items: center;
1744
+ justify-content: center;
1745
+ border-radius: 6px;
1746
+ transition: color 0.15s, background 0.15s;
1747
+ }
1748
+
1749
+ .scale-panel-close:hover {
1750
+ color: #ffffff;
1751
+ background: rgba(255, 255, 255, 0.1);
1752
+ }
1753
+
1754
+ .scale-slider-row {
1755
+ margin-bottom: 12px;
1756
+ }
1757
+
1758
+ .scale-slider-row:last-child {
1759
+ margin-bottom: 0;
1760
+ }
1761
+
1762
+ .scale-slider-label-wrap {
1763
+ display: flex;
1764
+ align-items: center;
1765
+ justify-content: space-between;
1766
+ font-size: 12px;
1767
+ font-weight: 500;
1768
+ color: #d4d4d8;
1769
+ margin-bottom: 5px;
1770
+ }
1771
+
1772
+ .scale-badge {
1773
+ font-family: var(--font-code);
1774
+ font-size: 11px;
1775
+ font-weight: 600;
1776
+ background: rgba(255, 255, 255, 0.1);
1777
+ padding: 1px 6px;
1778
+ border-radius: 4px;
1779
+ color: #38bdf8;
1780
+ }
1781
+
1782
+ .scale-range-slider {
1783
+ -webkit-appearance: none;
1784
+ appearance: none;
1785
+ width: 100%;
1786
+ height: 6px;
1787
+ border-radius: 3px;
1788
+ background: rgba(255, 255, 255, 0.18);
1789
+ outline: none;
1790
+ cursor: pointer;
1791
+ transition: background 0.15s;
1792
+ }
1793
+
1794
+ .scale-range-slider::-webkit-slider-thumb {
1795
+ -webkit-appearance: none;
1796
+ appearance: none;
1797
+ width: 16px;
1798
+ height: 16px;
1799
+ border-radius: 50%;
1800
+ background: var(--primary);
1801
+ cursor: pointer;
1802
+ box-shadow: 0 0 6px rgba(0, 0, 0, 0.5);
1803
+ transition: transform 0.1s;
1804
+ }
1805
+
1806
+ .scale-range-slider::-webkit-slider-thumb:hover {
1807
+ transform: scale(1.2);
1808
+ }
1809
+
1810
+ .density-presets {
1811
+ display: flex;
1812
+ gap: 8px;
1813
+ margin-top: 14px;
1814
+ }
1815
+
1816
+ .density-chip {
1817
+ flex: 1;
1818
+ background: rgba(255, 255, 255, 0.06);
1819
+ border: 1px solid rgba(255, 255, 255, 0.12);
1820
+ border-radius: 8px;
1821
+ color: #d4d4d8;
1822
+ font-size: 11px;
1823
+ font-weight: 600;
1824
+ padding: 6px 0;
1825
+ cursor: pointer;
1826
+ text-align: center;
1827
+ transition: all 0.15s ease;
1828
+ }
1829
+
1830
+ .density-chip:hover {
1831
+ background: rgba(255, 255, 255, 0.12);
1832
+ color: #ffffff;
1833
+ }
1834
+
1835
+ .density-chip.active {
1836
+ background: rgba(56, 189, 248, 0.2);
1837
+ border-color: #38bdf8;
1838
+ color: #38bdf8;
1839
+ }
1840
+
1841
+ .scale-independent-footer {
1842
+ display: flex;
1843
+ justify-content: flex-end;
1844
+ margin-top: 12px;
1845
+ padding-top: 10px;
1846
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
1847
+ }
1848
+
1849
+ .btn-reset-scales {
1850
+ background: transparent;
1851
+ border: 1px solid rgba(255, 255, 255, 0.15);
1852
+ border-radius: 6px;
1853
+ color: #a1a1aa;
1854
+ font-size: 11px;
1855
+ font-weight: 600;
1856
+ padding: 4px 10px;
1857
+ cursor: pointer;
1858
+ transition: all 0.15s ease;
1859
+ }
1860
+
1861
+ .btn-reset-scales:hover {
1862
+ background: rgba(255, 255, 255, 0.1);
1863
+ color: #ffffff;
1864
+ border-color: rgba(255, 255, 255, 0.3);
1865
+ }
1866
+
1867
+ /* \u2500\u2500\u2500 Step-by-Step Reveal (Animation Steps / Build-In) \u2500\u2500\u2500 */
1868
+ .step-reveal,
1869
+ [data-step],
1870
+ [data-reveal],
1871
+ [data-click],
1872
+ .v-click {
1873
+ opacity: 0;
1874
+ transform: translateY(10px);
1875
+ transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1), transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
1876
+ pointer-events: none;
1877
+ }
1878
+
1879
+ .step-reveal.revealed,
1880
+ [data-step].revealed,
1881
+ [data-reveal].revealed,
1882
+ [data-click].revealed,
1883
+ .v-click.visible,
1884
+ body.is-editing .step-reveal,
1885
+ body.is-editing [data-step],
1886
+ body.is-editing [data-reveal],
1887
+ body.is-editing [data-click],
1888
+ body.is-editing .v-click,
1889
+ body.mode-deck .step-reveal,
1890
+ body.mode-deck [data-step],
1891
+ body.mode-deck [data-reveal],
1892
+ body.mode-deck [data-click],
1893
+ body.mode-deck .v-click {
1894
+ opacity: 1 !important;
1895
+ transform: translateY(0) !important;
1896
+ pointer-events: auto !important;
1897
+ }
1898
+
1899
+ /* \u2500\u2500\u2500 Deck Mode (Continuous Vertical Scroll) \u2500\u2500\u2500 */
1900
+ body.mode-deck {
1901
+ overflow-y: auto !important;
1902
+ height: auto !important;
1903
+ min-height: 100vh;
1904
+ background-color: #08080c !important;
1905
+ user-select: auto !important;
1906
+ }
1907
+
1908
+ body.mode-deck #viewport {
1909
+ position: relative !important;
1910
+ width: 100% !important;
1911
+ height: auto !important;
1912
+ min-height: 100vh;
1913
+ overflow-y: visible !important;
1914
+ display: flex !important;
1915
+ flex-direction: column !important;
1916
+ align-items: center !important;
1917
+ padding: 60px 24px 140px 24px !important;
1918
+ gap: 36px !important;
1919
+ background-color: transparent !important;
1920
+ }
1921
+
1922
+ body.mode-deck .slide-canvas {
1923
+ position: relative !important;
1924
+ top: auto !important;
1925
+ left: auto !important;
1926
+ transform: none !important;
1927
+ width: 100% !important;
1928
+ max-width: 1200px !important;
1929
+ height: auto !important;
1930
+ aspect-ratio: 16 / 9 !important;
1931
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.6) !important;
1932
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
1933
+ border-radius: 8px !important;
1934
+ }
1935
+
1936
+ body.mode-deck .slide-page {
1937
+ display: block !important;
1938
+ opacity: 1 !important;
1939
+ position: relative !important;
1940
+ width: 100% !important;
1941
+ height: 100% !important;
1942
+ }
1943
+
1944
+ /* Deck Notes Card under each slide */
1945
+ .deck-notes-card {
1946
+ display: none;
1947
+ width: 100%;
1948
+ max-width: 1200px;
1949
+ margin-top: -16px;
1950
+ margin-bottom: 24px;
1951
+ background: rgba(20, 20, 28, 0.95);
1952
+ border: 1px solid rgba(255, 255, 255, 0.12);
1953
+ border-left: 4px solid var(--primary);
1954
+ border-radius: 8px;
1955
+ padding: 16px 22px;
1956
+ color: #e4e4e7;
1957
+ font-size: 14px;
1958
+ line-height: 1.65;
1959
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
1960
+ }
1961
+
1962
+ .deck-notes-card-header {
1963
+ font-size: 12px;
1964
+ font-weight: 700;
1965
+ text-transform: uppercase;
1966
+ letter-spacing: 0.06em;
1967
+ color: var(--primary);
1968
+ margin-bottom: 6px;
1969
+ display: flex;
1970
+ align-items: center;
1971
+ gap: 6px;
1972
+ }
1973
+
1974
+ body.mode-deck .deck-notes-card,
1975
+ body.show-deck-notes .deck-notes-card {
1976
+ display: block;
1977
+ }
1978
+
1979
+ /* \u2500\u2500\u2500 Presenter Drawer (In-Page) \u2500\u2500\u2500 */
1980
+ #presenter-drawer {
1981
+ position: fixed;
1982
+ bottom: 0;
1983
+ left: 0;
1984
+ width: 100%;
1985
+ max-height: 380px;
1986
+ background: rgba(14, 14, 20, 0.96);
1987
+ backdrop-filter: blur(24px);
1988
+ -webkit-backdrop-filter: blur(24px);
1989
+ border-top: 1px solid rgba(255, 255, 255, 0.18);
1990
+ border-top-left-radius: 18px;
1991
+ border-top-right-radius: 18px;
1992
+ padding: 18px 28px 24px 28px;
1993
+ z-index: 10005;
1994
+ box-shadow: 0 -15px 40px rgba(0, 0, 0, 0.7);
1995
+ color: #f4f4f5;
1996
+ display: flex;
1997
+ flex-direction: column;
1998
+ gap: 12px;
1999
+ transform: translateY(100%);
2000
+ transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
2001
+ }
2002
+
2003
+ #presenter-drawer.open {
2004
+ transform: translateY(0);
2005
+ }
2006
+
2007
+ .presenter-drawer-header {
2008
+ display: flex;
2009
+ align-items: center;
2010
+ justify-content: space-between;
2011
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
2012
+ padding-bottom: 12px;
2013
+ }
2014
+
2015
+ .presenter-meta-group {
2016
+ display: flex;
2017
+ align-items: center;
2018
+ gap: 18px;
2019
+ }
2020
+
2021
+ .presenter-timer-badge {
2022
+ font-family: var(--font-code);
2023
+ font-size: 15px;
2024
+ font-weight: 700;
2025
+ color: #38bdf8;
2026
+ background: rgba(56, 189, 248, 0.12);
2027
+ border: 1px solid rgba(56, 189, 248, 0.3);
2028
+ padding: 3px 10px;
2029
+ border-radius: 6px;
2030
+ display: flex;
2031
+ align-items: center;
2032
+ gap: 6px;
2033
+ }
2034
+
2035
+ .presenter-notes-box {
2036
+ flex: 1;
2037
+ overflow-y: auto;
2038
+ font-size: 16px;
2039
+ line-height: 1.7;
2040
+ color: #f1f5f9;
2041
+ background: rgba(0, 0, 0, 0.25);
2042
+ border: 1px solid rgba(255, 255, 255, 0.08);
2043
+ border-radius: 8px;
2044
+ padding: 14px 18px;
2045
+ max-height: 220px;
2046
+ user-select: text;
2047
+ }
2048
+
2049
+ /* \u2500\u2500\u2500 Print Modal / Dialog \u2500\u2500\u2500 */
2050
+ #print-modal {
2051
+ position: fixed;
2052
+ top: 0;
2053
+ left: 0;
2054
+ width: 100vw;
2055
+ height: 100vh;
2056
+ background: rgba(0, 0, 0, 0.65);
2057
+ backdrop-filter: blur(8px);
2058
+ z-index: 10010;
2059
+ display: none;
2060
+ align-items: center;
2061
+ justify-content: center;
2062
+ }
2063
+
2064
+ #print-modal.open {
2065
+ display: flex;
2066
+ }
2067
+
2068
+ .print-dialog-card {
2069
+ width: 440px;
2070
+ background: #181822;
2071
+ border: 1px solid rgba(255, 255, 255, 0.2);
2072
+ border-radius: 16px;
2073
+ padding: 24px;
2074
+ box-shadow: 0 25px 60px rgba(0, 0, 0, 0.7);
2075
+ color: #ffffff;
2076
+ display: flex;
2077
+ flex-direction: column;
2078
+ gap: 18px;
2079
+ }
2080
+
2081
+ .print-dialog-title {
2082
+ font-size: 17px;
2083
+ font-weight: 700;
2084
+ display: flex;
2085
+ align-items: center;
2086
+ gap: 8px;
2087
+ }
2088
+
2089
+ .print-option-row {
2090
+ display: flex;
2091
+ align-items: center;
2092
+ gap: 10px;
2093
+ background: rgba(255, 255, 255, 0.05);
2094
+ padding: 12px 14px;
2095
+ border-radius: 8px;
2096
+ cursor: pointer;
2097
+ font-size: 14px;
2098
+ }
2099
+
2100
+ .print-dialog-actions {
2101
+ display: flex;
2102
+ align-items: center;
2103
+ justify-content: flex-end;
2104
+ gap: 10px;
2105
+ }
2106
+
2107
+ /* \u2500\u2500\u2500 Controls Extra Buttons & Badges \u2500\u2500\u2500 */
2108
+ .step-badge {
2109
+ font-family: var(--font-code);
2110
+ font-size: 11px;
2111
+ font-weight: 600;
2112
+ background: rgba(255, 255, 255, 0.12);
2113
+ color: #a5f3fc;
2114
+ padding: 2px 7px;
2115
+ border-radius: 4px;
2116
+ display: none;
2117
+ }
2118
+
2119
+ .control-divider {
2120
+ width: 1px;
2121
+ height: 16px;
2122
+ background: rgba(255, 255, 255, 0.18);
2123
+ margin: 0 2px;
2124
+ }
2125
+
2126
+ /* \u2500\u2500\u2500 Print Engine (@media print) \u2500\u2500\u2500 */
2127
+ @media print {
2128
+ @page {
2129
+ size: landscape;
2130
+ margin: 0;
2131
+ }
2132
+
2133
+ html, body {
2134
+ width: 100% !important;
2135
+ height: auto !important;
2136
+ background: #ffffff !important;
2137
+ color: #000000 !important;
2138
+ overflow: visible !important;
2139
+ user-select: text !important;
2140
+ }
2141
+
2142
+ #controls,
2143
+ #edit-bar,
2144
+ #scale-panel,
2145
+ #progress-bar,
2146
+ .edit-hotzone,
2147
+ .edit-toggle,
2148
+ #presenter-drawer,
2149
+ #print-modal {
2150
+ display: none !important;
2151
+ }
2152
+
2153
+ #viewport {
2154
+ position: static !important;
2155
+ width: 100% !important;
2156
+ height: auto !important;
2157
+ display: block !important;
2158
+ padding: 0 !important;
2159
+ background: transparent !important;
2160
+ overflow: visible !important;
2161
+ }
2162
+
2163
+ .slide-canvas {
2164
+ position: relative !important;
2165
+ top: auto !important;
2166
+ left: auto !important;
2167
+ transform: none !important;
2168
+ width: 100vw !important;
2169
+ height: 56.25vw !important; /* 16:9 ratio */
2170
+ max-height: 100vh !important;
2171
+ margin: 0 auto !important;
2172
+ page-break-inside: avoid !important;
2173
+ break-inside: avoid !important;
2174
+ page-break-after: always !important;
2175
+ break-after: page !important;
2176
+ box-shadow: none !important;
2177
+ border: none !important;
2178
+ border-radius: 0 !important;
2179
+ background-color: var(--bg) !important;
2180
+ -webkit-print-color-adjust: exact !important;
2181
+ print-color-adjust: exact !important;
2182
+ }
2183
+
2184
+ .slide-page {
2185
+ display: block !important;
2186
+ opacity: 1 !important;
2187
+ position: relative !important;
2188
+ width: 100% !important;
2189
+ height: 100% !important;
2190
+ }
2191
+
2192
+ /* Step elements always visible when printing */
2193
+ .step-reveal, [data-step], [data-reveal], [data-click], .v-click {
2194
+ opacity: 1 !important;
2195
+ transform: none !important;
2196
+ }
2197
+
2198
+ /* When printing with notes */
2199
+ body.print-with-notes .slide-canvas {
2200
+ height: 48vw !important;
2201
+ page-break-after: avoid !important;
2202
+ break-after: avoid !important;
2203
+ }
2204
+
2205
+ body.print-with-notes .deck-notes-card {
2206
+ display: block !important;
2207
+ page-break-after: always !important;
2208
+ break-after: page !important;
2209
+ background: #f8fafc !important;
2210
+ border: 1px solid #cbd5e1 !important;
2211
+ border-left: 5px solid #2563eb !important;
2212
+ color: #1e293b !important;
2213
+ margin: 12px auto 32px auto !important;
2214
+ width: 95vw !important;
2215
+ box-shadow: none !important;
2216
+ font-size: 12pt !important;
2217
+ line-height: 1.5 !important;
2218
+ padding: 12px 18px !important;
2219
+ }
2220
+
2221
+ body.print-with-notes .deck-notes-card-header {
2222
+ color: #1e40af !important;
2223
+ font-size: 10pt !important;
2224
+ }
2225
+ }
2226
+
2227
+ ${theme.customCss || ""}
2228
+ </style>
2229
+ ${customHeadTags}
2230
+ </head>
2231
+ <body>
2232
+ <div id="viewport">
2233
+ <div class="slide-canvas" id="canvas">
2234
+ <div id="progress-bar"></div>
2235
+ ${slidesHtml}
2236
+ </div>
2237
+ </div>
2238
+
2239
+ <!-- WYSIWYG Inline Edit UI (Level 1) -->
2240
+ <div class="edit-hotzone" id="edit-hotzone"></div>
2241
+ <button class="edit-toggle" id="btn-edit-toggle" title="Toggle Inline Edit Mode (E)">
2242
+ <span>\u270F\uFE0F</span>
2243
+ <span id="edit-toggle-label">Edit Mode (E)</span>
2244
+ </button>
2245
+
2246
+ <!-- Floating Scale Tuning Panel -->
2247
+ <div id="scale-panel" style="display: none;">
2248
+ <div class="scale-panel-header">
2249
+ <div class="scale-panel-title">
2250
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M21 3 3 21"/><path d="m14 4 6 6"/><path d="m4 14 6 6"/></svg>
2251
+ <span>Scale Tuning</span>
2252
+ </div>
2253
+ <div class="scale-mode-switch">
2254
+ <button type="button" class="mode-tab active" id="tab-mode-unified">Unified</button>
2255
+ <button type="button" class="mode-tab" id="tab-mode-independent">Independent</button>
2256
+ </div>
2257
+ <button type="button" class="scale-panel-close" id="btn-scale-panel-close" title="Close Panel">\u2715</button>
2258
+ </div>
2259
+
2260
+ <!-- Unified Mode Content -->
2261
+ <div id="scale-unified-content" class="scale-content-block">
2262
+ <div class="scale-slider-row">
2263
+ <div class="scale-slider-label-wrap">
2264
+ <span class="scale-slider-label">Scale All</span>
2265
+ <span class="scale-badge" id="badge-scale-factor">100%</span>
2266
+ </div>
2267
+ <input type="range" id="input-scale-factor" min="70" max="150" value="100" class="scale-range-slider" />
2268
+ </div>
2269
+ <div class="density-presets">
2270
+ <button type="button" class="density-chip" data-scale="85">Compact (85%)</button>
2271
+ <button type="button" class="density-chip active" data-scale="100">Default (100%)</button>
2272
+ <button type="button" class="density-chip" data-scale="120">Spacious (120%)</button>
2273
+ </div>
2274
+ </div>
2275
+
2276
+ <!-- Independent Mode Content -->
2277
+ <div id="scale-independent-content" class="scale-content-block" style="display: none;">
2278
+ <div class="scale-slider-row">
2279
+ <div class="scale-slider-label-wrap">
2280
+ <span class="scale-slider-label">Headings (H1\u2013H4)</span>
2281
+ <span class="scale-badge" id="badge-scale-headings">100%</span>
2282
+ </div>
2283
+ <input type="range" id="input-scale-headings" min="70" max="150" value="100" class="scale-range-slider" />
2284
+ </div>
2285
+ <div class="scale-slider-row">
2286
+ <div class="scale-slider-label-wrap">
2287
+ <span class="scale-slider-label">Body &amp; Lists</span>
2288
+ <span class="scale-badge" id="badge-scale-body">100%</span>
2289
+ </div>
2290
+ <input type="range" id="input-scale-body" min="70" max="150" value="100" class="scale-range-slider" />
2291
+ </div>
2292
+ <div class="scale-slider-row">
2293
+ <div class="scale-slider-label-wrap">
2294
+ <span class="scale-slider-label">Code &amp; Notes</span>
2295
+ <span class="scale-badge" id="badge-scale-code">100%</span>
2296
+ </div>
2297
+ <input type="range" id="input-scale-code" min="70" max="150" value="100" class="scale-range-slider" />
2298
+ </div>
2299
+ <div class="scale-slider-row">
2300
+ <div class="scale-slider-label-wrap">
2301
+ <span class="scale-slider-label">Padding &amp; Gaps</span>
2302
+ <span class="scale-badge" id="badge-scale-spacing">100%</span>
2303
+ </div>
2304
+ <input type="range" id="input-scale-spacing" min="70" max="150" value="100" class="scale-range-slider" />
2305
+ </div>
2306
+ <div class="scale-independent-footer">
2307
+ <button type="button" class="btn-reset-scales" id="btn-reset-scales">\u21BA Reset All</button>
2308
+ </div>
2309
+ </div>
2310
+ </div>
2311
+
2312
+ <div id="edit-bar" style="display: none;">
2313
+ <span style="font-size: 13px; font-weight: 700; color: #a1a1aa; text-transform: uppercase; letter-spacing: 0.05em; padding-left: 4px;">Editing</span>
2314
+ <div class="edit-bar-divider"></div>
2315
+ <button class="edit-bar-btn" id="btn-format-bold" title="Bold (Cmd+B)"><strong>B</strong></button>
2316
+ <button class="edit-bar-btn" id="btn-format-italic" title="Italic (Cmd+I)"><em>I</em></button>
2317
+ <button class="edit-bar-btn" id="btn-format-underline" title="Underline (Cmd+U)"><u>U</u></button>
2318
+ <div class="edit-bar-divider"></div>
2319
+ <button class="edit-bar-btn" id="btn-editbar-scale" title="Tune Element Scales (Unified / Independent)">
2320
+ <span>\u{1F4D0}</span>
2321
+ <span>Scale</span>
2322
+ </button>
2323
+ <div class="edit-bar-divider"></div>
2324
+ <button class="edit-bar-btn" id="btn-reset-edits" title="Restore original content">\u21BA Reset</button>
2325
+ <button class="edit-bar-btn save-btn" id="btn-save-html" title="Save and download clean HTML file">\u{1F4BE} Save HTML</button>
2326
+ <button class="edit-bar-btn" id="btn-done-edit" title="Done editing (E)">\u2715 Done</button>
2327
+ </div>
2328
+
2329
+ <!-- Presenter Drawer (Quick In-Page Notes & Timer) -->
2330
+ <div id="presenter-drawer">
2331
+ <div class="presenter-drawer-header">
2332
+ <div class="presenter-meta-group">
2333
+ <span style="font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: #a1a1aa;">Presenter Console</span>
2334
+ <div class="presenter-timer-badge">
2335
+ <span>\u23F1</span>
2336
+ <span id="presenter-timer-text">00:00</span>
2337
+ </div>
2338
+ </div>
2339
+ <div style="display: flex; align-items: center; gap: 8px;">
2340
+ <button type="button" class="btn-reset-scales" id="btn-open-dual-presenter" title="Open separate synced presenter console window for multi-monitor display">
2341
+ <span>Launch 2nd Window \u2197</span>
2342
+ </button>
2343
+ <button type="button" class="scale-panel-close" id="btn-close-presenter-drawer" title="Close Drawer (P)">\u2715</button>
2344
+ </div>
2345
+ </div>
2346
+ <div class="presenter-notes-box" id="presenter-notes-text">
2347
+ No notes for this slide.
2348
+ </div>
2349
+ </div>
2350
+
2351
+ <!-- Print Configuration Modal -->
2352
+ <div id="print-modal">
2353
+ <div class="print-dialog-card">
2354
+ <div class="print-dialog-title">
2355
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
2356
+ <span>Print &amp; Export PDF</span>
2357
+ </div>
2358
+ <label class="print-option-row">
2359
+ <input type="checkbox" id="chk-print-notes" style="width: 18px; height: 18px; accent-color: var(--primary); cursor: pointer;" />
2360
+ <span style="user-select: none;">In k\xE8m ghi ch\xFA gi\u1EA3ng vi\xEAn (Handout / Teacher Notes)</span>
2361
+ </label>
2362
+ <p style="font-size: 12px; color: #a1a1aa; line-height: 1.5; margin: 0;">
2363
+ \u{1F4A1} Ch\u1ECDn trang in Kh\u1ED5 ngang (Landscape), B\u1EADt "Background graphics" trong h\u1ED9p tho\u1EA1i in c\u1EE7a tr\xECnh duy\u1EC7t \u0111\u1EC3 m\xE0u s\u1EAFc hi\u1EC3n th\u1ECB \u0111\xFAng 100%.
2364
+ </p>
2365
+ <div class="print-dialog-actions">
2366
+ <button type="button" class="btn-reset-scales" id="btn-cancel-print">H\u1EE7y</button>
2367
+ <button type="button" class="edit-bar-btn save-btn" id="btn-start-print" style="padding: 6px 16px;">\u{1F5A8}\uFE0F In ngay (Print)</button>
2368
+ </div>
2369
+ </div>
2370
+ </div>
2371
+
2372
+ <div id="controls">
2373
+ <button class="control-btn" id="btn-prev" title="Previous Slide (\u2190)">
2374
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"></polyline></svg>
2375
+ </button>
2376
+ <span id="slide-counter">01 / ${String(slideCount).padStart(2, "0")}</span>
2377
+ <span id="step-badge" class="step-badge">1/1</span>
2378
+ <button class="control-btn" id="btn-next" title="Next Slide (\u2192)">
2379
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"></polyline></svg>
2380
+ </button>
2381
+ <div class="control-divider"></div>
2382
+ <button class="control-btn" id="btn-controls-presenter" title="Presenter Console &amp; Notes (P)">
2383
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>
2384
+ </button>
2385
+ <button class="control-btn" id="btn-controls-view" title="Toggle Slide / Deck View (D)">
2386
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" x2="21" y1="6" y2="6"/><line x1="3" x2="21" y1="12" y2="12"/><line x1="3" x2="21" y1="18" y2="18"/></svg>
2387
+ </button>
2388
+ <button class="control-btn" id="btn-controls-print" title="Print &amp; Export PDF (Cmd+P)">
2389
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
2390
+ </button>
2391
+ <button class="control-btn" id="btn-controls-scale" title="Tune Scale (\u{1F4D0})">
2392
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 3 3 21"/><path d="m14 4 6 6"/><path d="m4 14 6 6"/></svg>
2393
+ </button>
2394
+ <button class="control-btn" id="btn-fullscreen" title="Toggle Fullscreen (F)">
2395
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path></svg>
2396
+ </button>
2397
+ </div>
2398
+
2399
+ <script>
2400
+ (function() {
2401
+ const TOTAL_SLIDES = ${slideCount};
2402
+ let currentSlide = 0;
2403
+ let isEditMode = false;
2404
+ const STORAGE_KEY = 'deck_edit_' + encodeURIComponent(document.title.trim() || 'default');
2405
+ const originalCanvasHtml = document.getElementById('canvas').innerHTML;
2406
+
2407
+ const canvas = document.getElementById('canvas');
2408
+ const slides = document.querySelectorAll('.slide-page');
2409
+ const progressBar = document.getElementById('progress-bar');
2410
+ const counter = document.getElementById('slide-counter');
2411
+
2412
+ // Edit UI elements
2413
+ const hotzone = document.getElementById('edit-hotzone');
2414
+ const editToggleBtn = document.getElementById('btn-edit-toggle');
2415
+ const editBar = document.getElementById('edit-bar');
2416
+ const btnBold = document.getElementById('btn-format-bold');
2417
+ const btnItalic = document.getElementById('btn-format-italic');
2418
+ const btnUnderline = document.getElementById('btn-format-underline');
2419
+ const btnReset = document.getElementById('btn-reset-edits');
2420
+ const btnSave = document.getElementById('btn-save-html');
2421
+ const btnDone = document.getElementById('btn-done-edit');
2422
+
2423
+ // Scale Panel UI Elements
2424
+ const scalePanel = document.getElementById('scale-panel');
2425
+ const tabModeUnified = document.getElementById('tab-mode-unified');
2426
+ const tabModeIndependent = document.getElementById('tab-mode-independent');
2427
+ const btnScalePanelClose = document.getElementById('btn-scale-panel-close');
2428
+ const scaleUnifiedContent = document.getElementById('scale-unified-content');
2429
+ const scaleIndependentContent = document.getElementById('scale-independent-content');
2430
+
2431
+ const inputScaleFactor = document.getElementById('input-scale-factor');
2432
+ const badgeScaleFactor = document.getElementById('badge-scale-factor');
2433
+ const inputScaleHeadings = document.getElementById('input-scale-headings');
2434
+ const badgeScaleHeadings = document.getElementById('badge-scale-headings');
2435
+ const inputScaleBody = document.getElementById('input-scale-body');
2436
+ const badgeScaleBody = document.getElementById('badge-scale-body');
2437
+ const inputScaleCode = document.getElementById('input-scale-code');
2438
+ const badgeScaleCode = document.getElementById('badge-scale-code');
2439
+ const inputScaleSpacing = document.getElementById('input-scale-spacing');
2440
+ const badgeScaleSpacing = document.getElementById('badge-scale-spacing');
2441
+ const btnResetScales = document.getElementById('btn-reset-scales');
2442
+ const densityChips = document.querySelectorAll('.density-chip');
2443
+
2444
+ const btnEditbarScale = document.getElementById('btn-editbar-scale');
2445
+ const btnControlsScale = document.getElementById('btn-controls-scale');
2446
+
2447
+ // Presenter, Deck & Print UI Elements
2448
+ const stepBadge = document.getElementById('step-badge');
2449
+ const btnPresenter = document.getElementById('btn-controls-presenter');
2450
+ const btnViewMode = document.getElementById('btn-controls-view');
2451
+ const btnPrint = document.getElementById('btn-controls-print');
2452
+ const presenterDrawer = document.getElementById('presenter-drawer');
2453
+ const btnClosePresenterDrawer = document.getElementById('btn-close-presenter-drawer');
2454
+ const btnOpenDualPresenter = document.getElementById('btn-open-dual-presenter');
2455
+ const presenterNotesText = document.getElementById('presenter-notes-text');
2456
+ const printModal = document.getElementById('print-modal');
2457
+ const chkPrintNotes = document.getElementById('chk-print-notes');
2458
+ const btnStartPrint = document.getElementById('btn-start-print');
2459
+ const btnCancelPrint = document.getElementById('btn-cancel-print');
2460
+
2461
+ let isDeckMode = false;
2462
+ let presenterWindow = null;
2463
+ let timerSeconds = 0;
2464
+ let timerInterval = null;
2465
+ let isTimerRunning = false;
2466
+
2467
+ const SCALE_STORAGE_KEY = 'deck_scale_' + encodeURIComponent(document.title.trim() || 'default');
2468
+ let scaleState = {
2469
+ mode: 'unified', // 'unified' | 'independent'
2470
+ factor: 1.0,
2471
+ headings: 1.0,
2472
+ body: 1.0,
2473
+ code: 1.0,
2474
+ spacing: 1.0
2475
+ };
2476
+
2477
+ // Restore saved scale state if present
2478
+ try {
2479
+ const savedScale = localStorage.getItem(SCALE_STORAGE_KEY);
2480
+ if (savedScale) {
2481
+ const parsed = JSON.parse(savedScale);
2482
+ if (parsed && typeof parsed === 'object') {
2483
+ scaleState = Object.assign(scaleState, parsed);
2484
+ }
2485
+ }
2486
+ } catch (e) {}
2487
+
2488
+ // Auto-scale 1920x1080 canvas to fit window
2489
+ function resizeCanvas() {
2490
+ if (isDeckMode) return;
2491
+ const vw = window.innerWidth;
2492
+ const vh = window.innerHeight;
2493
+ const scaleX = vw / 1920;
2494
+ const scaleY = vh / 1080;
2495
+ const scale = Math.min(scaleX, scaleY);
2496
+ canvas.style.transform = 'translate(-50%, -50%) scale(' + scale + ')';
2497
+ }
2498
+
2499
+ window.addEventListener('resize', resizeCanvas);
2500
+ resizeCanvas();
2501
+
2502
+ function getSlideNotes(slideIndex) {
2503
+ if (slideIndex < 0 || slideIndex >= slides.length) return '';
2504
+ const slide = slides[slideIndex];
2505
+ const rawNotes = slide.getAttribute('data-notes');
2506
+ if (!rawNotes) return '';
2507
+ try {
2508
+ return decodeURIComponent(rawNotes);
2509
+ } catch (e) {
2510
+ return rawNotes;
2511
+ }
2512
+ }
2513
+
2514
+ function renderTimerText() {
2515
+ const mins = String(Math.floor(timerSeconds / 60)).padStart(2, '0');
2516
+ const secs = String(timerSeconds % 60).padStart(2, '0');
2517
+ const str = mins + ':' + secs;
2518
+ const badge = document.getElementById('presenter-timer-text');
2519
+ if (badge) badge.textContent = str;
2520
+ return str;
2521
+ }
2522
+
2523
+ function startTimer() {
2524
+ if (timerInterval) return;
2525
+ isTimerRunning = true;
2526
+ timerInterval = setInterval(() => {
2527
+ timerSeconds++;
2528
+ renderTimerText();
2529
+ }, 1000);
2530
+ }
2531
+
2532
+ function pauseTimer() {
2533
+ if (timerInterval) {
2534
+ clearInterval(timerInterval);
2535
+ timerInterval = null;
2536
+ }
2537
+ isTimerRunning = false;
2538
+ }
2539
+
2540
+ function resetTimer() {
2541
+ pauseTimer();
2542
+ timerSeconds = 0;
2543
+ renderTimerText();
2544
+ }
2545
+
2546
+ // Step-by-Step Reveal (Animation Steps / Build-In)
2547
+ function getSlideSteps(slideIndex) {
2548
+ if (slideIndex < 0 || slideIndex >= slides.length) return [];
2549
+ const slide = slides[slideIndex];
2550
+ let stepEls = Array.from(slide.querySelectorAll('[data-step], [data-reveal], [data-click], .step-reveal, .v-click, .step-item'));
2551
+ if (stepEls.length === 0) {
2552
+ const autoContainers = slide.querySelectorAll('[data-auto-reveal="true"], .auto-reveal, .step-group');
2553
+ autoContainers.forEach(container => {
2554
+ const children = container.querySelectorAll('li, .card-item, .step-card, .compare-col');
2555
+ children.forEach(c => {
2556
+ c.classList.add('step-reveal');
2557
+ stepEls.push(c);
2558
+ });
2559
+ });
2560
+ }
2561
+ return stepEls;
2562
+ }
2563
+
2564
+ function updateStepBadge() {
2565
+ if (!stepBadge) return;
2566
+ if (isDeckMode) {
2567
+ stepBadge.style.display = 'none';
2568
+ return;
2569
+ }
2570
+ const steps = getSlideSteps(currentSlide);
2571
+ if (steps.length === 0) {
2572
+ stepBadge.style.display = 'none';
2573
+ return;
2574
+ }
2575
+ const revealedCount = steps.filter(el => el.classList.contains('revealed') || el.classList.contains('visible')).length;
2576
+ stepBadge.style.display = 'inline-block';
2577
+ stepBadge.textContent = 'Step ' + revealedCount + '/' + steps.length;
2578
+ }
2579
+
2580
+ function stepForward() {
2581
+ if (isEditMode || isDeckMode) {
2582
+ nextSlide();
2583
+ return;
2584
+ }
2585
+ const steps = getSlideSteps(currentSlide);
2586
+ if (steps.length > 0) {
2587
+ const nextUnrevealed = steps.find(el => !el.classList.contains('revealed') && !el.classList.contains('visible'));
2588
+ if (nextUnrevealed) {
2589
+ nextUnrevealed.classList.add('revealed', 'visible');
2590
+ updateStepBadge();
2591
+ broadcastDeckState();
2592
+ return;
2593
+ }
2594
+ }
2595
+ if (currentSlide < TOTAL_SLIDES - 1) {
2596
+ showSlide(currentSlide + 1);
2597
+ }
2598
+ }
2599
+
2600
+ function stepBackward() {
2601
+ if (isEditMode || isDeckMode) {
2602
+ prevSlide();
2603
+ return;
2604
+ }
2605
+ const steps = getSlideSteps(currentSlide);
2606
+ if (steps.length > 0) {
2607
+ const revealedList = steps.filter(el => el.classList.contains('revealed') || el.classList.contains('visible'));
2608
+ if (revealedList.length > 0) {
2609
+ const lastRevealed = revealedList[revealedList.length - 1];
2610
+ lastRevealed.classList.remove('revealed', 'visible');
2611
+ updateStepBadge();
2612
+ broadcastDeckState();
2613
+ return;
2614
+ }
2615
+ }
2616
+ if (currentSlide > 0) {
2617
+ showSlide(currentSlide - 1);
2618
+ const prevSteps = getSlideSteps(currentSlide);
2619
+ prevSteps.forEach(s => s.classList.add('revealed', 'visible'));
2620
+ updateStepBadge();
2621
+ broadcastDeckState();
2622
+ }
2623
+ }
2624
+
2625
+ // Presenter State & Sync
2626
+ const SYNC_CHANNEL_NAME = 'deck_sync_' + encodeURIComponent(document.title.trim() || 'default');
2627
+ let syncChannel = null;
2628
+ try {
2629
+ syncChannel = new BroadcastChannel(SYNC_CHANNEL_NAME);
2630
+ syncChannel.onmessage = function(e) {
2631
+ if (!e.data) return;
2632
+ if (e.data.type === 'COMMAND') {
2633
+ if (e.data.action === 'NEXT') stepForward();
2634
+ else if (e.data.action === 'PREV') stepBackward();
2635
+ else if (e.data.action === 'GOTO' && typeof e.data.index === 'number') showSlide(e.data.index);
2636
+ else if (e.data.action === 'RESET_TIMER') resetTimer();
2637
+ else if (e.data.action === 'TOGGLE_TIMER') {
2638
+ if (isTimerRunning) pauseTimer(); else startTimer();
2639
+ }
2640
+ } else if (e.data.type === 'REQUEST_STATE') {
2641
+ broadcastDeckState();
2642
+ }
2643
+ };
2644
+ } catch (err) {}
2645
+
2646
+ function broadcastDeckState() {
2647
+ const currentNotes = getSlideNotes(currentSlide);
2648
+ const nextNotes = getSlideNotes(currentSlide + 1);
2649
+ const payload = {
2650
+ type: 'STATE_UPDATE',
2651
+ currentSlide: currentSlide,
2652
+ totalSlides: TOTAL_SLIDES,
2653
+ notes: currentNotes,
2654
+ nextNotes: nextNotes,
2655
+ title: document.title,
2656
+ timerSeconds: timerSeconds,
2657
+ timerRunning: isTimerRunning
2658
+ };
2659
+
2660
+ if (syncChannel) {
2661
+ try { syncChannel.postMessage(payload); } catch (e) {}
2662
+ }
2663
+ if (presenterWindow && !presenterWindow.closed) {
2664
+ try { presenterWindow.postMessage(payload, '*'); } catch (e) {}
2665
+ }
2666
+ updatePresenterDrawerUI();
2667
+ }
2668
+
2669
+ function updatePresenterDrawerUI() {
2670
+ if (!presenterNotesText) return;
2671
+ const notes = getSlideNotes(currentSlide);
2672
+ if (notes && notes.trim()) {
2673
+ presenterNotesText.innerHTML = notes.replace(/
2674
+ /g, '<br/>');
2675
+ } else {
2676
+ presenterNotesText.innerHTML = '<span style="opacity: 0.5; font-style: italic;">No speaker notes for Slide ' + (currentSlide + 1) + '.</span>';
2677
+ }
2678
+ }
2679
+
2680
+ function togglePresenterDrawer() {
2681
+ if (!presenterDrawer) return;
2682
+ const isOpen = presenterDrawer.classList.toggle('open');
2683
+ if (isOpen) {
2684
+ startTimer();
2685
+ updatePresenterDrawerUI();
2686
+ }
2687
+ }
2688
+
2689
+ function openPresenterConsole() {
2690
+ startTimer();
2691
+ const popupName = 'PresenterConsole_' + encodeURIComponent(document.title.replace(/W/g, ''));
2692
+ const win = window.open('', popupName, 'width=1100,height=720,menubar=no,toolbar=no,location=no,status=no');
2693
+ if (!win) {
2694
+ togglePresenterDrawer();
2695
+ return;
2696
+ }
2697
+ presenterWindow = win;
2698
+
2699
+ const currentNotes = getSlideNotes(currentSlide);
2700
+ const curSlideTitle = 'Slide ' + (currentSlide + 1) + ' / ' + TOTAL_SLIDES;
2701
+
2702
+ const consoleHtml = '<!DOCTYPE html>
2703
+ <html>
2704
+ <head>
2705
+ ' +
2706
+ '<meta charset="UTF-8"><title>Presenter Console \u2014 ' + (document.title || 'Slides') + '</title>
2707
+ ' +
2708
+ '<style>
2709
+ ' +
2710
+ '* { box-sizing: border-box; margin: 0; padding: 0; }
2711
+ ' +
2712
+ 'body { background: #0c0d14; color: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display: flex; flex-direction: column; height: 100vh; overflow: hidden; user-select: none; }
2713
+ ' +
2714
+ '.top-bar { display: flex; align-items: center; justify-content: space-between; padding: 12px 24px; background: #141522; border-bottom: 1px solid rgba(255,255,255,0.12); }
2715
+ ' +
2716
+ '.timer-badge { font-family: monospace; font-size: 20px; font-weight: 700; color: #38bdf8; background: rgba(56,189,248,0.15); border: 1px solid rgba(56,189,248,0.3); padding: 4px 14px; border-radius: 8px; display: flex; align-items: center; gap: 8px; }
2717
+ ' +
2718
+ '.grid-container { flex: 1; display: grid; grid-template-columns: 1fr 1fr; gap: 20px; padding: 20px 24px; overflow: hidden; }
2719
+ ' +
2720
+ '.panel-card { background: #181926; border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; display: flex; flex-direction: column; overflow: hidden; }
2721
+ ' +
2722
+ '.card-header { padding: 10px 16px; background: rgba(255,255,255,0.04); border-bottom: 1px solid rgba(255,255,255,0.08); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: #a1a1aa; display: flex; justify-content: space-between; }
2723
+ ' +
2724
+ '.notes-content { flex: 1; padding: 20px; font-size: 19px; line-height: 1.7; overflow-y: auto; color: #f1f5f9; user-select: text; white-space: pre-wrap; }
2725
+ ' +
2726
+ '.preview-box { flex: 1; display: flex; align-items: center; justify-content: center; background: #000; overflow: hidden; padding: 16px; }
2727
+ ' +
2728
+ '.btn { background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15); color: #fff; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600; }
2729
+ ' +
2730
+ '.btn:hover { background: rgba(255,255,255,0.2); }
2731
+ ' +
2732
+ '.btn-nav { font-size: 16px; padding: 10px 24px; border-radius: 8px; }
2733
+ ' +
2734
+ '.bottom-nav { display: flex; align-items: center; justify-content: space-between; padding: 14px 24px; background: #141522; border-top: 1px solid rgba(255,255,255,0.12); }
2735
+ ' +
2736
+ '</style>
2737
+ </head>
2738
+ <body>
2739
+ ' +
2740
+ '<div class="top-bar">
2741
+ ' +
2742
+ ' <div style="font-weight: 700; font-size: 16px;">' + (document.title || 'Slide Deck') + '</div>
2743
+ ' +
2744
+ ' <div style="display: flex; align-items: center; gap: 12px;">
2745
+ ' +
2746
+ ' <div class="timer-badge">\u23F1 <span id="timer-val">00:00</span></div>
2747
+ ' +
2748
+ ' <button class="btn" id="btn-toggle-t">Pause</button>
2749
+ ' +
2750
+ ' <button class="btn" id="btn-reset-t">Reset</button>
2751
+ ' +
2752
+ ' </div>
2753
+ ' +
2754
+ '</div>
2755
+ ' +
2756
+ '<div class="grid-container">
2757
+ ' +
2758
+ ' <div class="panel-card">
2759
+ ' +
2760
+ ' <div class="card-header"><span>CURRENT SLIDE</span><span id="slide-num">' + curSlideTitle + '</span></div>
2761
+ ' +
2762
+ ' <div class="preview-box"><div style="font-size: 22px; color: #94a3b8; text-align: center;">Active Slide On Screen<br/><span style="font-size: 14px; color: #38bdf8;">(Broadcasting Synced)</span></div></div>
2763
+ ' +
2764
+ ' <div class="card-header" style="border-top: 1px solid rgba(255,255,255,0.08);"><span>NEXT SLIDE PREVIEW</span></div>
2765
+ ' +
2766
+ ' <div style="padding: 12px 16px; font-size: 14px; color: #94a3b8; background: rgba(0,0,0,0.3); min-height: 80px;" id="next-preview-text">Upcoming: Slide ' + (currentSlide + 2) + '</div>
2767
+ ' +
2768
+ ' </div>
2769
+ ' +
2770
+ ' <div class="panel-card">
2771
+ ' +
2772
+ ' <div class="card-header"><span>SPEAKER NOTES / TALK TRACK</span><div><button class="btn" id="font-dn" style="padding: 2px 8px;">A-</button> <button class="btn" id="font-up" style="padding: 2px 8px;">A+</button></div></div>
2773
+ ' +
2774
+ ' <div class="notes-content" id="speaker-notes">' + (currentNotes ? currentNotes : '<span style="opacity: 0.4;">No speaker notes for this slide.</span>') + '</div>
2775
+ ' +
2776
+ ' </div>
2777
+ ' +
2778
+ '</div>
2779
+ ' +
2780
+ '<div class="bottom-nav">
2781
+ ' +
2782
+ ' <button class="btn btn-nav" id="btn-prev-p">\u2039 Previous (\u2190)</button>
2783
+ ' +
2784
+ ' <span style="font-size: 14px; color: #a1a1aa;">Use Arrow Keys or Space to advance</span>
2785
+ ' +
2786
+ ' <button class="btn btn-nav" id="btn-next-p" style="background: #0284c7; border-color: #0284c7;">Next (\u2192 / Space) \u203A</button>
2787
+ ' +
2788
+ '</div>
2789
+ ' +
2790
+ '<script>
2791
+ ' +
2792
+ ' const bc = new BroadcastChannel("' + SYNC_CHANNEL_NAME + '");
2793
+ ' +
2794
+ ' let curSecs = ' + timerSeconds + ';
2795
+ ' +
2796
+ ' function sendCmd(action, idx) { bc.postMessage({ type: "COMMAND", action: action, index: idx }); }
2797
+ ' +
2798
+ ' document.getElementById("btn-prev-p").onclick = () => sendCmd("PREV");
2799
+ ' +
2800
+ ' document.getElementById("btn-next-p").onclick = () => sendCmd("NEXT");
2801
+ ' +
2802
+ ' document.getElementById("btn-reset-t").onclick = () => sendCmd("RESET_TIMER");
2803
+ ' +
2804
+ ' document.getElementById("btn-toggle-t").onclick = () => sendCmd("TOGGLE_TIMER");
2805
+ ' +
2806
+ ' let curFontSize = 19;
2807
+ ' +
2808
+ ' document.getElementById("font-up").onclick = () => { curFontSize += 2; document.getElementById("speaker-notes").style.fontSize = curFontSize + "px"; };
2809
+ ' +
2810
+ ' document.getElementById("font-dn").onclick = () => { curFontSize = Math.max(12, curFontSize - 2); document.getElementById("speaker-notes").style.fontSize = curFontSize + "px"; };
2811
+ ' +
2812
+ ' window.onkeydown = (e) => {
2813
+ ' +
2814
+ ' if (["ArrowRight", "Space", "PageDown"].includes(e.code)) { e.preventDefault(); sendCmd("NEXT"); }
2815
+ ' +
2816
+ ' else if (["ArrowLeft", "PageUp"].includes(e.code)) { e.preventDefault(); sendCmd("PREV"); }
2817
+ ' +
2818
+ ' };
2819
+ ' +
2820
+ ' bc.onmessage = (e) => {
2821
+ ' +
2822
+ ' if (!e.data || e.data.type !== "STATE_UPDATE") return;
2823
+ ' +
2824
+ ' document.getElementById("slide-num").textContent = "Slide " + (e.data.currentSlide + 1) + " / " + e.data.totalSlides;
2825
+ ' +
2826
+ ' document.getElementById("speaker-notes").textContent = e.data.notes || "No speaker notes for this slide.";
2827
+ ' +
2828
+ ' document.getElementById("next-preview-text").textContent = e.data.nextNotes ? "Next Notes: " + e.data.nextNotes : "Next: Slide " + (e.data.currentSlide + 2);
2829
+ ' +
2830
+ ' curSecs = e.data.timerSeconds || 0;
2831
+ ' +
2832
+ ' const m = String(Math.floor(curSecs / 60)).padStart(2, "0");
2833
+ ' +
2834
+ ' const s = String(curSecs % 60).padStart(2, "0");
2835
+ ' +
2836
+ ' document.getElementById("timer-val").textContent = m + ":" + s;
2837
+ ' +
2838
+ ' document.getElementById("btn-toggle-t").textContent = e.data.timerRunning ? "Pause" : "Start";
2839
+ ' +
2840
+ ' };
2841
+ ' +
2842
+ ' bc.postMessage({ type: "REQUEST_STATE" });
2843
+ ' +
2844
+ '<' + '/script>
2845
+ </body>
2846
+ </html>';
2847
+
2848
+ win.document.open();
2849
+ win.document.write(consoleHtml);
2850
+ win.document.close();
2851
+ }
2852
+
2853
+ // Deck Mode & Print Engine
2854
+ function ensureDeckNotesCards() {
2855
+ slides.forEach((slide, idx) => {
2856
+ const notes = getSlideNotes(idx);
2857
+ let existingCard = slide.parentElement.querySelector('.deck-notes-card[data-for-slide="' + idx + '"]');
2858
+ if (!existingCard && notes) {
2859
+ const card = document.createElement('div');
2860
+ card.className = 'deck-notes-card';
2861
+ card.setAttribute('data-for-slide', String(idx));
2862
+ card.innerHTML = '<div class="deck-notes-card-header"><span>\u{1F4DD}</span><span>Presenter Notes (Slide ' + (idx + 1) + ')</span></div><div>' + notes.replace(/
2863
+ /g, '<br/>') + '</div>';
2864
+ slide.after(card);
2865
+ }
2866
+ });
2867
+ }
2868
+
2869
+ function toggleDeckMode(force) {
2870
+ isDeckMode = typeof force === 'boolean' ? force : !isDeckMode;
2871
+ document.body.classList.toggle('mode-deck', isDeckMode);
2872
+ if (isDeckMode) {
2873
+ ensureDeckNotesCards();
2874
+ document.querySelectorAll('.step-reveal, [data-step], [data-reveal], [data-click], .v-click').forEach(el => {
2875
+ el.classList.add('revealed', 'visible');
2876
+ });
2877
+ } else {
2878
+ resizeCanvas();
2879
+ }
2880
+ updateStepBadge();
2881
+ }
2882
+
2883
+ function openPrintModal() {
2884
+ if (printModal) printModal.classList.add('open');
2885
+ }
2886
+
2887
+ function closePrintModal() {
2888
+ if (printModal) printModal.classList.remove('open');
2889
+ }
2890
+
2891
+ function executePrint() {
2892
+ closePrintModal();
2893
+ const withNotes = chkPrintNotes ? chkPrintNotes.checked : false;
2894
+ document.body.classList.toggle('print-with-notes', withNotes);
2895
+ ensureDeckNotesCards();
2896
+ document.querySelectorAll('.step-reveal, [data-step], [data-reveal], [data-click], .v-click').forEach(el => {
2897
+ el.classList.add('revealed', 'visible');
2898
+ });
2899
+ setTimeout(() => {
2900
+ window.print();
2901
+ }, 100);
2902
+ }
2903
+
2904
+ // Show slide by index
2905
+ function showSlide(index) {
2906
+ if (index < 0) index = 0;
2907
+ if (index >= TOTAL_SLIDES) index = TOTAL_SLIDES - 1;
2908
+ currentSlide = index;
2909
+
2910
+ slides.forEach((s, idx) => {
2911
+ if (idx === currentSlide) {
2912
+ s.classList.add('active');
2913
+ } else {
2914
+ s.classList.remove('active');
2915
+ }
2916
+ });
2917
+
2918
+ // Reset step state for newly shown slide
2919
+ const steps = getSlideSteps(currentSlide);
2920
+ steps.forEach(s => s.classList.remove('revealed', 'visible'));
2921
+ updateStepBadge();
2922
+ updatePresenterDrawerUI();
2923
+ broadcastDeckState();
2924
+
2925
+ // Update progress bar
2926
+ const pct = TOTAL_SLIDES > 1 ? ((currentSlide) / (TOTAL_SLIDES - 1)) * 100 : 100;
2927
+ if (progressBar) progressBar.style.width = pct + '%';
2928
+
2929
+ // Update counter
2930
+ if (counter) {
2931
+ const curStr = String(currentSlide + 1).padStart(2, '0');
2932
+ const totStr = String(TOTAL_SLIDES).padStart(2, '0');
2933
+ counter.textContent = curStr + ' / ' + totStr;
2934
+ }
2935
+
2936
+ // Send postMessage to parent window if embedded in iframe
2937
+ if (window.parent && window.parent !== window) {
2938
+ window.parent.postMessage({
2939
+ type: 'PRESENTATION_SLIDE_CHANGE',
2940
+ currentIndex: currentSlide,
2941
+ totalSlides: TOTAL_SLIDES
2942
+ }, '*');
2943
+ }
2944
+ }
2945
+
2946
+ function nextSlide() {
2947
+ showSlide(currentSlide + 1);
2948
+ }
2949
+
2950
+ function prevSlide() {
2951
+ showSlide(currentSlide - 1);
2952
+ }
2953
+
2954
+ function toggleFullscreen() {
2955
+ if (!document.fullscreenElement) {
2956
+ document.documentElement.requestFullscreen().catch(() => {});
2957
+ } else {
2958
+ document.exitFullscreen().catch(() => {});
2959
+ }
2960
+ }
2961
+
2962
+ // \u2500\u2500\u2500 Scale Architecture Controller (Unified vs Independent) \u2500\u2500\u2500
2963
+ function applyScales() {
2964
+ const root = document.documentElement;
2965
+ root.style.setProperty('--scale-mode', scaleState.mode);
2966
+ root.style.setProperty('--scale-factor', scaleState.factor);
2967
+ root.style.setProperty('--scale-headings', scaleState.headings);
2968
+ root.style.setProperty('--scale-body', scaleState.body);
2969
+ root.style.setProperty('--scale-code', scaleState.code);
2970
+ root.style.setProperty('--scale-spacing', scaleState.spacing);
2971
+
2972
+ const isUnified = scaleState.mode === 'unified';
2973
+ const effHeadings = isUnified ? scaleState.factor : scaleState.headings;
2974
+ const effBody = isUnified ? scaleState.factor : scaleState.body;
2975
+ const effCode = isUnified ? scaleState.factor : scaleState.code;
2976
+ const effSpacing = isUnified ? scaleState.factor : scaleState.spacing;
2977
+
2978
+ root.style.setProperty('--scale-headings-eff', effHeadings);
2979
+ root.style.setProperty('--scale-body-eff', effBody);
2980
+ root.style.setProperty('--scale-code-eff', effCode);
2981
+ root.style.setProperty('--scale-spacing-eff', effSpacing);
2982
+
2983
+ // Synchronize UI widgets
2984
+ if (inputScaleFactor) inputScaleFactor.value = Math.round(scaleState.factor * 100);
2985
+ if (badgeScaleFactor) badgeScaleFactor.textContent = Math.round(scaleState.factor * 100) + '%';
2986
+
2987
+ if (inputScaleHeadings) inputScaleHeadings.value = Math.round(scaleState.headings * 100);
2988
+ if (badgeScaleHeadings) badgeScaleHeadings.textContent = Math.round(scaleState.headings * 100) + '%';
2989
+
2990
+ if (inputScaleBody) inputScaleBody.value = Math.round(scaleState.body * 100);
2991
+ if (badgeScaleBody) badgeScaleBody.textContent = Math.round(scaleState.body * 100) + '%';
2992
+
2993
+ if (inputScaleCode) inputScaleCode.value = Math.round(scaleState.code * 100);
2994
+ if (badgeScaleCode) badgeScaleCode.textContent = Math.round(scaleState.code * 100) + '%';
2995
+
2996
+ if (inputScaleSpacing) inputScaleSpacing.value = Math.round(scaleState.spacing * 100);
2997
+ if (badgeScaleSpacing) badgeScaleSpacing.textContent = Math.round(scaleState.spacing * 100) + '%';
2998
+
2999
+ // Update density chips
3000
+ densityChips.forEach(chip => {
3001
+ const s = parseInt(chip.getAttribute('data-scale'), 10);
3002
+ chip.classList.toggle('active', isUnified && Math.round(scaleState.factor * 100) === s);
3003
+ });
3004
+
3005
+ // Save to localStorage
3006
+ try {
3007
+ localStorage.setItem(SCALE_STORAGE_KEY, JSON.stringify(scaleState));
3008
+ } catch (e) {}
3009
+
3010
+ // Notify parent React app if embedded
3011
+ if (window.parent && window.parent !== window) {
3012
+ window.parent.postMessage({
3013
+ type: 'SLIDE_SCALE_CHANGED',
3014
+ scaleState: Object.assign({}, scaleState),
3015
+ effective: {
3016
+ headings: effHeadings,
3017
+ body: effBody,
3018
+ code: effCode,
3019
+ spacing: effSpacing
3020
+ }
3021
+ }, '*');
3022
+ }
3023
+ }
3024
+
3025
+ function setScaleMode(mode) {
3026
+ scaleState.mode = mode;
3027
+ if (tabModeUnified && tabModeIndependent) {
3028
+ tabModeUnified.classList.toggle('active', mode === 'unified');
3029
+ tabModeIndependent.classList.toggle('active', mode === 'independent');
3030
+ }
3031
+ if (scaleUnifiedContent && scaleIndependentContent) {
3032
+ scaleUnifiedContent.style.display = mode === 'unified' ? 'block' : 'none';
3033
+ scaleIndependentContent.style.display = mode === 'independent' ? 'block' : 'none';
3034
+ }
3035
+ applyScales();
3036
+ }
3037
+
3038
+ function toggleScalePanel(show) {
3039
+ if (!scalePanel) return;
3040
+ const currentOpen = scalePanel.style.display !== 'none';
3041
+ const willOpen = show !== undefined ? show : !currentOpen;
3042
+ scalePanel.style.display = willOpen ? 'block' : 'none';
3043
+ if (willOpen) {
3044
+ setScaleMode(scaleState.mode);
3045
+ }
3046
+ }
3047
+
3048
+ // \u2500\u2500\u2500 Scale Panel Event Listeners \u2500\u2500\u2500
3049
+ if (tabModeUnified) tabModeUnified.addEventListener('click', () => setScaleMode('unified'));
3050
+ if (tabModeIndependent) tabModeIndependent.addEventListener('click', () => setScaleMode('independent'));
3051
+ if (btnScalePanelClose) btnScalePanelClose.addEventListener('click', () => toggleScalePanel(false));
3052
+
3053
+ if (inputScaleFactor) {
3054
+ inputScaleFactor.addEventListener('input', (e) => {
3055
+ scaleState.factor = parseFloat(e.target.value) / 100;
3056
+ applyScales();
3057
+ });
3058
+ }
3059
+ if (inputScaleHeadings) {
3060
+ inputScaleHeadings.addEventListener('input', (e) => {
3061
+ scaleState.headings = parseFloat(e.target.value) / 100;
3062
+ applyScales();
3063
+ });
3064
+ }
3065
+ if (inputScaleBody) {
3066
+ inputScaleBody.addEventListener('input', (e) => {
3067
+ scaleState.body = parseFloat(e.target.value) / 100;
3068
+ applyScales();
3069
+ });
3070
+ }
3071
+ if (inputScaleCode) {
3072
+ inputScaleCode.addEventListener('input', (e) => {
3073
+ scaleState.code = parseFloat(e.target.value) / 100;
3074
+ applyScales();
3075
+ });
3076
+ }
3077
+ if (inputScaleSpacing) {
3078
+ inputScaleSpacing.addEventListener('input', (e) => {
3079
+ scaleState.spacing = parseFloat(e.target.value) / 100;
3080
+ applyScales();
3081
+ });
3082
+ }
3083
+
3084
+ densityChips.forEach(chip => {
3085
+ chip.addEventListener('click', () => {
3086
+ const val = parseInt(chip.getAttribute('data-scale'), 10) / 100;
3087
+ scaleState.factor = val;
3088
+ setScaleMode('unified');
3089
+ });
3090
+ });
3091
+
3092
+ if (btnResetScales) {
3093
+ btnResetScales.addEventListener('click', () => {
3094
+ scaleState.factor = 1.0;
3095
+ scaleState.headings = 1.0;
3096
+ scaleState.body = 1.0;
3097
+ scaleState.code = 1.0;
3098
+ scaleState.spacing = 1.0;
3099
+ applyScales();
3100
+ });
3101
+ }
3102
+
3103
+ if (btnEditbarScale) btnEditbarScale.addEventListener('click', () => toggleScalePanel());
3104
+ if (btnControlsScale) btnControlsScale.addEventListener('click', () => toggleScalePanel());
3105
+
3106
+ // \u2500\u2500\u2500 Level 1 Inline Editing Controller \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3107
+ function toggleEditMode(force) {
3108
+ isEditMode = force !== undefined ? force : !isEditMode;
3109
+
3110
+ document.body.classList.toggle('edit-mode-active', isEditMode);
3111
+ editToggleBtn.classList.toggle('active', isEditMode);
3112
+ editBar.style.display = isEditMode ? 'flex' : 'none';
3113
+
3114
+ const editableSelectors = 'h1, h2, h3, h4, p, li, span, pre code, .opt-badge';
3115
+ const targets = canvas.querySelectorAll(editableSelectors);
3116
+
3117
+ targets.forEach(el => {
3118
+ if (isEditMode) {
3119
+ el.setAttribute('contenteditable', 'true');
3120
+ el.setAttribute('spellcheck', 'false');
3121
+ } else {
3122
+ el.removeAttribute('contenteditable');
3123
+ }
3124
+ });
3125
+
3126
+ if (window.parent && window.parent !== window) {
3127
+ window.parent.postMessage({
3128
+ type: 'SLIDE_EDIT_MODE_CHANGED',
3129
+ isEditMode: isEditMode
3130
+ }, '*');
3131
+ }
3132
+ }
3133
+
3134
+ // Hotzone hover with 400ms delay timeout (frontend-slides standard)
3135
+ let hideHotzoneTimeout = null;
3136
+ if (hotzone && editToggleBtn) {
3137
+ hotzone.addEventListener('mouseenter', () => {
3138
+ clearTimeout(hideHotzoneTimeout);
3139
+ editToggleBtn.classList.add('show');
3140
+ });
3141
+ hotzone.addEventListener('mouseleave', () => {
3142
+ hideHotzoneTimeout = setTimeout(() => {
3143
+ if (!isEditMode) editToggleBtn.classList.remove('show');
3144
+ }, 400);
3145
+ });
3146
+ editToggleBtn.addEventListener('mouseenter', () => {
3147
+ clearTimeout(hideHotzoneTimeout);
3148
+ });
3149
+ editToggleBtn.addEventListener('mouseleave', () => {
3150
+ hideHotzoneTimeout = setTimeout(() => {
3151
+ if (!isEditMode) editToggleBtn.classList.remove('show');
3152
+ }, 400);
3153
+ });
3154
+ hotzone.addEventListener('click', () => toggleEditMode());
3155
+ editToggleBtn.addEventListener('click', () => toggleEditMode());
3156
+ }
3157
+
3158
+ // Format actions
3159
+ if (btnBold) btnBold.addEventListener('click', () => document.execCommand('bold'));
3160
+ if (btnItalic) btnItalic.addEventListener('click', () => document.execCommand('italic'));
3161
+ if (btnUnderline) btnUnderline.addEventListener('click', () => document.execCommand('underline'));
3162
+ if (btnDone) btnDone.addEventListener('click', () => toggleEditMode(false));
3163
+
3164
+ // Reset action
3165
+ if (btnReset) {
3166
+ btnReset.addEventListener('click', () => {
3167
+ if (confirm('Are you sure you want to reset all edits back to original?')) {
3168
+ canvas.innerHTML = originalCanvasHtml;
3169
+ try { localStorage.removeItem(STORAGE_KEY); } catch (e) {}
3170
+ showSlide(currentSlide);
3171
+ if (isEditMode) toggleEditMode(true);
3172
+ }
3173
+ });
3174
+ }
3175
+
3176
+ // Auto-save to LocalStorage on edit
3177
+ let saveTimer = null;
3178
+ canvas.addEventListener('input', () => {
3179
+ if (!isEditMode) return;
3180
+ clearTimeout(saveTimer);
3181
+ saveTimer = setTimeout(() => {
3182
+ try {
3183
+ localStorage.setItem(STORAGE_KEY, canvas.innerHTML);
3184
+ } catch (e) {}
3185
+ }, 500);
3186
+ });
3187
+
3188
+ // Restore from localStorage if present
3189
+ try {
3190
+ const saved = localStorage.getItem(STORAGE_KEY);
3191
+ if (saved && saved.trim()) {
3192
+ canvas.innerHTML = saved;
3193
+ }
3194
+ } catch (e) {}
3195
+
3196
+ // Save & Clean HTML Export
3197
+ function exportAndSaveHtml() {
3198
+ const wasEditing = isEditMode;
3199
+ if (wasEditing) toggleEditMode(false);
3200
+ if (scalePanel) scalePanel.style.display = 'none';
3201
+
3202
+ // Take snapshot of full HTML
3203
+ let cleanHtml = '<!DOCTYPE html>
3204
+ ' + document.documentElement.outerHTML;
3205
+
3206
+ // Clean any temporary runtime inline styles or attributes
3207
+ cleanHtml = cleanHtml
3208
+ .replace(/contenteditable="true"/g, '')
3209
+ .replace(/spellcheck="false"/g, '')
3210
+ .replace(/edit-mode-active/g, '')
3211
+ .replace(/class="edit-toggle[^"]*"/g, 'class="edit-toggle"');
3212
+
3213
+ // Embed current scale settings into :root style definition so exported HTML is permanently styled with chosen scales
3214
+ const scaleVarsReplacement =
3215
+ '--scale-mode: ' + scaleState.mode + ';
3216
+ ' +
3217
+ ' --scale-factor: ' + scaleState.factor + ';
3218
+ ' +
3219
+ ' --scale-headings: ' + scaleState.headings + ';
3220
+ ' +
3221
+ ' --scale-body: ' + scaleState.body + ';
3222
+ ' +
3223
+ ' --scale-code: ' + scaleState.code + ';
3224
+ ' +
3225
+ ' --scale-spacing: ' + scaleState.spacing + ';
3226
+ ' +
3227
+ ' --scale-headings-eff: ' + (scaleState.mode === 'unified' ? scaleState.factor : scaleState.headings) + ';
3228
+ ' +
3229
+ ' --scale-body-eff: ' + (scaleState.mode === 'unified' ? scaleState.factor : scaleState.body) + ';
3230
+ ' +
3231
+ ' --scale-code-eff: ' + (scaleState.mode === 'unified' ? scaleState.factor : scaleState.code) + ';
3232
+ ' +
3233
+ ' --scale-spacing-eff: ' + (scaleState.mode === 'unified' ? scaleState.factor : scaleState.spacing) + ';';
3234
+
3235
+ cleanHtml = cleanHtml.replace(/--scale-mode:s*unified;[sS]*?--scale-spacing-eff:s*1.0;/, scaleVarsReplacement);
3236
+
3237
+ // Create Blob and trigger download
3238
+ const blob = new Blob([cleanHtml], { type: 'text/html;charset=utf-8' });
3239
+ const url = URL.createObjectURL(blob);
3240
+ const a = document.createElement('a');
3241
+ const filename = (document.title.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase() || 'presentation') + '.html';
3242
+ a.href = url;
3243
+ a.download = filename;
3244
+ document.body.appendChild(a);
3245
+ a.click();
3246
+ document.body.removeChild(a);
3247
+ URL.revokeObjectURL(url);
3248
+
3249
+ // Notify parent React frame if embedded
3250
+ if (window.parent && window.parent !== window) {
3251
+ window.parent.postMessage({
3252
+ type: 'SLIDE_HTML_SAVED',
3253
+ html: cleanHtml,
3254
+ filename: filename
3255
+ }, '*');
3256
+ }
3257
+
3258
+ if (wasEditing) toggleEditMode(true);
3259
+ }
3260
+
3261
+ if (btnSave) btnSave.addEventListener('click', exportAndSaveHtml);
3262
+
3263
+ // Keyboard navigation & Shortcuts
3264
+ window.addEventListener('keydown', function(e) {
3265
+ // Guard: If typing inside contenteditable, allow normal cursor movement and typing
3266
+ if (e.target.isContentEditable || e.target.getAttribute('contenteditable') === 'true') {
3267
+ if (e.key === 'Escape') {
3268
+ e.target.blur();
3269
+ }
3270
+ return;
3271
+ }
3272
+
3273
+ // Shortcut: E key toggles edit mode
3274
+ if (e.key === 'e' || e.key === 'E') {
3275
+ e.preventDefault();
3276
+ toggleEditMode();
3277
+ return;
3278
+ }
3279
+
3280
+ // Shortcut: Cmd+S / Ctrl+S saves HTML
3281
+ if ((e.metaKey || e.ctrlKey) && (e.key === 's' || e.key === 'S')) {
3282
+ e.preventDefault();
3283
+ exportAndSaveHtml();
3284
+ return;
3285
+ }
3286
+
3287
+ // Shortcut: Cmd+P / Ctrl+P opens Print & PDF dialog
3288
+ if ((e.metaKey || e.ctrlKey) && (e.key === 'p' || e.key === 'P')) {
3289
+ e.preventDefault();
3290
+ openPrintModal();
3291
+ return;
3292
+ }
3293
+
3294
+ // Shortcut: P toggles Presenter Console / Drawer
3295
+ if (e.key === 'p' || e.key === 'P') {
3296
+ e.preventDefault();
3297
+ togglePresenterDrawer();
3298
+ return;
3299
+ }
3300
+
3301
+ // Shortcut: D toggles Deck Mode (continuous scroll)
3302
+ if (e.key === 'd' || e.key === 'D') {
3303
+ e.preventDefault();
3304
+ toggleDeckMode();
3305
+ return;
3306
+ }
3307
+
3308
+ if (['ArrowRight', 'ArrowDown', 'Space', 'PageDown'].includes(e.code)) {
3309
+ e.preventDefault();
3310
+ stepForward();
3311
+ } else if (['ArrowLeft', 'ArrowUp', 'PageUp'].includes(e.code)) {
3312
+ e.preventDefault();
3313
+ stepBackward();
3314
+ } else if (e.code === 'KeyF') {
3315
+ e.preventDefault();
3316
+ toggleFullscreen();
3317
+ } else if (e.code === 'Home') {
3318
+ e.preventDefault();
3319
+ showSlide(0);
3320
+ } else if (e.code === 'End') {
3321
+ e.preventDefault();
3322
+ showSlide(TOTAL_SLIDES - 1);
3323
+ }
3324
+ });
3325
+
3326
+ // Canvas click to advance step / slide in Presentation mode
3327
+ document.getElementById('viewport').addEventListener('click', function(e) {
3328
+ if (isEditMode || isDeckMode) return;
3329
+ if (e.target.closest('#controls, #edit-bar, #scale-panel, #presenter-drawer, #print-modal, .edit-toggle, .quiz-option, button, a, input, select')) return;
3330
+ stepForward();
3331
+ });
3332
+
3333
+ // Quiz interactivity: click option to toggle selected state
3334
+ document.addEventListener('click', function(e) {
3335
+ if (isEditMode) return; // In edit mode, let user click to edit text instead of quiz logic
3336
+ const option = e.target.closest('.quiz-option');
3337
+ if (option) {
3338
+ const parentGrid = option.parentElement;
3339
+ if (parentGrid) {
3340
+ parentGrid.querySelectorAll('.quiz-option').forEach(el => el.classList.remove('selected'));
3341
+ }
3342
+ option.classList.add('selected');
3343
+
3344
+ const currentSlideEl = option.closest('.slide-page');
3345
+ if (currentSlideEl) {
3346
+ const exp = currentSlideEl.querySelector('.quiz-explanation');
3347
+ if (exp) {
3348
+ exp.style.opacity = '1';
3349
+ exp.style.filter = 'none';
3350
+ }
3351
+ }
3352
+ }
3353
+ });
3354
+
3355
+ // Controls click
3356
+ document.getElementById('btn-next').addEventListener('click', stepForward);
3357
+ document.getElementById('btn-prev').addEventListener('click', stepBackward);
3358
+ document.getElementById('btn-fullscreen').addEventListener('click', toggleFullscreen);
3359
+
3360
+ if (btnPresenter) btnPresenter.addEventListener('click', togglePresenterDrawer);
3361
+ if (btnViewMode) btnViewMode.addEventListener('click', () => toggleDeckMode());
3362
+ if (btnPrint) btnPrint.addEventListener('click', openPrintModal);
3363
+ if (btnClosePresenterDrawer) btnClosePresenterDrawer.addEventListener('click', togglePresenterDrawer);
3364
+ if (btnOpenDualPresenter) btnOpenDualPresenter.addEventListener('click', openPresenterConsole);
3365
+ if (btnStartPrint) btnStartPrint.addEventListener('click', executePrint);
3366
+ if (btnCancelPrint) btnCancelPrint.addEventListener('click', closePrintModal);
3367
+ if (printModal) {
3368
+ printModal.addEventListener('click', function(e) {
3369
+ if (e.target === printModal) closePrintModal();
3370
+ });
3371
+ }
3372
+
3373
+ // Listen for messages from parent container
3374
+ window.addEventListener('message', function(e) {
3375
+ if (!e.data || typeof e.data !== 'object') return;
3376
+ if (e.data.type === 'GOTO_SLIDE' && typeof e.data.index === 'number') {
3377
+ showSlide(e.data.index);
3378
+ } else if (e.data.type === 'NEXT_SLIDE' || e.data.type === 'STEP_FORWARD') {
3379
+ stepForward();
3380
+ } else if (e.data.type === 'PREV_SLIDE' || e.data.type === 'STEP_BACKWARD') {
3381
+ stepBackward();
3382
+ } else if (e.data.type === 'TOGGLE_EDIT_MODE') {
3383
+ toggleEditMode();
3384
+ } else if (e.data.type === 'EXPORT_HTML') {
3385
+ exportAndSaveHtml();
3386
+ } else if (e.data.type === 'TOGGLE_PRESENTER') {
3387
+ togglePresenterDrawer();
3388
+ } else if (e.data.type === 'OPEN_PRESENTER_CONSOLE') {
3389
+ openPresenterConsole();
3390
+ } else if (e.data.type === 'TOGGLE_DECK_MODE') {
3391
+ toggleDeckMode();
3392
+ } else if (e.data.type === 'PRINT_DECK') {
3393
+ executePrint();
3394
+ } else if (e.data.type === 'SET_SCALE') {
3395
+ if (e.data.mode) setScaleMode(e.data.mode);
3396
+ if (typeof e.data.factor === 'number') scaleState.factor = e.data.factor;
3397
+ if (typeof e.data.headings === 'number') scaleState.headings = e.data.headings;
3398
+ if (typeof e.data.body === 'number') scaleState.body = e.data.body;
3399
+ if (typeof e.data.code === 'number') scaleState.code = e.data.code;
3400
+ if (typeof e.data.spacing === 'number') scaleState.spacing = e.data.spacing;
3401
+ applyScales();
3402
+ } else if (e.data.type === 'TOGGLE_SCALE_PANEL') {
3403
+ toggleScalePanel();
3404
+ }
3405
+ });
3406
+
3407
+ // Initial setup
3408
+ applyScales();
3409
+ setScaleMode(scaleState.mode);
3410
+ showSlide(0);
3411
+ })();
3412
+ </script>
3413
+ </body>
3414
+ </html>`;
3415
+ }
3416
+
3417
+ // src/html-engine/compiler.ts
3418
+ function compileHtmlDeck(deckData, options) {
3419
+ const theme = options?.themeOverride || resolveHtmlTheme(deckData.theme);
3420
+ const slides = deckData.slides || [];
3421
+ const slideCount = Math.max(slides.length, 1);
3422
+ const renderedSlides = slides.map((slide, index) => {
3423
+ const preset = HTML_SLIDE_PRESETS[slide.layoutId] || HTML_SLIDE_PRESETS["hero-cover"];
3424
+ const innerHtml = preset.template(slide.slots || {}, theme);
3425
+ const notesAttr = slide.notes ? ` data-notes="${encodeURIComponent(slide.notes)}"` : "";
3426
+ const customClass = slide.customClass ? ` ${slide.customClass}` : "";
3427
+ return `
3428
+ <!-- Slide ${index + 1}: ${preset.name} -->
3429
+ <section class="slide-page${customClass}" data-index="${index}" data-layout="${preset.id}"${notesAttr}>
3430
+ ${innerHtml}
3431
+ </section>
3432
+ `;
3433
+ });
3434
+ const slidesHtml = renderedSlides.join("\n");
3435
+ const fullHtml = generateHtmlShell({
3436
+ title: deckData.title || "Presentation",
3437
+ theme,
3438
+ slidesHtml,
3439
+ slideCount,
3440
+ customHeadTags: options?.customHeadTags
3441
+ });
3442
+ return {
3443
+ html: fullHtml,
3444
+ slideCount,
3445
+ theme,
3446
+ title: deckData.title
3447
+ };
3448
+ }
3449
+
3450
+ exports.HTML_SLIDE_PRESETS = HTML_SLIDE_PRESETS;
3451
+ exports.HTML_THEMES = HTML_THEMES;
3452
+ exports.SLIDE_LAYOUT_PRESETS = SLIDE_LAYOUT_PRESETS;
3453
+ exports.compileHtmlDeck = compileHtmlDeck;
3454
+ exports.generateHtmlShell = generateHtmlShell;
3455
+ exports.getSlideLayoutPresetById = getSlideLayoutPresetById;
3456
+ exports.getSlideLayoutPresetsByCategory = getSlideLayoutPresetsByCategory;
3457
+ exports.removeSlideElementFromMarkdown = removeSlideElementFromMarkdown;
3458
+ exports.resolveHtmlTheme = resolveHtmlTheme;
3459
+ exports.serializeLayoutToComment = serializeLayoutToComment;
3460
+ exports.splitMarkdownSlides = splitMarkdownSlides;
3461
+ exports.updateSlideLayoutInMarkdown = updateSlideLayoutInMarkdown;
3462
+ exports.updateSlideTextInMarkdown = updateSlideTextInMarkdown;
3463
+ //# sourceMappingURL=chunk-53GLUCYC.cjs.map
3464
+ //# sourceMappingURL=chunk-53GLUCYC.cjs.map