simplex-web 0.2.1__tar.gz → 0.2.2__tar.gz

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 (125) hide show
  1. {simplex_web-0.2.1 → simplex_web-0.2.2}/.gitignore +2 -2
  2. {simplex_web-0.2.1 → simplex_web-0.2.2}/PKG-INFO +5 -2
  3. {simplex_web-0.2.1 → simplex_web-0.2.2}/README.md +3 -0
  4. {simplex_web-0.2.1 → simplex_web-0.2.2}/pyproject.toml +4 -4
  5. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/builder.py +30 -4
  6. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/sidenotes.py +3 -1
  7. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/site_config.py +12 -0
  8. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/README.md +8 -3
  9. simplex_web-0.2.2/src/simplex/web/static/notes.js +183 -0
  10. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/simplex.css +145 -15
  11. simplex_web-0.2.2/src/simplex/web/static/tailwind.input.css +16 -0
  12. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/viewer.js +1 -1
  13. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/templates/base.html +27 -12
  14. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/templates/deck.html +1 -1
  15. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/templates/revealjs.html.j2 +11 -6
  16. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/vendor.py +106 -7
  17. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/web/test_builder.py +35 -1
  18. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/web/test_sidenotes.py +2 -0
  19. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/web/test_site_config.py +8 -0
  20. simplex_web-0.2.1/src/simplex/web/static/notes.js +0 -68
  21. simplex_web-0.2.1/src/simplex/web/static/tailwind.js +0 -64
  22. {simplex_web-0.2.1 → simplex_web-0.2.2}/LICENSE +0 -0
  23. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/README.md +0 -0
  24. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/cli/README.md +0 -0
  25. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/cli/__init__.py +0 -0
  26. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/cli/commands.py +0 -0
  27. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/deck/README.md +0 -0
  28. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/deck/__init__.py +0 -0
  29. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/deck/_template/assets/.gitkeep +0 -0
  30. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/deck/_template/assets/code/.gitkeep +0 -0
  31. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/deck/_template/assets/figures/.gitkeep +0 -0
  32. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/deck/_template/deck.toml +0 -0
  33. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/deck/_template/manim.cfg +0 -0
  34. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/deck/_template/notes.md +0 -0
  35. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/deck/_template/refs.bib +0 -0
  36. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/deck/_template/slides/__init__.py +0 -0
  37. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/deck/_template/slides/intro.py +0 -0
  38. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/deck/config.py +0 -0
  39. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/deck/registry.py +0 -0
  40. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/deck/scaffold.py +0 -0
  41. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/deck/section.py +0 -0
  42. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/engine/README.md +0 -0
  43. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/render/README.md +0 -0
  44. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/render/__init__.py +0 -0
  45. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/render/html.py +0 -0
  46. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/render/pdf.py +0 -0
  47. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/render/pptx.py +0 -0
  48. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/render/reconcile.py +0 -0
  49. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/render/runner.py +0 -0
  50. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/render/thumbnail.py +0 -0
  51. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/slides/README.md +0 -0
  52. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/slides/components/README.md +0 -0
  53. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/theme/README.md +0 -0
  54. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/README.md +0 -0
  55. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/__init__.py +0 -0
  56. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/bibliography.py +0 -0
  57. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/bibtex.py +0 -0
  58. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/callouts.py +0 -0
  59. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/citations.py +0 -0
  60. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/equations.py +0 -0
  61. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/notes.py +0 -0
  62. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/refs.py +0 -0
  63. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/slide_ref.py +0 -0
  64. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/.gitkeep +0 -0
  65. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/fonts/lato/lato-latin-400-italic.woff2 +0 -0
  66. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/fonts/lato/lato-latin-400-normal.woff2 +0 -0
  67. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/fonts/lato/lato-latin-700-italic.woff2 +0 -0
  68. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/fonts/lato/lato-latin-700-normal.woff2 +0 -0
  69. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/fonts/lato/lato-latin-900-normal.woff2 +0 -0
  70. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/fonts/merriweather/merriweather-latin-400-italic.woff2 +0 -0
  71. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/fonts/merriweather/merriweather-latin-400-normal.woff2 +0 -0
  72. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/fonts/merriweather/merriweather-latin-700-italic.woff2 +0 -0
  73. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/fonts/merriweather/merriweather-latin-700-normal.woff2 +0 -0
  74. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/fonts/merriweather/merriweather-latin-900-normal.woff2 +0 -0
  75. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/htmx.min.js +0 -0
  76. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/katex/auto-render.min.js +0 -0
  77. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  78. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  79. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  80. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  81. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  82. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  83. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  84. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  85. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  86. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/katex/katex.min.css +0 -0
  87. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/katex/katex.min.js +0 -0
  88. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/lucide/README.md +0 -0
  89. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/lucide/lucide.min.js +0 -0
  90. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/reveal.js/reset.css +0 -0
  91. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/reveal.js/reveal.css +0 -0
  92. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/static/reveal.js/reveal.js +0 -0
  93. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/templates/README.md +0 -0
  94. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/templates/_carousel.html +0 -0
  95. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/templates/index.html +0 -0
  96. {simplex_web-0.2.1 → simplex_web-0.2.2}/src/simplex/web/templates/section.html +0 -0
  97. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/README.md +0 -0
  98. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/__init__.py +0 -0
  99. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/cli/README.md +0 -0
  100. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/cli/__init__.py +0 -0
  101. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/cli/test_help.py +0 -0
  102. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/cli/test_new.py +0 -0
  103. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/cli/test_render.py +0 -0
  104. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/deck/README.md +0 -0
  105. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/deck/__init__.py +0 -0
  106. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/deck/test_config.py +0 -0
  107. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/deck/test_registry.py +0 -0
  108. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/deck/test_scaffold.py +0 -0
  109. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/deck/test_section.py +0 -0
  110. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/engine/README.md +0 -0
  111. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/render/README.md +0 -0
  112. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/render/__init__.py +0 -0
  113. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/render/test_html.py +0 -0
  114. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/render/test_reconcile.py +0 -0
  115. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/render/test_runner.py +0 -0
  116. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/render/test_thumbnail.py +0 -0
  117. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/theme/README.md +0 -0
  118. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/web/README.md +0 -0
  119. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/web/__init__.py +0 -0
  120. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/web/test_bibliography.py +0 -0
  121. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/web/test_callouts.py +0 -0
  122. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/web/test_citations.py +0 -0
  123. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/web/test_equations.py +0 -0
  124. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/web/test_notes.py +0 -0
  125. {simplex_web-0.2.1 → simplex_web-0.2.2}/tests/web/test_slide_ref.py +0 -0
@@ -27,8 +27,8 @@ decks/**/slides/files/
27
27
  # Generated site
28
28
  site/
29
29
 
30
- # Vendored web runtime assets (fetched on demand by simplex.web.vendor)
31
- src/simplex/web/static/tailwind.js
30
+ # Vendored web runtime assets (fetched/compiled on demand by simplex.web.vendor)
31
+ src/simplex/web/static/tailwind.css
32
32
  src/simplex/web/static/htmx.min.js
33
33
  src/simplex/web/static/katex/
34
34
  src/simplex/web/static/reveal.js/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simplex-web
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Manim-slides presentation framework with a generated web portal.
5
5
  Project-URL: Homepage, https://github.com/shlomi-perles/simplex
6
6
  Project-URL: Issues, https://github.com/shlomi-perles/simplex/issues
@@ -27,7 +27,7 @@ Classifier: Typing :: Typed
27
27
  Requires-Python: >=3.13
28
28
  Requires-Dist: av>=15.0
29
29
  Requires-Dist: jinja2>=3.1
30
- Requires-Dist: manim-simplex>=0.2.0
30
+ Requires-Dist: manim-simplex>=0.2.1
31
31
  Requires-Dist: manim-slides>=5.1.7
32
32
  Requires-Dist: markdown-it-py>=3.0
33
33
  Requires-Dist: mdit-py-plugins>=0.4
@@ -233,6 +233,9 @@ nav = [
233
233
  ]
234
234
  ```
235
235
 
236
+ Links labeled `GitHub` are shown as the footer GitHub icon, next to the
237
+ `Built with Simplex` mark.
238
+
236
239
  Deployment-only settings are read from environment variables:
237
240
 
238
241
  - `SIMPLEX_BASE_URL`
@@ -188,6 +188,9 @@ nav = [
188
188
  ]
189
189
  ```
190
190
 
191
+ Links labeled `GitHub` are shown as the footer GitHub icon, next to the
192
+ `Built with Simplex` mark.
193
+
191
194
  Deployment-only settings are read from environment variables:
192
195
 
193
196
  - `SIMPLEX_BASE_URL`
@@ -32,7 +32,7 @@ classifiers = [
32
32
  "Typing :: Typed",
33
33
  ]
34
34
  dependencies = [
35
- "manim-simplex>=0.2.0",
35
+ "manim-simplex>=0.2.1",
36
36
  "manim-slides>=5.1.7",
37
37
  "pydantic>=2.7",
38
38
  "pydantic-settings>=2.3",
@@ -72,7 +72,7 @@ license-files = ["LICENSE"]
72
72
  name = "simplex-web"
73
73
  readme = "README.md"
74
74
  requires-python = ">=3.13"
75
- version = "0.2.1"
75
+ version = "0.2.2"
76
76
 
77
77
  [project.scripts]
78
78
  simplex = "simplex.cli.commands:app"
@@ -109,7 +109,7 @@ artifacts = [
109
109
  "src/simplex/web/static/htmx.min.js",
110
110
  "src/simplex/web/static/katex/**/*",
111
111
  "src/simplex/web/static/reveal.js/**/*",
112
- "src/simplex/web/static/tailwind.js",
112
+ "src/simplex/web/static/tailwind.input.css",
113
113
  ]
114
114
  only-include = [
115
115
  "LICENSE",
@@ -130,7 +130,7 @@ artifacts = [
130
130
  "src/simplex/web/static/htmx.min.js",
131
131
  "src/simplex/web/static/katex/**/*",
132
132
  "src/simplex/web/static/reveal.js/**/*",
133
- "src/simplex/web/static/tailwind.js",
133
+ "src/simplex/web/static/tailwind.input.css",
134
134
  ]
135
135
  packages = ["src/simplex"]
136
136
 
@@ -18,6 +18,7 @@ No render cache. Manim's per-animation cache + ``save_sections=True``
18
18
  """
19
19
 
20
20
  import contextlib
21
+ import hashlib
21
22
  import shutil
22
23
  import subprocess
23
24
  from datetime import UTC, datetime, time
@@ -36,6 +37,28 @@ from simplex.web.bibliography import Bibliography
36
37
  from simplex.web.site_config import SiteConfig
37
38
 
38
39
 
40
+ def _static_source_dir() -> Path:
41
+ return Path(__file__).parent / "static"
42
+
43
+
44
+ def _file_version(path: Path) -> str:
45
+ """Short content hash for cache-busting generated asset URLs."""
46
+ if not path.exists() or not path.is_file():
47
+ return ""
48
+ digest = hashlib.blake2s(digest_size=6)
49
+ with path.open("rb") as f:
50
+ for chunk in iter(lambda: f.read(1024 * 1024), b""):
51
+ digest.update(chunk)
52
+ return digest.hexdigest()
53
+
54
+
55
+ def _with_version(url: str, version: str) -> str:
56
+ if not version:
57
+ return url
58
+ separator = "&" if "?" in url else "?"
59
+ return f"{url}{separator}v={version}"
60
+
61
+
39
62
  def _jinja(site_cfg: SiteConfig) -> Environment:
40
63
  env = Environment(
41
64
  loader=PackageLoader("simplex.web", "templates"),
@@ -45,7 +68,9 @@ def _jinja(site_cfg: SiteConfig) -> Environment:
45
68
  )
46
69
 
47
70
  def static(path: str) -> str:
48
- return site_cfg.url("static/" + path.lstrip("/"))
71
+ clean = path.lstrip("/")
72
+ url = site_cfg.url("static/" + clean)
73
+ return _with_version(url, _file_version(_static_source_dir() / clean))
49
74
 
50
75
  globals_: dict[str, Any] = cast(dict[str, Any], env.globals)
51
76
  globals_["static"] = static
@@ -55,13 +80,13 @@ def _jinja(site_cfg: SiteConfig) -> Environment:
55
80
 
56
81
  def _copy_static(site_dir: Path) -> None:
57
82
  """Copy bundled static assets into ``site/static/``."""
58
- src = Path(__file__).parent / "static"
83
+ src = _static_source_dir()
59
84
  src.mkdir(parents=True, exist_ok=True)
60
85
  vendor.ensure(src)
61
86
  dst = site_dir / "static"
62
87
  dst.mkdir(parents=True, exist_ok=True)
63
88
  for entry in src.iterdir():
64
- if entry.name in {"README.md", ".gitkeep"}:
89
+ if entry.name in {"README.md", ".gitkeep", "tailwind.input.css"}:
65
90
  continue
66
91
  target = dst / entry.name
67
92
  if entry.is_dir():
@@ -178,7 +203,7 @@ def _build_deck(
178
203
  for main in manifest.main_slides
179
204
  )
180
205
 
181
- html.render_html(
206
+ slides_html = html.render_html(
182
207
  deck,
183
208
  manifest.model_copy(update={"main_slides": enriched}),
184
209
  output_dir=deck_out,
@@ -206,6 +231,7 @@ def _build_deck(
206
231
  has_notes_pdf=_has_notes_pdf(deck_out),
207
232
  notes_html=notes_html,
208
233
  palette_css=render_web_css(deck.resolved_web_palette()),
234
+ slides_version=_file_version(slides_html),
209
235
  )
210
236
  (deck_out / "index.html").write_text(page, encoding="utf-8")
211
237
  return (
@@ -69,7 +69,9 @@ def _extract_bodies(html: str) -> dict[str, str]:
69
69
 
70
70
  def _render_sidenote(n: str, num: str, body: str) -> str:
71
71
  return (
72
- f'<label for="sn-toggle-{n}" class="sidenote-ref" id="snref-{n}">{num}</label>'
72
+ f'<label for="sn-toggle-{n}" class="sidenote-ref" id="snref-{n}" '
73
+ f'role="button" tabindex="0" aria-controls="sn-{n}" aria-expanded="false">'
74
+ f"{num}</label>"
73
75
  f'<input type="checkbox" id="sn-toggle-{n}" class="sidenote-toggle" '
74
76
  'aria-hidden="true" />'
75
77
  f'<aside class="sidenote" id="sn-{n}" role="note">{body}</aside>'
@@ -9,6 +9,7 @@ import os
9
9
  import tomllib
10
10
  from pathlib import Path
11
11
  from typing import Any, Self
12
+ from urllib.parse import urlparse
12
13
 
13
14
  from pydantic import BaseModel, ConfigDict
14
15
 
@@ -44,6 +45,17 @@ class SiteConfig(BaseModel):
44
45
  return f"/{clean}"
45
46
  return f"{base}/{clean}"
46
47
 
48
+ def is_external_url(self, href: str) -> bool:
49
+ """Return true for links that should not be prefixed by `base_url`."""
50
+ parsed = urlparse(href)
51
+ return bool(parsed.scheme or parsed.netloc)
52
+
53
+ def nav_url(self, href: str) -> str:
54
+ """Resolve a committed nav href without breaking external links."""
55
+ if self.is_external_url(href) or href.startswith("#"):
56
+ return href
57
+ return self.url(href)
58
+
47
59
  @classmethod
48
60
  def load(cls, repo_root: Path | None = None) -> Self:
49
61
  repo_root = repo_root or Path.cwd()
@@ -6,11 +6,15 @@ Vendored runtime assets, copied verbatim to `site/static/` at build time.
6
6
 
7
7
  - `simplex.css` -- site-specific styles (carousel, deck page, academic
8
8
  notes typography, Tufte sidenotes, citations, bibliography).
9
- - `viewer.js` -- parent-page bridge for the deck iframe + carousel arrows.
9
+ - `tailwind.input.css` -- Tailwind v4 source (CSS-first config + `@source`
10
+ globs). Compiled to `tailwind.css` by `simplex.web.vendor`.
11
+ - `viewer.js`, `notes.js` -- parent-page bridge for the deck iframe + carousel
12
+ arrows, and the standalone notes view.
10
13
 
11
14
  ## Vendored for builds (not committed)
12
15
 
13
- - `tailwind.js` (Tailwind Play CDN -- JIT runtime, required for arbitrary-value classes)
16
+ - `tailwind.css` (compiled from `tailwind.input.css` by the Tailwind v4
17
+ standalone CLI; binary cached per-user, not shipped in the wheel)
14
18
  - `katex/` (CSS + fonts + JS + auto-render)
15
19
  - `reveal.js/` (`reveal.js`, `reveal.css`, `reset.css`)
16
20
  - `htmx.min.js` (optional, kept for future progressive enhancement)
@@ -20,4 +24,5 @@ Vendored runtime assets, copied verbatim to `site/static/` at build time.
20
24
  ## Don't
21
25
 
22
26
  - Don't load these via CDN -- vendoring keeps Pages offline-safe.
23
- - Don't edit the vendored files; upgrade them with `scripts/vendor.sh`.
27
+ - Don't edit the vendored files; bump pinned versions in
28
+ `simplex/web/vendor.py` and let the next build refetch.
@@ -0,0 +1,183 @@
1
+ /* Auto-fit display math + viewer plumbing for the notes pane.
2
+ *
3
+ * KaTeX renders display equations at their natural width; long lines
4
+ * overflow the .deck-notes column and force a horizontal scroll bar.
5
+ * We measure each `.katex-display` against its container and apply a
6
+ * `transform: scale(...)` to shrink it just enough to fit -- so the
7
+ * reader never has to scroll for math that's only a little too wide.
8
+ *
9
+ * Equations narrower than the column are untouched. Equations that
10
+ * would have to shrink past `MIN_SCALE` keep their scroll bar (we
11
+ * don't want to render math at unreadable sizes).
12
+ */
13
+
14
+ (function () {
15
+ "use strict";
16
+
17
+ var MIN_SCALE = 0.55;
18
+
19
+ function fitOne(host) {
20
+ var inner = host.querySelector(".katex");
21
+ if (!inner) return;
22
+
23
+ // Reset state from prior runs so resize re-measures naturally.
24
+ inner.style.removeProperty("transform");
25
+ inner.style.removeProperty("transform-origin");
26
+ inner.style.removeProperty("display");
27
+ host.style.removeProperty("min-height");
28
+ host.classList.remove("katex-fitted");
29
+
30
+ var available = host.clientWidth;
31
+ var natural = inner.scrollWidth;
32
+ if (!available || !natural || natural <= available) return;
33
+
34
+ var scale = available / natural;
35
+ if (scale < MIN_SCALE) return; // leave scrollbar -- too wide to scale.
36
+
37
+ inner.style.transformOrigin = "left top";
38
+ inner.style.transform = "scale(" + scale + ")";
39
+ inner.style.display = "block";
40
+ // `transform` doesn't affect layout, so we reclaim the height the
41
+ // scaled equation would have used; otherwise an empty band appears
42
+ // below the math.
43
+ var scaledHeight = inner.offsetHeight * scale;
44
+ host.style.minHeight = scaledHeight + "px";
45
+ host.classList.add("katex-fitted");
46
+ }
47
+
48
+ function fitAll() {
49
+ var hosts = document.querySelectorAll(".deck-notes .katex-display");
50
+ for (var i = 0; i < hosts.length; i++) fitOne(hosts[i]);
51
+ }
52
+
53
+ function initSidenotePopovers() {
54
+ var refs = document.querySelectorAll(".sidenote-ref[for]");
55
+ if (!refs.length) return;
56
+
57
+ var narrow = window.matchMedia ? window.matchMedia("(max-width: 1279px)") : null;
58
+ var popover = null;
59
+ var activeRef = null;
60
+
61
+ function isNarrow() {
62
+ return !narrow || narrow.matches;
63
+ }
64
+
65
+ function noteFor(ref) {
66
+ var input = document.getElementById(ref.getAttribute("for") || "");
67
+ var note = input && input.nextElementSibling;
68
+ if (!note || !note.classList || !note.classList.contains("sidenote")) return null;
69
+ return note;
70
+ }
71
+
72
+ function ensurePopover() {
73
+ if (popover) return popover;
74
+ popover = document.createElement("div");
75
+ popover.className = "sidenote-popover";
76
+ popover.setAttribute("role", "dialog");
77
+ popover.setAttribute("aria-modal", "true");
78
+ popover.setAttribute("aria-hidden", "true");
79
+ popover.innerHTML =
80
+ '<button type="button" class="sidenote-popover-backdrop" data-sidenote-close aria-label="Close note"></button>' +
81
+ '<div class="sidenote-popover-sheet">' +
82
+ '<div class="sidenote-popover-header">' +
83
+ '<span data-sidenote-title>Note</span>' +
84
+ '<button type="button" class="sidenote-popover-close" data-sidenote-close aria-label="Close note">x</button>' +
85
+ "</div>" +
86
+ '<div class="sidenote-popover-content" data-sidenote-content></div>' +
87
+ "</div>";
88
+ document.body.appendChild(popover);
89
+ popover.querySelectorAll("[data-sidenote-close]").forEach(function (btn) {
90
+ btn.addEventListener("click", close);
91
+ });
92
+ return popover;
93
+ }
94
+
95
+ function close() {
96
+ if (!popover) return;
97
+ popover.classList.remove("is-open");
98
+ popover.setAttribute("aria-hidden", "true");
99
+ document.body.classList.remove("sidenote-popover-open");
100
+ if (activeRef) {
101
+ activeRef.setAttribute("aria-expanded", "false");
102
+ try { activeRef.focus({ preventScroll: true }); } catch (_) { activeRef.focus(); }
103
+ }
104
+ activeRef = null;
105
+ }
106
+
107
+ function open(ref) {
108
+ var note = noteFor(ref);
109
+ if (!note) return;
110
+ var sheet = ensurePopover();
111
+ var content = sheet.querySelector("[data-sidenote-content]");
112
+ var title = sheet.querySelector("[data-sidenote-title]");
113
+ var closeBtn = sheet.querySelector(".sidenote-popover-close");
114
+ if (content) content.innerHTML = note.innerHTML;
115
+ if (title) title.textContent = "Note " + ref.textContent.trim();
116
+ activeRef = ref;
117
+ ref.setAttribute("aria-expanded", "true");
118
+ sheet.classList.add("is-open");
119
+ sheet.setAttribute("aria-hidden", "false");
120
+ document.body.classList.add("sidenote-popover-open");
121
+ if (content && window.renderMathInElement) {
122
+ try {
123
+ window.renderMathInElement(content, {
124
+ delimiters: [
125
+ { left: "\\[", right: "\\]", display: true },
126
+ { left: "\\(", right: "\\)", display: false },
127
+ ],
128
+ throwOnError: false,
129
+ ignoredTags: ["script", "noscript", "style", "textarea", "pre", "code"],
130
+ });
131
+ } catch (_) {}
132
+ }
133
+ if (closeBtn) {
134
+ try { closeBtn.focus({ preventScroll: true }); } catch (_) { closeBtn.focus(); }
135
+ }
136
+ }
137
+
138
+ refs.forEach(function (ref) {
139
+ ref.addEventListener("click", function (e) {
140
+ if (!isNarrow()) return;
141
+ e.preventDefault();
142
+ open(ref);
143
+ });
144
+ ref.addEventListener("keydown", function (e) {
145
+ if (!isNarrow()) return;
146
+ if (e.key !== "Enter" && e.key !== " ") return;
147
+ e.preventDefault();
148
+ open(ref);
149
+ });
150
+ });
151
+
152
+ document.addEventListener("keydown", function (e) {
153
+ if (e.key === "Escape") close();
154
+ });
155
+ if (narrow && typeof narrow.addEventListener === "function") {
156
+ narrow.addEventListener("change", function () {
157
+ if (!isNarrow()) close();
158
+ });
159
+ }
160
+ }
161
+
162
+ // Expose for the KaTeX onload hook in base.html to call once math is
163
+ // typeset.
164
+ window.simplexFitMath = fitAll;
165
+
166
+ // Re-fit on viewport changes (debounced via rAF).
167
+ var pending = null;
168
+ function schedule() {
169
+ if (pending !== null) cancelAnimationFrame(pending);
170
+ pending = requestAnimationFrame(function () {
171
+ pending = null;
172
+ fitAll();
173
+ });
174
+ }
175
+ window.addEventListener("resize", schedule);
176
+ window.addEventListener("load", schedule);
177
+
178
+ if (document.readyState === "loading") {
179
+ document.addEventListener("DOMContentLoaded", initSidenotePopovers);
180
+ } else {
181
+ initSidenotePopovers();
182
+ }
183
+ })();
@@ -390,13 +390,38 @@ body {
390
390
  text-decoration: none;
391
391
  }
392
392
  .site-footer a:hover { color: var(--canvas-text); }
393
+ .site-footer a.site-footer-simplex,
394
+ .site-footer a.site-footer-simplex:hover {
395
+ color: #e50914;
396
+ font-weight: 900;
397
+ text-decoration: underline;
398
+ text-decoration-thickness: 2px;
399
+ text-underline-offset: 0.16em;
400
+ }
401
+ .site-footer-github {
402
+ width: 2.2rem;
403
+ height: 2.2rem;
404
+ justify-content: center;
405
+ border-radius: 999px;
406
+ color: var(--canvas-muted);
407
+ }
408
+ .site-footer-github:hover {
409
+ background: var(--site-surface-hover);
410
+ color: var(--canvas-text);
411
+ }
412
+ .site-footer-github .github-square-icon {
413
+ width: 1.45rem;
414
+ height: 1.45rem;
415
+ }
393
416
  @media (max-width: 760px) {
394
417
  .site-nav-wrap {
395
- width: 100vw;
418
+ width: auto;
419
+ max-width: 100%;
396
420
  box-sizing: border-box;
397
421
  padding: 0.75rem 0.5rem 0.45rem;
398
422
  }
399
423
  .site-navbar {
424
+ position: relative;
400
425
  display: flex;
401
426
  flex-wrap: nowrap;
402
427
  width: 100%;
@@ -406,24 +431,32 @@ body {
406
431
  align-items: center;
407
432
  gap: 0.25rem;
408
433
  padding: 0.35rem;
434
+ overflow: visible;
409
435
  }
410
436
  .site-brand {
411
437
  flex: 0 1 auto;
412
438
  min-width: 0;
439
+ max-width: calc(100% - 5.45rem);
413
440
  padding: 0 0.65rem;
414
441
  justify-content: flex-start;
415
442
  }
443
+ .site-brand > span {
444
+ min-width: 0;
445
+ overflow: hidden;
446
+ text-overflow: ellipsis;
447
+ }
416
448
  .site-nav-links {
417
- flex: 0 1 auto;
418
- width: auto;
419
- overflow: visible;
420
- padding: 0;
449
+ display: none;
421
450
  }
422
451
  .site-nav-actions {
452
+ position: static;
453
+ transform: none;
423
454
  flex: 0 0 auto;
424
455
  width: auto;
456
+ min-width: 0;
425
457
  justify-content: flex-end;
426
- margin-left: 0;
458
+ margin-left: auto;
459
+ z-index: auto;
427
460
  }
428
461
  .site-resource-link {
429
462
  width: 2.45rem;
@@ -725,6 +758,8 @@ body {
725
758
  border-radius: var(--radius-card);
726
759
  border: 1px solid var(--border);
727
760
  padding: 0.9rem;
761
+ scrollbar-width: none;
762
+ -ms-overflow-style: none;
728
763
  }
729
764
  @media (min-width: 1024px) {
730
765
  .deck-sidebar {
@@ -750,6 +785,12 @@ body {
750
785
  display: flex;
751
786
  flex-direction: column;
752
787
  gap: 0.6rem;
788
+ scrollbar-width: none;
789
+ -ms-overflow-style: none;
790
+ }
791
+ .deck-sidebar::-webkit-scrollbar,
792
+ .deck-slide-list::-webkit-scrollbar {
793
+ display: none;
753
794
  }
754
795
  @media (max-width: 1023px) {
755
796
  .deck-slide-list {
@@ -760,11 +801,6 @@ body {
760
801
  padding-bottom: 0.4rem;
761
802
  scrollbar-color: var(--fg-dim) transparent;
762
803
  }
763
- .deck-slide-list::-webkit-scrollbar { height: 6px; }
764
- .deck-slide-list::-webkit-scrollbar-thumb {
765
- background: var(--fg-dim);
766
- border-radius: 4px;
767
- }
768
804
  .deck-slide-list > li {
769
805
  flex: 0 0 220px;
770
806
  scroll-snap-align: start;
@@ -1329,6 +1365,10 @@ body {
1329
1365
  .sidenote-ref::before { content: "["; }
1330
1366
  .sidenote-ref::after { content: "]"; }
1331
1367
  .sidenote-ref:hover { background: rgba(138, 90, 0, 0.12); }
1368
+ .sidenote-ref[role="button"]:focus-visible {
1369
+ outline: 2px solid var(--notes-accent);
1370
+ outline-offset: 2px;
1371
+ }
1332
1372
 
1333
1373
  .sidenote {
1334
1374
  display: block;
@@ -1372,6 +1412,91 @@ body {
1372
1412
  }
1373
1413
  }
1374
1414
 
1415
+ .sidenote-popover {
1416
+ position: fixed;
1417
+ inset: 0;
1418
+ z-index: 120;
1419
+ opacity: 0;
1420
+ pointer-events: none;
1421
+ transition: opacity 170ms ease;
1422
+ }
1423
+ .sidenote-popover.is-open {
1424
+ opacity: 1;
1425
+ pointer-events: auto;
1426
+ }
1427
+ .sidenote-popover-backdrop {
1428
+ position: absolute;
1429
+ inset: 0;
1430
+ border: 0;
1431
+ background: rgba(8, 10, 12, 0.58);
1432
+ backdrop-filter: blur(8px);
1433
+ }
1434
+ .sidenote-popover-sheet {
1435
+ position: absolute;
1436
+ left: max(0.75rem, env(safe-area-inset-left));
1437
+ right: max(0.75rem, env(safe-area-inset-right));
1438
+ bottom: max(0.75rem, env(safe-area-inset-bottom));
1439
+ max-height: min(70vh, 32rem);
1440
+ display: flex;
1441
+ flex-direction: column;
1442
+ overflow: hidden;
1443
+ border: 1px solid var(--notes-rule);
1444
+ border-radius: 8px;
1445
+ background: var(--notes-bg);
1446
+ color: var(--notes-fg);
1447
+ box-shadow: 0 26px 80px -32px rgba(0, 0, 0, 0.65);
1448
+ transform: translateY(1rem);
1449
+ transition: transform 190ms ease;
1450
+ }
1451
+ .sidenote-popover.is-open .sidenote-popover-sheet {
1452
+ transform: translateY(0);
1453
+ }
1454
+ .sidenote-popover-header {
1455
+ display: flex;
1456
+ align-items: center;
1457
+ justify-content: space-between;
1458
+ gap: 0.75rem;
1459
+ padding: 0.75rem 0.9rem;
1460
+ border-bottom: 1px solid var(--notes-rule);
1461
+ font-family: var(--font-sans);
1462
+ color: var(--notes-accent);
1463
+ font-size: 0.78rem;
1464
+ font-weight: 900;
1465
+ text-transform: uppercase;
1466
+ letter-spacing: 0;
1467
+ }
1468
+ .sidenote-popover-close {
1469
+ width: 2rem;
1470
+ height: 2rem;
1471
+ display: inline-flex;
1472
+ align-items: center;
1473
+ justify-content: center;
1474
+ border: 1px solid var(--notes-rule);
1475
+ border-radius: 999px;
1476
+ background: var(--notes-sidenote-bg);
1477
+ color: var(--notes-fg);
1478
+ font-family: var(--font-sans);
1479
+ font-size: 1rem;
1480
+ font-weight: 900;
1481
+ line-height: 1;
1482
+ cursor: pointer;
1483
+ }
1484
+ .sidenote-popover-content {
1485
+ overflow: auto;
1486
+ padding: 0.95rem 1rem 1.05rem;
1487
+ font-family: var(--font-sans);
1488
+ font-size: 0.95rem;
1489
+ line-height: 1.55;
1490
+ color: var(--notes-fg-soft);
1491
+ text-align: left;
1492
+ hyphens: manual;
1493
+ }
1494
+ .sidenote-popover-content > :first-child { margin-top: 0; }
1495
+ .sidenote-popover-content > :last-child { margin-bottom: 0; }
1496
+ body.sidenote-popover-open {
1497
+ overflow: hidden;
1498
+ }
1499
+
1375
1500
  /* ------------------------------------------------------------------ */
1376
1501
  /* Facelift overrides */
1377
1502
  /* ------------------------------------------------------------------ */
@@ -1615,9 +1740,13 @@ body {
1615
1740
  background: var(--site-bg);
1616
1741
  transition: filter 180ms ease;
1617
1742
  }
1618
- .deck-grid.is-grayscale .deck-iframe,
1619
- .deck-grid.is-grayscale .deck-slide-thumb img {
1620
- filter: grayscale(1);
1743
+ .deck-grid.is-light-preview .deck-viewer-frame,
1744
+ .deck-grid.is-light-preview .deck-slide-thumb {
1745
+ background: #f4f4f4;
1746
+ }
1747
+ .deck-grid.is-light-preview .deck-iframe,
1748
+ .deck-grid.is-light-preview .deck-slide-thumb img {
1749
+ filter: invert(1) hue-rotate(180deg) saturate(0.9) contrast(0.96) brightness(1.08);
1621
1750
  }
1622
1751
  .deck-controls {
1623
1752
  justify-content: space-between;
@@ -1698,7 +1827,7 @@ body {
1698
1827
  .deck-settings-panel {
1699
1828
  position: absolute;
1700
1829
  right: 0;
1701
- top: calc(100% + 0.45rem);
1830
+ bottom: calc(100% + 0.45rem);
1702
1831
  z-index: 50;
1703
1832
  width: 15rem;
1704
1833
  padding: 0.45rem;
@@ -1706,6 +1835,7 @@ body {
1706
1835
  border-radius: 8px;
1707
1836
  background: var(--site-surface-solid);
1708
1837
  box-shadow: var(--shadow-card);
1838
+ transform-origin: bottom right;
1709
1839
  }
1710
1840
  .deck-setting-row {
1711
1841
  display: flex;
@@ -0,0 +1,16 @@
1
+ /* Tailwind CSS v4 source for the Simplex portal.
2
+ *
3
+ * Compiled to `tailwind.css` (gitignored) by `simplex.web.vendor` at build
4
+ * time using the v4 standalone CLI. Sources below are scanned for class
5
+ * names; only the utilities actually used in the templates end up in the
6
+ * output, so the shipped stylesheet stays tiny.
7
+ *
8
+ * To add new sources (e.g. user-deck HTML), append more `@source` lines.
9
+ * Theme tweaks belong in a `@theme { ... }` block here -- not in a
10
+ * `tailwind.config.js`, which v4 has replaced with CSS-first config.
11
+ */
12
+
13
+ @import "tailwindcss";
14
+
15
+ @source "../templates/**/*.html";
16
+ @source "../templates/**/*.j2";
@@ -269,7 +269,7 @@
269
269
 
270
270
  function syncColorSetting() {
271
271
  if (!colorSetting) return;
272
- deck.classList.toggle("is-grayscale", !colorSetting.checked);
272
+ deck.classList.toggle("is-light-preview", colorSetting.checked);
273
273
  }
274
274
 
275
275
  function closeSettings() {