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,428 @@
1
+ /* Simplex parent-side viewer glue.
2
+ *
3
+ * - Wires carousel arrows + keyboard nav on the home page.
4
+ * - Bridges the deck iframe (RevealJS) <-> sidebar / controls / slide-refs
5
+ * via postMessage. The iframe template is at
6
+ * src/simplex/web/templates/revealjs.html.j2 and emits
7
+ * {type:'simplex.slide', idx, total} on every slide change.
8
+ */
9
+
10
+ (function () {
11
+ "use strict";
12
+
13
+ function initIcons() {
14
+ if (!window.lucide || typeof window.lucide.createIcons !== "function") return;
15
+ try {
16
+ window.lucide.createIcons({
17
+ icons: window.lucide.icons,
18
+ nameAttr: "data-lucide",
19
+ attrs: {
20
+ "stroke-width": 1.9,
21
+ "aria-hidden": "true",
22
+ },
23
+ });
24
+ document.querySelectorAll(".icon-fallback").forEach(function (fallback) {
25
+ var parent = fallback.parentElement;
26
+ if (parent && parent.querySelector("svg[data-lucide]")) {
27
+ fallback.classList.add("is-hidden");
28
+ }
29
+ });
30
+ } catch (_) {
31
+ document.querySelectorAll(".icon-fallback").forEach(function (fallback) {
32
+ fallback.classList.remove("is-hidden");
33
+ });
34
+ }
35
+ }
36
+
37
+ function initTheme() {
38
+ var root = document.documentElement;
39
+ var button = document.querySelector("[data-theme-toggle]");
40
+ var media = window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null;
41
+
42
+ function storedTheme() {
43
+ try { return localStorage.getItem("simplex-theme"); }
44
+ catch (_) { return null; }
45
+ }
46
+ function systemTheme() {
47
+ return media && media.matches ? "dark" : "light";
48
+ }
49
+ function apply(theme, persist) {
50
+ root.dataset.theme = theme;
51
+ root.style.colorScheme = theme;
52
+ if (persist) {
53
+ try { localStorage.setItem("simplex-theme", theme); } catch (_) {}
54
+ }
55
+ if (button) {
56
+ button.setAttribute(
57
+ "aria-label",
58
+ theme === "dark" ? "Switch to light theme" : "Switch to dark theme"
59
+ );
60
+ button.setAttribute(
61
+ "title",
62
+ theme === "dark" ? "Switch to light theme" : "Switch to dark theme"
63
+ );
64
+ }
65
+ try {
66
+ window.dispatchEvent(new CustomEvent("simplex.theme", { detail: { theme: theme } }));
67
+ } catch (_) {}
68
+ }
69
+
70
+ apply(storedTheme() || root.dataset.theme || systemTheme(), false);
71
+ if (button) {
72
+ button.addEventListener("click", function () {
73
+ apply(root.dataset.theme === "dark" ? "light" : "dark", true);
74
+ });
75
+ }
76
+ if (media && typeof media.addEventListener === "function") {
77
+ media.addEventListener("change", function () {
78
+ if (!storedTheme()) apply(systemTheme(), false);
79
+ });
80
+ }
81
+ }
82
+
83
+ function initPreviewGifs() {
84
+ var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
85
+ var saveData = navigator.connection && navigator.connection.saveData;
86
+ if (reduce || saveData) return;
87
+
88
+ var images = Array.prototype.slice.call(document.querySelectorAll("img[data-preview-gif]"));
89
+ if (!images.length) return;
90
+
91
+ function load() {
92
+ images.forEach(function (img) {
93
+ var src = img.dataset.previewGif;
94
+ if (!src || img.dataset.previewLoaded === "true") return;
95
+ img.dataset.previewLoaded = "true";
96
+ var gif = new Image();
97
+ gif.decoding = "async";
98
+ gif.onload = function () {
99
+ img.src = src;
100
+ img.classList.add("is-preview-gif");
101
+ };
102
+ gif.src = src;
103
+ });
104
+ }
105
+
106
+ function schedule() {
107
+ if ("requestIdleCallback" in window) {
108
+ window.requestIdleCallback(load, { timeout: 1800 });
109
+ } else {
110
+ window.setTimeout(load, 500);
111
+ }
112
+ }
113
+
114
+ if (document.readyState === "complete") schedule();
115
+ else window.addEventListener("load", schedule, { once: true });
116
+ }
117
+
118
+ // ------------------------------------------------------------------
119
+ // Carousel (home page).
120
+ // ------------------------------------------------------------------
121
+ function initCarousels() {
122
+ document.querySelectorAll(".carousel-section").forEach(function (section) {
123
+ var track = section.querySelector(".carousel-track");
124
+ if (!track) return;
125
+ var prev = section.querySelector('.carousel-arrow[data-dir="prev"]');
126
+ var next = section.querySelector('.carousel-arrow[data-dir="next"]');
127
+ function step(delta) {
128
+ track.scrollBy({ left: delta, behavior: "smooth" });
129
+ }
130
+ function syncArrows() {
131
+ if (!prev || !next) return;
132
+ var atStart = track.scrollLeft <= 4;
133
+ var atEnd = track.scrollLeft + track.clientWidth >= track.scrollWidth - 4;
134
+ prev.hidden = atStart;
135
+ next.hidden = atEnd;
136
+ }
137
+ if (prev) prev.addEventListener("click", function () { step(-track.clientWidth * 0.9); });
138
+ if (next) next.addEventListener("click", function () { step(track.clientWidth * 0.9); });
139
+ track.addEventListener("scroll", syncArrows, { passive: true });
140
+ track.addEventListener("keydown", function (e) {
141
+ if (e.key === "ArrowRight") { e.preventDefault(); step(track.clientWidth * 0.9); }
142
+ else if (e.key === "ArrowLeft") { e.preventDefault(); step(-track.clientWidth * 0.9); }
143
+ });
144
+ // Initial state -- after layout.
145
+ requestAnimationFrame(syncArrows);
146
+ window.addEventListener("resize", syncArrows);
147
+ });
148
+ }
149
+
150
+ // ------------------------------------------------------------------
151
+ // Deck page: iframe bridge + sidebar + controls + slide-refs.
152
+ // ------------------------------------------------------------------
153
+ function initDeck() {
154
+ var deck = document.querySelector("[data-deck-slug]");
155
+ if (!deck) return;
156
+ var iframe = deck.querySelector("iframe.deck-iframe");
157
+ var frame = deck.querySelector(".deck-viewer-frame");
158
+ var counter = deck.querySelector("[data-counter]");
159
+ var playBtn = deck.querySelector('[data-control="toggle-play"]');
160
+ var slideButtons = deck.querySelectorAll("[data-slide-target]");
161
+ var slideList = deck.querySelector(".deck-slide-list");
162
+ var sidebar = deck.querySelector(".deck-sidebar");
163
+ var controls = deck.querySelectorAll("[data-control]");
164
+ var slideRefs = deck.querySelectorAll(".slide-ref[data-slide]");
165
+ var settings = deck.querySelector("[data-settings]");
166
+ var settingsToggle = deck.querySelector("[data-settings-toggle]");
167
+ var settingsPanel = deck.querySelector("[data-settings-panel]");
168
+ var colorSetting = deck.querySelector('[data-setting="slide-color"]');
169
+ var slideNumberSetting = deck.querySelector('[data-setting="slide-number"]');
170
+ var clockSetting = deck.querySelector('[data-setting="clock"]');
171
+ var total = parseInt(deck.dataset.slideCount || "0", 10) || slideButtons.length;
172
+ var currentIdx = 0;
173
+
174
+ function targetOrigin() {
175
+ if (!iframe || !iframe.src) return "*";
176
+ try { return new URL(iframe.src, location.href).origin; }
177
+ catch (_) { return "*"; }
178
+ }
179
+ function send(message) {
180
+ if (!iframe || !iframe.contentWindow) return;
181
+ iframe.contentWindow.postMessage(message, targetOrigin());
182
+ }
183
+ function syncThemeSetting() {
184
+ send({
185
+ type: "simplex.set-theme",
186
+ theme: document.documentElement.dataset.theme || "dark",
187
+ });
188
+ }
189
+ function sendChromeSettings() {
190
+ window.setTimeout(function () {
191
+ syncChromeSettings();
192
+ syncThemeSetting();
193
+ }, 0);
194
+ window.setTimeout(function () {
195
+ syncChromeSettings();
196
+ syncThemeSetting();
197
+ }, 250);
198
+ }
199
+
200
+ function centerActiveCard(btn) {
201
+ var candidates = [slideList, sidebar];
202
+ for (var i = 0; i < candidates.length; i += 1) {
203
+ var scroller = candidates[i];
204
+ if (!scroller) continue;
205
+ var canX = scroller.scrollWidth > scroller.clientWidth + 1;
206
+ var canY = scroller.scrollHeight > scroller.clientHeight + 1;
207
+ if (!canX && !canY) continue;
208
+
209
+ var cardRect = btn.getBoundingClientRect();
210
+ var scrollRect = scroller.getBoundingClientRect();
211
+ var options = { behavior: "smooth" };
212
+ if (canX) {
213
+ options.left =
214
+ scroller.scrollLeft +
215
+ cardRect.left -
216
+ scrollRect.left -
217
+ (scrollRect.width - cardRect.width) / 2;
218
+ }
219
+ if (canY) {
220
+ options.top =
221
+ scroller.scrollTop +
222
+ cardRect.top -
223
+ scrollRect.top -
224
+ (scrollRect.height - cardRect.height) / 2;
225
+ }
226
+ scroller.scrollTo(options);
227
+ return;
228
+ }
229
+ }
230
+
231
+ // ``idx`` is the 1-based main-slide number broadcast by the iframe
232
+ // (extracted from ``data-main-index`` on the current section's main
233
+ // ancestor). It matches ``MainSlide.index`` and ``data-slide-target``
234
+ // on each sidebar card, so highlight + counter share one vocabulary
235
+ // and the active card stays highlighted while the user scrubs through
236
+ // sub-stops of the same main slide.
237
+ function setActive(idx) {
238
+ currentIdx = idx;
239
+ if (counter) counter.textContent = idx + " / " + total;
240
+ slideButtons.forEach(function (btn) {
241
+ var t = parseInt(btn.dataset.slideTarget, 10);
242
+ if (t === idx) {
243
+ btn.setAttribute("aria-current", "true");
244
+ centerActiveCard(btn);
245
+ } else {
246
+ btn.removeAttribute("aria-current");
247
+ }
248
+ });
249
+ }
250
+
251
+ function setPlayState(playing) {
252
+ if (!playBtn) return;
253
+ playBtn.dataset.state = playing ? "playing" : "paused";
254
+ playBtn.setAttribute("aria-label", playing ? "Pause" : "Play");
255
+ playBtn.setAttribute("title", playing ? "Pause" : "Play");
256
+ }
257
+
258
+ function boolAttr(name) {
259
+ return deck.dataset[name] === "true";
260
+ }
261
+
262
+ function syncChromeSettings() {
263
+ send({
264
+ type: "simplex.set-chrome",
265
+ slideNumber: !!(slideNumberSetting && slideNumberSetting.checked),
266
+ clock: !!(clockSetting && clockSetting.checked),
267
+ });
268
+ }
269
+
270
+ function syncColorSetting() {
271
+ if (!colorSetting) return;
272
+ deck.classList.toggle("is-grayscale", !colorSetting.checked);
273
+ }
274
+
275
+ function closeSettings() {
276
+ if (!settingsPanel || !settingsToggle) return;
277
+ settingsPanel.hidden = true;
278
+ settingsToggle.setAttribute("aria-expanded", "false");
279
+ }
280
+
281
+ function toggleSettings() {
282
+ if (!settingsPanel || !settingsToggle) return;
283
+ var open = settingsPanel.hidden;
284
+ settingsPanel.hidden = !open;
285
+ settingsToggle.setAttribute("aria-expanded", open ? "true" : "false");
286
+ }
287
+
288
+ window.addEventListener("message", function (e) {
289
+ var d = e.data || {};
290
+ if (typeof d !== "object") return;
291
+ if (d.type === "simplex.slide") {
292
+ if (Number.isInteger(d.total) && d.total > 0) total = d.total;
293
+ if (Number.isInteger(d.idx)) setActive(d.idx);
294
+ } else if (d.type === "simplex.play-state") {
295
+ setPlayState(!!d.playing);
296
+ }
297
+ });
298
+
299
+ function fullscreenTarget() {
300
+ // Prefer the viewer-frame container so the controls stay visible
301
+ // around the slide. Fall back to the iframe itself if the container
302
+ // is missing (older markup) or rejected by the browser.
303
+ return frame || iframe;
304
+ }
305
+ function nativeFullscreen(el) {
306
+ if (!el) return false;
307
+ var fn =
308
+ el.requestFullscreen ||
309
+ el.webkitRequestFullscreen ||
310
+ el.mozRequestFullScreen;
311
+ if (!fn) return false;
312
+ try {
313
+ var p = fn.call(el);
314
+ if (p && typeof p.catch === "function") { p.catch(function () {}); }
315
+ return true;
316
+ } catch (_) { return false; }
317
+ }
318
+ function exitFullscreen() {
319
+ var fn =
320
+ document.exitFullscreen ||
321
+ document.webkitExitFullscreen ||
322
+ document.mozCancelFullScreen;
323
+ if (fn) { try { fn.call(document); } catch (_) {} }
324
+ }
325
+ function isFullscreen() {
326
+ return !!(
327
+ document.fullscreenElement ||
328
+ document.webkitFullscreenElement ||
329
+ document.mozFullScreenElement
330
+ );
331
+ }
332
+ function toggleFullscreen() {
333
+ if (isFullscreen()) { exitFullscreen(); return; }
334
+ if (nativeFullscreen(fullscreenTarget())) return;
335
+ // Parent-side request was blocked or not available -- ask the iframe
336
+ // to enter fullscreen on its own document (works even when the parent
337
+ // path is gated by permission policy).
338
+ send({ type: "simplex.fullscreen" });
339
+ }
340
+
341
+ slideButtons.forEach(function (btn) {
342
+ btn.addEventListener("click", function () {
343
+ var t = parseInt(btn.dataset.slideTarget, 10);
344
+ if (!Number.isInteger(t)) return;
345
+ send({ type: "simplex.goto", idx: t });
346
+ });
347
+ });
348
+
349
+ controls.forEach(function (btn) {
350
+ btn.addEventListener("click", function (e) {
351
+ e.preventDefault();
352
+ var ctl = btn.dataset.control;
353
+ if (ctl === "next") send({ type: "simplex.next" });
354
+ else if (ctl === "prev") send({ type: "simplex.prev" });
355
+ else if (ctl === "restart") send({ type: "simplex.restart" });
356
+ else if (ctl === "toggle-play") send({ type: "simplex.toggle-play" });
357
+ else if (ctl === "fullscreen") toggleFullscreen();
358
+ });
359
+ });
360
+
361
+ slideRefs.forEach(function (a) {
362
+ a.addEventListener("click", function (e) {
363
+ e.preventDefault();
364
+ if (a.classList.contains("slide-ref-stale")) return;
365
+ var idx = parseInt(a.dataset.slide, 10);
366
+ if (!Number.isInteger(idx)) return;
367
+ send({ type: "simplex.goto", idx: idx });
368
+ if (iframe && iframe.scrollIntoView) {
369
+ iframe.scrollIntoView({ behavior: "smooth", block: "start" });
370
+ }
371
+ });
372
+ });
373
+
374
+ // Forward parent keyboard arrows to the iframe (when nothing else is focused).
375
+ document.addEventListener("keydown", function (e) {
376
+ var t = e.target;
377
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
378
+ if (e.key === "ArrowRight") { e.preventDefault(); send({ type: "simplex.next" }); }
379
+ else if (e.key === "ArrowLeft") { e.preventDefault(); send({ type: "simplex.prev" }); }
380
+ });
381
+
382
+ if (colorSetting) {
383
+ colorSetting.checked = true;
384
+ colorSetting.addEventListener("change", syncColorSetting);
385
+ syncColorSetting();
386
+ }
387
+ if (slideNumberSetting) {
388
+ slideNumberSetting.checked = boolAttr("defaultSlideNumber");
389
+ slideNumberSetting.addEventListener("change", syncChromeSettings);
390
+ }
391
+ if (clockSetting) {
392
+ clockSetting.checked = boolAttr("defaultClock");
393
+ clockSetting.addEventListener("change", syncChromeSettings);
394
+ }
395
+ if (iframe) iframe.addEventListener("load", sendChromeSettings);
396
+ window.addEventListener("simplex.theme", syncThemeSetting);
397
+ if (settingsToggle) {
398
+ settingsToggle.addEventListener("click", function (e) {
399
+ e.preventDefault();
400
+ e.stopPropagation();
401
+ toggleSettings();
402
+ });
403
+ }
404
+ document.addEventListener("click", function (e) {
405
+ if (!settings || settings.contains(e.target)) return;
406
+ closeSettings();
407
+ });
408
+ document.addEventListener("keydown", function (e) {
409
+ if (e.key === "Escape") closeSettings();
410
+ });
411
+ }
412
+
413
+ if (document.readyState === "loading") {
414
+ document.addEventListener("DOMContentLoaded", function () {
415
+ initTheme();
416
+ initIcons();
417
+ initPreviewGifs();
418
+ initCarousels();
419
+ initDeck();
420
+ });
421
+ } else {
422
+ initTheme();
423
+ initIcons();
424
+ initPreviewGifs();
425
+ initCarousels();
426
+ initDeck();
427
+ }
428
+ })();
@@ -0,0 +1,19 @@
1
+ # web/templates/
2
+
3
+ Jinja2 templates.
4
+
5
+ ## Files
6
+
7
+ - `base.html` -- chrome (skip-link, header/nav, GA snippet, footer).
8
+ - `index.html` -- home page: one carousel per section.
9
+ - `_carousel.html` -- partial used by the home page.
10
+ - `section.html` -- "view all" grid for one section.
11
+ - `deck.html` -- viewer + sidebar + controls + notes.
12
+ - `revealjs.html.j2` -- the iframe payload; loaded into `slides.html`
13
+ with the postMessage bridge.
14
+
15
+ ## Don't
16
+
17
+ - Don't hand-edit generated HTML under `site/`. Re-render via `simplex build`.
18
+ - Don't put logic in templates -- compute it in `builder.py` and pass it in.
19
+ - Don't reference external CDNs. All assets are vendored under `static/`.
@@ -0,0 +1,117 @@
1
+ {# Carousel for one section. Receives `section`, `site`, `thumbs`,
2
+ `preview_gifs`, `deck_dates`, and optional `carousel_variant`. #}
3
+ <section
4
+ id="{{ section.config.slug }}"
5
+ class="carousel-section{% if carousel_variant == 'latest' %} carousel-section-latest{% endif %}"
6
+ data-section="{{ section.config.slug }}"
7
+ aria-labelledby="hdr-{{ section.config.slug }}"
8
+ >
9
+ <header class="carousel-heading">
10
+ <div>
11
+ <div class="carousel-heading-line">
12
+ <h2 id="hdr-{{ section.config.slug }}">{{ section.config.title }}</h2>
13
+ </div>
14
+ {% if section.config.blurb %}
15
+ <p>{{ section.config.blurb }}</p>
16
+ {% endif %}
17
+ </div>
18
+ {% if carousel_variant != "latest" %}
19
+ <a
20
+ href="{{ site.url('sections/' + section.config.slug + '/') }}"
21
+ class="carousel-section-link"
22
+ >
23
+ <span>View all</span>
24
+ <i data-lucide="arrow-up-right" aria-hidden="true"></i>
25
+ </a>
26
+ {% endif %}
27
+ </header>
28
+
29
+ <div class="carousel-shell">
30
+ <button
31
+ class="carousel-arrow carousel-arrow-prev"
32
+ data-dir="prev"
33
+ aria-label="Scroll {{ section.config.title }} left"
34
+ title="Previous"
35
+ type="button"
36
+ >
37
+ <i data-lucide="chevron-left" aria-hidden="true"></i>
38
+ </button>
39
+
40
+ <ol
41
+ class="carousel-track"
42
+ role="list"
43
+ tabindex="0"
44
+ aria-label="{% if carousel_variant == 'latest' %}Latest decks{% else %}{{ section.config.title }} decks{% endif %}"
45
+ >
46
+ {% for deck in section.decks %}
47
+ {% set thumb = thumbs.get(deck.slug) %}
48
+ {% set gif = preview_gifs.get(deck.slug) %}
49
+ {% set created = deck_dates.get(deck.slug) %}
50
+ <li class="carousel-card-wrap">
51
+ <a
52
+ href="{{ site.url('decks/' + deck.slug + '/') }}"
53
+ class="carousel-card"
54
+ aria-label="Open {{ deck.title }}"
55
+ >
56
+ <div class="carousel-thumb">
57
+ {% if thumb %}
58
+ <img
59
+ src="{{ site.url('decks/' + deck.slug + '/' + thumb) }}"
60
+ {% if gif %}data-preview-gif="{{ site.url('decks/' + deck.slug + '/' + gif) }}"{% endif %}
61
+ alt=""
62
+ loading="lazy"
63
+ decoding="async"
64
+ width="480"
65
+ height="270"
66
+ />
67
+ {% else %}
68
+ <div class="carousel-thumb-empty" aria-hidden="true">
69
+ <span>{{ deck.title[:1] }}</span>
70
+ </div>
71
+ {% endif %}
72
+ <div class="carousel-scrim" aria-hidden="true"></div>
73
+ <div class="carousel-card-content">
74
+ <div class="carousel-card-top">
75
+ {% if deck.category %}
76
+ <span class="carousel-badge">{{ deck.category }}</span>
77
+ {% endif %}
78
+ {% if created %}
79
+ <span class="carousel-created">
80
+ <i data-lucide="calendar" aria-hidden="true"></i>
81
+ {{ created }}
82
+ </span>
83
+ {% endif %}
84
+ </div>
85
+ <div class="carousel-card-bottom">
86
+ <h3>{{ deck.title }}</h3>
87
+ {% if deck.summary %}
88
+ <p>{{ deck.summary }}</p>
89
+ {% endif %}
90
+ <div class="carousel-card-actions">
91
+ <span class="carousel-play">
92
+ <i data-lucide="play" aria-hidden="true"></i>
93
+ Play now
94
+ </span>
95
+ {% if deck.duration_minutes %}
96
+ <span class="carousel-duration">{{ deck.duration_minutes }} min</span>
97
+ {% endif %}
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </a>
103
+ </li>
104
+ {% endfor %}
105
+ </ol>
106
+
107
+ <button
108
+ class="carousel-arrow carousel-arrow-next"
109
+ data-dir="next"
110
+ aria-label="Scroll {{ section.config.title }} right"
111
+ title="Next"
112
+ type="button"
113
+ >
114
+ <i data-lucide="chevron-right" aria-hidden="true"></i>
115
+ </button>
116
+ </div>
117
+ </section>
@@ -0,0 +1,110 @@
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>{% block title %}{{ site.brand }}{% endblock %}</title>
7
+ <meta name="description" content="{{ site.tagline or (site.brand + ' decks') }}" />
8
+ {% block head_meta %}{% endblock %}
9
+ <script>
10
+ (function () {
11
+ try {
12
+ var stored = localStorage.getItem("simplex-theme");
13
+ var prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
14
+ var theme = stored || (prefersDark ? "dark" : "light");
15
+ document.documentElement.dataset.theme = theme;
16
+ document.documentElement.style.colorScheme = theme;
17
+ } catch (_) {
18
+ document.documentElement.dataset.theme = "dark";
19
+ document.documentElement.style.colorScheme = "dark";
20
+ }
21
+ })();
22
+ </script>
23
+ <script src="{{ static('tailwind.js') }}"></script>
24
+ <link rel="stylesheet" href="{{ static('katex/katex.min.css') }}" />
25
+ {% if palette_css %}
26
+ <style>{{ palette_css|safe }}</style>
27
+ {% endif %}
28
+ <link rel="stylesheet" href="{{ static('simplex.css') }}" />
29
+ {% if site.ga_enabled %}
30
+ <script async src="https://www.googletagmanager.com/gtag/js?id={{ site.ga_tag }}"></script>
31
+ <script>
32
+ window.dataLayer = window.dataLayer || [];
33
+ function gtag(){dataLayer.push(arguments);}
34
+ gtag('js', new Date());
35
+ gtag('config', '{{ site.ga_tag }}', { anonymize_ip: true });
36
+ </script>
37
+ {% endif %}
38
+ </head>
39
+ <body class="min-h-screen antialiased">
40
+ <a href="#main" class="skip-link">Skip to content</a>
41
+ <header class="site-nav-wrap">
42
+ <nav class="site-navbar" aria-label="Primary">
43
+ <a href="{{ site.url('/') }}" class="site-brand" aria-label="{{ site.brand }} home">
44
+ <span>{{ site.brand }}</span>
45
+ </a>
46
+ <div class="site-nav-links">
47
+ {% for link in site.nav %}
48
+ {% set label = link.label|lower %}
49
+ {% set is_github = "github" in label %}
50
+ {% set icon = "book-open" if "docs" in label else ("home" if "home" in label or "deck" in label else "circle-dot") %}
51
+ <a
52
+ href="{{ link.href }}"
53
+ class="site-nav-link{% if is_github %} site-nav-link-icon-only{% endif %}"
54
+ {% if is_github %}aria-label="GitHub"{% endif %}
55
+ {% if link.href.startswith('http') %}target="_blank" rel="noreferrer"{% endif %}
56
+ >
57
+ {% if is_github %}
58
+ <span class="github-square-icon" aria-hidden="true"></span>
59
+ {% else %}
60
+ <i data-lucide="{{ icon }}" aria-hidden="true"></i>
61
+ <span class="icon-fallback" aria-hidden="true">{{ link.label[:1] }}</span>
62
+ <span>{{ link.label }}</span>
63
+ {% endif %}
64
+ </a>
65
+ {% endfor %}
66
+ </div>
67
+ <div class="site-nav-actions">
68
+ {% block nav_actions %}{% endblock %}
69
+ <button
70
+ type="button"
71
+ class="theme-toggle"
72
+ data-theme-toggle
73
+ aria-label="Toggle color theme"
74
+ title="Toggle color theme"
75
+ >
76
+ <i data-lucide="sun" class="theme-icon theme-icon-sun" aria-hidden="true"></i>
77
+ <i data-lucide="moon" class="theme-icon theme-icon-moon" aria-hidden="true"></i>
78
+ <span class="theme-toggle-fallback icon-fallback" aria-hidden="true">◐</span>
79
+ </button>
80
+ </div>
81
+ </nav>
82
+ </header>
83
+ <main id="main" class="{% block main_class %}px-6 py-8 max-w-7xl mx-auto{% endblock %}">
84
+ {% block content %}{% endblock %}
85
+ </main>
86
+ <footer class="site-footer">
87
+ {% set footer = namespace(simplex_href='https://github.com/shlomi-perles/simplex') %}
88
+ {% for link in site.nav %}
89
+ {% if "github" in link.label|lower %}
90
+ {% set footer.simplex_href = link.href %}
91
+ {% endif %}
92
+ {% endfor %}
93
+ <span>Built with <a href="{{ footer.simplex_href }}" target="_blank" rel="noreferrer">Simplex</a></span>
94
+ </footer>
95
+ <script src="{{ static('lucide/lucide.min.js') }}" defer></script>
96
+ <script src="{{ static('viewer.js') }}" defer></script>
97
+ <script src="{{ static('notes.js') }}" defer></script>
98
+ <script defer src="{{ static('katex/katex.min.js') }}"></script>
99
+ <script defer src="{{ static('katex/auto-render.min.js') }}"
100
+ onload="renderMathInElement(document.body, {
101
+ delimiters: [
102
+ { left: '\\[', right: '\\]', display: true },
103
+ { left: '\\(', right: '\\)', display: false }
104
+ ],
105
+ throwOnError: false,
106
+ ignoredTags: ['script','noscript','style','textarea','pre','code']
107
+ });
108
+ window.simplexFitMath && window.simplexFitMath();"></script>
109
+ </body>
110
+ </html>