simplex-web 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. simplex/README.md +32 -0
  2. simplex/cli/README.md +13 -0
  3. simplex/cli/__init__.py +5 -0
  4. simplex/cli/commands.py +384 -0
  5. simplex/deck/README.md +19 -0
  6. simplex/deck/__init__.py +7 -0
  7. simplex/deck/_template/assets/.gitkeep +0 -0
  8. simplex/deck/_template/assets/code/.gitkeep +0 -0
  9. simplex/deck/_template/assets/figures/.gitkeep +0 -0
  10. simplex/deck/_template/deck.toml +11 -0
  11. simplex/deck/_template/manim.cfg +3 -0
  12. simplex/deck/_template/notes.md +27 -0
  13. simplex/deck/_template/refs.bib +12 -0
  14. simplex/deck/_template/slides/__init__.py +7 -0
  15. simplex/deck/_template/slides/intro.py +21 -0
  16. simplex/deck/config.py +207 -0
  17. simplex/deck/registry.py +110 -0
  18. simplex/deck/scaffold.py +86 -0
  19. simplex/deck/section.py +40 -0
  20. simplex/engine/README.md +9 -0
  21. simplex/render/README.md +46 -0
  22. simplex/render/__init__.py +1 -0
  23. simplex/render/html.py +132 -0
  24. simplex/render/pdf.py +32 -0
  25. simplex/render/pptx.py +32 -0
  26. simplex/render/reconcile.py +350 -0
  27. simplex/render/runner.py +116 -0
  28. simplex/render/thumbnail.py +374 -0
  29. simplex/slides/README.md +9 -0
  30. simplex/slides/components/README.md +9 -0
  31. simplex/theme/README.md +9 -0
  32. simplex/web/README.md +33 -0
  33. simplex/web/__init__.py +1 -0
  34. simplex/web/bibliography.py +248 -0
  35. simplex/web/bibtex.py +129 -0
  36. simplex/web/builder.py +321 -0
  37. simplex/web/callouts.py +134 -0
  38. simplex/web/citations.py +118 -0
  39. simplex/web/equations.py +79 -0
  40. simplex/web/notes.py +135 -0
  41. simplex/web/refs.py +60 -0
  42. simplex/web/sidenotes.py +76 -0
  43. simplex/web/site_config.py +71 -0
  44. simplex/web/slide_ref.py +54 -0
  45. simplex/web/static/.gitkeep +0 -0
  46. simplex/web/static/README.md +23 -0
  47. simplex/web/static/fonts/lato/lato-latin-400-italic.woff2 +0 -0
  48. simplex/web/static/fonts/lato/lato-latin-400-normal.woff2 +0 -0
  49. simplex/web/static/fonts/lato/lato-latin-700-italic.woff2 +0 -0
  50. simplex/web/static/fonts/lato/lato-latin-700-normal.woff2 +0 -0
  51. simplex/web/static/fonts/lato/lato-latin-900-normal.woff2 +0 -0
  52. simplex/web/static/fonts/merriweather/merriweather-latin-400-italic.woff2 +0 -0
  53. simplex/web/static/fonts/merriweather/merriweather-latin-400-normal.woff2 +0 -0
  54. simplex/web/static/fonts/merriweather/merriweather-latin-700-italic.woff2 +0 -0
  55. simplex/web/static/fonts/merriweather/merriweather-latin-700-normal.woff2 +0 -0
  56. simplex/web/static/fonts/merriweather/merriweather-latin-900-normal.woff2 +0 -0
  57. simplex/web/static/htmx.min.js +1 -0
  58. simplex/web/static/katex/auto-render.min.js +1 -0
  59. simplex/web/static/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  60. simplex/web/static/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  61. simplex/web/static/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  62. simplex/web/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  63. simplex/web/static/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  64. simplex/web/static/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  65. simplex/web/static/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  66. simplex/web/static/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  67. simplex/web/static/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  68. simplex/web/static/katex/katex.min.css +1 -0
  69. simplex/web/static/katex/katex.min.js +1 -0
  70. simplex/web/static/lucide/README.md +7 -0
  71. simplex/web/static/lucide/lucide.min.js +12 -0
  72. simplex/web/static/notes.js +68 -0
  73. simplex/web/static/reveal.js/reset.css +30 -0
  74. simplex/web/static/reveal.js/reveal.css +8 -0
  75. simplex/web/static/reveal.js/reveal.js +9 -0
  76. simplex/web/static/simplex.css +1870 -0
  77. simplex/web/static/tailwind.js +64 -0
  78. simplex/web/static/viewer.js +428 -0
  79. simplex/web/templates/README.md +19 -0
  80. simplex/web/templates/_carousel.html +117 -0
  81. simplex/web/templates/base.html +110 -0
  82. simplex/web/templates/deck.html +149 -0
  83. simplex/web/templates/index.html +20 -0
  84. simplex/web/templates/revealjs.html.j2 +374 -0
  85. simplex/web/templates/section.html +74 -0
  86. simplex/web/vendor.py +148 -0
  87. simplex_web-0.2.0.dist-info/METADATA +166 -0
  88. simplex_web-0.2.0.dist-info/RECORD +91 -0
  89. simplex_web-0.2.0.dist-info/WHEEL +4 -0
  90. simplex_web-0.2.0.dist-info/entry_points.txt +2 -0
  91. simplex_web-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,149 @@
1
+ {% extends "base.html" %}
2
+ {% block title %}{{ deck.title }} -- {{ site.brand }}{% endblock %}
3
+ {% block nav_actions %}
4
+ {% if has_pdf %}
5
+ <a
6
+ href="{{ deck.slug }}.pdf"
7
+ class="site-resource-link"
8
+ download
9
+ >
10
+ <span class="pdf-file-icon" aria-hidden="true"><span>PDF</span></span>
11
+ <span>ppt</span>
12
+ </a>
13
+ {% endif %}
14
+ {% if has_notes_pdf %}
15
+ <a
16
+ href="notes.pdf"
17
+ class="site-resource-link"
18
+ download
19
+ >
20
+ <i data-lucide="file-text" aria-hidden="true"></i>
21
+ <span class="icon-fallback" aria-hidden="true">N</span>
22
+ <span>Notes PDF</span>
23
+ </a>
24
+ {% endif %}
25
+ {% endblock %}
26
+ {% block main_class %}deck-main{% endblock %}
27
+ {% block content %}
28
+ <section
29
+ class="deck-grid"
30
+ data-deck-slug="{{ deck.slug }}"
31
+ data-slide-count="{{ slide_count }}"
32
+ data-default-slide-number="{% if deck.web.show_slide_number %}true{% else %}false{% endif %}"
33
+ data-default-clock="{% if deck.web.show_clock %}true{% else %}false{% endif %}"
34
+ >
35
+ <div class="deck-viewer">
36
+ <div class="deck-viewer-frame">
37
+ <iframe
38
+ src="slides.html"
39
+ title="{{ deck.title }} presentation"
40
+ allowfullscreen
41
+ allow="autoplay; fullscreen"
42
+ loading="lazy"
43
+ class="deck-iframe"
44
+ ></iframe>
45
+ </div>
46
+ <div class="deck-controls" role="toolbar" aria-label="Slide controls">
47
+ <div class="deck-control-group" aria-label="Playback">
48
+ <button data-control="prev" class="deck-ctl-btn" aria-label="Previous slide" title="Previous" type="button">
49
+ <i data-lucide="chevron-left" aria-hidden="true"></i>
50
+ <span class="icon-fallback" aria-hidden="true">‹</span>
51
+ </button>
52
+ <button data-control="restart" class="deck-ctl-btn" aria-label="Restart slide" title="Restart" type="button">
53
+ <i data-lucide="rotate-ccw" aria-hidden="true"></i>
54
+ <span class="icon-fallback" aria-hidden="true">↺</span>
55
+ </button>
56
+ <button data-control="toggle-play" class="deck-ctl-btn deck-ctl-play" aria-label="Play" title="Play" data-state="paused" type="button">
57
+ <i data-lucide="play" class="deck-ctl-icon-play" aria-hidden="true"></i>
58
+ <i data-lucide="pause" class="deck-ctl-icon-pause" aria-hidden="true"></i>
59
+ <span class="icon-fallback deck-ctl-icon-play" aria-hidden="true">▶</span>
60
+ <span class="icon-fallback deck-ctl-icon-pause" aria-hidden="true">Ⅱ</span>
61
+ </button>
62
+ <button data-control="next" class="deck-ctl-btn" aria-label="Next slide" title="Next" type="button">
63
+ <i data-lucide="chevron-right" aria-hidden="true"></i>
64
+ <span class="icon-fallback" aria-hidden="true">›</span>
65
+ </button>
66
+ <button data-control="fullscreen" class="deck-ctl-btn" aria-label="Fullscreen" title="Fullscreen" type="button">
67
+ <i data-lucide="maximize" aria-hidden="true"></i>
68
+ <span class="icon-fallback" aria-hidden="true">⛶</span>
69
+ </button>
70
+ <span class="deck-counter" data-counter aria-live="polite">1 / {{ slide_count }}</span>
71
+ </div>
72
+ <div class="deck-settings" data-settings>
73
+ <button
74
+ type="button"
75
+ class="deck-settings-trigger"
76
+ data-settings-toggle
77
+ aria-expanded="false"
78
+ aria-haspopup="true"
79
+ >
80
+ <i data-lucide="settings" aria-hidden="true"></i>
81
+ <span class="icon-fallback" aria-hidden="true">⚙</span>
82
+ <span>Player Settings</span>
83
+ </button>
84
+ <div class="deck-settings-panel" data-settings-panel hidden>
85
+ <label class="deck-setting-row">
86
+ <span><i data-lucide="palette" aria-hidden="true"></i>Slide Color</span>
87
+ <input type="checkbox" data-setting="slide-color" checked />
88
+ </label>
89
+ <label class="deck-setting-row">
90
+ <span><i data-lucide="list-ordered" aria-hidden="true"></i>Enumeration</span>
91
+ <input
92
+ type="checkbox"
93
+ data-setting="slide-number"
94
+ {% if deck.web.show_slide_number %}checked{% endif %}
95
+ />
96
+ </label>
97
+ <label class="deck-setting-row">
98
+ <span><i data-lucide="clock" aria-hidden="true"></i>Clock</span>
99
+ <input type="checkbox" data-setting="clock" {% if deck.web.show_clock %}checked{% endif %} />
100
+ </label>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <aside class="deck-sidebar" aria-label="Slide list">
107
+ <ol class="deck-slide-list" role="list">
108
+ {% for slide in slides %}
109
+ <li>
110
+ <button
111
+ type="button"
112
+ class="deck-slide-card"
113
+ data-slide-target="{{ slide.index }}"
114
+ {% if loop.first %}aria-current="true"{% endif %}
115
+ >
116
+ <div class="deck-slide-thumb">
117
+ {% if slide.thumbnail %}
118
+ <img
119
+ src="{{ slide.thumbnail }}"
120
+ alt=""
121
+ loading="lazy"
122
+ decoding="async"
123
+ />
124
+ {% else %}
125
+ <div class="deck-slide-thumb-empty" aria-hidden="true"></div>
126
+ {% endif %}
127
+ <div class="deck-slide-overlay">
128
+ <span class="deck-slide-title">
129
+ {{ slide.name or slide.scene }}
130
+ </span>
131
+ {% set duration_seconds = slide.duration_s|round(0, "ceil")|int %}
132
+ <span class="deck-slide-duration">
133
+ {{ "%d:%02d"|format(duration_seconds // 60, duration_seconds % 60) }}
134
+ </span>
135
+ </div>
136
+ </div>
137
+ </button>
138
+ </li>
139
+ {% endfor %}
140
+ </ol>
141
+ </aside>
142
+
143
+ {% if notes_html %}
144
+ <article class="deck-notes prose prose-invert max-w-none">
145
+ {{ notes_html|safe }}
146
+ </article>
147
+ {% endif %}
148
+ </section>
149
+ {% endblock %}
@@ -0,0 +1,20 @@
1
+ {% extends "base.html" %}
2
+ {% block title %}{{ site.brand }} -- Decks{% endblock %}
3
+ {% block content %}
4
+ {% if not registry.sections %}
5
+ <p class="text-neutral-400">
6
+ No decks yet. Run <code class="px-1 bg-neutral-800 rounded">simplex new my-deck</code>
7
+ to create one.
8
+ </p>
9
+ {% else %}
10
+ {% if latest_section %}
11
+ {% set section = latest_section %}
12
+ {% set carousel_variant = "latest" %}
13
+ {% include "_carousel.html" %}
14
+ {% endif %}
15
+ {% set carousel_variant = "section" %}
16
+ {% for section in registry.sections %}
17
+ {% include "_carousel.html" %}
18
+ {% endfor %}
19
+ {% endif %}
20
+ {% endblock %}
@@ -0,0 +1,374 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5" />
6
+ <title>{{ deck.title }} -- slides</title>
7
+ <link rel="stylesheet" href="{{ static_prefix }}/reveal.js/reset.css" />
8
+ <link rel="stylesheet" href="{{ static_prefix }}/reveal.js/reveal.css" />
9
+ <style>
10
+ {{ palette_css|safe }}
11
+ </style>
12
+ <style>
13
+ :root {
14
+ --simplex-stage-bg: #2b2b2b;
15
+ --simplex-stage-progress: rgba(255, 255, 255, 0.08);
16
+ }
17
+ :root[data-theme="light"] {
18
+ --simplex-stage-bg: #f3f5f7;
19
+ --simplex-stage-progress: rgba(21, 31, 40, 0.14);
20
+ }
21
+ html, body {
22
+ background: var(--simplex-stage-bg);
23
+ color: var(--simplex-text, #f8f8f8);
24
+ height: 100%;
25
+ width: 100%;
26
+ margin: 0;
27
+ padding: 0;
28
+ overflow: hidden;
29
+ touch-action: manipulation;
30
+ }
31
+ .reveal, .reveal .slides {
32
+ background: var(--simplex-stage-bg);
33
+ width: 100% !important;
34
+ height: 100% !important;
35
+ inset: 0;
36
+ }
37
+ /* Override reveal's transform-based scaling: we want the video to fill
38
+ the viewer box natively. Reveal still drives slide changes; only the
39
+ layout math is ours. */
40
+ .reveal .slides {
41
+ top: 0 !important;
42
+ left: 0 !important;
43
+ transform: none !important;
44
+ zoom: 1 !important;
45
+ }
46
+ .reveal .slides > section,
47
+ .reveal .slides > section > section {
48
+ position: absolute;
49
+ inset: 0;
50
+ width: 100% !important;
51
+ height: 100% !important;
52
+ padding: 0;
53
+ margin: 0;
54
+ display: flex;
55
+ align-items: center;
56
+ justify-content: center;
57
+ background: var(--simplex-stage-bg);
58
+ }
59
+ .reveal video {
60
+ max-width: 100%;
61
+ max-height: 100%;
62
+ width: 100%;
63
+ height: 100%;
64
+ object-fit: contain;
65
+ background: var(--simplex-stage-bg);
66
+ display: block;
67
+ }
68
+ .reveal .controls { display: none !important; }
69
+ .reveal .progress {
70
+ background: var(--simplex-stage-progress);
71
+ height: 3px;
72
+ color: var(--simplex-accent, #ffd700);
73
+ }
74
+ .reveal .progress span { background: var(--simplex-accent, #ffd700); }
75
+ .tap-zone {
76
+ position: fixed;
77
+ top: 0;
78
+ bottom: 0;
79
+ width: 33vw;
80
+ z-index: 30;
81
+ background: transparent;
82
+ cursor: pointer;
83
+ -webkit-tap-highlight-color: transparent;
84
+ }
85
+ .tap-zone.next { right: 0; width: 67vw; }
86
+ .tap-zone.prev { left: 0; }
87
+ @media (prefers-reduced-motion: reduce) {
88
+ .reveal .slides section { transition: none !important; }
89
+ }
90
+ .empty-slide {
91
+ font-family: var(--simplex-font-sans, ui-sans-serif, system-ui, sans-serif);
92
+ font-size: 1.25rem;
93
+ color: rgba(255,255,255,.65);
94
+ padding: 2rem;
95
+ text-align: center;
96
+ }
97
+ .simplex-chrome {
98
+ position: fixed;
99
+ z-index: 40;
100
+ font-family: var(--simplex-font-sans, ui-sans-serif, system-ui, sans-serif);
101
+ font-size: 1rem;
102
+ color: var(--simplex-text-muted, rgba(255,255,255,.65));
103
+ pointer-events: none;
104
+ }
105
+ .simplex-chrome.clock { bottom: 10px; left: 10px; }
106
+ .simplex-chrome.slide-number { bottom: 10px; right: 10px; }
107
+ </style>
108
+ {% if deck_custom_css %}
109
+ <style>
110
+ {{ deck_custom_css|safe }}
111
+ </style>
112
+ {% endif %}
113
+ </head>
114
+ <body>
115
+ <div class="reveal" role="region" aria-label="{{ deck.title }} slide viewer">
116
+ <div class="slides">
117
+ {% for main in main_slides %}
118
+ <section
119
+ data-main-index="{{ main.index }}"
120
+ data-scene="{{ main.scene }}"
121
+ data-name="{{ main.name }}"
122
+ >
123
+ {% for sub in main.subsections %}
124
+ <section
125
+ data-sub-name="{{ sub.name }}"
126
+ {% if "loop" in sub.section_type %}data-background-video-loop{% endif %}
127
+ >
128
+ {% if sub.video_href %}
129
+ <video
130
+ data-autoplay
131
+ muted
132
+ playsinline
133
+ preload="metadata"
134
+ aria-label="{{ main.name }} - {{ sub.name }}"
135
+ >
136
+ <source src="{{ sub.video_href }}" type="video/mp4" />
137
+ </video>
138
+ {% else %}
139
+ <div class="empty-slide">
140
+ <p>{{ main.name }} - {{ sub.name }}</p>
141
+ <p style="font-size:.85rem;opacity:.6">no video segment available</p>
142
+ </div>
143
+ {% endif %}
144
+ </section>
145
+ {% endfor %}
146
+ {% if not main.subsections %}
147
+ <section data-sub-name="empty">
148
+ <div class="empty-slide">
149
+ <p>{{ main.name }}</p>
150
+ <p style="font-size:.85rem;opacity:.6">no rendered video for this slide yet</p>
151
+ </div>
152
+ </section>
153
+ {% endif %}
154
+ </section>
155
+ {% endfor %}
156
+ </div>
157
+ </div>
158
+
159
+ <button class="tap-zone prev" aria-hidden="true" tabindex="-1"
160
+ data-tap="prev"></button>
161
+ <button class="tap-zone next" aria-hidden="true" tabindex="-1"
162
+ data-tap="next"></button>
163
+
164
+ <div
165
+ class="simplex-chrome clock"
166
+ id="simplex-clock"
167
+ aria-hidden="true"
168
+ {% if not show_clock %}hidden{% endif %}
169
+ ></div>
170
+ <div
171
+ class="simplex-chrome slide-number"
172
+ id="simplex-slide-number"
173
+ aria-hidden="true"
174
+ {% if not show_slide_number %}hidden{% endif %}
175
+ ></div>
176
+
177
+ <script src="{{ static_prefix }}/reveal.js/reveal.js"></script>
178
+ <script>
179
+ (function () {
180
+ const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
181
+ const isTouch = window.matchMedia('(hover: none) and (pointer: coarse)').matches;
182
+ const parentOrigin = (function () {
183
+ try { return window.parent.location.origin || '*'; } catch (_) { return '*'; }
184
+ })();
185
+ const chromeState = {
186
+ clock: {% if show_clock %}true{% else %}false{% endif %},
187
+ slideNumber: {% if show_slide_number %}true{% else %}false{% endif %},
188
+ };
189
+
190
+ Reveal.initialize({
191
+ embedded: true,
192
+ controls: false,
193
+ controlsTutorial: false,
194
+ progress: true,
195
+ hash: false,
196
+ // Remap all arrow keys to flat next/prev so direction never matters,
197
+ // even when the slide grid has vertical sub-stops. Keys: 37 left,
198
+ // 38 up, 39 right, 40 down, 32 space, 33 PageUp, 34 PageDown.
199
+ keyboard: {
200
+ 37: 'prev', 38: 'prev', 33: 'prev',
201
+ 39: 'next', 40: 'next', 32: 'next', 34: 'next',
202
+ },
203
+ // Disable Reveal's built-in swipe gestures. Mobile navigation goes
204
+ // through the .tap-zone elements below; this keeps tap-right = next
205
+ // and tap-left = prev regardless of horizontal/vertical slide layout.
206
+ touch: false,
207
+ transition: reduce ? 'none' : '{{ transition }}',
208
+ backgroundTransition: 'none',
209
+ autoPlayMedia: true,
210
+ slideNumber: false,
211
+ loop: false,
212
+ center: false,
213
+ // Reveal's scale-to-fit math fights us when the video already knows
214
+ // its aspect; disableLayout: true means our CSS sizes every slide.
215
+ disableLayout: true,
216
+ viewDistance: 2,
217
+ }).then(function () {
218
+ function currentMainIndex() {
219
+ // Resolve the active 1-based main-slide index from the closest
220
+ // ancestor ``<section data-main-index>``. Works for both top-level
221
+ // mains and their vertical sub-stops -- on a sub-slide the
222
+ // closest() walk still lands on the surrounding main, so the
223
+ // sidebar stays highlighted on the right thumbnail card while the
224
+ // user scrubs through sub-stops.
225
+ const slide = Reveal.getCurrentSlide();
226
+ if (!slide) return 0;
227
+ const main = slide.closest('[data-main-index]') || slide;
228
+ const raw = main.getAttribute('data-main-index');
229
+ return raw ? parseInt(raw, 10) || 0 : 0;
230
+ }
231
+ function emit() {
232
+ const idx = Reveal.getIndices();
233
+ const total = {{ main_slide_count }};
234
+ renderSlideNumber();
235
+ try {
236
+ window.parent.postMessage(
237
+ {
238
+ type: 'simplex.slide',
239
+ idx: currentMainIndex(),
240
+ sub: idx.v || 0,
241
+ total: total,
242
+ },
243
+ parentOrigin
244
+ );
245
+ } catch (_) { /* parent gone or different origin */ }
246
+ }
247
+ function renderSlideNumber() {
248
+ const el = document.getElementById('simplex-slide-number');
249
+ if (!el) return;
250
+ el.textContent = currentMainIndex() + ' / ' + {{ main_slide_count }};
251
+ }
252
+ function applyChrome() {
253
+ const clock = document.getElementById('simplex-clock');
254
+ const slideNumber = document.getElementById('simplex-slide-number');
255
+ if (clock) clock.hidden = !chromeState.clock;
256
+ if (slideNumber) slideNumber.hidden = !chromeState.slideNumber;
257
+ renderSlideNumber();
258
+ }
259
+ Reveal.on('slidechanged', emit);
260
+ Reveal.on('ready', emit);
261
+ applyChrome();
262
+
263
+ window.addEventListener('message', function (e) {
264
+ const data = e.data || {};
265
+ if (typeof data !== 'object') return;
266
+ if (data.type === 'simplex.goto' && Number.isInteger(data.idx)) {
267
+ // ``data.idx`` is the 1-based main-slide number from the sidebar
268
+ // (``data-slide-target``). Reveal's horizontal index is 0-based,
269
+ // so subtract one before dispatching.
270
+ Reveal.slide(data.idx - 1, data.sub || 0);
271
+ } else if (data.type === 'simplex.next') {
272
+ Reveal.next();
273
+ } else if (data.type === 'simplex.prev') {
274
+ Reveal.prev();
275
+ } else if (data.type === 'simplex.restart') {
276
+ const v = document.querySelector('.reveal .slides .present video');
277
+ if (v) {
278
+ try { v.currentTime = 0; } catch (_) { /* */ }
279
+ const p = v.play();
280
+ if (p && typeof p.catch === 'function') p.catch(function () {});
281
+ }
282
+ } else if (data.type === 'simplex.toggle-play') {
283
+ const v = document.querySelector('.reveal .slides .present video');
284
+ if (v) {
285
+ if (v.paused) {
286
+ if (v.ended || v.currentTime >= Math.max(0, v.duration - 0.05)) {
287
+ try { v.currentTime = 0; } catch (_) { /* */ }
288
+ }
289
+ const p = v.play();
290
+ if (p && typeof p.catch === 'function') p.catch(function () {});
291
+ } else {
292
+ v.pause();
293
+ }
294
+ try {
295
+ window.parent.postMessage(
296
+ { type: 'simplex.play-state', playing: !v.paused },
297
+ parentOrigin
298
+ );
299
+ } catch (_) { /* */ }
300
+ }
301
+ } else if (data.type === 'simplex.fullscreen') {
302
+ const el = document.documentElement;
303
+ const fn = el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen;
304
+ const exit = document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen;
305
+ const inFs = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement;
306
+ try {
307
+ if (inFs) { if (exit) { exit.call(document); } }
308
+ else if (fn) { fn.call(el); }
309
+ } catch (_) { /* */ }
310
+ } else if (data.type === 'simplex.set-chrome') {
311
+ if (typeof data.clock === 'boolean') chromeState.clock = data.clock;
312
+ if (typeof data.slideNumber === 'boolean') chromeState.slideNumber = data.slideNumber;
313
+ applyChrome();
314
+ } else if (data.type === 'simplex.set-theme' && typeof data.theme === 'string') {
315
+ document.documentElement.dataset.theme = data.theme;
316
+ document.documentElement.style.colorScheme = data.theme;
317
+ }
318
+ });
319
+
320
+ document.addEventListener('play', function (e) {
321
+ if (!e.target || e.target.tagName !== 'VIDEO') return;
322
+ try {
323
+ window.parent.postMessage(
324
+ { type: 'simplex.play-state', playing: true }, parentOrigin
325
+ );
326
+ } catch (_) { /* */ }
327
+ }, true);
328
+ document.addEventListener('pause', function (e) {
329
+ if (!e.target || e.target.tagName !== 'VIDEO') return;
330
+ try {
331
+ window.parent.postMessage(
332
+ { type: 'simplex.play-state', playing: false }, parentOrigin
333
+ );
334
+ } catch (_) { /* */ }
335
+ }, true);
336
+
337
+ if (isTouch) {
338
+ document.querySelectorAll('.tap-zone').forEach(function (z) {
339
+ z.addEventListener('click', function (ev) {
340
+ ev.preventDefault();
341
+ if (z.dataset.tap === 'next') { Reveal.next(); }
342
+ else { Reveal.prev(); }
343
+ }, { passive: false });
344
+ });
345
+ } else {
346
+ document.querySelectorAll('.tap-zone').forEach(function (z) {
347
+ z.style.display = 'none';
348
+ });
349
+ }
350
+ });
351
+
352
+ // Wall clock in the corner. Updates once per second; pad single
353
+ // digits so the layout doesn't reflow.
354
+ (function () {
355
+ const el = document.getElementById('simplex-clock');
356
+ if (!el) return;
357
+ const pad = (n) => String(n).padStart(2, '0');
358
+ const tick = () => {
359
+ const now = new Date();
360
+ el.textContent = pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds());
361
+ };
362
+ tick();
363
+ setInterval(tick, 1000);
364
+ })();
365
+
366
+ {% if watch %}
367
+ // --watch mode: subscribe to SSE for live reload on render.
368
+ const es = new EventSource('/_simplex/events');
369
+ es.addEventListener('reload', () => window.location.reload());
370
+ {% endif %}
371
+ })();
372
+ </script>
373
+ </body>
374
+ </html>
@@ -0,0 +1,74 @@
1
+ {% extends "base.html" %}
2
+ {% block title %}{{ section.config.title }} -- {{ site.brand }}{% endblock %}
3
+ {% block content %}
4
+ <section class="section-page-heading">
5
+ <div class="carousel-heading-line">
6
+ <h1>{{ section.config.title }}</h1>
7
+ </div>
8
+ {% if section.config.blurb %}
9
+ <p>{{ section.config.blurb }}</p>
10
+ {% endif %}
11
+ </section>
12
+
13
+ <ol class="section-deck-grid" role="list">
14
+ {% for deck in section.decks %}
15
+ {% set thumb = thumbs.get(deck.slug) %}
16
+ {% set gif = preview_gifs.get(deck.slug) %}
17
+ {% set created = deck_dates.get(deck.slug) %}
18
+ <li>
19
+ <a
20
+ href="{{ site.url('decks/' + deck.slug + '/') }}"
21
+ class="carousel-card"
22
+ aria-label="Open {{ deck.title }}"
23
+ >
24
+ <div class="carousel-thumb">
25
+ {% if thumb %}
26
+ <img
27
+ src="{{ site.url('decks/' + deck.slug + '/' + thumb) }}"
28
+ {% if gif %}data-preview-gif="{{ site.url('decks/' + deck.slug + '/' + gif) }}"{% endif %}
29
+ alt=""
30
+ loading="lazy"
31
+ decoding="async"
32
+ width="480"
33
+ height="270"
34
+ />
35
+ {% else %}
36
+ <div class="carousel-thumb-empty" aria-hidden="true">
37
+ <span>{{ deck.title[:1] }}</span>
38
+ </div>
39
+ {% endif %}
40
+ <div class="carousel-scrim" aria-hidden="true"></div>
41
+ <div class="carousel-card-content">
42
+ <div class="carousel-card-top">
43
+ {% if deck.category %}
44
+ <span class="carousel-badge">{{ deck.category }}</span>
45
+ {% endif %}
46
+ {% if created %}
47
+ <span class="carousel-created">
48
+ <i data-lucide="calendar" aria-hidden="true"></i>
49
+ {{ created }}
50
+ </span>
51
+ {% endif %}
52
+ </div>
53
+ <div class="carousel-card-bottom">
54
+ <h2>{{ deck.title }}</h2>
55
+ {% if deck.summary %}
56
+ <p>{{ deck.summary }}</p>
57
+ {% endif %}
58
+ <div class="carousel-card-actions">
59
+ <span class="carousel-play">
60
+ <i data-lucide="play" aria-hidden="true"></i>
61
+ Play now
62
+ </span>
63
+ {% if deck.duration_minutes %}
64
+ <span class="carousel-duration">{{ deck.duration_minutes }} min</span>
65
+ {% endif %}
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </a>
71
+ </li>
72
+ {% endfor %}
73
+ </ol>
74
+ {% endblock %}