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,208 @@
|
|
|
1
|
+
{% raw %}
|
|
2
|
+
|
|
3
|
+
function frontMatterHaystack(item) {
|
|
4
|
+
var fm = (item && item.front_matter) || item || {};
|
|
5
|
+
var parts = [];
|
|
6
|
+
Object.keys(fm).forEach(function (key) {
|
|
7
|
+
var val = fm[key];
|
|
8
|
+
if (val == null || val === '') return;
|
|
9
|
+
if (Array.isArray(val)) {
|
|
10
|
+
parts.push(val.join(' '));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (typeof val === 'object') return;
|
|
14
|
+
parts.push(String(val));
|
|
15
|
+
});
|
|
16
|
+
if (item && item.slug) parts.push(String(item.slug));
|
|
17
|
+
if (item && item.key) parts.push(String(item.key));
|
|
18
|
+
return parts.join(' ').toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function matchesFrontMatterQuery(item, query) {
|
|
22
|
+
var q = String(query || '').trim().toLowerCase();
|
|
23
|
+
if (!q) return true;
|
|
24
|
+
return frontMatterHaystack(item).indexOf(q) !== -1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var ListingFilter = {
|
|
28
|
+
frontMatterHaystack: frontMatterHaystack,
|
|
29
|
+
matchesFrontMatterQuery: matchesFrontMatterQuery,
|
|
30
|
+
|
|
31
|
+
create: function (options) {
|
|
32
|
+
options = options || {};
|
|
33
|
+
var root = options.root;
|
|
34
|
+
if (!root) return null;
|
|
35
|
+
|
|
36
|
+
var trigger = root.querySelector('[data-listing-filter-trigger]');
|
|
37
|
+
var panel = root.querySelector('[data-listing-filter-panel]');
|
|
38
|
+
var input = root.querySelector('[data-listing-filter-q]');
|
|
39
|
+
var chipsRoot = root.querySelector('[data-listing-filter-chips]');
|
|
40
|
+
var state = {
|
|
41
|
+
q: String(options.initialQuery || '').trim(),
|
|
42
|
+
tag: options.initialTag || options.initialCategory || null,
|
|
43
|
+
medium: options.initialMedium || null,
|
|
44
|
+
sort: options.initialSort || options.defaultSort || 'updated',
|
|
45
|
+
type: options.initialType || null
|
|
46
|
+
};
|
|
47
|
+
var onChange = typeof options.onChange === 'function' ? options.onChange : function () {};
|
|
48
|
+
var chipGroups = options.chipGroups || [];
|
|
49
|
+
|
|
50
|
+
function setExpanded(open) {
|
|
51
|
+
if (!trigger || !panel) return;
|
|
52
|
+
trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
53
|
+
panel.hidden = !open;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderChips() {
|
|
57
|
+
if (!chipsRoot) return;
|
|
58
|
+
chipsRoot.innerHTML = '';
|
|
59
|
+
chipGroups.forEach(function (group) {
|
|
60
|
+
if (!group.options || !group.options.length) return;
|
|
61
|
+
var section = document.createElement('div');
|
|
62
|
+
section.className = 'listing-filter-group';
|
|
63
|
+
if (group.label) {
|
|
64
|
+
var heading = document.createElement('p');
|
|
65
|
+
heading.className = 'listing-filter-group-label font-label text-on-surface-variant';
|
|
66
|
+
heading.textContent = group.label;
|
|
67
|
+
section.appendChild(heading);
|
|
68
|
+
}
|
|
69
|
+
var row = document.createElement('div');
|
|
70
|
+
row.className = 'listing-filter-chips';
|
|
71
|
+
row.setAttribute('role', 'group');
|
|
72
|
+
if (group.label) row.setAttribute('aria-label', group.label);
|
|
73
|
+
group.options.forEach(function (option) {
|
|
74
|
+
var btn = document.createElement('button');
|
|
75
|
+
btn.type = 'button';
|
|
76
|
+
btn.className = 'listing-filter-chip font-body';
|
|
77
|
+
btn.dataset.filterKey = group.key;
|
|
78
|
+
btn.dataset.filterValue = option.value == null ? '' : String(option.value);
|
|
79
|
+
if (option.swatch) {
|
|
80
|
+
btn.innerHTML = '<span class="listing-filter-swatch ' + option.swatch + '" aria-hidden="true"></span>' +
|
|
81
|
+
escapeHtml(option.label);
|
|
82
|
+
} else {
|
|
83
|
+
btn.textContent = option.label;
|
|
84
|
+
}
|
|
85
|
+
var active = state[group.key] === option.value ||
|
|
86
|
+
(option.value === null && (state[group.key] == null || state[group.key] === ''));
|
|
87
|
+
if (active) btn.classList.add('is-active');
|
|
88
|
+
// Without this the active filter is conveyed by colour alone.
|
|
89
|
+
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
90
|
+
btn.addEventListener('click', function () {
|
|
91
|
+
state[group.key] = option.value;
|
|
92
|
+
if (group.key !== 'q') state.q = input ? input.value.trim() : state.q;
|
|
93
|
+
renderChips();
|
|
94
|
+
syncUrl();
|
|
95
|
+
onChange(state);
|
|
96
|
+
});
|
|
97
|
+
row.appendChild(btn);
|
|
98
|
+
});
|
|
99
|
+
section.appendChild(row);
|
|
100
|
+
chipsRoot.appendChild(section);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function syncUrl() {
|
|
105
|
+
if (options.syncUrl === false) return;
|
|
106
|
+
var p = new URLSearchParams(window.location.search);
|
|
107
|
+
['q', 'tag', 'category', 'medium', 'sort', 'type', 'categoria', 'language', 'ano', 'page'].forEach(function (key) {
|
|
108
|
+
p.delete(key);
|
|
109
|
+
});
|
|
110
|
+
if (state.q) p.set('q', state.q);
|
|
111
|
+
if (state.tag) p.set(options.tagParam || options.categoryParam || 'tag', state.tag);
|
|
112
|
+
if (state.medium) p.set('medium', state.medium);
|
|
113
|
+
if (state.type) p.set('type', state.type);
|
|
114
|
+
if (state.sort && state.sort !== (options.defaultSort || 'updated')) p.set('sort', state.sort);
|
|
115
|
+
var qs = p.toString();
|
|
116
|
+
var next = window.location.pathname + (qs ? '?' + qs : '');
|
|
117
|
+
var current = window.location.pathname + window.location.search;
|
|
118
|
+
if (next !== current) history.replaceState(null, '', next);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function applyQueryFromInput() {
|
|
122
|
+
state.q = input ? input.value.trim() : '';
|
|
123
|
+
renderChips();
|
|
124
|
+
syncUrl();
|
|
125
|
+
onChange(state);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (input) {
|
|
129
|
+
input.value = state.q;
|
|
130
|
+
var debounceMs = typeof options.searchDebounceMs === 'number' ? options.searchDebounceMs : 300;
|
|
131
|
+
var debounceTimer = null;
|
|
132
|
+
input.addEventListener('input', function () {
|
|
133
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
134
|
+
debounceTimer = setTimeout(applyQueryFromInput, debounceMs);
|
|
135
|
+
});
|
|
136
|
+
input.addEventListener('keydown', function (ev) {
|
|
137
|
+
if (ev.key === 'Enter') {
|
|
138
|
+
ev.preventDefault();
|
|
139
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
140
|
+
applyQueryFromInput();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (trigger && panel) {
|
|
146
|
+
trigger.addEventListener('click', function () {
|
|
147
|
+
setExpanded(panel.hidden);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
document.addEventListener('click', function (ev) {
|
|
152
|
+
if (!root || root.hidden) return;
|
|
153
|
+
if (root.contains(ev.target)) return;
|
|
154
|
+
setExpanded(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
renderChips();
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
root: root,
|
|
161
|
+
getState: function () { return Object.assign({}, state); },
|
|
162
|
+
setVisible: function (visible) {
|
|
163
|
+
root.hidden = !visible;
|
|
164
|
+
if (!visible) setExpanded(false);
|
|
165
|
+
},
|
|
166
|
+
applyQueryFromInput: applyQueryFromInput,
|
|
167
|
+
refreshChipGroups: function (nextGroups) {
|
|
168
|
+
chipGroups = nextGroups || [];
|
|
169
|
+
renderChips();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
filterItems: function (items, state, options) {
|
|
175
|
+
options = options || {};
|
|
176
|
+
state = state || {};
|
|
177
|
+
var list = (items || []).slice();
|
|
178
|
+
list = list.filter(function (item) {
|
|
179
|
+
if (!matchesFrontMatterQuery(item, state.q)) return false;
|
|
180
|
+
if (state.tag) {
|
|
181
|
+
var fm = item.front_matter || {};
|
|
182
|
+
var itemTags = fm.tags || item.tags;
|
|
183
|
+
if (Array.isArray(itemTags) && itemTags.length) {
|
|
184
|
+
// An entry carries several tags; matching any one of them keeps it.
|
|
185
|
+
if (itemTags.indexOf(state.tag) === -1) return false;
|
|
186
|
+
} else {
|
|
187
|
+
// Resources still classify on a single category axis.
|
|
188
|
+
var cat = fm.category || item.category || '';
|
|
189
|
+
if (cat !== state.tag) return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (state.medium && typeof options.mediumKey === 'function') {
|
|
193
|
+
if (options.mediumKey(item) !== state.medium) return false;
|
|
194
|
+
}
|
|
195
|
+
if (state.type) {
|
|
196
|
+
var nodeType = (item.collection || item.type || '').toString();
|
|
197
|
+
if (nodeType !== state.type) return false;
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
});
|
|
201
|
+
if (typeof options.sortItems === 'function') {
|
|
202
|
+
list.sort(function (a, b) { return options.sortItems(a, b, state.sort); });
|
|
203
|
+
}
|
|
204
|
+
return list;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
{% endraw %}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
{% raw %}
|
|
2
|
+
|
|
3
|
+
/* Architectural Ledger blocks.
|
|
4
|
+
*
|
|
5
|
+
* Shared renderers for the pieces every listing repeats: meta strips,
|
|
6
|
+
* language pills, tag chips, directional CTAs, and the three card shapes.
|
|
7
|
+
* Each layout used to grow its own copy of these, which is why the same
|
|
8
|
+
* card drifted between the writing index and the home page.
|
|
9
|
+
*
|
|
10
|
+
* House rule: a block renders only when the field backing it is present
|
|
11
|
+
* and non-empty. Nothing here derives a value, counts anything, or
|
|
12
|
+
* substitutes a placeholder — an absent field means an absent element.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
var LANGUAGE_LABELS = {
|
|
16
|
+
enus: 'EN',
|
|
17
|
+
ptbr: 'PT'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/* [EN] / [PT]. Empty language renders nothing rather than a neutral pill,
|
|
21
|
+
* since roughly half the corpus has never declared one.
|
|
22
|
+
*
|
|
23
|
+
* Writing declares `enus` / `ptbr`; resources use the underscored form
|
|
24
|
+
* (`en_us`, `pt_br`) and include a handful of third languages. Separators
|
|
25
|
+
* are dropped before the lookup so both spellings resolve, and a language
|
|
26
|
+
* outside the pair still renders under its primary subtag rather than
|
|
27
|
+
* being discarded — the value is really there.
|
|
28
|
+
*/
|
|
29
|
+
function renderLangPill(language, options) {
|
|
30
|
+
var raw = String(language || '').trim().toLowerCase();
|
|
31
|
+
if (!raw) return '';
|
|
32
|
+
var code = raw.replace(/[_-]/g, '');
|
|
33
|
+
var label = LANGUAGE_LABELS[code];
|
|
34
|
+
if (!label) {
|
|
35
|
+
// A regional variant is still the same language: en_gb is English, so
|
|
36
|
+
// it takes the English pill rather than the neutral one. Resolve the
|
|
37
|
+
// primary subtag against the same vocabulary before giving up.
|
|
38
|
+
var primary = code.slice(0, 2);
|
|
39
|
+
var canonical = Object.keys(LANGUAGE_LABELS).filter(function (key) {
|
|
40
|
+
return key.slice(0, 2) === primary;
|
|
41
|
+
})[0];
|
|
42
|
+
if (canonical) {
|
|
43
|
+
label = LANGUAGE_LABELS[canonical];
|
|
44
|
+
code = canonical;
|
|
45
|
+
} else if (options && options.allowUnknown) {
|
|
46
|
+
// Only where a caller genuinely holds languages beyond the two the
|
|
47
|
+
// site publishes in. Sources has one French entry; dropping its pill
|
|
48
|
+
// would discard a value that is really there. Everywhere else an
|
|
49
|
+
// unrecognised code is a typo, and showing it would hide the error.
|
|
50
|
+
label = primary.toUpperCase();
|
|
51
|
+
code = 'other';
|
|
52
|
+
} else {
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return [
|
|
57
|
+
'<span class="lang-pill lang-pill--', escapeAttr(code), '" data-language="',
|
|
58
|
+
escapeAttr(raw.replace(/[_-]/g, '')), '">', escapeHtml(label), '</span>'
|
|
59
|
+
].join('');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Tag chips. Optional href turns each chip into a filter link. */
|
|
63
|
+
function renderTagChips(tags, options) {
|
|
64
|
+
options = options || {};
|
|
65
|
+
var list = (Array.isArray(tags) ? tags : [])
|
|
66
|
+
.map(function (t) { return String(t || '').trim(); })
|
|
67
|
+
.filter(function (t) { return t !== ''; });
|
|
68
|
+
if (!list.length) return '';
|
|
69
|
+
if (options.limit) list = list.slice(0, options.limit);
|
|
70
|
+
|
|
71
|
+
var chips = list.map(function (tag) {
|
|
72
|
+
var inner = escapeHtml(tag);
|
|
73
|
+
if (typeof options.hrefFor === 'function') {
|
|
74
|
+
return '<a class="ledger-chip" href="' + escapeAttr(options.hrefFor(tag)) +
|
|
75
|
+
'">' + inner + '</a>';
|
|
76
|
+
}
|
|
77
|
+
return '<span class="ledger-chip">' + inner + '</span>';
|
|
78
|
+
});
|
|
79
|
+
return '<div class="ledger-chip-row">' + chips.join('') + '</div>';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Mono meta strip: the ledger line above a title. Callers pass whatever
|
|
83
|
+
* they have; separators appear only between surviving parts, so a missing
|
|
84
|
+
* category never leaves a dangling slash. */
|
|
85
|
+
function renderMetaStrip(parts, options) {
|
|
86
|
+
options = options || {};
|
|
87
|
+
var kept = (parts || []).filter(function (p) {
|
|
88
|
+
return p != null && String(p).trim() !== '';
|
|
89
|
+
});
|
|
90
|
+
if (!kept.length) return '';
|
|
91
|
+
var sep = '<span class="ledger-meta-sep" aria-hidden="true">/</span>';
|
|
92
|
+
var cls = 'ledger-meta' + (options.className ? ' ' + options.className : '');
|
|
93
|
+
return '<div class="' + cls + '">' + kept.join(sep) + '</div>';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Directional CTA. Plain calls keep the trailing arrow (label, then a
|
|
97
|
+
* `.ledger-cta-arrow` glyph that slides right on hover) — that motion is
|
|
98
|
+
* a "go to" cue and reads correctly after the label. A destination-typed
|
|
99
|
+
* `icon` (repository/article/external/details) is a leading marker
|
|
100
|
+
* instead, like a small label icon, not a directional cue: it renders
|
|
101
|
+
* before the label in its own `.ledger-cta-icon` span, which does not
|
|
102
|
+
* take part in the hover slide. */
|
|
103
|
+
function renderCtaLink(href, label, options) {
|
|
104
|
+
options = options || {};
|
|
105
|
+
if (!href || !label) return '';
|
|
106
|
+
var cls = 'ledger-cta' + (options.className ? ' ' + options.className : '');
|
|
107
|
+
if (options.icon) {
|
|
108
|
+
return [
|
|
109
|
+
'<a class="', cls, '" href="', escapeAttr(href), '">',
|
|
110
|
+
'<span class="material-symbols-outlined ledger-cta-icon" aria-hidden="true">', escapeHtml(options.icon), '</span>',
|
|
111
|
+
'<span>', escapeHtml(label), '</span>',
|
|
112
|
+
'</a>'
|
|
113
|
+
].join('');
|
|
114
|
+
}
|
|
115
|
+
return [
|
|
116
|
+
'<a class="', cls, '" href="', escapeAttr(href), '">',
|
|
117
|
+
'<span>', escapeHtml(label), '</span>',
|
|
118
|
+
'<span class="material-symbols-outlined ledger-cta-arrow" aria-hidden="true">chevron_right</span>',
|
|
119
|
+
'</a>'
|
|
120
|
+
].join('');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Section banner: kicker, title, and an optional right-aligned note. */
|
|
124
|
+
function renderSectionBanner(options) {
|
|
125
|
+
options = options || {};
|
|
126
|
+
if (!options.title) return '';
|
|
127
|
+
var html = '<div class="ledger-section-banner">';
|
|
128
|
+
html += '<div class="ledger-section-lead">';
|
|
129
|
+
if (options.kicker) {
|
|
130
|
+
html += '<span class="ledger-kicker">' +
|
|
131
|
+
escapeHtml(options.kicker) + '</span>';
|
|
132
|
+
}
|
|
133
|
+
html += '<h2 class="section-title">' + escapeHtml(options.title) + '</h2>';
|
|
134
|
+
html += '</div>';
|
|
135
|
+
if (options.note) {
|
|
136
|
+
html += '<span class="ledger-section-note">' +
|
|
137
|
+
escapeHtml(options.note) + '</span>';
|
|
138
|
+
}
|
|
139
|
+
html += '</div>';
|
|
140
|
+
return html;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* The featured entry.
|
|
144
|
+
*
|
|
145
|
+
* The pick is random, so a listing does not always lead with the same
|
|
146
|
+
* piece — but it is held for the session, so paging through the archive
|
|
147
|
+
* does not reshuffle the lead, and the home page and the writing index
|
|
148
|
+
* agree on which article is featured. Closing the tab draws again.
|
|
149
|
+
*
|
|
150
|
+
* Callers that lead on something other than an article pass their own
|
|
151
|
+
* `storageKey`, so the two picks are independent, and their own
|
|
152
|
+
* `eligible` when the default candidate rule does not apply to them. The
|
|
153
|
+
* default is an article's: only entries carrying a thumbnail, because the
|
|
154
|
+
* panel has an image column and one that reserves space for a picture it
|
|
155
|
+
* does not have is worse than no panel.
|
|
156
|
+
*/
|
|
157
|
+
var FEATURED_STORAGE_KEY = 'featured-article-slug';
|
|
158
|
+
|
|
159
|
+
function hasThumbnail(item) {
|
|
160
|
+
var fm = (item && item.front_matter) || {};
|
|
161
|
+
return String(fm.thumbnail || '').trim() !== '';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function readSessionSlug(key) {
|
|
165
|
+
try {
|
|
166
|
+
return window.sessionStorage.getItem(key);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return null; // private mode, or storage disabled
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function writeSessionSlug(key, slug) {
|
|
173
|
+
try {
|
|
174
|
+
window.sessionStorage.setItem(key, slug);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
/* Not being able to remember the pick is survivable: the page still
|
|
177
|
+
renders, it just may lead with a different entry next navigation. */
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function pickFeatured(items, options) {
|
|
182
|
+
var opts = options || {};
|
|
183
|
+
var key = opts.storageKey || FEATURED_STORAGE_KEY;
|
|
184
|
+
var candidates = (items || []).filter(opts.eligible || hasThumbnail);
|
|
185
|
+
if (!candidates.length) return null;
|
|
186
|
+
|
|
187
|
+
var held = readSessionSlug(key);
|
|
188
|
+
if (held) {
|
|
189
|
+
var match = candidates.filter(function (item) { return item.slug === held; })[0];
|
|
190
|
+
if (match) return match;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
var chosen = candidates[Math.floor(Math.random() * candidates.length)];
|
|
194
|
+
if (chosen && chosen.slug) writeSessionSlug(key, chosen.slug);
|
|
195
|
+
return chosen;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* Shared card internals, so the three shapes below cannot drift apart. */
|
|
199
|
+
function blockFields(item) {
|
|
200
|
+
var fm = (item && item.front_matter) || item || {};
|
|
201
|
+
return {
|
|
202
|
+
fm: fm,
|
|
203
|
+
title: fm.title || item.title || item.slug || '',
|
|
204
|
+
date: fm.date || item.date || '',
|
|
205
|
+
lastUpdated: fm.last_updated || item.last_updated || '',
|
|
206
|
+
language: fm.language || item.language || '',
|
|
207
|
+
tags: fm.tags || item.tags || [],
|
|
208
|
+
preview: typeof listingPreview === 'function' ? listingPreview(item) : ''
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Ledger row — the dense horizontal entry used by the rascunhos folio and
|
|
213
|
+
* the writing archive. */
|
|
214
|
+
function renderLedgerRow(item, options) {
|
|
215
|
+
options = options || {};
|
|
216
|
+
var f = blockFields(item);
|
|
217
|
+
var href = options.href || '#';
|
|
218
|
+
var locale = options.locale || 'en';
|
|
219
|
+
|
|
220
|
+
// Callers may supply their own strip parts, the way renderSystemCard
|
|
221
|
+
// does; the default is the date and the language pill. A listing whose
|
|
222
|
+
// entries are all one language gets nothing from the pill and has its
|
|
223
|
+
// own facts to put in that slot, and passing them in beats patching the
|
|
224
|
+
// strip after construction.
|
|
225
|
+
var meta = renderMetaStrip(options.metaParts || [
|
|
226
|
+
formatDate(f.date, locale),
|
|
227
|
+
renderLangPill(f.language)
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
var row = document.createElement('article');
|
|
231
|
+
row.className = 'ledger-row group';
|
|
232
|
+
|
|
233
|
+
var html = '';
|
|
234
|
+
if (meta) html += '<div class="ledger-row-head">' + meta + '</div>';
|
|
235
|
+
html += '<h3 class="ledger-row-title card-title"><a href="' +
|
|
236
|
+
escapeAttr(href) + '">' + escapeHtml(f.title) + '</a></h3>';
|
|
237
|
+
if (f.preview) {
|
|
238
|
+
html += '<p class="ledger-row-copy font-body">' +
|
|
239
|
+
escapeHtml(f.preview) + '</p>';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
var chips = renderTagChips(f.tags, { hrefFor: options.tagHref });
|
|
243
|
+
var cta = renderCtaLink(href, options.ctaLabel);
|
|
244
|
+
if (chips || cta) {
|
|
245
|
+
html += '<div class="ledger-row-foot">' + chips + cta + '</div>';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
row.innerHTML = html;
|
|
249
|
+
return row;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* Archive card — the vertical card used on grids. */
|
|
253
|
+
function renderArchiveCard(item, options) {
|
|
254
|
+
options = options || {};
|
|
255
|
+
var f = blockFields(item);
|
|
256
|
+
var href = options.href || '#';
|
|
257
|
+
var locale = options.locale || 'en';
|
|
258
|
+
|
|
259
|
+
var card = document.createElement('article');
|
|
260
|
+
card.className = 'ledger-card group';
|
|
261
|
+
card.setAttribute('data-dynamic-article', 'true');
|
|
262
|
+
|
|
263
|
+
var html = '<div class="ledger-card-body">';
|
|
264
|
+
// Callers may supply their own strip parts, as renderLedgerRow lets
|
|
265
|
+
// them; the default is the date and the language pill. A grid whose
|
|
266
|
+
// entries are all one language gets nothing from the pill.
|
|
267
|
+
var meta = renderMetaStrip(options.metaParts || [
|
|
268
|
+
formatDate(f.date, locale),
|
|
269
|
+
renderLangPill(f.language)
|
|
270
|
+
]);
|
|
271
|
+
if (meta) html += meta;
|
|
272
|
+
html += '<h3 class="card-title ledger-card-title"><a href="' +
|
|
273
|
+
escapeAttr(href) + '">' + escapeHtml(f.title) + '</a></h3>';
|
|
274
|
+
if (f.preview) {
|
|
275
|
+
html += '<p class="ledger-card-copy font-body">' +
|
|
276
|
+
escapeHtml(f.preview) + '</p>';
|
|
277
|
+
}
|
|
278
|
+
html += renderTagChips(f.tags, { hrefFor: options.tagHref, limit: options.tagLimit });
|
|
279
|
+
// The meta strip above already carries the date, so the footer copy is
|
|
280
|
+
// opt-in rather than the default — printing it twice is the common bug.
|
|
281
|
+
var foot = '';
|
|
282
|
+
if (options.showFooterDates) {
|
|
283
|
+
foot += renderCardFooterDates(f.date, f.lastUpdated, locale);
|
|
284
|
+
}
|
|
285
|
+
foot += renderCtaLink(href, options.ctaLabel);
|
|
286
|
+
// .ledger-card-foot draws a rule above itself, so an empty one rules off
|
|
287
|
+
// nothing. renderLedgerRow guards this the same way.
|
|
288
|
+
if (foot) html += '<div class="ledger-card-foot">' + foot + '</div>';
|
|
289
|
+
html += '</div>';
|
|
290
|
+
|
|
291
|
+
card.innerHTML = html;
|
|
292
|
+
return card;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* Card media. Stills, not clips: the simulation captures these projects
|
|
296
|
+
* publish run to several megabytes each (ARCO's city race is 5.9 MB,
|
|
297
|
+
* FRET's 3.5 MB), and autoplaying a grid of them on an index page is not
|
|
298
|
+
* defensible on a mobile connection. The videos stay in the project
|
|
299
|
+
* bodies, where a reader has chosen to be.
|
|
300
|
+
*
|
|
301
|
+
* A video URL still renders rather than vanishing, but it downloads
|
|
302
|
+
* nothing until the reader asks for it. */
|
|
303
|
+
function renderPosterMedia(src, alt) {
|
|
304
|
+
var url = String(src || '').trim();
|
|
305
|
+
if (!url) return '';
|
|
306
|
+
var isVideo = /\.(mp4|webm|mov)(\?|#|$)/i.test(url);
|
|
307
|
+
if (isVideo) {
|
|
308
|
+
return [
|
|
309
|
+
'<div class="ledger-poster">',
|
|
310
|
+
'<video src="', escapeAttr(url), '" controls preload="none" ',
|
|
311
|
+
'playsinline aria-label="', escapeAttr(alt || ''), '"></video>',
|
|
312
|
+
'</div>'
|
|
313
|
+
].join('');
|
|
314
|
+
}
|
|
315
|
+
return [
|
|
316
|
+
'<div class="ledger-poster">',
|
|
317
|
+
'<img src="', escapeAttr(url), '" alt="', escapeAttr(alt || ''),
|
|
318
|
+
'" loading="lazy" decoding="async"',
|
|
319
|
+
// Thumbnails are frequently hotlinked to third-party hosts. When one
|
|
320
|
+
// dies, remove the frame rather than leaving an empty plate beNewsreader.
|
|
321
|
+
' onerror="var p=this.closest(\'.ledger-poster\'); if (p) p.remove();"',
|
|
322
|
+
'>',
|
|
323
|
+
'</div>'
|
|
324
|
+
].join('');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* System card — the project shape. Carries the fields projects actually
|
|
328
|
+
* have: label, status, and a repository link. */
|
|
329
|
+
function renderSystemCard(item, options) {
|
|
330
|
+
options = options || {};
|
|
331
|
+
var f = blockFields(item);
|
|
332
|
+
var fm = f.fm;
|
|
333
|
+
var href = options.href || '#';
|
|
334
|
+
var name = fm.project || f.title;
|
|
335
|
+
|
|
336
|
+
var card = document.createElement('article');
|
|
337
|
+
card.className = 'ledger-system-card group';
|
|
338
|
+
|
|
339
|
+
var html = renderPosterMedia(fm.thumbnail, name);
|
|
340
|
+
// Callers may supply their own strip parts; the default is whatever the
|
|
341
|
+
// project declares about itself.
|
|
342
|
+
var parts = options.metaParts || [
|
|
343
|
+
fm.label ? escapeHtml(fm.label) : '',
|
|
344
|
+
fm.status ? '<span class="ledger-status">' + escapeHtml(fm.status) + '</span>' : ''
|
|
345
|
+
];
|
|
346
|
+
var head = renderMetaStrip(parts);
|
|
347
|
+
if (head) html += head;
|
|
348
|
+
|
|
349
|
+
// The title's anchor is the card's only link to the project, and CSS
|
|
350
|
+
// stretches it over the whole card (see .ledger-system-card in
|
|
351
|
+
// base.css), so the poster and the tagline navigate as well. That is
|
|
352
|
+
// what replaced the Details CTA this footer used to carry.
|
|
353
|
+
html += '<h3 class="card-title ledger-card-title"><a class="ledger-card-stretch" href="' +
|
|
354
|
+
escapeAttr(href) + '">' + escapeHtml(name) + '</a></h3>';
|
|
355
|
+
|
|
356
|
+
var summary = fm.tagline || f.preview;
|
|
357
|
+
if (summary) {
|
|
358
|
+
html += '<p class="ledger-card-copy font-body">' +
|
|
359
|
+
escapeHtml(summary) + '</p>';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
html += renderTagChips(f.tags, { hrefFor: options.tagHref });
|
|
363
|
+
|
|
364
|
+
// Outbound links only. `.ledger-card-foot` draws a rule above itself, so
|
|
365
|
+
// an empty one rules off nothing; renderLedgerRow and renderArchiveCard
|
|
366
|
+
// guard it the same way.
|
|
367
|
+
var links = [];
|
|
368
|
+
if (fm.github) links.push(renderCtaLink(fm.github, options.repoLabel || 'Repository', { icon: 'code' }));
|
|
369
|
+
if (fm.article_url) links.push(renderCtaLink(fm.article_url, options.articleLabel || 'Article', { icon: 'article' }));
|
|
370
|
+
if (fm.external_url) links.push(renderCtaLink(fm.external_url, options.externalLabel || 'Visit', { icon: 'public' }));
|
|
371
|
+
if (links.length) {
|
|
372
|
+
html += '<div class="ledger-card-foot ledger-card-links">' + links.join('') + '</div>';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
card.innerHTML = html;
|
|
376
|
+
return card;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
{% endraw %}
|