pandorga 1.2.1

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 (376) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +77 -0
  3. data/DESIGN.md +1434 -0
  4. data/LICENSE +21 -0
  5. data/README.md +81 -0
  6. data/_data/themes/dark.yml +97 -0
  7. data/_data/themes/light.yml +120 -0
  8. data/_data/themes/shared.yml +37 -0
  9. data/_data/translations.yml +85 -0
  10. data/_includes/chord-tab.html +38 -0
  11. data/_includes/content-runtime/00-config.html +22 -0
  12. data/_includes/content-runtime/10-utils.html +176 -0
  13. data/_includes/content-runtime/20-markdown.html +264 -0
  14. data/_includes/content-runtime/25-code-blocks.html +315 -0
  15. data/_includes/content-runtime/30-includes-bib.html +190 -0
  16. data/_includes/content-runtime/30-includes-xcite.html +122 -0
  17. data/_includes/content-runtime/30-includes.html +365 -0
  18. data/_includes/content-runtime/40-listing.html +225 -0
  19. data/_includes/content-runtime/41-listing-helpers.html +71 -0
  20. data/_includes/content-runtime/42-listing-filter.html +208 -0
  21. data/_includes/content-runtime/43-blocks.html +379 -0
  22. data/_includes/content-runtime/50-detail.html +861 -0
  23. data/_includes/content-runtime/55-toc.html +176 -0
  24. data/_includes/content-runtime/60-fragments.html +567 -0
  25. data/_includes/content-runtime/90-bootstrap.html +76 -0
  26. data/_includes/cut-in.html +10 -0
  27. data/_includes/cv/cv-box.html +18 -0
  28. data/_includes/dict-card.html +24 -0
  29. data/_includes/figure.html +36 -0
  30. data/_includes/home/band-index.html +43 -0
  31. data/_includes/home/band-listing.html +49 -0
  32. data/_includes/mathjax.html +10 -0
  33. data/_includes/ongoing-product.html +1 -0
  34. data/_includes/page/content-runtime.html +20 -0
  35. data/_includes/page/date_evolution.html +19 -0
  36. data/_includes/page/figure-lightbox.html +102 -0
  37. data/_includes/page/formatted_date.html +15 -0
  38. data/_includes/page/hero-default.html +18 -0
  39. data/_includes/page/listing-runtime-attrs.html +8 -0
  40. data/_includes/page/mermaid.html +101 -0
  41. data/_includes/page/network-graph.html +1253 -0
  42. data/_includes/page/page-header.html +41 -0
  43. data/_includes/page/professional-connections.html +2 -0
  44. data/_includes/page/site-footer.html +101 -0
  45. data/_includes/page/timeline-section.html +868 -0
  46. data/_includes/plotly.html +22 -0
  47. data/_includes/theme/dev-reexport.html +100 -0
  48. data/_includes/theme/favicons.html +4 -0
  49. data/_includes/theme/font-loader.html +55 -0
  50. data/_includes/theme/shell-theme-boot.html +115 -0
  51. data/_includes/theme/site-logo.html +6 -0
  52. data/_includes/theme/tailwind-theme-colors.json.liquid +65 -0
  53. data/_includes/theme/tailwind-theme-fonts.json.liquid +6 -0
  54. data/_includes/theme/theme-context.liquid +20 -0
  55. data/_includes/theme/theme-toggle.html +94 -0
  56. data/_includes/theme/theme-vars-block.liquid +161 -0
  57. data/_includes/theme/theme-vars.html +16 -0
  58. data/_includes/transcription.html +80 -0
  59. data/_includes/tweet.html +22 -0
  60. data/_includes/video.html +11 -0
  61. data/_includes/xcite.html +3 -0
  62. data/_includes/youtube-short.html +26 -0
  63. data/_includes/youtube.html +12 -0
  64. data/_layouts/article.html +27 -0
  65. data/_layouts/articles.html +534 -0
  66. data/_layouts/bibliography.html +417 -0
  67. data/_layouts/blog.html +544 -0
  68. data/_layouts/cv.html +393 -0
  69. data/_layouts/home.html +319 -0
  70. data/_layouts/network.html +9 -0
  71. data/_layouts/pandorga/articles.html +4 -0
  72. data/_layouts/pandorga/bibliography.html +4 -0
  73. data/_layouts/pandorga/blog.html +4 -0
  74. data/_layouts/pandorga/cv.html +4 -0
  75. data/_layouts/pandorga/media.html +4 -0
  76. data/_layouts/pandorga/network.html +4 -0
  77. data/_layouts/pandorga/portfolio.html +4 -0
  78. data/_layouts/post.html +7 -0
  79. data/_layouts/product.html +5 -0
  80. data/_layouts/project.html +27 -0
  81. data/_layouts/projects.html +534 -0
  82. data/_layouts/resources.html +387 -0
  83. data/_layouts/shell.html +203 -0
  84. data/_layouts/text.html +246 -0
  85. data/_plugins/bibliography_artifact.rb +57 -0
  86. data/_plugins/content_backend.rb +37 -0
  87. data/_plugins/content_collections.rb +51 -0
  88. data/_plugins/content_detail_paths.rb +58 -0
  89. data/_plugins/content_headers.rb +73 -0
  90. data/_plugins/content_keys.rb +310 -0
  91. data/_plugins/content_local_serve.rb +168 -0
  92. data/_plugins/content_site_adapter.rb +159 -0
  93. data/_plugins/content_validators_hook.rb +20 -0
  94. data/_plugins/content_writing_index.rb +52 -0
  95. data/_plugins/content_writing_paths.rb +55 -0
  96. data/_plugins/force_utf8_encoding.rb +13 -0
  97. data/_plugins/git_chronology.rb +211 -0
  98. data/_plugins/git_metadata.rb +43 -0
  99. data/_plugins/job_sections.rb +25 -0
  100. data/_plugins/lib/content_validators.rb +27 -0
  101. data/_plugins/lib/resource_validator.rb +56 -0
  102. data/_plugins/lib/tag_validator.rb +153 -0
  103. data/_plugins/lib/text_excerpt.rb +56 -0
  104. data/_plugins/lib/writing_entry.rb +107 -0
  105. data/_plugins/local_detail_rewrites.rb +194 -0
  106. data/_plugins/timeline.rb +507 -0
  107. data/_plugins/writing_slug_limits.rb +70 -0
  108. data/assets/css/article.css +1330 -0
  109. data/assets/css/base.css +1432 -0
  110. data/assets/css/cv.css +13 -0
  111. data/assets/css/global-link.css +94 -0
  112. data/assets/css/ledger/bibliography.css +307 -0
  113. data/assets/css/ledger/chrome.css +118 -0
  114. data/assets/css/ledger/cv.css +836 -0
  115. data/assets/css/ledger/detail.css +1270 -0
  116. data/assets/css/ledger/footer.css +298 -0
  117. data/assets/css/ledger/home.css +1490 -0
  118. data/assets/css/ledger/network.css +106 -0
  119. data/assets/css/ledger/projects.css +716 -0
  120. data/assets/css/ledger/rascunhos.css +379 -0
  121. data/assets/css/ledger/sources.css +327 -0
  122. data/assets/css/ledger/writing.css +754 -0
  123. data/assets/tailwind-play.js +15 -0
  124. data/examples/full/Gemfile +7 -0
  125. data/examples/full/_config.yml +92 -0
  126. data/examples/full/_redirects +3 -0
  127. data/examples/full/content/collections/articles/2026-01-01-welcome.md +9 -0
  128. data/examples/full/content/collections/data/contacts.yml +3 -0
  129. data/examples/full/content/collections/data/education.yml +4 -0
  130. data/examples/full/content/collections/jobs/research-fellow.md +12 -0
  131. data/examples/full/content/collections/posts/2026-01-02-note.md +9 -0
  132. data/examples/full/content/collections/products/ledger-tool.md +6 -0
  133. data/examples/full/content/collections/projects/demo-kit.md +10 -0
  134. data/examples/full/content/collections/resources/style-guide.md +7 -0
  135. data/examples/full/content/pages/cv/summary.md +1 -0
  136. data/examples/full/content/pages/headers.yml +20 -0
  137. data/examples/full/index.html +4 -0
  138. data/examples/minimal/Gemfile +7 -0
  139. data/examples/minimal/_config.yml +47 -0
  140. data/examples/minimal/_redirects +3 -0
  141. data/examples/minimal/content/collections/articles/2026-01-01-hello.md +10 -0
  142. data/examples/minimal/content/collections/articles/2026-01-02-second.md +9 -0
  143. data/examples/minimal/content/pages/headers.yml +6 -0
  144. data/examples/minimal/index.html +4 -0
  145. data/exe/pandorga +7 -0
  146. data/functions/api/studio/[[path]].js +562 -0
  147. data/functions/api/studio/_lib/auth.js +131 -0
  148. data/functions/api/studio/_lib/blocks.js +156 -0
  149. data/functions/api/studio/_lib/gemini.js +62 -0
  150. data/functions/api/studio/_lib/github.js +278 -0
  151. data/functions/api/studio/_lib/guidelines.js +143 -0
  152. data/functions/api/studio/_lib/paths.js +76 -0
  153. data/functions/api/studio/_lib/prompts.js +165 -0
  154. data/functions/api/studio/_lib/rate-limit.js +40 -0
  155. data/functions/api/studio/_lib/rewrite.js +136 -0
  156. data/functions/api/studio/_lib/sanitize.js +143 -0
  157. data/lib/pandorga/cli.rb +66 -0
  158. data/lib/pandorga/commands/doctor.rb +66 -0
  159. data/lib/pandorga/commands/export.rb +30 -0
  160. data/lib/pandorga/commands/install_functions.rb +41 -0
  161. data/lib/pandorga/commands/new.rb +113 -0
  162. data/lib/pandorga/commands/publish.rb +53 -0
  163. data/lib/pandorga/commands/serve.rb +34 -0
  164. data/lib/pandorga/commands/studio_schema.rb +309 -0
  165. data/lib/pandorga/jekyll/generator.rb +74 -0
  166. data/lib/pandorga/jekyll/plugins/bibliography_artifact.rb +57 -0
  167. data/lib/pandorga/jekyll/plugins/content_backend.rb +37 -0
  168. data/lib/pandorga/jekyll/plugins/content_collections.rb +51 -0
  169. data/lib/pandorga/jekyll/plugins/content_detail_paths.rb +58 -0
  170. data/lib/pandorga/jekyll/plugins/content_headers.rb +73 -0
  171. data/lib/pandorga/jekyll/plugins/content_keys.rb +310 -0
  172. data/lib/pandorga/jekyll/plugins/content_local_serve.rb +168 -0
  173. data/lib/pandorga/jekyll/plugins/content_site_adapter.rb +159 -0
  174. data/lib/pandorga/jekyll/plugins/content_validators_hook.rb +20 -0
  175. data/lib/pandorga/jekyll/plugins/content_writing_index.rb +52 -0
  176. data/lib/pandorga/jekyll/plugins/content_writing_paths.rb +55 -0
  177. data/lib/pandorga/jekyll/plugins/force_utf8_encoding.rb +13 -0
  178. data/lib/pandorga/jekyll/plugins/git_chronology.rb +211 -0
  179. data/lib/pandorga/jekyll/plugins/git_metadata.rb +43 -0
  180. data/lib/pandorga/jekyll/plugins/job_sections.rb +25 -0
  181. data/lib/pandorga/jekyll/plugins/lib/content_validators.rb +27 -0
  182. data/lib/pandorga/jekyll/plugins/lib/resource_validator.rb +56 -0
  183. data/lib/pandorga/jekyll/plugins/lib/tag_validator.rb +153 -0
  184. data/lib/pandorga/jekyll/plugins/lib/text_excerpt.rb +56 -0
  185. data/lib/pandorga/jekyll/plugins/lib/writing_entry.rb +107 -0
  186. data/lib/pandorga/jekyll/plugins/local_detail_rewrites.rb +194 -0
  187. data/lib/pandorga/jekyll/plugins/timeline.rb +507 -0
  188. data/lib/pandorga/jekyll/plugins/writing_slug_limits.rb +70 -0
  189. data/lib/pandorga/jekyll.rb +15 -0
  190. data/lib/pandorga/object_store_env.rb +51 -0
  191. data/lib/pandorga/registry/schema.yml +115 -0
  192. data/lib/pandorga/registry.rb +279 -0
  193. data/lib/pandorga/version.rb +5 -0
  194. data/lib/pandorga.rb +19 -0
  195. data/scripts/cdn-allowlist.json +19 -0
  196. data/scripts/content/backfill-content-keys.rb +99 -0
  197. data/scripts/content/band_art.rb +362 -0
  198. data/scripts/content/export-content-json.rb +741 -0
  199. data/scripts/content/flatten-ansi-capture.mjs +153 -0
  200. data/scripts/content/hero_portraits.rb +28 -0
  201. data/scripts/content/publish-content-to-object-store.sh +78 -0
  202. data/scripts/content/publish-content-to-r2.sh +5 -0
  203. data/scripts/content/sync-content-json-to-r2.rb +167 -0
  204. data/scripts/content/sync-content-redirects.rb +12 -0
  205. data/scripts/generate-configuration-doc.rb +127 -0
  206. data/scripts/lib/content_license.rb +50 -0
  207. data/scripts/lib/content_storage_config.rb +70 -0
  208. data/scripts/lib/normalize-display-math.mjs +52 -0
  209. data/scripts/lib/r2_json_sync.rb +76 -0
  210. data/scripts/lib/withdrawn_public_objects.rb +23 -0
  211. data/scripts/lib/writing_index_contract.rb +61 -0
  212. data/scripts/pdf/md-to-pdf.mjs +1400 -0
  213. data/scripts/pdf/md-to-pdf.sh +27 -0
  214. data/scripts/test/fixtures/dual-template/_config.yml +32 -0
  215. data/scripts/test/fixtures/dual-template/index.html +4 -0
  216. data/scripts/test/render-parity/article-note.json +5 -0
  217. data/scripts/test/render-parity/blockquote.json +5 -0
  218. data/scripts/test/render-parity/blocks/ansi-basic.json +7 -0
  219. data/scripts/test/render-parity/blocks/ansi-bold-is-bright.json +7 -0
  220. data/scripts/test/render-parity/blocks/ansi-drops-cursor-moves.json +7 -0
  221. data/scripts/test/render-parity/blocks/ansi-escapes-markup.json +7 -0
  222. data/scripts/test/render-parity/blocks/ansi-extended-colour.json +7 -0
  223. data/scripts/test/render-parity/blocks/ansi-real-control-byte.json +7 -0
  224. data/scripts/test/render-parity/blocks/archive-card-date-once.json +19 -0
  225. data/scripts/test/render-parity/blocks/archive-card-empty-foot.json +7 -0
  226. data/scripts/test/render-parity/blocks/archive-card-footer-dates.json +22 -0
  227. data/scripts/test/render-parity/blocks/archive-card-meta-parts.json +26 -0
  228. data/scripts/test/render-parity/blocks/archive-card.json +32 -0
  229. data/scripts/test/render-parity/blocks/cta-link.json +13 -0
  230. data/scripts/test/render-parity/blocks/lang-pill-pt.json +8 -0
  231. data/scripts/test/render-parity/blocks/lang-pill.json +8 -0
  232. data/scripts/test/render-parity/blocks/ledger-row-meta-parts.json +29 -0
  233. data/scripts/test/render-parity/blocks/ledger-row.json +35 -0
  234. data/scripts/test/render-parity/blocks/meta-strip.json +27 -0
  235. data/scripts/test/render-parity/blocks/poster-media-image.json +8 -0
  236. data/scripts/test/render-parity/blocks/poster-media-video.json +9 -0
  237. data/scripts/test/render-parity/blocks/section-banner.json +13 -0
  238. data/scripts/test/render-parity/blocks/system-card-custom-meta.json +22 -0
  239. data/scripts/test/render-parity/blocks/system-card.json +48 -0
  240. data/scripts/test/render-parity/blocks/tag-chips-links.json +12 -0
  241. data/scripts/test/render-parity/blocks/tag-chips.json +9 -0
  242. data/scripts/test/render-parity/cut-in-blank-line.json +9 -0
  243. data/scripts/test/render-parity/cut-in-right.json +9 -0
  244. data/scripts/test/render-parity/cut-in.json +8 -0
  245. data/scripts/test/render-parity/figure.json +5 -0
  246. data/scripts/test/render-parity/load-runtime.mjs +337 -0
  247. data/scripts/test/render-parity/ongoing-product.json +5 -0
  248. data/scripts/test/render-parity/plotly.json +5 -0
  249. data/scripts/test/render-parity/poem.json +5 -0
  250. data/scripts/test/render-parity/run-absent-field-checks.mjs +232 -0
  251. data/scripts/test/render-parity/run-block-fixture.mjs +86 -0
  252. data/scripts/test/render-parity/run-detail-parity-checks.mjs +123 -0
  253. data/scripts/test/render-parity/run-field-flow-checks.mjs +227 -0
  254. data/scripts/test/render-parity/run-fixture.mjs +159 -0
  255. data/scripts/test/render-parity/selectors.mjs +43 -0
  256. data/scripts/test/render-parity/video.json +5 -0
  257. data/scripts/test/render-parity/youtube-short.json +10 -0
  258. data/scripts/test/render-parity/youtube.json +8 -0
  259. data/scripts/test/test-absent-field-absent-element.rb +43 -0
  260. data/scripts/test/test-cdn-pins.rb +44 -0
  261. data/scripts/test/test-cloudflare-watch-paths-doc.rb +31 -0
  262. data/scripts/test/test-configuration-doc.rb +36 -0
  263. data/scripts/test/test-content-detail-urls.rb +346 -0
  264. data/scripts/test/test-content-keys.rb +110 -0
  265. data/scripts/test/test-content-license.rb +45 -0
  266. data/scripts/test/test-content-render-parity.rb +38 -0
  267. data/scripts/test/test-content-validators.rb +12 -0
  268. data/scripts/test/test-css-structure.rb +72 -0
  269. data/scripts/test/test-cv-export-contract.rb +158 -0
  270. data/scripts/test/test-detail-render-parity.rb +40 -0
  271. data/scripts/test/test-dev-reexport.rb +122 -0
  272. data/scripts/test/test-doctor.rb +39 -0
  273. data/scripts/test/test-dual-template-instances.rb +87 -0
  274. data/scripts/test/test-dual-template-runtime.rb +167 -0
  275. data/scripts/test/test-examples-build.rb +60 -0
  276. data/scripts/test/test-export-content-json-incremental.rb +138 -0
  277. data/scripts/test/test-export.rb +53 -0
  278. data/scripts/test/test-exported-fields-reach-the-view.rb +60 -0
  279. data/scripts/test/test-form-control-theming.rb +143 -0
  280. data/scripts/test/test-git-chronology.rb +414 -0
  281. data/scripts/test/test-hero-portraits.mjs +97 -0
  282. data/scripts/test/test-hero-portraits.rb +162 -0
  283. data/scripts/test/test-home-band-art.rb +135 -0
  284. data/scripts/test/test-install-functions.rb +39 -0
  285. data/scripts/test/test-language-export.rb +79 -0
  286. data/scripts/test/test-listing-filter-contract.rb +119 -0
  287. data/scripts/test/test-math-row-breaks.mjs +79 -0
  288. data/scripts/test/test-math-row-breaks.rb +18 -0
  289. data/scripts/test/test-new.rb +113 -0
  290. data/scripts/test/test-no-personal-data.rb +85 -0
  291. data/scripts/test/test-public-cv-export.rb +164 -0
  292. data/scripts/test/test-r2-config-consistency.rb +57 -0
  293. data/scripts/test/test-r2-json-sync.rb +193 -0
  294. data/scripts/test/test-registry-home.rb +34 -0
  295. data/scripts/test/test-runtime-js-syntax.rb +64 -0
  296. data/scripts/test/test-slim-writing-index.rb +126 -0
  297. data/scripts/test/test-studio-api-contract.rb +99 -0
  298. data/scripts/test/test-studio-content-files.rb +79 -0
  299. data/scripts/test/test-studio-liquid-roundtrip.mjs +179 -0
  300. data/scripts/test/test-studio-rate-limit.mjs +70 -0
  301. data/scripts/test/test-studio-rewrite-api-contract.rb +59 -0
  302. data/scripts/test/test-studio-rewrite-sanitize.mjs +210 -0
  303. data/scripts/test/test-studio-schema-composition.rb +126 -0
  304. data/scripts/test/test-studio-slug-config.rb +167 -0
  305. data/scripts/test/test-template-packages.rb +51 -0
  306. data/scripts/test/test-text-excerpt.rb +51 -0
  307. data/scripts/test/test-unwrap-media-src.mjs +39 -0
  308. data/scripts/test/test-unwrap-media-src.rb +19 -0
  309. data/scripts/test/test-withdrawn-content-export.rb +69 -0
  310. data/scripts/test/test-writing-index-export-slugs.rb +84 -0
  311. data/scripts/test/test-writing-slug-limits.rb +61 -0
  312. data/scripts/test/test-xcite-chronology.rb +106 -0
  313. data/scripts/validate.sh +74 -0
  314. data/scripts/visual/build-cv-pages-json.js +33 -0
  315. data/scripts/visual/build-site-pages-json.js +94 -0
  316. data/scripts/visual/publish-visual-inspection-to-r2.sh +42 -0
  317. data/scripts/visual/take-screenshots.js +88 -0
  318. data/scripts/visual/visual-inspection-pages.json +113 -0
  319. data/studio/assets/index-B_j0pMD5.css +1 -0
  320. data/studio/assets/index-Ch4YEv9g.js +285 -0
  321. data/studio/index.html +27 -0
  322. data/studio/schema.yml +869 -0
  323. data/studio-app/README.md +27 -0
  324. data/studio-app/index.html +26 -0
  325. data/studio-app/package-lock.json +2499 -0
  326. data/studio-app/package.json +26 -0
  327. data/studio-app/skills-lock.json +47 -0
  328. data/studio-app/src/api/client.js +71 -0
  329. data/studio-app/src/editor/body-editor.js +70 -0
  330. data/studio-app/src/editor/liquid-core.js +382 -0
  331. data/studio-app/src/editor/liquid.js +77 -0
  332. data/studio-app/src/lib/document.js +207 -0
  333. data/studio-app/src/lib/list-controls.js +317 -0
  334. data/studio-app/src/lib/schema.js +66 -0
  335. data/studio-app/src/lib/yaml-doc.js +100 -0
  336. data/studio-app/src/main.js +3693 -0
  337. data/studio-app/src/media/browser.js +578 -0
  338. data/studio-app/src/preview/render.js +430 -0
  339. data/studio-app/src/styles/studio.css +2826 -0
  340. data/studio-app/vite.config.js +25 -0
  341. data/templates/articles/README.md +40 -0
  342. data/templates/articles/fixtures/article.md +9 -0
  343. data/templates/articles/screenshots/README.md +6 -0
  344. data/templates/articles/studio.yml +73 -0
  345. data/templates/articles/template.yml +21 -0
  346. data/templates/bibliography/README.md +41 -0
  347. data/templates/bibliography/fixtures/references.json +9 -0
  348. data/templates/bibliography/screenshots/README.md +6 -0
  349. data/templates/bibliography/studio.yml +66 -0
  350. data/templates/bibliography/template.yml +18 -0
  351. data/templates/blog/README.md +38 -0
  352. data/templates/blog/fixtures/post.md +9 -0
  353. data/templates/blog/screenshots/README.md +6 -0
  354. data/templates/blog/studio.yml +69 -0
  355. data/templates/blog/template.yml +20 -0
  356. data/templates/cv/README.md +34 -0
  357. data/templates/cv/fixtures/job.md +12 -0
  358. data/templates/cv/screenshots/README.md +6 -0
  359. data/templates/cv/studio.yml +200 -0
  360. data/templates/cv/template.yml +25 -0
  361. data/templates/media/README.md +38 -0
  362. data/templates/media/fixtures/resource.md +7 -0
  363. data/templates/media/screenshots/README.md +6 -0
  364. data/templates/media/studio.yml +58 -0
  365. data/templates/media/template.yml +17 -0
  366. data/templates/network/README.md +34 -0
  367. data/templates/network/fixtures/sources.json +3 -0
  368. data/templates/network/screenshots/README.md +6 -0
  369. data/templates/network/studio.yml +28 -0
  370. data/templates/network/template.yml +10 -0
  371. data/templates/portfolio/README.md +39 -0
  372. data/templates/portfolio/fixtures/project.md +10 -0
  373. data/templates/portfolio/screenshots/README.md +6 -0
  374. data/templates/portfolio/studio.yml +89 -0
  375. data/templates/portfolio/template.yml +18 -0
  376. metadata +505 -0
@@ -0,0 +1,22 @@
1
+ {% if include.src %}
2
+ <figure class="plotly-figure" style="margin: 1.5rem auto; text-align: center; max-width: {% if include.width %}{{ include.width }}{% else %}100{% endif %}%;">
3
+ {% assign _cap = include.caption | strip %}
4
+ {% if _cap != "" %}
5
+ {% assign _last = _cap | slice: -1, 1 %}
6
+ {% unless _last == '.' or _last == '!' or _last == '?' %}
7
+ {% assign _cap = _cap | append: '.' %}
8
+ {% endunless %}
9
+ {% endif %}
10
+ <iframe
11
+ class="plotly-embed-frame"
12
+ src="{{ include.src }}"
13
+ title="{{ include.alt | default: include.caption | default: 'Interactive chart' }}"
14
+ loading="lazy"
15
+ scrolling="no"
16
+ style="width: 100%; height: {% if include.height %}{{ include.height }}{% else %}480{% endif %}px; border: 0; display: block; border-radius: 0; background: transparent;"
17
+ ></iframe>
18
+ {% if include.caption or include.source %}
19
+ <figcaption>{% if _cap != "" %}{{ _cap }}{% endif %}{% if include.source %} Source: {{ include.source | strip }}{% endif %}</figcaption>
20
+ {% endif %}
21
+ </figure>
22
+ {% endif %}
@@ -0,0 +1,100 @@
1
+ <div id="dev-reexport" class="contents">
2
+ <button id="dev-reexport-btn" type="button"
3
+ class="flex items-center justify-center w-7 h-7 p-1 border border-tertiary/40 hover:border-tertiary text-tertiary hover:bg-surface-container-low transition-colors duration-300 cursor-pointer shrink-0"
4
+ aria-label="Export content JSON"
5
+ title="Export">
6
+ <span id="dev-reexport-label" class="sr-only">Export</span>
7
+ <span id="dev-reexport-icon" class="material-symbols-outlined text-xl" aria-hidden="true">sync</span>
8
+ </button>
9
+ <button id="dev-open-studio-btn" type="button"
10
+ class="flex items-center justify-center w-7 h-7 p-1 border border-tertiary/40 hover:border-tertiary text-tertiary hover:bg-surface-container-low transition-colors duration-300 cursor-pointer shrink-0"
11
+ aria-label="Open Studio"
12
+ title="Open Studio"
13
+ data-studio-href="{{ site.baseurl }}/studio/">
14
+ <span class="sr-only">Open Studio</span>
15
+ <span class="material-symbols-outlined text-xl" aria-hidden="true">architecture</span>
16
+ </button>
17
+ </div>
18
+ <script>
19
+ (function () {
20
+ var openBtn = document.getElementById("dev-open-studio-btn");
21
+ if (openBtn) {
22
+ openBtn.addEventListener("click", function (e) {
23
+ e.preventDefault();
24
+ var href = openBtn.getAttribute("data-studio-href") || "/studio/";
25
+ window.open(href, "_blank");
26
+ });
27
+ }
28
+
29
+ var btn = document.getElementById("dev-reexport-btn");
30
+ var label = document.getElementById("dev-reexport-label");
31
+ var icon = document.getElementById("dev-reexport-icon");
32
+ if (!btn || !label || !icon) return;
33
+
34
+ var busy = false;
35
+
36
+ function forcePageRefresh() {
37
+ try {
38
+ var url = new URL(window.location.href);
39
+ url.searchParams.set("_refresh", String(Date.now()));
40
+ window.location.replace(url.toString());
41
+ } catch (err) {
42
+ window.location.reload();
43
+ }
44
+ }
45
+
46
+ btn.addEventListener("click", function (e) {
47
+ e.preventDefault();
48
+ if (busy) return;
49
+ busy = true;
50
+ btn.disabled = true;
51
+ btn.classList.add("is-busy");
52
+ btn.setAttribute("aria-busy", "true");
53
+
54
+ var originalText = label.textContent;
55
+ var originalIcon = icon.textContent;
56
+ var originalTitle = btn.title || "Export";
57
+
58
+ label.textContent = "Exporting";
59
+ btn.title = "Exporting...";
60
+ icon.textContent = "sync";
61
+
62
+ fetch("/__dev/reexport", {
63
+ method: "POST",
64
+ headers: { "Accept": "application/json" }
65
+ })
66
+ .then(function (response) {
67
+ return response.json().then(function (data) {
68
+ if (!response.ok || !data.ok) {
69
+ throw new Error((data && data.error) || ("Export failed: " + response.status));
70
+ }
71
+ return data;
72
+ });
73
+ })
74
+ .then(function () {
75
+ btn.classList.remove("is-busy");
76
+ icon.textContent = "check";
77
+ label.textContent = "Refreshed";
78
+ btn.title = "Refreshed!";
79
+ setTimeout(function () {
80
+ forcePageRefresh();
81
+ }, 200);
82
+ })
83
+ .catch(function (err) {
84
+ console.error("Local dev export error:", err);
85
+ btn.classList.remove("is-busy");
86
+ icon.textContent = "error";
87
+ label.textContent = "Failed";
88
+ btn.title = "Export failed";
89
+ setTimeout(function () {
90
+ icon.textContent = originalIcon;
91
+ label.textContent = originalText;
92
+ btn.title = originalTitle;
93
+ btn.disabled = false;
94
+ btn.removeAttribute("aria-busy");
95
+ busy = false;
96
+ }, 2500);
97
+ });
98
+ });
99
+ })();
100
+ </script>
@@ -0,0 +1,4 @@
1
+ <link rel="icon" href="/assets/icons/favicon.svg" type="image/svg+xml" sizes="any" />
2
+ <link rel="icon" href="/assets/icons/favicon-32.png" type="image/png" sizes="32x32" />
3
+ <link rel="apple-touch-icon" href="/assets/icons/apple-touch-icon.png" />
4
+ <link rel="icon" href="/favicon.ico" sizes="16x16 32x32" />
@@ -0,0 +1,55 @@
1
+ {% include theme/theme-context.liquid %}
2
+ {% assign font_title = sf.title | default: "Lora" %}
3
+ {% assign font_subtitle = sf.subtitle | default: "Lato" %}
4
+ {% assign font_label = sf.label | default: "Ysabeau SC" %}
5
+ {% assign font_body = sf.body | default: "Newsreader" %}
6
+ {% assign font_mono = sf.mono %}
7
+ {% assign font_code = sf.code %}
8
+ {% assign font_cv_body = sf.cv_body %}
9
+ {% assign title_slug = font_title | replace: ' ', '+' %}
10
+ {% assign subtitle_slug = font_subtitle | replace: ' ', '+' %}
11
+ {% assign label_slug = font_label | replace: ' ', '+' %}
12
+ {% assign body_slug = font_body | replace: ' ', '+' %}
13
+ {% assign mono_slug = font_mono | replace: ' ', '+' %}
14
+ {% assign code_slug = font_code | replace: ' ', '+' %}
15
+ {% assign cv_body_slug = font_cv_body | replace: ' ', '+' %}
16
+ {% assign title_spec = sf.google_title_spec | default: "ital,wght@0,400;0,500;0,700;1,400;1,500" %}
17
+ {% assign subtitle_spec = sf.google_subtitle_spec | default: "ital,wght@0,300;0,400;0,500;0,700;1,400;1,500" %}
18
+ {% assign label_spec = sf.google_label_spec | default: "ital,wght@0,400;0,500;0,700;1,400;1,500" %}
19
+ {% assign body_spec = sf.google_body_spec | default: "ital,wght@0,300;0,400;0,500;0,700;1,400;1,500;1,700" %}
20
+ {% assign mono_spec = sf.google_mono_spec | default: "wght@400;500;700" %}
21
+ {% assign code_spec = sf.google_code_spec | default: "wght@300;400;500;600;700" %}
22
+ {% assign cv_body_spec = sf.google_cv_body_spec | default: "wght@300;400;500;600;700" %}
23
+ {% comment %}
24
+ Build the Google Fonts URL without repeating a family. Title is always
25
+ requested. Body / label / subtitle / mono / code append only when their
26
+ family name differs from ones already on the URL.
27
+ {% endcomment %}
28
+ {% assign fonts_href = "https://fonts.googleapis.com/css2?family=" | append: title_slug | append: ":" | append: title_spec %}
29
+ {% unless font_label == font_title %}
30
+ {% assign fonts_href = fonts_href | append: "&family=" | append: label_slug | append: ":" | append: label_spec %}
31
+ {% endunless %}
32
+ {% unless font_body == font_title or font_body == font_label %}
33
+ {% assign fonts_href = fonts_href | append: "&family=" | append: body_slug | append: ":" | append: body_spec %}
34
+ {% endunless %}
35
+ {% if font_mono and font_mono != '' %}
36
+ {% unless font_mono == font_title or font_mono == font_label or font_mono == font_body %}
37
+ {% assign fonts_href = fonts_href | append: "&family=" | append: mono_slug | append: ":" | append: mono_spec %}
38
+ {% endunless %}
39
+ {% endif %}
40
+ {% if font_code and font_code != '' %}
41
+ {% unless font_code == font_title or font_code == font_label or font_code == font_body or font_code == font_mono %}
42
+ {% assign fonts_href = fonts_href | append: "&family=" | append: code_slug | append: ":" | append: code_spec %}
43
+ {% endunless %}
44
+ {% endif %}
45
+ {% if font_subtitle != font_body and font_subtitle != font_label and font_subtitle != font_title %}
46
+ {% assign fonts_href = fonts_href | append: "&family=" | append: subtitle_slug | append: ":" | append: subtitle_spec %}
47
+ {% endif %}
48
+ {% if font_cv_body and font_cv_body != '' %}
49
+ {% unless font_cv_body == font_title or font_cv_body == font_label or font_cv_body == font_body or font_cv_body == font_subtitle %}
50
+ {% assign fonts_href = fonts_href | append: "&family=" | append: cv_body_slug | append: ":" | append: cv_body_spec %}
51
+ {% endunless %}
52
+ {% endif %}
53
+ {% assign fonts_href = fonts_href | append: "&display=swap" %}
54
+ <link rel="preload" as="style" href="{{ fonts_href }}" />
55
+ <link href="{{ fonts_href }}" rel="stylesheet" />
@@ -0,0 +1,115 @@
1
+ <script>
2
+ (function () {
3
+ var config = {
4
+ active: {{ site.shell_theme.active | default: "system" | jsonify }},
5
+ storageKey: {{ site.shell_theme.storage_key | default: "site-theme" | jsonify }},
6
+ available: {{ site.shell_theme.available | default: site.shell_theme.active | jsonify }}
7
+ };
8
+ var root = document.documentElement;
9
+ var media = window.matchMedia("(prefers-color-scheme: dark)");
10
+ var paletteChoices = config.available.filter(function (name) {
11
+ return name === "light" || name === "dark";
12
+ });
13
+ if (paletteChoices.length === 0) {
14
+ paletteChoices = ["light"];
15
+ }
16
+
17
+ function resolvePalette(choice) {
18
+ if (choice === "system") {
19
+ return media.matches ? "dark" : "light";
20
+ }
21
+ if (paletteChoices.indexOf(choice) !== -1) {
22
+ return choice;
23
+ }
24
+ return paletteChoices[0];
25
+ }
26
+
27
+ function applyTheme(choice) {
28
+ var palette = resolvePalette(choice);
29
+ root.setAttribute("data-theme", palette);
30
+ root.style.colorScheme = palette;
31
+ if (window.ShellTheme) {
32
+ window.ShellTheme.active = choice;
33
+ window.ShellTheme.resolved = palette;
34
+ }
35
+ }
36
+
37
+ var choice = config.active;
38
+ try {
39
+ var stored = localStorage.getItem(config.storageKey);
40
+ if (stored && config.available.indexOf(stored) !== -1) {
41
+ choice = stored;
42
+ }
43
+ } catch (e) { /* ignore */ }
44
+
45
+ applyTheme(choice);
46
+
47
+ window.ShellTheme = {
48
+ active: choice,
49
+ resolved: resolvePalette(choice),
50
+ storageKey: config.storageKey,
51
+ available: config.available.slice(),
52
+ paletteChoices: paletteChoices.slice(),
53
+ setTheme: function (name) {
54
+ if (config.available.indexOf(name) === -1) return;
55
+ try { localStorage.setItem(config.storageKey, name); } catch (e) { /* ignore */ }
56
+ this.active = name;
57
+ applyTheme(name);
58
+ document.dispatchEvent(new CustomEvent("shell-theme-change", {
59
+ detail: { choice: name, resolved: this.resolved }
60
+ }));
61
+ }
62
+ };
63
+
64
+ if (typeof media.addEventListener === "function") {
65
+ media.addEventListener("change", function () {
66
+ if (window.ShellTheme.active === "system") {
67
+ applyTheme("system");
68
+ document.dispatchEvent(new CustomEvent("shell-theme-change", {
69
+ detail: { choice: "system", resolved: window.ShellTheme.resolved }
70
+ }));
71
+ }
72
+ });
73
+ } else if (typeof media.addListener === "function") {
74
+ media.addListener(function () {
75
+ if (window.ShellTheme.active === "system") {
76
+ applyTheme("system");
77
+ }
78
+ });
79
+ }
80
+ })();
81
+ </script>
82
+ <script>
83
+ (function () {
84
+ var offsets = [-4, -3, -2, -1, 0, 1, 2, 3, 4];
85
+ var key = "site-prose-offset";
86
+
87
+ function read() {
88
+ try {
89
+ var n = Number(localStorage.getItem(key));
90
+ if (offsets.indexOf(n) !== -1) return n;
91
+ } catch (e) { /* ignore */ }
92
+ return 0;
93
+ }
94
+
95
+ function apply(n) {
96
+ if (offsets.indexOf(n) === -1) n = 0;
97
+ document.documentElement.style.setProperty("--prose-size-offset", n + "pt");
98
+ return n;
99
+ }
100
+
101
+ window.ShellProseSize = {
102
+ offsets: offsets.slice(),
103
+ storageKey: key,
104
+ read: read,
105
+ apply: apply,
106
+ set: function (n) {
107
+ n = apply(Number(n));
108
+ try { localStorage.setItem(key, String(n)); } catch (e) { /* ignore */ }
109
+ return n;
110
+ }
111
+ };
112
+
113
+ apply(read());
114
+ })();
115
+ </script>
@@ -0,0 +1,6 @@
1
+ <svg class="site-logo" viewBox="0 0 32 32" role="img" aria-hidden="true" focusable="false">
2
+ <polygon class="site-logo-primary" points="0,0 23,0 15,17 0,20"/>
3
+ <polygon class="site-logo-secondary" points="23,0 32,0 32,32 17,21 15,17"/>
4
+ <polygon class="site-logo-tertiary" points="0,20 15,17 17,21 32,32 0,32"/>
5
+ <polygon class="site-logo-frame" points="1,1 31,1 31,31 1,31"/>
6
+ </svg>
@@ -0,0 +1,65 @@
1
+ {
2
+ "primary": "var(--color-primary)",
3
+ "secondary": "var(--color-secondary)",
4
+ "tertiary": "var(--color-tertiary)",
5
+ "primary-container": "var(--color-primary-container)",
6
+ "secondary-container": "var(--color-secondary-container)",
7
+ "tertiary-container": "var(--color-tertiary-container)",
8
+ "on-primary": "var(--color-on-primary)",
9
+ "on-secondary": "var(--color-on-secondary)",
10
+ "on-tertiary": "var(--color-on-tertiary)",
11
+ "on-primary-container": "var(--color-on-primary-container)",
12
+ "on-secondary-container": "var(--color-on-secondary-container)",
13
+ "on-tertiary-container": "var(--color-on-tertiary-container)",
14
+ "on-surface": "var(--color-on-surface)",
15
+ "on-surface-variant": "var(--color-on-surface-variant)",
16
+ "on-background": "var(--color-on-background)",
17
+ "outline": "var(--color-outline)",
18
+ "outline-variant": "var(--color-outline-variant)",
19
+ "gentle-faint": "var(--border-gentle-faint)",
20
+ "gentle-subtle": "var(--border-gentle-subtle)",
21
+ "gentle-divider": "var(--border-gentle-divider)",
22
+ "gentle": "var(--border-gentle)",
23
+ "gentle-hover": "var(--border-gentle-hover)",
24
+ "background": "var(--color-background)",
25
+ "background-secondary": "var(--color-background-secondary)",
26
+ "surface": "var(--color-surface)",
27
+ "surface-bright": "var(--color-surface-bright)",
28
+ "surface-container-lowest": "var(--color-surface-container-lowest)",
29
+ "surface-container-low": "var(--color-surface-container-low)",
30
+ "surface-container": "var(--color-surface-container)",
31
+ "surface-container-high": "var(--color-surface-container-high)",
32
+ "surface-container-highest": "var(--color-surface-container-highest)",
33
+ "surface-variant": "var(--color-surface-variant)",
34
+ "surface-dim": "var(--color-surface-dim)",
35
+ "panel-contrast": "var(--color-panel-contrast)",
36
+ "surface-card": "var(--color-surface-card)",
37
+ "surface-card-contrast": "var(--color-surface-card-contrast)",
38
+ "surface-high": "var(--color-surface-high)",
39
+ "surface-high-contrast": "var(--color-surface-high-contrast)",
40
+ "surface-tint": "var(--color-surface-tint)",
41
+ "inverse-primary": "var(--color-inverse-primary)",
42
+ "inverse-surface": "var(--color-inverse-surface)",
43
+ "inverse-on-surface": "var(--color-inverse-on-surface)",
44
+ "primary-fixed": "var(--color-primary-fixed)",
45
+ "primary-fixed-dim": "var(--color-primary-fixed-dim)",
46
+ "secondary-fixed": "var(--color-secondary-fixed)",
47
+ "secondary-fixed-dim": "var(--color-secondary-fixed-dim)",
48
+ "tertiary-fixed": "var(--color-tertiary-fixed)",
49
+ "tertiary-fixed-dim": "var(--color-tertiary-fixed-dim)",
50
+ "on-primary-fixed": "var(--color-on-primary-fixed)",
51
+ "on-primary-fixed-variant": "var(--color-on-primary-fixed-variant)",
52
+ "on-secondary-fixed": "var(--color-on-secondary-fixed)",
53
+ "on-secondary-fixed-variant": "var(--color-on-secondary-fixed-variant)",
54
+ "on-tertiary-fixed": "var(--color-on-tertiary-fixed)",
55
+ "on-tertiary-fixed-variant": "var(--color-on-tertiary-fixed-variant)",
56
+ "error": "var(--color-error)",
57
+ "on-error": "var(--color-on-error)",
58
+ "error-container": "var(--color-error-container)",
59
+ "on-error-container": "var(--color-on-error-container)",
60
+ "res-youtube": "var(--color-res-youtube)",
61
+ "res-podcast": "var(--color-res-podcast)",
62
+ "res-doc": "var(--color-res-doc)",
63
+ "link": "var(--color-link)",
64
+ "link-hover": "var(--color-link-hover)"
65
+ }
@@ -0,0 +1,6 @@
1
+ {% include theme/theme-context.liquid %}
2
+ {% assign font_title = sf.title | default: "Lora" %}
3
+ {% assign font_subtitle = sf.subtitle | default: "Lato" %}
4
+ {% assign font_label = sf.label | default: "Ysabeau SC" %}
5
+ {% assign font_body = sf.body | default: "Newsreader" %}
6
+ {"title":["var(--font-title)"],"subtitle":["var(--font-subtitle)","serif"],"label":["var(--font-label)","serif"],"body":["var(--font-body)","sans-serif"],"headline":["var(--font-title)"],"editorial":["var(--font-title)"],"serif":["var(--font-title)"],"sans":["var(--font-body)","sans-serif"],"mono":["var(--font-mono)"],"code":["var(--font-code)"]}
@@ -0,0 +1,20 @@
1
+ {% comment %}
2
+ Resolves the active colour theme from _config.yml → shell_theme.active and
3
+ _data/themes/<name>.yml. Fonts and other non-colour tokens come from
4
+ _data/themes/shared.yml so light/dark cannot drift apart on type.
5
+ Sets tc, tn, tl, sf (colors, neutrals, links, fonts), ts (static),
6
+ sm (motion), sg (geometry).
7
+ {% endcomment %}
8
+ {% assign theme_active = site.shell_theme.active | default: "system" %}
9
+ {% assign theme_profile = site.data.themes[theme_active] %}
10
+ {% if theme_profile == nil %}
11
+ {% assign theme_profile = site.data.themes.light %}
12
+ {% endif %}
13
+ {% assign theme_shared = site.data.themes.shared %}
14
+ {% assign tc = theme_profile.colors %}
15
+ {% assign tn = theme_profile.neutrals %}
16
+ {% assign tl = theme_profile.links %}
17
+ {% assign sf = theme_shared.fonts %}
18
+ {% assign sm = theme_shared.motion %}
19
+ {% assign sg = theme_shared.geometry %}
20
+ {% assign ts = theme_profile.static %}
@@ -0,0 +1,94 @@
1
+ <div id="theme-toggle" class="relative flex items-center shrink-0">
2
+ <button id="theme-toggle-btn" type="button"
3
+ class="flex items-center justify-center w-7 h-7 p-1 text-on-surface-variant hover:text-primary transition-colors duration-300"
4
+ aria-label="Theme"
5
+ title="Theme"
6
+ aria-haspopup="true" aria-expanded="false" aria-controls="theme-toggle-menu">
7
+ <span class="sr-only">Theme</span>
8
+ <span id="theme-toggle-icon" class="material-symbols-outlined text-xl" aria-hidden="true">light_mode</span>
9
+ <span id="theme-toggle-state" class="sr-only">current: light</span>
10
+ </button>
11
+ <div id="theme-toggle-menu"
12
+ class="hidden absolute right-0 top-full mt-1 min-w-[9.5rem] py-1 bg-surface border border-gentle-subtle shadow-sm z-50"
13
+ role="menu" aria-label="Choose color theme">
14
+ <button type="button" role="menuitemradio" data-theme-choice="light"
15
+ class="theme-choice w-full flex items-center gap-2.5 px-3 py-2 text-left font-body text-on-surface-variant hover:text-primary hover:bg-surface-container-low transition-colors leading-none">
16
+ <span class="material-symbols-outlined text-lg shrink-0 flex items-center justify-center" aria-hidden="true">light_mode</span>
17
+ <span class="inline-flex items-center leading-none">Light</span>
18
+ </button>
19
+ <button type="button" role="menuitemradio" data-theme-choice="dark"
20
+ class="theme-choice w-full flex items-center gap-2.5 px-3 py-2 text-left font-body text-on-surface-variant hover:text-primary hover:bg-surface-container-low transition-colors leading-none">
21
+ <span class="material-symbols-outlined text-lg shrink-0 flex items-center justify-center" aria-hidden="true">dark_mode</span>
22
+ <span class="inline-flex items-center leading-none">Dark</span>
23
+ </button>
24
+ <button type="button" role="menuitemradio" data-theme-choice="system"
25
+ class="theme-choice w-full flex items-center gap-2.5 px-3 py-2 text-left font-body text-on-surface-variant hover:text-primary hover:bg-surface-container-low transition-colors leading-none">
26
+ <span class="material-symbols-outlined text-lg shrink-0 flex items-center justify-center" aria-hidden="true">routine</span>
27
+ <span class="inline-flex items-center leading-none">System</span>
28
+ </button>
29
+ </div>
30
+ </div>
31
+ <script>
32
+ (function () {
33
+ var icons = { light: "light_mode", dark: "dark_mode", system: "routine" };
34
+ // The visible "Theme" word is the accessible name now; this only appends
35
+ // the current choice for screen readers.
36
+ var states = { light: "current: light", dark: "current: dark", system: "current: system" };
37
+
38
+ function syncToggle() {
39
+ var theme = window.ShellTheme;
40
+ if (!theme) return;
41
+ var active = theme.active;
42
+ var iconEl = document.getElementById("theme-toggle-icon");
43
+ var stateEl = document.getElementById("theme-toggle-state");
44
+ if (iconEl) iconEl.textContent = icons[active] || icons.light;
45
+ if (stateEl) stateEl.textContent = states[active] || states.light;
46
+ document.querySelectorAll(".theme-choice").forEach(function (el) {
47
+ var choice = el.getAttribute("data-theme-choice");
48
+ var selected = choice === active;
49
+ el.setAttribute("aria-checked", String(selected));
50
+ el.classList.toggle("text-primary", selected);
51
+ el.classList.toggle("font-semibold", selected);
52
+ el.classList.toggle("bg-surface-container-low", selected);
53
+ });
54
+ }
55
+
56
+ function init() {
57
+ var btn = document.getElementById("theme-toggle-btn");
58
+ var menu = document.getElementById("theme-toggle-menu");
59
+ if (!btn || !menu || !window.ShellTheme) return;
60
+
61
+ syncToggle();
62
+
63
+ btn.addEventListener("click", function (e) {
64
+ e.stopPropagation();
65
+ var open = !menu.classList.contains("hidden");
66
+ menu.classList.toggle("hidden", open);
67
+ btn.setAttribute("aria-expanded", String(!open));
68
+ });
69
+
70
+ menu.querySelectorAll(".theme-choice").forEach(function (el) {
71
+ el.addEventListener("click", function () {
72
+ var choice = el.getAttribute("data-theme-choice");
73
+ window.ShellTheme.setTheme(choice);
74
+ syncToggle();
75
+ menu.classList.add("hidden");
76
+ btn.setAttribute("aria-expanded", "false");
77
+ });
78
+ });
79
+
80
+ document.addEventListener("click", function () {
81
+ menu.classList.add("hidden");
82
+ btn.setAttribute("aria-expanded", "false");
83
+ });
84
+
85
+ document.addEventListener("shell-theme-change", syncToggle);
86
+ }
87
+
88
+ if (document.readyState === "loading") {
89
+ document.addEventListener("DOMContentLoaded", init);
90
+ } else {
91
+ init();
92
+ }
93
+ })();
94
+ </script>