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,544 @@
1
+ ---
2
+ layout: shell
3
+ ---
4
+ {%- assign blog_locale = page.lang | default: site.pandorga.identity.locale | default: "en" -%}
5
+ {%- assign blog_title = page.title | default: "Blog" -%}
6
+
7
+ <main class="relative max-w-7xl mx-auto px-4 sm:px-8 md:px-16 pt-10 pb-16 rascunhos-main{% if page.accent and page.accent != 'blue' %} page-accent--{{ page.accent }}{% endif %}"
8
+ {% include page/listing-runtime-attrs.html
9
+ default_collection="posts"
10
+ default_path="/pages/blog/"
11
+ default_detail="/posts/:slug/" %}>
12
+
13
+ {%- comment -%}
14
+ Masthead. The shard lives here rather than inside the shared page header
15
+ include, which every listing uses and which no single page may restyle.
16
+ {%- endcomment -%}
17
+ <div class="rascunhos-masthead" data-rascunhos-masthead>
18
+ <span class="shard-accent rascunhos-shard" aria-hidden="true"></span>
19
+ {% include page/page-header.html key=page.key
20
+ header_class="rascunhos-header" %}
21
+ </div>
22
+
23
+ <div id="blog-listing" class="rascunhos-body">
24
+ {%- comment -%}
25
+ Same sidebar pattern as the articles listing: keyword box, facets, and
26
+ licence. Platform UI strings are English defaults; dates follow
27
+ site.pandorga.identity.locale (or page.lang).
28
+ {%- endcomment -%}
29
+ <aside class="writing-rail listing-filter" data-listing-filter-root hidden>
30
+ <button type="button" class="writing-rail-trigger listing-filter-trigger"
31
+ data-listing-filter-trigger aria-expanded="false"
32
+ aria-controls="rascunhos-filter-panel">
33
+ <span class="material-symbols-outlined" aria-hidden="true">filter_list</span>
34
+ <span class="font-label writing-rail-trigger-label">Filter</span>
35
+ </button>
36
+
37
+ <div id="rascunhos-filter-panel" class="writing-rail-panel" data-listing-filter-panel hidden>
38
+ <p class="listing-filter-group-label font-label text-on-surface-variant">Keyword</p>
39
+ <label class="writing-rail-search">
40
+ <span class="sr-only">Filter entries by keyword</span>
41
+ <input type="search" class="listing-filter-search writing-rail-input font-body"
42
+ data-listing-filter-q placeholder="Title, summary, tag…" autocomplete="off">
43
+ <span class="material-symbols-outlined writing-rail-search-icon" aria-hidden="true">search</span>
44
+ </label>
45
+ <div data-listing-filter-chips></div>
46
+ </div>
47
+
48
+ {%- comment -%}
49
+ Citation and reuse. Filled from the export manifest's licence record
50
+ and nothing else — no declared licence, no block.
51
+ {%- endcomment -%}
52
+ <section class="writing-rail-licence" data-writing-licence hidden></section>
53
+ </aside>
54
+
55
+ <div class="rascunhos-ledger">
56
+ {%- comment -%}
57
+ Both halves of this line are read off the live index: the ordering is
58
+ the active sort, the tally is the length of the filtered list. Neither
59
+ is a standing claim about the archive.
60
+ {%- endcomment -%}
61
+ <div class="rascunhos-ledger-head">
62
+ <span data-rascunhos-order></span>
63
+ <span data-rascunhos-tally></span>
64
+ </div>
65
+
66
+ <div class="rascunhos-entries" id="posts-list">
67
+ <section id="empty-state" class="rascunhos-state">
68
+ <p class="font-body text-on-surface-variant italic">Loading entries...</p>
69
+ </section>
70
+ <section id="no-results" class="rascunhos-state hidden">
71
+ <p class="font-body text-on-surface-variant italic">Nothing matches these filters yet.</p>
72
+ </section>
73
+ </div>
74
+
75
+ <div id="pagination-controls" class="rascunhos-pagination hidden" aria-label="{{ blog_title }} pages"></div>
76
+ </div>
77
+ </div>
78
+
79
+ <div id="blog-detail" class="hidden">
80
+ <div class="grid grid-cols-1 gap-8 lg:gap-12 items-start text-detail-grid">
81
+ <aside id="blog-detail-sidebar"></aside>
82
+ <div id="blog-detail-main"></div>
83
+ </div>
84
+ </div>
85
+
86
+ </main>
87
+ {% include mathjax.html %}
88
+
89
+ <script>
90
+ (function () {
91
+ 'use strict';
92
+
93
+ var runtime = window.ContentRuntime;
94
+ if (!runtime || !runtime.ListingFilter) return;
95
+
96
+ var configRoot = document.querySelector('[data-listing-collection]');
97
+ var collection = (configRoot && configRoot.getAttribute('data-listing-collection')) || 'posts';
98
+ var listingPath = (configRoot && configRoot.getAttribute('data-listing-path')) || '/pages/blog/';
99
+ var detailPattern = (configRoot && configRoot.getAttribute('data-listing-detail')) || '/posts/:slug/';
100
+ if (listingPath.slice(-1) !== '/') listingPath += '/';
101
+
102
+ var listing = runtime.Listing;
103
+ var perPage = parseInt('{{ site.posts_per_page | default: 6 }}', 10);
104
+ var params = new URLSearchParams(window.location.search);
105
+ var selectedPost = runtime.contentSlug('post') ||
106
+ runtime.slugFromDetailPattern(window.location.pathname, detailPattern);
107
+ var currentPage = Math.max(1, parseInt(params.get('page') || '1', 10));
108
+
109
+ var grid = document.getElementById('posts-list');
110
+ var emptyState = document.getElementById('empty-state');
111
+ var noResults = document.getElementById('no-results');
112
+ var controls = document.getElementById('pagination-controls');
113
+ var listingRoot = document.getElementById('blog-listing');
114
+ var detailRoot = document.getElementById('blog-detail');
115
+ var detailSidebar = document.getElementById('blog-detail-sidebar');
116
+ var detailMain = document.getElementById('blog-detail-main');
117
+ var masthead = document.querySelector('[data-rascunhos-masthead]');
118
+ var pageHeader = document.querySelector('[data-content-page-header="{{ page.key }}"]');
119
+ var filterRoot = document.querySelector('[data-listing-filter-root]');
120
+ var orderEl = document.querySelector('[data-rascunhos-order]');
121
+ var tallyEl = document.querySelector('[data-rascunhos-tally]');
122
+ var licenceEl = document.querySelector('[data-writing-licence]');
123
+
124
+ var allPosts = [];
125
+ var filterUi = null;
126
+ var dateLocale = '{{ blog_locale }}';
127
+ if (dateLocale === 'ptbr' || dateLocale === 'pt_BR') dateLocale = 'pt-BR';
128
+
129
+ /* Folio number by slug. Assigned once over the whole log, oldest first, so
130
+ * an entry keeps the number it was written under no matter which filter,
131
+ * sort or page it is being looked at through. A number that changed when
132
+ * you pressed a chip would be a row index dressed up as a fact. */
133
+ var folioBySlug = Object.create(null);
134
+
135
+ var SORT_LABELS = {
136
+ published: 'Order: date written',
137
+ updated: 'Order: last revised',
138
+ random: 'Order: random'
139
+ };
140
+
141
+ var MONTH_FORMAT = new Intl.DateTimeFormat(dateLocale, {
142
+ month: 'long',
143
+ year: 'numeric'
144
+ });
145
+
146
+ function postDate(post) {
147
+ return String((post.front_matter && post.front_matter.date) || post.date || '');
148
+ }
149
+
150
+ function postUpdated(post) {
151
+ return String((post.front_matter && post.front_matter.last_updated) || post.last_updated || '');
152
+ }
153
+
154
+ /* Two entries share 2026-03-23, so date alone is not a total order and the
155
+ * folio numbers would shuffle between loads. The slug settles it. */
156
+ function chronologically(a, b) {
157
+ var byDate = postDate(a).localeCompare(postDate(b));
158
+ return byDate !== 0 ? byDate : String(a.slug || '').localeCompare(String(b.slug || ''));
159
+ }
160
+
161
+ function assignFolios(posts) {
162
+ posts.slice().sort(chronologically).forEach(function (post, index) {
163
+ folioBySlug[post.slug] = index + 1;
164
+ });
165
+ }
166
+
167
+ function sortItems(a, b, sortKey) {
168
+ if (sortKey === 'random') {
169
+ var ak = (a && (a.slug || a.key)) || '';
170
+ var bk = (b && (b.slug || b.key)) || '';
171
+ return runtime.sessionRandomCompare(ak, bk, collection);
172
+ }
173
+ if (sortKey === 'updated') {
174
+ var au = postUpdated(a) || postDate(a);
175
+ var bu = postUpdated(b) || postDate(b);
176
+ var byUpdated = String(bu).localeCompare(String(au));
177
+ return byUpdated !== 0 ? byUpdated : chronologically(b, a);
178
+ }
179
+ return chronologically(b, a);
180
+ }
181
+
182
+ function postTags(post) {
183
+ var list = (post.front_matter && post.front_matter.tags) || [];
184
+ return Array.isArray(list) ? list.filter(Boolean) : [];
185
+ }
186
+
187
+ function entryUrl(slug) {
188
+ return runtime.detailUrl(detailPattern, slug);
189
+ }
190
+
191
+ function tagHref(tag) {
192
+ return listingPath + '?tag=' + encodeURIComponent(tag);
193
+ }
194
+
195
+ function buildChipGroups(tagCounts) {
196
+ var tags = Object.keys(tagCounts).sort(function (a, b) {
197
+ return a.localeCompare(b, dateLocale);
198
+ });
199
+ // The count next to each tag is the number of entries that tag would
200
+ // return, so the deck never offers a filter that leads nowhere.
201
+ return [
202
+ {
203
+ key: 'tag',
204
+ label: 'Form',
205
+ options: [{ value: null, label: 'All (' + allPosts.length + ')' }].concat(tags.map(function (tag) {
206
+ return { value: tag, label: tag + ' (' + tagCounts[tag] + ')' };
207
+ }))
208
+ },
209
+ {
210
+ key: 'sort',
211
+ label: 'Order by',
212
+ options: [
213
+ { value: 'published', label: 'Date written' },
214
+ { value: 'updated', label: 'Last revised' },
215
+ { value: 'random', label: 'Random' }
216
+ ]
217
+ }
218
+ ];
219
+ }
220
+
221
+ function visiblePosts(state) {
222
+ return runtime.ListingFilter.filterItems(allPosts, state, { sortItems: sortItems });
223
+ }
224
+
225
+ function renderLedgerHead(state, shown) {
226
+ if (orderEl) orderEl.textContent = SORT_LABELS[state.sort] || SORT_LABELS.published;
227
+ if (!tallyEl) return;
228
+ var total = allPosts.length;
229
+ var word = total === 1 ? 'entry' : 'entries';
230
+ tallyEl.textContent = shown === total
231
+ ? total + ' ' + word
232
+ : shown + ' of ' + total + ' ' + word;
233
+ }
234
+
235
+ /* ── Notebook furniture ──────────────────────────────────────────────
236
+ * Two devices, both read straight off `date`: the run of months and the
237
+ * folio number. The months carry the rhythm on their own — a jump across
238
+ * years already says how long the pause was, so nothing spells it out.
239
+ * Nothing here counts words, estimates a reading time or names a place. */
240
+
241
+ function monthKey(value) {
242
+ return String(value).slice(0, 7);
243
+ }
244
+
245
+ function monthLabel(value) {
246
+ var parts = String(value).slice(0, 10).split('-').map(Number);
247
+ if (parts.length !== 3 || parts.some(isNaN)) return '';
248
+ var label = MONTH_FORMAT.format(new Date(parts[0], parts[1] - 1, parts[2]));
249
+ return label.charAt(0).toUpperCase() + label.slice(1);
250
+ }
251
+
252
+ function band(className) {
253
+ var el = document.createElement('div');
254
+ el.className = 'rascunhos-band' + className;
255
+ el.setAttribute('data-dynamic-post', 'true');
256
+ var body = document.createElement('div');
257
+ body.className = 'rascunhos-band-body';
258
+ el.appendChild(body);
259
+ return { root: el, body: body };
260
+ }
261
+
262
+ function renderMonthHeading(value) {
263
+ var label = monthLabel(value);
264
+ if (!label) return;
265
+ var parts = band('rascunhos-month');
266
+ var heading = document.createElement('h3');
267
+ heading.className = 'rascunhos-month-label';
268
+ heading.textContent = label;
269
+ parts.body.appendChild(heading);
270
+ grid.insertBefore(parts.root, noResults);
271
+ }
272
+
273
+ /* The entry's ledger line: folio, written, revised.
274
+ *
275
+ * The shared block builds it as date plus the language pill. When every
276
+ * entry shares one language the pill distinguishes nothing, so the strip
277
+ * is rebuilt with the three facts that belong to an entry: its number in
278
+ * the log, the day it was written, and the day it was last rewritten.
279
+ * The revision is derived from body edits alone, so it marks a rewrite and
280
+ * not a change of tag.
281
+ *
282
+ * Rebuilt through renderMetaStrip rather than edited in place, because
283
+ * that is what drops a separator along with the part it separates: an
284
+ * entry never revised ends after its date, not on a dangling slash. */
285
+ function entryMeta(post) {
286
+ var fm = post.front_matter || {};
287
+ var folio = folioBySlug[post.slug];
288
+ var updated = postUpdated(post);
289
+ var revised = updated && updated.slice(0, 10) !== String(fm.date || '').slice(0, 10)
290
+ ? '<span class="rascunhos-revision">Revised ' +
291
+ runtime.escapeHtml(runtime.formatDate(updated, dateLocale)) + '</span>'
292
+ : '';
293
+
294
+ return runtime.renderMetaStrip([
295
+ // A figure in a column, which is the remaining case for monospace.
296
+ folio ? '<span class="rascunhos-folio code-technical">#' +
297
+ String(folio).padStart(2, '0') + '</span>' : '',
298
+ runtime.escapeHtml(runtime.formatDate(fm.date, dateLocale)),
299
+ revised
300
+ ]);
301
+ }
302
+
303
+ /* One ledger entry. The shared block carries the date, the title, the tag
304
+ * chips and the body preview — posts almost never declare a `description`,
305
+ * so runtime.listingPreview falls through to the exported body excerpt,
306
+ * which is the real summary for this collection. */
307
+ function renderEntry(post) {
308
+ var fm = post.front_matter || {};
309
+ var href = entryUrl(post.slug);
310
+
311
+ var row = runtime.renderLedgerRow(post, {
312
+ href: href,
313
+ locale: dateLocale,
314
+ tagHref: tagHref
315
+ });
316
+ row.classList.add('rascunhos-row', 'rascunhos-band');
317
+ row.setAttribute('data-dynamic-post', 'true');
318
+
319
+ // The mockup files the classification top-right, opposite the ledger
320
+ // line, and drops the read-more link in favour of the title being the
321
+ // link. Lift the chips out of the footer rather than forking the shared
322
+ // block, and swap the strip for this page's three facts.
323
+ var head = row.querySelector('.ledger-row-head');
324
+ var meta = head && head.querySelector('.ledger-meta');
325
+ if (meta) meta.outerHTML = entryMeta(post);
326
+ var foot = row.querySelector('.ledger-row-foot');
327
+ var chips = foot && foot.querySelector('.ledger-chip-row');
328
+ if (head && chips) {
329
+ head.appendChild(chips);
330
+ foot.remove();
331
+ }
332
+
333
+ var body = document.createElement('div');
334
+ body.className = 'rascunhos-band-body';
335
+ while (row.firstChild) body.appendChild(row.firstChild);
336
+
337
+ // Thumbnails are set on almost every exported post, so the entry can
338
+ // afford a small plate to scan by. It is pasted in, not a hero: cropped
339
+ // small and desaturated, clearing only under the cursor. If the remote
340
+ // image fails, the row loses the column instead of gaining a
341
+ // placeholder.
342
+ if (fm.thumbnail) {
343
+ var media = document.createElement('div');
344
+ media.className = 'rascunhos-row-media';
345
+ var img = document.createElement('img');
346
+ img.src = runtime.buildUrl(fm.thumbnail);
347
+ // Decorative: the title next to it already names the entry.
348
+ img.alt = '';
349
+ img.loading = 'lazy';
350
+ img.decoding = 'async';
351
+ img.addEventListener('error', function () {
352
+ media.remove();
353
+ row.classList.remove('rascunhos-row--illustrated');
354
+ });
355
+ media.appendChild(img);
356
+ body.insertBefore(media, body.firstChild);
357
+ row.classList.add('rascunhos-row--illustrated');
358
+ }
359
+
360
+ row.appendChild(body);
361
+ grid.insertBefore(row, noResults);
362
+ }
363
+
364
+ function renderListing(state) {
365
+ Array.from(grid.querySelectorAll('[data-dynamic-post]')).forEach(function (el) { el.remove(); });
366
+ if (emptyState && emptyState.parentNode) emptyState.remove();
367
+
368
+ var visible = visiblePosts(state);
369
+ renderLedgerHead(state, visible.length);
370
+
371
+ if (visible.length === 0) {
372
+ if (noResults) noResults.classList.remove('hidden');
373
+ if (controls) controls.classList.add('hidden');
374
+ return;
375
+ }
376
+
377
+ if (noResults) noResults.classList.add('hidden');
378
+ var totalPages = Math.ceil(visible.length / perPage);
379
+ if (currentPage > totalPages) currentPage = totalPages;
380
+
381
+ var offset = (currentPage - 1) * perPage;
382
+ var page = visible.slice(offset, offset + perPage);
383
+ var previous = offset > 0 ? visible[offset - 1] : null;
384
+ var first = true;
385
+ var getEffectiveDate = function (p) {
386
+ if (!p) return '';
387
+ return state.sort === 'published' ? postDate(p) : (postUpdated(p) || postDate(p));
388
+ };
389
+
390
+ page.forEach(function (post) {
391
+ var date = getEffectiveDate(post);
392
+ var prevDate = getEffectiveDate(previous);
393
+ if (first || monthKey(date) !== monthKey(prevDate)) {
394
+ renderMonthHeading(date);
395
+ }
396
+ renderEntry(post);
397
+ previous = post;
398
+ first = false;
399
+ });
400
+
401
+ listing.renderPagination(controls, currentPage, totalPages);
402
+ }
403
+
404
+ function showListing() {
405
+ if (listingRoot) listingRoot.classList.remove('hidden');
406
+ if (detailRoot) detailRoot.classList.add('hidden');
407
+ if (masthead) masthead.classList.remove('hidden');
408
+ if (pageHeader) pageHeader.classList.remove('hidden');
409
+ if (filterUi) filterUi.setVisible(true);
410
+ if (detailMain) detailMain.innerHTML = '';
411
+ if (detailSidebar) detailSidebar.innerHTML = '';
412
+ runtime.removeRuntimeBackLink();
413
+ runtime.removeArticleToc();
414
+ }
415
+
416
+ async function renderPostDetail(post, posts) {
417
+ if (listingRoot) listingRoot.classList.add('hidden');
418
+ if (masthead) masthead.classList.add('hidden');
419
+ if (pageHeader) pageHeader.classList.add('hidden');
420
+ if (filterUi) filterUi.setVisible(false);
421
+ if (detailRoot) detailRoot.classList.remove('hidden');
422
+
423
+ var fm = post.front_matter || {};
424
+ var lang = fm.language || dateLocale;
425
+
426
+ if (detailSidebar) {
427
+ detailSidebar.innerHTML = runtime.buildTextSidebar({
428
+ frontMatter: fm,
429
+ fallbackTitle: post.slug,
430
+ dateHtml: runtime.renderDateEvolution(fm.date, fm.last_updated, dateLocale, {
431
+ publishedLabel: runtime.translate('published', lang),
432
+ updatedLabel: runtime.translate('updated', lang)
433
+ })
434
+ });
435
+ }
436
+
437
+ var index = (posts || []).findIndex(function (item) { return item.slug === post.slug; });
438
+
439
+ await runtime.renderRuntimeDetailSection({
440
+ detailAttribute: 'data-dynamic-post-detail',
441
+ bodyMarkdown: post.body_markdown,
442
+ frontMatter: fm,
443
+ mainColumn: detailMain,
444
+ sidebarEl: detailSidebar,
445
+ footerNav: {
446
+ lang: lang,
447
+ previousUrl: index >= 0 && index < posts.length - 1 ? entryUrl(posts[index + 1].slug) : null,
448
+ nextUrl: index > 0 ? entryUrl(posts[index - 1].slug) : null,
449
+ showRandom: index <= 0
450
+ }
451
+ });
452
+ }
453
+
454
+ function initFilter() {
455
+ // One chip per tag actually in use, so the deck never offers a filter
456
+ // that would return nothing.
457
+ var tagCounts = allPosts.reduce(function (acc, post) {
458
+ postTags(post).forEach(function (tag) {
459
+ acc[tag] = (acc[tag] || 0) + 1;
460
+ });
461
+ return acc;
462
+ }, {});
463
+
464
+ filterUi = runtime.ListingFilter.create({
465
+ root: filterRoot,
466
+ initialQuery: params.get('q') || '',
467
+ initialTag: params.get('tag') || params.get('categoria') || params.get('category') || null,
468
+ // Default order: date written.
469
+ initialSort: params.get('sort') || 'published',
470
+ defaultSort: 'published',
471
+ tagParam: 'tag',
472
+ chipGroups: buildChipGroups(tagCounts),
473
+ onChange: function (state) {
474
+ currentPage = 1;
475
+ renderListing(state);
476
+ }
477
+ });
478
+ if (filterRoot) filterRoot.hidden = false;
479
+ renderListing(filterUi.getState());
480
+ }
481
+
482
+ /* Citation and reuse, the rail's last group. Every word is the exported
483
+ * licence record; the page states no terms of its own, and a manifest
484
+ * without a licence leaves no block. Twin of the articles copy — the two
485
+ * listings wear the same sidebar with platform-default English labels. */
486
+ function mountLicence(manifest) {
487
+ if (!licenceEl) return;
488
+ var licence = manifest && manifest.content_license;
489
+ if (!licence || !licence.name) return;
490
+
491
+ var parts = ['<span class="writing-rail-licence-label font-label">Licensing &amp; reuse</span>'];
492
+ var named = licence.id ? licence.name + ' (' + licence.id + ')' : licence.name;
493
+ parts.push('<p class="writing-rail-licence-copy font-body">Entries published under ' +
494
+ runtime.escapeHtml(named) + '.</p>');
495
+ if (licence.rights_holder) {
496
+ parts.push('<p class="writing-rail-licence-copy font-body">Attribution to ' +
497
+ runtime.escapeHtml(licence.rights_holder) + '.</p>');
498
+ }
499
+ var href = licence.page_path || licence.url;
500
+ if (href) parts.push(runtime.renderCtaLink(href, 'Licence terms'));
501
+
502
+ licenceEl.innerHTML = parts.join('');
503
+ licenceEl.hidden = false;
504
+ }
505
+
506
+ function mountRailLicence() {
507
+ return runtime.loadJson('manifest.json')
508
+ .then(mountLicence)
509
+ .catch(function (error) { console.error(error); });
510
+ }
511
+
512
+ runtime.loadWritingIndex(collection).then(function (posts) {
513
+ allPosts = posts.slice();
514
+ assignFolios(allPosts);
515
+
516
+ var selected = selectedPost && runtime.resolveWritingAlias(allPosts, selectedPost);
517
+ if (selectedPost) {
518
+ if (selected && selected.slug && selected.slug !== selectedPost) {
519
+ runtime.adoptCanonicalDetailUrl(selected);
520
+ }
521
+ var slugToLoad = selected ? selected.slug : selectedPost;
522
+ return runtime.loadCollectionItem(collection, slugToLoad).then(function (post) {
523
+ return renderPostDetail(post, allPosts);
524
+ }).catch(function (error) {
525
+ runtime.reportDetailLoadError(error, {
526
+ emptyState: emptyState,
527
+ grid: grid,
528
+ noResults: noResults,
529
+ message: 'This post could not be loaded right now.'
530
+ });
531
+ });
532
+ }
533
+
534
+ showListing();
535
+ initFilter();
536
+ return mountRailLicence();
537
+ }).catch(function (error) {
538
+ console.error(error);
539
+ if (emptyState) {
540
+ emptyState.querySelector('p').textContent = 'Entries could not be loaded right now.';
541
+ }
542
+ });
543
+ })();
544
+ </script>