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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +77 -0
- data/DESIGN.md +1434 -0
- data/LICENSE +21 -0
- data/README.md +81 -0
- data/_data/themes/dark.yml +97 -0
- data/_data/themes/light.yml +120 -0
- data/_data/themes/shared.yml +37 -0
- data/_data/translations.yml +85 -0
- data/_includes/chord-tab.html +38 -0
- data/_includes/content-runtime/00-config.html +22 -0
- data/_includes/content-runtime/10-utils.html +176 -0
- data/_includes/content-runtime/20-markdown.html +264 -0
- data/_includes/content-runtime/25-code-blocks.html +315 -0
- data/_includes/content-runtime/30-includes-bib.html +190 -0
- data/_includes/content-runtime/30-includes-xcite.html +122 -0
- data/_includes/content-runtime/30-includes.html +365 -0
- data/_includes/content-runtime/40-listing.html +225 -0
- data/_includes/content-runtime/41-listing-helpers.html +71 -0
- data/_includes/content-runtime/42-listing-filter.html +208 -0
- data/_includes/content-runtime/43-blocks.html +379 -0
- data/_includes/content-runtime/50-detail.html +861 -0
- data/_includes/content-runtime/55-toc.html +176 -0
- data/_includes/content-runtime/60-fragments.html +567 -0
- data/_includes/content-runtime/90-bootstrap.html +76 -0
- data/_includes/cut-in.html +10 -0
- data/_includes/cv/cv-box.html +18 -0
- data/_includes/dict-card.html +24 -0
- data/_includes/figure.html +36 -0
- data/_includes/home/band-index.html +43 -0
- data/_includes/home/band-listing.html +49 -0
- data/_includes/mathjax.html +10 -0
- data/_includes/ongoing-product.html +1 -0
- data/_includes/page/content-runtime.html +20 -0
- data/_includes/page/date_evolution.html +19 -0
- data/_includes/page/figure-lightbox.html +102 -0
- data/_includes/page/formatted_date.html +15 -0
- data/_includes/page/hero-default.html +18 -0
- data/_includes/page/listing-runtime-attrs.html +8 -0
- data/_includes/page/mermaid.html +101 -0
- data/_includes/page/network-graph.html +1253 -0
- data/_includes/page/page-header.html +41 -0
- data/_includes/page/professional-connections.html +2 -0
- data/_includes/page/site-footer.html +101 -0
- data/_includes/page/timeline-section.html +868 -0
- data/_includes/plotly.html +22 -0
- data/_includes/theme/dev-reexport.html +100 -0
- data/_includes/theme/favicons.html +4 -0
- data/_includes/theme/font-loader.html +55 -0
- data/_includes/theme/shell-theme-boot.html +115 -0
- data/_includes/theme/site-logo.html +6 -0
- data/_includes/theme/tailwind-theme-colors.json.liquid +65 -0
- data/_includes/theme/tailwind-theme-fonts.json.liquid +6 -0
- data/_includes/theme/theme-context.liquid +20 -0
- data/_includes/theme/theme-toggle.html +94 -0
- data/_includes/theme/theme-vars-block.liquid +161 -0
- data/_includes/theme/theme-vars.html +16 -0
- data/_includes/transcription.html +80 -0
- data/_includes/tweet.html +22 -0
- data/_includes/video.html +11 -0
- data/_includes/xcite.html +3 -0
- data/_includes/youtube-short.html +26 -0
- data/_includes/youtube.html +12 -0
- data/_layouts/article.html +27 -0
- data/_layouts/articles.html +534 -0
- data/_layouts/bibliography.html +417 -0
- data/_layouts/blog.html +544 -0
- data/_layouts/cv.html +393 -0
- data/_layouts/home.html +319 -0
- data/_layouts/network.html +9 -0
- data/_layouts/pandorga/articles.html +4 -0
- data/_layouts/pandorga/bibliography.html +4 -0
- data/_layouts/pandorga/blog.html +4 -0
- data/_layouts/pandorga/cv.html +4 -0
- data/_layouts/pandorga/media.html +4 -0
- data/_layouts/pandorga/network.html +4 -0
- data/_layouts/pandorga/portfolio.html +4 -0
- data/_layouts/post.html +7 -0
- data/_layouts/product.html +5 -0
- data/_layouts/project.html +27 -0
- data/_layouts/projects.html +534 -0
- data/_layouts/resources.html +387 -0
- data/_layouts/shell.html +203 -0
- data/_layouts/text.html +246 -0
- data/_plugins/bibliography_artifact.rb +57 -0
- data/_plugins/content_backend.rb +37 -0
- data/_plugins/content_collections.rb +51 -0
- data/_plugins/content_detail_paths.rb +58 -0
- data/_plugins/content_headers.rb +73 -0
- data/_plugins/content_keys.rb +310 -0
- data/_plugins/content_local_serve.rb +168 -0
- data/_plugins/content_site_adapter.rb +159 -0
- data/_plugins/content_validators_hook.rb +20 -0
- data/_plugins/content_writing_index.rb +52 -0
- data/_plugins/content_writing_paths.rb +55 -0
- data/_plugins/force_utf8_encoding.rb +13 -0
- data/_plugins/git_chronology.rb +211 -0
- data/_plugins/git_metadata.rb +43 -0
- data/_plugins/job_sections.rb +25 -0
- data/_plugins/lib/content_validators.rb +27 -0
- data/_plugins/lib/resource_validator.rb +56 -0
- data/_plugins/lib/tag_validator.rb +153 -0
- data/_plugins/lib/text_excerpt.rb +56 -0
- data/_plugins/lib/writing_entry.rb +107 -0
- data/_plugins/local_detail_rewrites.rb +194 -0
- data/_plugins/timeline.rb +507 -0
- data/_plugins/writing_slug_limits.rb +70 -0
- data/assets/css/article.css +1330 -0
- data/assets/css/base.css +1432 -0
- data/assets/css/cv.css +13 -0
- data/assets/css/global-link.css +94 -0
- data/assets/css/ledger/bibliography.css +307 -0
- data/assets/css/ledger/chrome.css +118 -0
- data/assets/css/ledger/cv.css +836 -0
- data/assets/css/ledger/detail.css +1270 -0
- data/assets/css/ledger/footer.css +298 -0
- data/assets/css/ledger/home.css +1490 -0
- data/assets/css/ledger/network.css +106 -0
- data/assets/css/ledger/projects.css +716 -0
- data/assets/css/ledger/rascunhos.css +379 -0
- data/assets/css/ledger/sources.css +327 -0
- data/assets/css/ledger/writing.css +754 -0
- data/assets/tailwind-play.js +15 -0
- data/examples/full/Gemfile +7 -0
- data/examples/full/_config.yml +92 -0
- data/examples/full/_redirects +3 -0
- data/examples/full/content/collections/articles/2026-01-01-welcome.md +9 -0
- data/examples/full/content/collections/data/contacts.yml +3 -0
- data/examples/full/content/collections/data/education.yml +4 -0
- data/examples/full/content/collections/jobs/research-fellow.md +12 -0
- data/examples/full/content/collections/posts/2026-01-02-note.md +9 -0
- data/examples/full/content/collections/products/ledger-tool.md +6 -0
- data/examples/full/content/collections/projects/demo-kit.md +10 -0
- data/examples/full/content/collections/resources/style-guide.md +7 -0
- data/examples/full/content/pages/cv/summary.md +1 -0
- data/examples/full/content/pages/headers.yml +20 -0
- data/examples/full/index.html +4 -0
- data/examples/minimal/Gemfile +7 -0
- data/examples/minimal/_config.yml +47 -0
- data/examples/minimal/_redirects +3 -0
- data/examples/minimal/content/collections/articles/2026-01-01-hello.md +10 -0
- data/examples/minimal/content/collections/articles/2026-01-02-second.md +9 -0
- data/examples/minimal/content/pages/headers.yml +6 -0
- data/examples/minimal/index.html +4 -0
- data/exe/pandorga +7 -0
- data/functions/api/studio/[[path]].js +562 -0
- data/functions/api/studio/_lib/auth.js +131 -0
- data/functions/api/studio/_lib/blocks.js +156 -0
- data/functions/api/studio/_lib/gemini.js +62 -0
- data/functions/api/studio/_lib/github.js +278 -0
- data/functions/api/studio/_lib/guidelines.js +143 -0
- data/functions/api/studio/_lib/paths.js +76 -0
- data/functions/api/studio/_lib/prompts.js +165 -0
- data/functions/api/studio/_lib/rate-limit.js +40 -0
- data/functions/api/studio/_lib/rewrite.js +136 -0
- data/functions/api/studio/_lib/sanitize.js +143 -0
- data/lib/pandorga/cli.rb +66 -0
- data/lib/pandorga/commands/doctor.rb +66 -0
- data/lib/pandorga/commands/export.rb +30 -0
- data/lib/pandorga/commands/install_functions.rb +41 -0
- data/lib/pandorga/commands/new.rb +113 -0
- data/lib/pandorga/commands/publish.rb +53 -0
- data/lib/pandorga/commands/serve.rb +34 -0
- data/lib/pandorga/commands/studio_schema.rb +309 -0
- data/lib/pandorga/jekyll/generator.rb +74 -0
- data/lib/pandorga/jekyll/plugins/bibliography_artifact.rb +57 -0
- data/lib/pandorga/jekyll/plugins/content_backend.rb +37 -0
- data/lib/pandorga/jekyll/plugins/content_collections.rb +51 -0
- data/lib/pandorga/jekyll/plugins/content_detail_paths.rb +58 -0
- data/lib/pandorga/jekyll/plugins/content_headers.rb +73 -0
- data/lib/pandorga/jekyll/plugins/content_keys.rb +310 -0
- data/lib/pandorga/jekyll/plugins/content_local_serve.rb +168 -0
- data/lib/pandorga/jekyll/plugins/content_site_adapter.rb +159 -0
- data/lib/pandorga/jekyll/plugins/content_validators_hook.rb +20 -0
- data/lib/pandorga/jekyll/plugins/content_writing_index.rb +52 -0
- data/lib/pandorga/jekyll/plugins/content_writing_paths.rb +55 -0
- data/lib/pandorga/jekyll/plugins/force_utf8_encoding.rb +13 -0
- data/lib/pandorga/jekyll/plugins/git_chronology.rb +211 -0
- data/lib/pandorga/jekyll/plugins/git_metadata.rb +43 -0
- data/lib/pandorga/jekyll/plugins/job_sections.rb +25 -0
- data/lib/pandorga/jekyll/plugins/lib/content_validators.rb +27 -0
- data/lib/pandorga/jekyll/plugins/lib/resource_validator.rb +56 -0
- data/lib/pandorga/jekyll/plugins/lib/tag_validator.rb +153 -0
- data/lib/pandorga/jekyll/plugins/lib/text_excerpt.rb +56 -0
- data/lib/pandorga/jekyll/plugins/lib/writing_entry.rb +107 -0
- data/lib/pandorga/jekyll/plugins/local_detail_rewrites.rb +194 -0
- data/lib/pandorga/jekyll/plugins/timeline.rb +507 -0
- data/lib/pandorga/jekyll/plugins/writing_slug_limits.rb +70 -0
- data/lib/pandorga/jekyll.rb +15 -0
- data/lib/pandorga/object_store_env.rb +51 -0
- data/lib/pandorga/registry/schema.yml +115 -0
- data/lib/pandorga/registry.rb +279 -0
- data/lib/pandorga/version.rb +5 -0
- data/lib/pandorga.rb +19 -0
- data/scripts/cdn-allowlist.json +19 -0
- data/scripts/content/backfill-content-keys.rb +99 -0
- data/scripts/content/band_art.rb +362 -0
- data/scripts/content/export-content-json.rb +741 -0
- data/scripts/content/flatten-ansi-capture.mjs +153 -0
- data/scripts/content/hero_portraits.rb +28 -0
- data/scripts/content/publish-content-to-object-store.sh +78 -0
- data/scripts/content/publish-content-to-r2.sh +5 -0
- data/scripts/content/sync-content-json-to-r2.rb +167 -0
- data/scripts/content/sync-content-redirects.rb +12 -0
- data/scripts/generate-configuration-doc.rb +127 -0
- data/scripts/lib/content_license.rb +50 -0
- data/scripts/lib/content_storage_config.rb +70 -0
- data/scripts/lib/normalize-display-math.mjs +52 -0
- data/scripts/lib/r2_json_sync.rb +76 -0
- data/scripts/lib/withdrawn_public_objects.rb +23 -0
- data/scripts/lib/writing_index_contract.rb +61 -0
- data/scripts/pdf/md-to-pdf.mjs +1400 -0
- data/scripts/pdf/md-to-pdf.sh +27 -0
- data/scripts/test/fixtures/dual-template/_config.yml +32 -0
- data/scripts/test/fixtures/dual-template/index.html +4 -0
- data/scripts/test/render-parity/article-note.json +5 -0
- data/scripts/test/render-parity/blockquote.json +5 -0
- data/scripts/test/render-parity/blocks/ansi-basic.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-bold-is-bright.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-drops-cursor-moves.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-escapes-markup.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-extended-colour.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-real-control-byte.json +7 -0
- data/scripts/test/render-parity/blocks/archive-card-date-once.json +19 -0
- data/scripts/test/render-parity/blocks/archive-card-empty-foot.json +7 -0
- data/scripts/test/render-parity/blocks/archive-card-footer-dates.json +22 -0
- data/scripts/test/render-parity/blocks/archive-card-meta-parts.json +26 -0
- data/scripts/test/render-parity/blocks/archive-card.json +32 -0
- data/scripts/test/render-parity/blocks/cta-link.json +13 -0
- data/scripts/test/render-parity/blocks/lang-pill-pt.json +8 -0
- data/scripts/test/render-parity/blocks/lang-pill.json +8 -0
- data/scripts/test/render-parity/blocks/ledger-row-meta-parts.json +29 -0
- data/scripts/test/render-parity/blocks/ledger-row.json +35 -0
- data/scripts/test/render-parity/blocks/meta-strip.json +27 -0
- data/scripts/test/render-parity/blocks/poster-media-image.json +8 -0
- data/scripts/test/render-parity/blocks/poster-media-video.json +9 -0
- data/scripts/test/render-parity/blocks/section-banner.json +13 -0
- data/scripts/test/render-parity/blocks/system-card-custom-meta.json +22 -0
- data/scripts/test/render-parity/blocks/system-card.json +48 -0
- data/scripts/test/render-parity/blocks/tag-chips-links.json +12 -0
- data/scripts/test/render-parity/blocks/tag-chips.json +9 -0
- data/scripts/test/render-parity/cut-in-blank-line.json +9 -0
- data/scripts/test/render-parity/cut-in-right.json +9 -0
- data/scripts/test/render-parity/cut-in.json +8 -0
- data/scripts/test/render-parity/figure.json +5 -0
- data/scripts/test/render-parity/load-runtime.mjs +337 -0
- data/scripts/test/render-parity/ongoing-product.json +5 -0
- data/scripts/test/render-parity/plotly.json +5 -0
- data/scripts/test/render-parity/poem.json +5 -0
- data/scripts/test/render-parity/run-absent-field-checks.mjs +232 -0
- data/scripts/test/render-parity/run-block-fixture.mjs +86 -0
- data/scripts/test/render-parity/run-detail-parity-checks.mjs +123 -0
- data/scripts/test/render-parity/run-field-flow-checks.mjs +227 -0
- data/scripts/test/render-parity/run-fixture.mjs +159 -0
- data/scripts/test/render-parity/selectors.mjs +43 -0
- data/scripts/test/render-parity/video.json +5 -0
- data/scripts/test/render-parity/youtube-short.json +10 -0
- data/scripts/test/render-parity/youtube.json +8 -0
- data/scripts/test/test-absent-field-absent-element.rb +43 -0
- data/scripts/test/test-cdn-pins.rb +44 -0
- data/scripts/test/test-cloudflare-watch-paths-doc.rb +31 -0
- data/scripts/test/test-configuration-doc.rb +36 -0
- data/scripts/test/test-content-detail-urls.rb +346 -0
- data/scripts/test/test-content-keys.rb +110 -0
- data/scripts/test/test-content-license.rb +45 -0
- data/scripts/test/test-content-render-parity.rb +38 -0
- data/scripts/test/test-content-validators.rb +12 -0
- data/scripts/test/test-css-structure.rb +72 -0
- data/scripts/test/test-cv-export-contract.rb +158 -0
- data/scripts/test/test-detail-render-parity.rb +40 -0
- data/scripts/test/test-dev-reexport.rb +122 -0
- data/scripts/test/test-doctor.rb +39 -0
- data/scripts/test/test-dual-template-instances.rb +87 -0
- data/scripts/test/test-dual-template-runtime.rb +167 -0
- data/scripts/test/test-examples-build.rb +60 -0
- data/scripts/test/test-export-content-json-incremental.rb +138 -0
- data/scripts/test/test-export.rb +53 -0
- data/scripts/test/test-exported-fields-reach-the-view.rb +60 -0
- data/scripts/test/test-form-control-theming.rb +143 -0
- data/scripts/test/test-git-chronology.rb +414 -0
- data/scripts/test/test-hero-portraits.mjs +97 -0
- data/scripts/test/test-hero-portraits.rb +162 -0
- data/scripts/test/test-home-band-art.rb +135 -0
- data/scripts/test/test-install-functions.rb +39 -0
- data/scripts/test/test-language-export.rb +79 -0
- data/scripts/test/test-listing-filter-contract.rb +119 -0
- data/scripts/test/test-math-row-breaks.mjs +79 -0
- data/scripts/test/test-math-row-breaks.rb +18 -0
- data/scripts/test/test-new.rb +113 -0
- data/scripts/test/test-no-personal-data.rb +85 -0
- data/scripts/test/test-public-cv-export.rb +164 -0
- data/scripts/test/test-r2-config-consistency.rb +57 -0
- data/scripts/test/test-r2-json-sync.rb +193 -0
- data/scripts/test/test-registry-home.rb +34 -0
- data/scripts/test/test-runtime-js-syntax.rb +64 -0
- data/scripts/test/test-slim-writing-index.rb +126 -0
- data/scripts/test/test-studio-api-contract.rb +99 -0
- data/scripts/test/test-studio-content-files.rb +79 -0
- data/scripts/test/test-studio-liquid-roundtrip.mjs +179 -0
- data/scripts/test/test-studio-rate-limit.mjs +70 -0
- data/scripts/test/test-studio-rewrite-api-contract.rb +59 -0
- data/scripts/test/test-studio-rewrite-sanitize.mjs +210 -0
- data/scripts/test/test-studio-schema-composition.rb +126 -0
- data/scripts/test/test-studio-slug-config.rb +167 -0
- data/scripts/test/test-template-packages.rb +51 -0
- data/scripts/test/test-text-excerpt.rb +51 -0
- data/scripts/test/test-unwrap-media-src.mjs +39 -0
- data/scripts/test/test-unwrap-media-src.rb +19 -0
- data/scripts/test/test-withdrawn-content-export.rb +69 -0
- data/scripts/test/test-writing-index-export-slugs.rb +84 -0
- data/scripts/test/test-writing-slug-limits.rb +61 -0
- data/scripts/test/test-xcite-chronology.rb +106 -0
- data/scripts/validate.sh +74 -0
- data/scripts/visual/build-cv-pages-json.js +33 -0
- data/scripts/visual/build-site-pages-json.js +94 -0
- data/scripts/visual/publish-visual-inspection-to-r2.sh +42 -0
- data/scripts/visual/take-screenshots.js +88 -0
- data/scripts/visual/visual-inspection-pages.json +113 -0
- data/studio/assets/index-B_j0pMD5.css +1 -0
- data/studio/assets/index-Ch4YEv9g.js +285 -0
- data/studio/index.html +27 -0
- data/studio/schema.yml +869 -0
- data/studio-app/README.md +27 -0
- data/studio-app/index.html +26 -0
- data/studio-app/package-lock.json +2499 -0
- data/studio-app/package.json +26 -0
- data/studio-app/skills-lock.json +47 -0
- data/studio-app/src/api/client.js +71 -0
- data/studio-app/src/editor/body-editor.js +70 -0
- data/studio-app/src/editor/liquid-core.js +382 -0
- data/studio-app/src/editor/liquid.js +77 -0
- data/studio-app/src/lib/document.js +207 -0
- data/studio-app/src/lib/list-controls.js +317 -0
- data/studio-app/src/lib/schema.js +66 -0
- data/studio-app/src/lib/yaml-doc.js +100 -0
- data/studio-app/src/main.js +3693 -0
- data/studio-app/src/media/browser.js +578 -0
- data/studio-app/src/preview/render.js +430 -0
- data/studio-app/src/styles/studio.css +2826 -0
- data/studio-app/vite.config.js +25 -0
- data/templates/articles/README.md +40 -0
- data/templates/articles/fixtures/article.md +9 -0
- data/templates/articles/screenshots/README.md +6 -0
- data/templates/articles/studio.yml +73 -0
- data/templates/articles/template.yml +21 -0
- data/templates/bibliography/README.md +41 -0
- data/templates/bibliography/fixtures/references.json +9 -0
- data/templates/bibliography/screenshots/README.md +6 -0
- data/templates/bibliography/studio.yml +66 -0
- data/templates/bibliography/template.yml +18 -0
- data/templates/blog/README.md +38 -0
- data/templates/blog/fixtures/post.md +9 -0
- data/templates/blog/screenshots/README.md +6 -0
- data/templates/blog/studio.yml +69 -0
- data/templates/blog/template.yml +20 -0
- data/templates/cv/README.md +34 -0
- data/templates/cv/fixtures/job.md +12 -0
- data/templates/cv/screenshots/README.md +6 -0
- data/templates/cv/studio.yml +200 -0
- data/templates/cv/template.yml +25 -0
- data/templates/media/README.md +38 -0
- data/templates/media/fixtures/resource.md +7 -0
- data/templates/media/screenshots/README.md +6 -0
- data/templates/media/studio.yml +58 -0
- data/templates/media/template.yml +17 -0
- data/templates/network/README.md +34 -0
- data/templates/network/fixtures/sources.json +3 -0
- data/templates/network/screenshots/README.md +6 -0
- data/templates/network/studio.yml +28 -0
- data/templates/network/template.yml +10 -0
- data/templates/portfolio/README.md +39 -0
- data/templates/portfolio/fixtures/project.md +10 -0
- data/templates/portfolio/screenshots/README.md +6 -0
- data/templates/portfolio/studio.yml +89 -0
- data/templates/portfolio/template.yml +18 -0
- metadata +505 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: shell
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<main class="relative max-w-7xl mx-auto px-8 md:px-16 pt-10 pb-16"
|
|
6
|
+
{% include page/listing-runtime-attrs.html
|
|
7
|
+
default_collection="resources"
|
|
8
|
+
default_path="/pages/sources/" %}>
|
|
9
|
+
{%- comment -%}
|
|
10
|
+
The shards live here rather than inside the shared page header include,
|
|
11
|
+
which every listing uses and which no single page may restyle — the same
|
|
12
|
+
arrangement /pages/blog/ uses for its masthead. They bracket the grid
|
|
13
|
+
rather than sitting on it: a page of many small cards has no one panel to
|
|
14
|
+
accent, so the accent goes on the space above and below the run.
|
|
15
|
+
{%- endcomment -%}
|
|
16
|
+
<div class="sources-masthead">
|
|
17
|
+
<span class="shard-accent sources-masthead-shard" aria-hidden="true"></span>
|
|
18
|
+
{% include page/page-header.html key=page.key listing_filter=true spacer_class="mb-8"
|
|
19
|
+
header_class="sources-header mb-10 md:mb-12"
|
|
20
|
+
filter_label="Filter"
|
|
21
|
+
filter_placeholder="Match channels, links, descriptions…" %}
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<section class="scroll-mt-28 sources-body" id="sources">
|
|
25
|
+
<div class="listing-card-grid listing-card-grid--sources" id="resources-grid">
|
|
26
|
+
<p class="font-body text-on-surface-variant italic col-span-full">Loading sources...</p>
|
|
27
|
+
<section id="no-results" class="col-span-full py-16 hidden">
|
|
28
|
+
<p class="font-body text-on-surface-variant italic">Nothing matches these filters yet.</p>
|
|
29
|
+
</section>
|
|
30
|
+
</div>
|
|
31
|
+
<div id="resource-pagination" class="source-pagination"></div>
|
|
32
|
+
<span class="shard-accent shard-accent-beta sources-foot-shard" aria-hidden="true"></span>
|
|
33
|
+
</section>
|
|
34
|
+
</main>
|
|
35
|
+
|
|
36
|
+
<script>
|
|
37
|
+
(function () {
|
|
38
|
+
'use strict';
|
|
39
|
+
|
|
40
|
+
var runtime = window.ContentRuntime;
|
|
41
|
+
if (!runtime || !runtime.ListingFilter) return;
|
|
42
|
+
|
|
43
|
+
var configRoot = document.querySelector('[data-listing-collection]');
|
|
44
|
+
var collection = (configRoot && configRoot.getAttribute('data-listing-collection')) || 'resources';
|
|
45
|
+
var listingPath = (configRoot && configRoot.getAttribute('data-listing-path')) || '/pages/sources/';
|
|
46
|
+
if (listingPath.slice(-1) !== '/') listingPath += '/';
|
|
47
|
+
|
|
48
|
+
var listing = runtime.Listing;
|
|
49
|
+
var params = new URLSearchParams(window.location.search);
|
|
50
|
+
var grid = document.getElementById('resources-grid');
|
|
51
|
+
var noResults = document.getElementById('no-results');
|
|
52
|
+
var pagination = document.getElementById('resource-pagination');
|
|
53
|
+
var filterRoot = document.querySelector('[data-listing-filter-root]');
|
|
54
|
+
var pageSize = 12;
|
|
55
|
+
var currentPage = 1;
|
|
56
|
+
var allResources = [];
|
|
57
|
+
var allCards = [];
|
|
58
|
+
var filterUi = null;
|
|
59
|
+
|
|
60
|
+
/* Medium is derived from `type` (and, where an author left it implicit, from
|
|
61
|
+
the host in `link`). The four values map onto the brand triad plus ink;
|
|
62
|
+
nothing here invents a fifth. */
|
|
63
|
+
var MEDIUM_OPTIONS = [
|
|
64
|
+
{ value: 'youtube', label: 'YouTube' },
|
|
65
|
+
{ value: 'podcast', label: 'Podcast' },
|
|
66
|
+
{ value: 'document', label: 'Document' },
|
|
67
|
+
{ value: 'website', label: 'Website' }
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
if (!grid) return;
|
|
71
|
+
|
|
72
|
+
function mediumOf(fm) {
|
|
73
|
+
var type = String(fm.type || '').toLowerCase();
|
|
74
|
+
var link = String(fm.link || '').toLowerCase();
|
|
75
|
+
if (type === 'spotify' || type === 'podcast' || link.indexOf('spotify.com') !== -1) return 'podcast';
|
|
76
|
+
if (type === 'youtube' || link.indexOf('youtube.com') !== -1 || link.indexOf('youtu.be') !== -1) return 'youtube';
|
|
77
|
+
if (type === 'pdf' || type === 'document') return 'document';
|
|
78
|
+
return 'website';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function mediumLabel(value) {
|
|
82
|
+
for (var i = 0; i < MEDIUM_OPTIONS.length; i += 1) {
|
|
83
|
+
if (MEDIUM_OPTIONS[i].value === value) return MEDIUM_OPTIONS[i].label;
|
|
84
|
+
}
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function mediumKey(item) {
|
|
89
|
+
return mediumOf(item.front_matter || item);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* A resource records a full locale — `en_us`, `pt_br`, `en_gb` — which is
|
|
93
|
+
two facts in one string: the language before the separator and the
|
|
94
|
+
country after it. The card now shows both, as two chips, so each one
|
|
95
|
+
reads only its own half of the value.
|
|
96
|
+
|
|
97
|
+
Splitting a locale is the one thing both chips share, so it happens
|
|
98
|
+
once. An empty or malformed value yields an empty array and every chip
|
|
99
|
+
downstream of it renders nothing. */
|
|
100
|
+
function localeParts(language) {
|
|
101
|
+
var raw = String(language || '').trim().toLowerCase();
|
|
102
|
+
if (!raw) return [];
|
|
103
|
+
return raw.split(/[_-]/).filter(function (part) { return part !== ''; });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* The language half. The shared pill keys on the site's own two content
|
|
107
|
+
languages, so the locale folds onto that vocabulary before it is handed
|
|
108
|
+
over. Nothing is lost in the fold: `en_gb` and `en_au` are both English,
|
|
109
|
+
and the flag chip beside this one is already carrying the difference.
|
|
110
|
+
|
|
111
|
+
A third language is passed through for the pill to judge rather than
|
|
112
|
+
reprinted here, because a second spelling of the pill is how the two
|
|
113
|
+
drift apart. The pill currently declines anything outside its own two
|
|
114
|
+
codes, so the one French source shows three chips — which is the
|
|
115
|
+
correct failure: the fix belongs in renderLangPill, not in a copy of
|
|
116
|
+
it. */
|
|
117
|
+
var LANGUAGE_PILL_CODES = { en: 'enus', pt: 'ptbr' };
|
|
118
|
+
|
|
119
|
+
function languageChip(language) {
|
|
120
|
+
var parts = localeParts(language);
|
|
121
|
+
if (!parts.length) return '';
|
|
122
|
+
var primary = parts[0];
|
|
123
|
+
return runtime.renderLangPill(LANGUAGE_PILL_CODES[primary] || primary, { allowUnknown: true });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* The country half. The region subtag of a locale is an ISO 3166-1
|
|
127
|
+
alpha-2 code, which is what flag-icons — pinned in the shell — draws
|
|
128
|
+
from. A locale with no region (`pt` on its own) or a subtag that is not
|
|
129
|
+
two letters gets no flag rather than a guessed one: there is no honest
|
|
130
|
+
way to read a country out of a value that never names one.
|
|
131
|
+
|
|
132
|
+
The names are the accessible label, since a flag carries no text. An
|
|
133
|
+
unlisted region falls back to its own code, which is still true. */
|
|
134
|
+
var COUNTRY_NAMES = {
|
|
135
|
+
au: 'Australia',
|
|
136
|
+
br: 'Brazil',
|
|
137
|
+
ch: 'Switzerland',
|
|
138
|
+
fr: 'France',
|
|
139
|
+
gb: 'United Kingdom',
|
|
140
|
+
hk: 'Hong Kong',
|
|
141
|
+
hu: 'Hungary',
|
|
142
|
+
in: 'India',
|
|
143
|
+
it: 'Italy',
|
|
144
|
+
us: 'United States'
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
function countryChip(language) {
|
|
148
|
+
var parts = localeParts(language);
|
|
149
|
+
if (parts.length < 2) return '';
|
|
150
|
+
var region = parts[1];
|
|
151
|
+
if (!/^[a-z]{2}$/.test(region)) return '';
|
|
152
|
+
var name = COUNTRY_NAMES[region] || region.toUpperCase();
|
|
153
|
+
return '<span class="source-facet source-facet--country" title="' +
|
|
154
|
+
runtime.escapeAttr(name) + '"><span class="fi fi-' + runtime.escapeAttr(region) +
|
|
155
|
+
'" role="img" aria-label="' + runtime.escapeAttr(name) + '"></span></span>';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* The medium chip. `mediumOf` always answers, because the filter needs a
|
|
159
|
+
bucket for every card; the chip is stricter and stays silent unless a
|
|
160
|
+
resource actually declared a `type` or a `link` for that answer to be
|
|
161
|
+
read from. Colour comes from the card's --source-accent, so the chip and
|
|
162
|
+
the card edge cannot disagree about what medium this is. */
|
|
163
|
+
function mediumChip(fm, medium) {
|
|
164
|
+
var declared = String(fm.type || '').trim() || String(fm.link || '').trim();
|
|
165
|
+
var label = declared ? mediumLabel(medium) : '';
|
|
166
|
+
if (!label) return '';
|
|
167
|
+
return '<span class="source-facet source-facet--medium">' +
|
|
168
|
+
runtime.escapeHtml(label) + '</span>';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* The four facets a source is filed under: what it is, how it is
|
|
172
|
+
classified, what language it speaks, and where it speaks from. Each
|
|
173
|
+
renders only when the field beNewsreader it is populated, so a source missing
|
|
174
|
+
a category shows three chips and no gap where the fourth would be. */
|
|
175
|
+
function renderFacets(fm, medium) {
|
|
176
|
+
var chips = [
|
|
177
|
+
mediumChip(fm, medium),
|
|
178
|
+
runtime.renderTagChips(fm.category ? [fm.category] : [], { hrefFor: categoryHref }),
|
|
179
|
+
languageChip(fm.language),
|
|
180
|
+
countryChip(fm.language)
|
|
181
|
+
].filter(function (chip) { return chip !== ''; });
|
|
182
|
+
if (!chips.length) return '';
|
|
183
|
+
return '<div class="source-facets">' + chips.join('') + '</div>';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* The host, not the full URL: the ledger line wants the publisher, and a
|
|
187
|
+
tracking-laden path tells the reader nothing. */
|
|
188
|
+
function linkHost(link) {
|
|
189
|
+
var value = String(link || '').trim();
|
|
190
|
+
if (!value) return '';
|
|
191
|
+
return value.replace(/^https?:\/\//i, '').replace(/^www\./i, '').split(/[/?#]/)[0];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function categoryHref(category) {
|
|
195
|
+
return listingPath + '?category=' + encodeURIComponent(category);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function sortResources(a, b, sortKey) {
|
|
199
|
+
if (sortKey === 'random') {
|
|
200
|
+
var ak = a.slug || (a.front_matter && a.front_matter.name) || '';
|
|
201
|
+
var bk = b.slug || (b.front_matter && b.front_matter.name) || '';
|
|
202
|
+
return runtime.sessionRandomCompare(ak, bk, collection);
|
|
203
|
+
}
|
|
204
|
+
var na = String((a.front_matter && a.front_matter.name) || a.slug || '').toLowerCase();
|
|
205
|
+
var nb = String((b.front_matter && b.front_matter.name) || b.slug || '').toLowerCase();
|
|
206
|
+
return na.localeCompare(nb);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function buildChipGroups(categories, mediumsPresent) {
|
|
210
|
+
return [
|
|
211
|
+
{
|
|
212
|
+
/* The shared filter keys its single classification facet on `tag`.
|
|
213
|
+
Resources classify on `category`, which that facet already falls
|
|
214
|
+
back to, so the group key has to be `tag` for a click to land. */
|
|
215
|
+
key: 'tag',
|
|
216
|
+
label: 'Category',
|
|
217
|
+
options: [{ value: null, label: 'All' }].concat(categories.map(function (cat) {
|
|
218
|
+
return { value: cat, label: cat };
|
|
219
|
+
}))
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
key: 'medium',
|
|
223
|
+
label: 'Medium',
|
|
224
|
+
options: [{ value: null, label: 'All' }].concat(MEDIUM_OPTIONS.filter(function (opt) {
|
|
225
|
+
return mediumsPresent[opt.value];
|
|
226
|
+
}).map(function (opt) {
|
|
227
|
+
return { value: opt.value, label: opt.label, swatch: 'source-swatch source-swatch--' + opt.value };
|
|
228
|
+
}))
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
key: 'sort',
|
|
232
|
+
label: 'Sort by',
|
|
233
|
+
options: [
|
|
234
|
+
{ value: 'random', label: 'Random' },
|
|
235
|
+
{ value: 'title', label: 'Title' }
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function visibleResources(state) {
|
|
242
|
+
return runtime.ListingFilter.filterItems(allResources, state, {
|
|
243
|
+
mediumKey: mediumKey,
|
|
244
|
+
sortItems: sortResources
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function renderCard(resource) {
|
|
249
|
+
var fm = resource.front_matter || {};
|
|
250
|
+
var name = fm.name || resource.slug;
|
|
251
|
+
var link = fm.link || '';
|
|
252
|
+
var medium = mediumOf(fm);
|
|
253
|
+
|
|
254
|
+
var card = document.createElement('article');
|
|
255
|
+
card.className = 'ledger-card source-card group';
|
|
256
|
+
card.dataset.category = fm.category || '';
|
|
257
|
+
card.dataset.medium = medium;
|
|
258
|
+
|
|
259
|
+
var html = runtime.renderPosterMedia(runtime.buildUrl(fm.thumbnail), name);
|
|
260
|
+
if (html) {
|
|
261
|
+
html = '<div class="source-card-media">' +
|
|
262
|
+
'<span class="ledger-frame-corners" aria-hidden="true"></span>' +
|
|
263
|
+
html + '</div>';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
html += '<div class="ledger-card-body">';
|
|
267
|
+
html += renderFacets(fm, medium);
|
|
268
|
+
html += '<h3 class="card-title ledger-card-title source-card-title"><a href="' +
|
|
269
|
+
runtime.escapeAttr(link) + '">' + runtime.escapeHtml(name) + '</a></h3>';
|
|
270
|
+
if (fm.description) {
|
|
271
|
+
html += '<p class="ledger-card-copy font-body text-on-surface-variant">' +
|
|
272
|
+
runtime.escapeHtml(fm.description) + '</p>';
|
|
273
|
+
}
|
|
274
|
+
html += '<div class="ledger-card-foot">';
|
|
275
|
+
html += '<span class="source-host code-technical">' + runtime.escapeHtml(linkHost(link)) + '</span>';
|
|
276
|
+
html += runtime.renderCtaLink(link, 'Open');
|
|
277
|
+
html += '</div></div>';
|
|
278
|
+
|
|
279
|
+
card.innerHTML = html;
|
|
280
|
+
|
|
281
|
+
/* Sources live off-site, so every link out of a card opens in a new tab.
|
|
282
|
+
The category chip stays in-page and is deliberately left alone. */
|
|
283
|
+
Array.prototype.forEach.call(card.querySelectorAll('a[href^="http"]'), function (anchor) {
|
|
284
|
+
anchor.target = '_blank';
|
|
285
|
+
anchor.rel = 'noopener noreferrer';
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
return card;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function renderPagination(state, totalPages) {
|
|
292
|
+
if (!pagination) return;
|
|
293
|
+
pagination.innerHTML = '';
|
|
294
|
+
if (totalPages <= 1) return;
|
|
295
|
+
var prev = document.createElement('button');
|
|
296
|
+
prev.type = 'button';
|
|
297
|
+
prev.textContent = 'Previous';
|
|
298
|
+
prev.disabled = currentPage === 1;
|
|
299
|
+
prev.className = 'pagination-nav-btn';
|
|
300
|
+
prev.onclick = function () { currentPage -= 1; renderPage(state); };
|
|
301
|
+
var label = document.createElement('span');
|
|
302
|
+
label.className = 'pagination-count';
|
|
303
|
+
label.textContent = currentPage + ' / ' + totalPages;
|
|
304
|
+
var next = document.createElement('button');
|
|
305
|
+
next.type = 'button';
|
|
306
|
+
next.textContent = 'Next';
|
|
307
|
+
next.disabled = currentPage === totalPages;
|
|
308
|
+
next.className = prev.className;
|
|
309
|
+
next.onclick = function () { currentPage += 1; renderPage(state); };
|
|
310
|
+
pagination.append(prev, label, next);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function renderPage(state) {
|
|
314
|
+
allCards.forEach(function (card) { card.classList.add('resource-hidden'); });
|
|
315
|
+
var cardsBySlug = {};
|
|
316
|
+
allCards.forEach(function (card) { cardsBySlug[card.dataset.slug] = card; });
|
|
317
|
+
var sortedList = visibleResources(state);
|
|
318
|
+
var visible = [];
|
|
319
|
+
sortedList.forEach(function (r) {
|
|
320
|
+
var card = cardsBySlug[r.slug];
|
|
321
|
+
if (card) {
|
|
322
|
+
visible.push(card);
|
|
323
|
+
grid.appendChild(card);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
if (noResults) {
|
|
328
|
+
grid.appendChild(noResults);
|
|
329
|
+
noResults.classList.toggle('hidden', visible.length !== 0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
var totalPages = Math.max(1, Math.ceil(visible.length / pageSize));
|
|
333
|
+
if (currentPage > totalPages) currentPage = totalPages;
|
|
334
|
+
visible.slice((currentPage - 1) * pageSize, currentPage * pageSize).forEach(function (card) {
|
|
335
|
+
card.classList.remove('resource-hidden');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
renderPagination(state, totalPages);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function initFilter() {
|
|
342
|
+
var categories = Array.from(new Set(allResources.map(function (resource) {
|
|
343
|
+
return resource.front_matter && resource.front_matter.category;
|
|
344
|
+
}).filter(Boolean))).sort();
|
|
345
|
+
var mediumsPresent = {};
|
|
346
|
+
allResources.forEach(function (resource) {
|
|
347
|
+
mediumsPresent[mediumKey(resource)] = true;
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
filterUi = runtime.ListingFilter.create({
|
|
351
|
+
root: filterRoot,
|
|
352
|
+
initialQuery: params.get('q') || '',
|
|
353
|
+
initialCategory: params.get('category') || params.get('tag') || null,
|
|
354
|
+
initialMedium: params.get('medium') || null,
|
|
355
|
+
initialSort: params.get('sort') || 'random',
|
|
356
|
+
defaultSort: 'random',
|
|
357
|
+
categoryParam: 'category',
|
|
358
|
+
chipGroups: buildChipGroups(categories, mediumsPresent),
|
|
359
|
+
onChange: function (state) {
|
|
360
|
+
currentPage = 1;
|
|
361
|
+
renderPage(state);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
if (filterRoot) filterRoot.hidden = false;
|
|
366
|
+
renderPage(filterUi.getState());
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
listing.showLoadingState(grid, 'Loading sources...');
|
|
370
|
+
|
|
371
|
+
runtime.loadCollection('collections/' + collection, collection).then(function (resources) {
|
|
372
|
+
allResources = resources.slice();
|
|
373
|
+
grid.innerHTML = '';
|
|
374
|
+
allCards = allResources.map(function (resource) {
|
|
375
|
+
var card = renderCard(resource);
|
|
376
|
+
card.dataset.slug = resource.slug;
|
|
377
|
+
return card;
|
|
378
|
+
});
|
|
379
|
+
allCards.forEach(function (card) { grid.appendChild(card); });
|
|
380
|
+
if (noResults) grid.appendChild(noResults);
|
|
381
|
+
initFilter();
|
|
382
|
+
}).catch(function (error) {
|
|
383
|
+
console.error(error);
|
|
384
|
+
listing.showErrorState(grid, 'Sources could not be loaded right now.');
|
|
385
|
+
});
|
|
386
|
+
})();
|
|
387
|
+
</script>
|
data/_layouts/shell.html
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="{{ page.lang | default: 'en' }}">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
{% include theme/shell-theme-boot.html %}
|
|
6
|
+
<meta name="color-scheme" content="light dark" />
|
|
7
|
+
<meta charset="utf-8" />
|
|
8
|
+
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
|
9
|
+
{% include theme/favicons.html %}
|
|
10
|
+
{% assign identity_short = site.pandorga.identity.short_name | default: site.pandorga.identity.name | default: site.author %}
|
|
11
|
+
{% capture meta_title %}{% if page.title %}{{ page.title }} | {% endif %}{{ identity_short }}{% endcapture %}
|
|
12
|
+
{% assign meta_description = page.description | default: site.description %}
|
|
13
|
+
{% assign canonical_path = page.canonical_url | default: page.url %}
|
|
14
|
+
<title>{{ meta_title | strip | escape_once }}</title>
|
|
15
|
+
<meta name="description" content="{{ meta_description | escape_once }}" />
|
|
16
|
+
<!-- Open Graph / Social Sharing -->
|
|
17
|
+
{% assign og_type = page.og_type | default: "website" %}
|
|
18
|
+
<meta property="og:type" content="{{ og_type }}" />
|
|
19
|
+
<meta property="og:url" content="{{ canonical_path | absolute_url }}" />
|
|
20
|
+
<meta property="og:title" content="{{ meta_title | strip | escape_once }}" />
|
|
21
|
+
<meta property="og:description" content="{{ meta_description | escape_once }}" />
|
|
22
|
+
{% if og_type == "article" and page.date %}
|
|
23
|
+
<meta property="article:published_time" content="{{ page.date | date_to_xmlschema }}" />
|
|
24
|
+
{% endif %}
|
|
25
|
+
{% if og_type == "article" and page.last_updated %}
|
|
26
|
+
<meta property="article:modified_time" content="{{ page.last_updated | date_to_xmlschema }}" />
|
|
27
|
+
{% endif %}
|
|
28
|
+
{% assign og_image = page.og_image | default: page.thumbnail | default: "/assets/images/og-default-dark.png" %}
|
|
29
|
+
{% if og_image contains "://" %}
|
|
30
|
+
{% assign og_image_url = og_image %}
|
|
31
|
+
{% elsif site.content_api_base_url and og_image contains "media/" %}
|
|
32
|
+
{% if og_image contains "/media/" %}
|
|
33
|
+
{% assign og_image_url = site.content_api_base_url | append: og_image %}
|
|
34
|
+
{% else %}
|
|
35
|
+
{% assign og_image_url = site.content_api_base_url | append: "/" | append: og_image %}
|
|
36
|
+
{% endif %}
|
|
37
|
+
{% else %}
|
|
38
|
+
{% assign og_image_url = og_image | absolute_url %}
|
|
39
|
+
{% endif %}
|
|
40
|
+
{% assign og_image_ext = og_image_url | split: "?" | first | split: "." | last | downcase %}
|
|
41
|
+
{% if og_image_ext == "jpg" or og_image_ext == "jpeg" %}
|
|
42
|
+
{% assign og_image_type = "image/jpeg" %}
|
|
43
|
+
{% elsif og_image_ext == "webp" %}
|
|
44
|
+
{% assign og_image_type = "image/webp" %}
|
|
45
|
+
{% else %}
|
|
46
|
+
{% assign og_image_type = "image/png" %}
|
|
47
|
+
{% endif %}
|
|
48
|
+
<meta property="og:image" content="{{ og_image_url }}" />
|
|
49
|
+
<meta name="image" property="og:image" content="{{ og_image_url }}" />
|
|
50
|
+
<meta property="og:image:width" content="1200" />
|
|
51
|
+
<meta property="og:image:height" content="630" />
|
|
52
|
+
<meta property="og:image:type" content="{{ og_image_type }}" />
|
|
53
|
+
<!-- Twitter Card -->
|
|
54
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
55
|
+
<meta name="twitter:title" content="{{ meta_title | strip | escape_once }}" />
|
|
56
|
+
<meta name="twitter:description" content="{{ meta_description | escape_once }}" />
|
|
57
|
+
<meta name="twitter:image" content="{{ og_image_url }}" />
|
|
58
|
+
<!-- Canonical URL -->
|
|
59
|
+
<link rel="canonical" href="{{ canonical_path | absolute_url }}" />
|
|
60
|
+
<link rel="stylesheet" href="/assets/css/base.css" />
|
|
61
|
+
<link rel="stylesheet" href="/assets/css/article.css" />
|
|
62
|
+
{% include theme/theme-vars.html %}
|
|
63
|
+
<link rel="stylesheet" href="/assets/css/cv.css" />
|
|
64
|
+
<link rel="stylesheet" href="/assets/css/global-link.css" />
|
|
65
|
+
{%- comment -%} Per-page ledger styles. One file per page so page work stays
|
|
66
|
+
independent; shared component rules live in base.css. {%- endcomment -%}
|
|
67
|
+
{%- for sheet in site.static_files -%}
|
|
68
|
+
{%- if sheet.path contains "/assets/css/ledger/" -%}
|
|
69
|
+
<link rel="stylesheet" href="{{ sheet.path }}" />
|
|
70
|
+
{%- endif -%}
|
|
71
|
+
{%- endfor -%}
|
|
72
|
+
<link href="https://fonts.googleapis.com" rel="preconnect" />
|
|
73
|
+
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect" />
|
|
74
|
+
{% include theme/font-loader.html %}
|
|
75
|
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
|
|
76
|
+
rel="stylesheet" />
|
|
77
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.2.3/css/flag-icons.min.css" />
|
|
78
|
+
<style>
|
|
79
|
+
:root { --drop-cap-lines: {{ site.drop_cap_lines | default: 2 }}; }
|
|
80
|
+
</style>
|
|
81
|
+
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
|
82
|
+
<script src="/assets/tailwind-play.js"></script>
|
|
83
|
+
</head>
|
|
84
|
+
|
|
85
|
+
<body class="bg-background text-on-surface">
|
|
86
|
+
|
|
87
|
+
{% assign nav_active = nil %}
|
|
88
|
+
{% if page.collection == "articles" %}{% assign nav_active = "articles" %}{% endif %}
|
|
89
|
+
{% if page.collection == "posts" %}{% assign nav_active = "blog" %}{% endif %}
|
|
90
|
+
{% if page.collection == "products" %}{% assign nav_active = "cv" %}{% endif %}
|
|
91
|
+
{% if page.collection == "projects" %}{% assign nav_active = "projects" %}{% endif %}
|
|
92
|
+
{% unless nav_active %}{% assign nav_active = page.key %}{% endunless %}
|
|
93
|
+
|
|
94
|
+
{% assign _all_pages = site.pages %}
|
|
95
|
+
|
|
96
|
+
<!-- TopNavBar -->
|
|
97
|
+
<nav class="sticky top-0 z-50 bg-surface">
|
|
98
|
+
<div class="flex justify-between items-center w-full px-6 sm:px-8 md:px-10 pt-5 pb-3.5 leading-none max-w-7xl mx-auto">
|
|
99
|
+
<div class="flex items-center gap-3 sm:gap-4 min-w-0">
|
|
100
|
+
<a href="/" aria-label="{{ site.author }} – Home" class="shrink-0 inline-flex items-center gap-3 site-wordmark-link">
|
|
101
|
+
{% include theme/site-logo.html %}
|
|
102
|
+
<span class="site-wordmark">{{ site.author | default: site.title | split: " " | first }}</span>
|
|
103
|
+
</a>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{%- comment -%}
|
|
107
|
+
The mockup's nav is a flat, always-visible bar rather than a menu
|
|
108
|
+
behind a button. Links and order come from pandorga.pages via the
|
|
109
|
+
registry generator (site.data.nav). The Explore menu survives as the
|
|
110
|
+
narrow-viewport fallback.
|
|
111
|
+
{%- endcomment -%}
|
|
112
|
+
<nav class="site-rail" aria-label="Sections">
|
|
113
|
+
{%- for _group in site.data.nav -%}
|
|
114
|
+
{%- for _item in _group.pages -%}
|
|
115
|
+
{%- assign _key = _item.key -%}
|
|
116
|
+
{%- assign _p = _all_pages | where: "key", _key | first -%}
|
|
117
|
+
<a href="{{ _p.url }}" class="site-rail-link{% if nav_active == _key %} is-active{% endif %}"
|
|
118
|
+
{% if nav_active == _key %}aria-current="page"{% endif %}>{{ _p.title }}</a>
|
|
119
|
+
{%- endfor -%}
|
|
120
|
+
{%- endfor -%}
|
|
121
|
+
</nav>
|
|
122
|
+
|
|
123
|
+
<div class="flex items-center gap-4 sm:gap-5 shrink-0">
|
|
124
|
+
{% if site.content_include_unpublished or site.unpublished %}
|
|
125
|
+
{% include theme/dev-reexport.html %}
|
|
126
|
+
{% endif %}
|
|
127
|
+
{% include theme/theme-toggle.html %}
|
|
128
|
+
<div id="site-menu" class="relative flex items-center shrink-0">
|
|
129
|
+
<button id="mobile-menu-btn" type="button"
|
|
130
|
+
class="flex items-center justify-center w-7 h-7 p-1 text-on-surface-variant hover:text-primary transition-colors duration-300"
|
|
131
|
+
aria-label="Explore site pages"
|
|
132
|
+
title="Menu"
|
|
133
|
+
aria-haspopup="true" aria-expanded="false" aria-controls="mobile-menu">
|
|
134
|
+
<span class="sr-only">Explore</span>
|
|
135
|
+
<span class="material-symbols-outlined text-xl" aria-hidden="true">menu</span>
|
|
136
|
+
</button>
|
|
137
|
+
<div id="mobile-menu"
|
|
138
|
+
class="hidden absolute right-0 top-full mt-1 min-w-[14rem] py-2 bg-surface border border-gentle-subtle shadow-sm z-50"
|
|
139
|
+
role="menu" aria-label="Explore">
|
|
140
|
+
<div class="flex flex-col px-3 gap-0">
|
|
141
|
+
{% for _group in site.data.nav %}
|
|
142
|
+
{% unless forloop.first %}<div class="h-px bg-outline-variant/30 my-1.5" role="separator"></div>{% endunless %}
|
|
143
|
+
{% for _item in _group.pages %}
|
|
144
|
+
{% assign _key = _item.key %}
|
|
145
|
+
{% assign _p = _all_pages | where: "key", _key | first %}
|
|
146
|
+
{% comment %}The inactive state carries no weight class: Cabin's axis starts
|
|
147
|
+
at 400 and the font loader requests nothing below it, so the `font-light` this
|
|
148
|
+
used to set asked for a 300 that resolves straight back to 400 and changed
|
|
149
|
+
nothing on the page. The variant ink is what marks the state.{% endcomment %}
|
|
150
|
+
<a href="{{ _p.url }}" role="menuitem"
|
|
151
|
+
class="flex items-center gap-2.5 font-body {% if nav_active == _key %}text-primary font-semibold{% else %}text-on-surface-variant hover:text-primary{% endif %} transition-colors duration-300 py-2 px-1">
|
|
152
|
+
{% if _item.icon %}<span class="material-symbols-outlined text-lg shrink-0 flex items-center justify-center" aria-hidden="true">{{ _item.icon }}</span>{% endif %}
|
|
153
|
+
<span class="inline-flex items-center gap-1.5 leading-none">{{
|
|
154
|
+
_p.title }}{% if _p.badge %}<span class="marginalia font-label text-tertiary">{{ _p.badge }}</span>{% endif
|
|
155
|
+
%}</span>
|
|
156
|
+
</a>
|
|
157
|
+
{% endfor %}
|
|
158
|
+
{% endfor %}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
<!-- Tonal separator — "No-Line" design rule: background-color shift instead of a CSS border -->
|
|
165
|
+
<div class="bg-surface-container-low h-[1px]"></div>
|
|
166
|
+
</nav>
|
|
167
|
+
<script>
|
|
168
|
+
(function () {
|
|
169
|
+
var wrap = document.getElementById('site-menu');
|
|
170
|
+
var btn = document.getElementById('mobile-menu-btn');
|
|
171
|
+
var menu = document.getElementById('mobile-menu');
|
|
172
|
+
if (!wrap || !btn || !menu) return;
|
|
173
|
+
|
|
174
|
+
function setOpen(open) {
|
|
175
|
+
menu.classList.toggle('hidden', !open);
|
|
176
|
+
btn.setAttribute('aria-expanded', String(open));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
btn.addEventListener('click', function (event) {
|
|
180
|
+
event.stopPropagation();
|
|
181
|
+
setOpen(menu.classList.contains('hidden'));
|
|
182
|
+
});
|
|
183
|
+
document.addEventListener('click', function (event) {
|
|
184
|
+
if (!wrap.contains(event.target)) setOpen(false);
|
|
185
|
+
});
|
|
186
|
+
document.addEventListener('keydown', function (event) {
|
|
187
|
+
if (event.key === 'Escape') setOpen(false);
|
|
188
|
+
});
|
|
189
|
+
})();
|
|
190
|
+
</script>
|
|
191
|
+
|
|
192
|
+
{% include page/content-runtime.html %}
|
|
193
|
+
|
|
194
|
+
{{ content }}
|
|
195
|
+
|
|
196
|
+
{% include page/figure-lightbox.html %}
|
|
197
|
+
|
|
198
|
+
<!-- Footer -->
|
|
199
|
+
{% include page/site-footer.html nav_active=nav_active %}
|
|
200
|
+
|
|
201
|
+
</body>
|
|
202
|
+
|
|
203
|
+
</html>
|