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,534 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: shell
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
{%- comment -%}
|
|
6
|
+
Projects — index and client-side detail view.
|
|
7
|
+
|
|
8
|
+
This page is deliberately less modular than the rest of the site. Projects
|
|
9
|
+
are few, they change rarely, and each one owns a body full of figures,
|
|
10
|
+
videos, plots, diagrams and tables, so the presentation is written for that
|
|
11
|
+
material rather than routed through the generic listing/detail pair.
|
|
12
|
+
|
|
13
|
+
The binding rule still holds: every element here reads a front-matter field
|
|
14
|
+
that exists (`project`, `title`, `tagline`, `label`, `status`, `start_date`,
|
|
15
|
+
`end_date`, `github`, `article_url`, `external_url`, `tags`, `icon`,
|
|
16
|
+
`thumbnail`). Nothing counts, derives or invents. The mockup's metrics
|
|
17
|
+
strip, benchmark ledger, proof transcript, version badges and commit hashes
|
|
18
|
+
have no data source and are not built.
|
|
19
|
+
{%- endcomment -%}
|
|
20
|
+
|
|
21
|
+
<main class="projects-page relative max-w-7xl mx-auto px-4 sm:px-8 md:px-16 pt-10 pb-16" id="projects-main"
|
|
22
|
+
{% include page/listing-runtime-attrs.html
|
|
23
|
+
default_collection="projects"
|
|
24
|
+
default_path="/pages/projects/"
|
|
25
|
+
default_detail="/projects/:slug/" %}>
|
|
26
|
+
|
|
27
|
+
<div id="projects-page-header">
|
|
28
|
+
{% include page/page-header.html key=page.key
|
|
29
|
+
header_class="projects-masthead"
|
|
30
|
+
tags_class="projects-masthead-tags code-technical block hidden" %}
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div id="projects-listing">
|
|
34
|
+
<div id="projects-filter-note"></div>
|
|
35
|
+
<div id="projects-lead"></div>
|
|
36
|
+
<div id="projects-grid-banner"></div>
|
|
37
|
+
<div id="projects-grid" class="projects-grid"></div>
|
|
38
|
+
<p id="projects-state" class="projects-state font-body text-on-surface-variant italic">Loading projects…</p>
|
|
39
|
+
<div id="projects-repos"></div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div id="projects-detail" class="projects-detail hidden">
|
|
43
|
+
<div id="projects-detail-hero"></div>
|
|
44
|
+
{%- comment -%}
|
|
45
|
+
The tracks are the shared detail pair (`text-detail-grid`, defined in
|
|
46
|
+
ledger/detail.css), but this page holds the split back to 1024px instead
|
|
47
|
+
of 768px: at the tablet breakpoint the dossier is around 170px, which
|
|
48
|
+
broke a long project slug like `example/freshy` across three lines mid-word. 1024px is
|
|
49
|
+
also where the shared table of contents swaps from its disclosure to the
|
|
50
|
+
sidebar, so the two change together. ledger/projects.css keeps the
|
|
51
|
+
tablet band stacked.
|
|
52
|
+
{%- endcomment -%}
|
|
53
|
+
<div class="grid grid-cols-1 gap-8 lg:gap-12 items-start text-detail-grid">
|
|
54
|
+
<aside id="projects-detail-sidebar" class="projects-dossier-column"></aside>
|
|
55
|
+
<div id="projects-detail-main" class="projects-detail-column"></div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
</main>
|
|
60
|
+
|
|
61
|
+
<script>
|
|
62
|
+
(function () {
|
|
63
|
+
'use strict';
|
|
64
|
+
|
|
65
|
+
var runtime = window.ContentRuntime;
|
|
66
|
+
if (!runtime) return;
|
|
67
|
+
|
|
68
|
+
var esc = runtime.escapeHtml;
|
|
69
|
+
var escAttr = runtime.escapeAttr;
|
|
70
|
+
|
|
71
|
+
var configRoot = document.querySelector('[data-listing-collection]');
|
|
72
|
+
var collection = (configRoot && configRoot.getAttribute('data-listing-collection')) || 'projects';
|
|
73
|
+
var listingPath = (configRoot && configRoot.getAttribute('data-listing-path')) || '/pages/projects/';
|
|
74
|
+
var detailPattern = (configRoot && configRoot.getAttribute('data-listing-detail')) || '/projects/:slug/';
|
|
75
|
+
if (listingPath.slice(-1) !== '/') listingPath += '/';
|
|
76
|
+
|
|
77
|
+
var listing = document.getElementById('projects-listing');
|
|
78
|
+
var noteRoot = document.getElementById('projects-filter-note');
|
|
79
|
+
var leadRoot = document.getElementById('projects-lead');
|
|
80
|
+
var bannerRoot = document.getElementById('projects-grid-banner');
|
|
81
|
+
var grid = document.getElementById('projects-grid');
|
|
82
|
+
var state = document.getElementById('projects-state');
|
|
83
|
+
var reposRoot = document.getElementById('projects-repos');
|
|
84
|
+
|
|
85
|
+
var detail = document.getElementById('projects-detail');
|
|
86
|
+
var detailHero = document.getElementById('projects-detail-hero');
|
|
87
|
+
var detailSidebar = document.getElementById('projects-detail-sidebar');
|
|
88
|
+
var detailMain = document.getElementById('projects-detail-main');
|
|
89
|
+
var pageHeader = document.getElementById('projects-page-header');
|
|
90
|
+
|
|
91
|
+
var params = new URLSearchParams(window.location.search);
|
|
92
|
+
var selectedProject = runtime.contentSlug('project') ||
|
|
93
|
+
runtime.slugFromDetailPattern(window.location.pathname, detailPattern);
|
|
94
|
+
var selectedTag = (params.get('tag') || '').trim();
|
|
95
|
+
|
|
96
|
+
function entryUrl(slug) {
|
|
97
|
+
return runtime.detailUrl(detailPattern, slug);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ── Field readers ──────────────────────────────────────────────────
|
|
101
|
+
Each returns '' when its field is unset, so an absent field drops the
|
|
102
|
+
element rather than leaving a placeholder beNewsreader. */
|
|
103
|
+
|
|
104
|
+
function projectName(item) {
|
|
105
|
+
var fm = item.front_matter || {};
|
|
106
|
+
return fm.project || fm.title || item.slug || '';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* `status` is almost always the word the date span already ends with, so
|
|
110
|
+
an ongoing project would otherwise read "Ongoing" twice in one line.
|
|
111
|
+
Any other status — archived, paused — has no such carrier and is shown
|
|
112
|
+
on its own. */
|
|
113
|
+
function periodText(fm) {
|
|
114
|
+
if (!fm.start_date) return '';
|
|
115
|
+
var span = String(fm.start_date);
|
|
116
|
+
if (fm.status === 'ongoing') return span + ' – Ongoing';
|
|
117
|
+
if (fm.end_date) return span + ' – ' + fm.end_date;
|
|
118
|
+
return span;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function statusPart(fm) {
|
|
122
|
+
var value = String(fm.status || '').trim();
|
|
123
|
+
if (!value) return '';
|
|
124
|
+
if (value.toLowerCase() === 'ongoing' && fm.start_date) return '';
|
|
125
|
+
return '<span class="ledger-status">' + esc(value) + '</span>';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function iconMark(fm, className) {
|
|
129
|
+
if (!fm.icon) return '';
|
|
130
|
+
return '<span class="material-symbols-outlined ' + className + '" aria-hidden="true">' +
|
|
131
|
+
esc(fm.icon) + '</span>';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* The ledger line above every project name: glyph, working period, the
|
|
135
|
+
project's own one-line stack description, and any status the period
|
|
136
|
+
does not already carry. */
|
|
137
|
+
function ledgerLine(fm, iconClass) {
|
|
138
|
+
var period = periodText(fm);
|
|
139
|
+
var lead = period ? '<span class="ledger-span">' + esc(period) + '</span>' : '';
|
|
140
|
+
return runtime.renderMetaStrip([
|
|
141
|
+
lead,
|
|
142
|
+
fm.label ? esc(fm.label) : '',
|
|
143
|
+
statusPart(fm)
|
|
144
|
+
]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Media plate. A project with a `thumbnail` shows it; one without shows
|
|
148
|
+
its own `icon` glyph on the panel step instead. The fallback is not a
|
|
149
|
+
placeholder for a missing image — `icon` is set on every project and is
|
|
150
|
+
already how the site marks a project elsewhere — so a project with no
|
|
151
|
+
imagery still reads as finished rather than as broken. */
|
|
152
|
+
function plateHtml(item, modifier) {
|
|
153
|
+
var fm = item.front_matter || {};
|
|
154
|
+
var poster = runtime.renderPosterMedia(fm.thumbnail, projectName(item));
|
|
155
|
+
var cls = 'projects-plate' + (modifier ? ' ' + modifier : '');
|
|
156
|
+
if (poster) return '<div class="' + cls + '">' + poster + '</div>';
|
|
157
|
+
if (!fm.icon) return '';
|
|
158
|
+
return '<div class="' + cls + ' projects-plate--mark">' +
|
|
159
|
+
'<span class="material-symbols-outlined projects-plate-glyph" aria-hidden="true">' +
|
|
160
|
+
esc(fm.icon) + '</span></div>';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function tagHref(tag) {
|
|
164
|
+
return listingPath + '?tag=' + encodeURIComponent(tag);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Outbound links, in the order a reader wants them: the code, the writing
|
|
168
|
+
about it, the thing itself. */
|
|
169
|
+
function outboundLinks(fm) {
|
|
170
|
+
var links = [];
|
|
171
|
+
if (fm.github) links.push(runtime.renderCtaLink(fm.github, 'Source'));
|
|
172
|
+
if (fm.article_url) links.push(runtime.renderCtaLink(fm.article_url, 'Read the article'));
|
|
173
|
+
if (fm.external_url) links.push(runtime.renderCtaLink(fm.external_url, 'Visit'));
|
|
174
|
+
return links;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function repoPath(url) {
|
|
178
|
+
return String(url || '')
|
|
179
|
+
.replace(/^https?:\/\/(www\.)?github\.com\//i, '')
|
|
180
|
+
.replace(/\/+$/, '');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function articleSlugFromUrl(url) {
|
|
184
|
+
var match = String(url || '').match(/\/articles\/([^\/?#]+)/i);
|
|
185
|
+
return match ? decodeURIComponent(match[1]) : '';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* Resolve the linked article's title so the dossier names the piece rather
|
|
189
|
+
than saying "Read the article". Falls back when the index has no match. */
|
|
190
|
+
async function articleTitleFor(url) {
|
|
191
|
+
var slug = articleSlugFromUrl(url);
|
|
192
|
+
if (!slug) return '';
|
|
193
|
+
try {
|
|
194
|
+
var items = await runtime.loadWritingIndex('articles');
|
|
195
|
+
var hit = (items || []).find(function (entry) {
|
|
196
|
+
return entry && entry.slug === slug;
|
|
197
|
+
});
|
|
198
|
+
if (!hit) return '';
|
|
199
|
+
var fm = hit.front_matter || {};
|
|
200
|
+
return String(fm.title || hit.title || '').trim();
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return '';
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* ── Index ──────────────────────────────────────────────────────────── */
|
|
207
|
+
|
|
208
|
+
/* The featured project is drawn the way the writing listings draw their
|
|
209
|
+
featured article — at random from the whole index, then held in session
|
|
210
|
+
storage so navigating around the site does not reshuffle it. It keeps
|
|
211
|
+
its own key, because the article the home page leads with is a separate
|
|
212
|
+
pick, and every project is a candidate: one without a `thumbnail` still
|
|
213
|
+
leads with its glyph plate, so the shared thumbnail rule does not
|
|
214
|
+
apply here. */
|
|
215
|
+
var FEATURED_PICK = {
|
|
216
|
+
storageKey: 'featured-project-slug',
|
|
217
|
+
eligible: function () { return true; }
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/* Lead plate. One entry opens the page at full width, the way a portfolio
|
|
221
|
+
leads with one piece of work rather than seven equal thumbnails. */
|
|
222
|
+
function renderLead(item) {
|
|
223
|
+
var fm = item.front_matter || {};
|
|
224
|
+
var href = entryUrl(item.slug);
|
|
225
|
+
var name = projectName(item);
|
|
226
|
+
var line = ledgerLine(fm, 'ledger-system-icon');
|
|
227
|
+
|
|
228
|
+
var html = '<article class="projects-lead">';
|
|
229
|
+
html += '<span class="shard-accent projects-lead-shard" aria-hidden="true"></span>';
|
|
230
|
+
/* The plate links to the same place as the title beside it. It is out of
|
|
231
|
+
the tab order so keyboard users are not made to pass the same
|
|
232
|
+
destination twice, but it keeps its accessible name — the poster's alt
|
|
233
|
+
— rather than being hidden from assistive technology, which would be
|
|
234
|
+
wrong on something focusable at all. */
|
|
235
|
+
html += '<a class="projects-lead-media" href="' + escAttr(href) + '" tabindex="-1">' +
|
|
236
|
+
plateHtml(item, 'projects-plate--lead') + '</a>';
|
|
237
|
+
html += '<div class="projects-lead-body">';
|
|
238
|
+
if (line) html += line;
|
|
239
|
+
html += '<h2 class="projects-lead-title"><a href="' + escAttr(href) + '">' + esc(name) + '</a></h2>';
|
|
240
|
+
if (fm.title && fm.title !== name) {
|
|
241
|
+
html += '<p class="projects-lead-subtitle">' + esc(fm.title) + '</p>';
|
|
242
|
+
}
|
|
243
|
+
if (fm.tagline) {
|
|
244
|
+
html += '<p class="projects-lead-copy font-body">' + esc(fm.tagline) + '</p>';
|
|
245
|
+
}
|
|
246
|
+
html += runtime.renderTagChips(fm.tags, { hrefFor: tagHref });
|
|
247
|
+
/* The plate and the title already lead to the project, so the row keeps
|
|
248
|
+
only what leaves the site. It disappears when a project publishes no
|
|
249
|
+
outbound link at all. */
|
|
250
|
+
var leadLinks = outboundLinks(fm);
|
|
251
|
+
if (leadLinks.length) {
|
|
252
|
+
html += '<div class="projects-lead-links ledger-card-links">' + leadLinks.join('') + '</div>';
|
|
253
|
+
}
|
|
254
|
+
html += '</div></article>';
|
|
255
|
+
leadRoot.innerHTML = html;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* Grid card. The shared system card already carries exactly the project
|
|
259
|
+
shape — poster, ledger line, name, tagline, chips, links — so it stays
|
|
260
|
+
the block of record; the only thing grafted on is the glyph plate for a
|
|
261
|
+
project that publishes no imagery, which the shared block has no hook
|
|
262
|
+
for. */
|
|
263
|
+
function renderCard(item) {
|
|
264
|
+
var fm = item.front_matter || {};
|
|
265
|
+
var period = periodText(fm);
|
|
266
|
+
var card = runtime.renderSystemCard(item, {
|
|
267
|
+
href: entryUrl(item.slug),
|
|
268
|
+
repoLabel: 'Source',
|
|
269
|
+
articleLabel: 'Read the article',
|
|
270
|
+
externalLabel: 'Visit',
|
|
271
|
+
metaParts: [
|
|
272
|
+
period ? '<span class="ledger-span">' + esc(period) + '</span>' : '',
|
|
273
|
+
fm.label ? esc(fm.label) : '',
|
|
274
|
+
statusPart(fm)
|
|
275
|
+
],
|
|
276
|
+
tagHref: tagHref
|
|
277
|
+
});
|
|
278
|
+
card.setAttribute('data-dynamic-project', 'true');
|
|
279
|
+
/* The glyph reads as part of the name, so it sits on the title's line at
|
|
280
|
+
the title's size rather than shrunk into the metadata above it. */
|
|
281
|
+
var cardTitle = card.querySelector('.ledger-card-title');
|
|
282
|
+
if (cardTitle && fm.icon) {
|
|
283
|
+
cardTitle.insertAdjacentHTML('afterbegin', iconMark(fm, 'projects-title-icon'));
|
|
284
|
+
}
|
|
285
|
+
if (!fm.thumbnail) {
|
|
286
|
+
var plate = plateHtml(item, 'projects-plate--card');
|
|
287
|
+
if (plate) card.insertAdjacentHTML('afterbegin', plate);
|
|
288
|
+
}
|
|
289
|
+
return card;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* Closing ledger. One row per repository that front matter actually
|
|
293
|
+
names — five of seven today. The mockup's licence stamp, spec download
|
|
294
|
+
and commit hashes have nothing beNewsreader them and are not drawn. */
|
|
295
|
+
function renderRepositories(items) {
|
|
296
|
+
var rows = items.filter(function (item) {
|
|
297
|
+
return (item.front_matter || {}).github;
|
|
298
|
+
});
|
|
299
|
+
if (!rows.length) {
|
|
300
|
+
reposRoot.innerHTML = '';
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
var html = '<section class="projects-repos">';
|
|
304
|
+
html += runtime.renderSectionBanner({ kicker: 'Open source', title: 'Repositories' });
|
|
305
|
+
html += '<div class="projects-repo-list">';
|
|
306
|
+
rows.forEach(function (item) {
|
|
307
|
+
var fm = item.front_matter || {};
|
|
308
|
+
html += '<a class="projects-repo" href="' + escAttr(fm.github) + '">' +
|
|
309
|
+
'<span class="projects-repo-name">' + esc(projectName(item)) + '</span>' +
|
|
310
|
+
'<span class="projects-repo-path code-technical">' + esc(repoPath(fm.github)) + '</span>' +
|
|
311
|
+
'<span class="material-symbols-outlined ledger-cta-arrow" aria-hidden="true">chevron_right</span>' +
|
|
312
|
+
'</a>';
|
|
313
|
+
});
|
|
314
|
+
html += '</div></section>';
|
|
315
|
+
reposRoot.innerHTML = html;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* The count is a figure, so it takes the notation role; the words around
|
|
319
|
+
it are chrome labels and stay in the label and body faces. */
|
|
320
|
+
function renderFilterNote(tag, count, total) {
|
|
321
|
+
noteRoot.innerHTML = '<div class="projects-filter-note">' +
|
|
322
|
+
'<span class="projects-filter-note-label">Filtered by</span>' +
|
|
323
|
+
'<span class="ledger-chip">' + esc(tag) + '</span>' +
|
|
324
|
+
'<span class="projects-filter-note-count code-technical">' + count + ' of ' + total + '</span>' +
|
|
325
|
+
'<a class="ledger-cta" href="' + escAttr(listingPath) + '"><span>Show all</span></a>' +
|
|
326
|
+
'</div>';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function matchesTag(item, tag) {
|
|
330
|
+
var tags = (item.front_matter || {}).tags || [];
|
|
331
|
+
return (Array.isArray(tags) ? tags : []).some(function (value) {
|
|
332
|
+
return String(value).toLowerCase() === tag.toLowerCase();
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function bySortOrder(a, b) {
|
|
337
|
+
var ao = (a.front_matter && a.front_matter.sort_order) || 0;
|
|
338
|
+
var bo = (b.front_matter && b.front_matter.sort_order) || 0;
|
|
339
|
+
return Number(ao) - Number(bo);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function showListing() {
|
|
343
|
+
if (listing) listing.classList.remove('hidden');
|
|
344
|
+
if (detail) detail.classList.add('hidden');
|
|
345
|
+
if (pageHeader) pageHeader.classList.remove('hidden');
|
|
346
|
+
if (detailMain) detailMain.innerHTML = '';
|
|
347
|
+
if (detailSidebar) detailSidebar.innerHTML = '';
|
|
348
|
+
if (detailHero) detailHero.innerHTML = '';
|
|
349
|
+
runtime.removeArticleToc();
|
|
350
|
+
runtime.removeRuntimeBackLink();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function renderListing(projects) {
|
|
354
|
+
var ordered = projects.slice().sort(bySortOrder);
|
|
355
|
+
var visible = selectedTag
|
|
356
|
+
? ordered.filter(function (item) { return matchesTag(item, selectedTag); })
|
|
357
|
+
: ordered;
|
|
358
|
+
|
|
359
|
+
grid.innerHTML = '';
|
|
360
|
+
leadRoot.innerHTML = '';
|
|
361
|
+
bannerRoot.innerHTML = '';
|
|
362
|
+
noteRoot.innerHTML = '';
|
|
363
|
+
|
|
364
|
+
if (selectedTag) renderFilterNote(selectedTag, visible.length, ordered.length);
|
|
365
|
+
|
|
366
|
+
if (!visible.length) {
|
|
367
|
+
state.textContent = 'No project carries that tag.';
|
|
368
|
+
state.classList.remove('hidden');
|
|
369
|
+
reposRoot.innerHTML = '';
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
state.classList.add('hidden');
|
|
373
|
+
|
|
374
|
+
/* A filtered view is a short answer to a question, so it stays a flat
|
|
375
|
+
grid; the lead plate is for the unfiltered page, where the drawn entry
|
|
376
|
+
is genuinely the opening statement. The grid below is then everything
|
|
377
|
+
but that entry, so nothing is listed twice. */
|
|
378
|
+
var rest = visible;
|
|
379
|
+
if (!selectedTag && visible.length > 1) {
|
|
380
|
+
var featured = runtime.pickFeatured(ordered, FEATURED_PICK) || visible[0];
|
|
381
|
+
renderLead(featured);
|
|
382
|
+
rest = visible.filter(function (item) { return item.slug !== featured.slug; });
|
|
383
|
+
bannerRoot.innerHTML = runtime.renderSectionBanner({
|
|
384
|
+
kicker: 'Index',
|
|
385
|
+
title: 'Other projects'
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
rest.forEach(function (item) { grid.appendChild(renderCard(item)); });
|
|
390
|
+
renderRepositories(visible);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/* ── Detail ─────────────────────────────────────────────────────────── */
|
|
394
|
+
|
|
395
|
+
/* Full-width masthead. The generic detail view puts the title in a narrow
|
|
396
|
+
third-column dossier, where "Base Operating System for Sensors and
|
|
397
|
+
Actuators" breaks over five lines. A project leads with its short name,
|
|
398
|
+
carries its long title underneath, and states what it is before the
|
|
399
|
+
reader has to scroll. */
|
|
400
|
+
function renderHero(item) {
|
|
401
|
+
var fm = item.front_matter || {};
|
|
402
|
+
var name = projectName(item);
|
|
403
|
+
var line = ledgerLine(fm, 'projects-hero-icon');
|
|
404
|
+
|
|
405
|
+
var html = '<header class="projects-hero">';
|
|
406
|
+
html += '<span class="shard-accent projects-hero-shard" aria-hidden="true"></span>';
|
|
407
|
+
html += '<div class="projects-hero-body">';
|
|
408
|
+
if (line) html += line;
|
|
409
|
+
html += '<h1 class="projects-hero-title">' + esc(name) + '</h1>';
|
|
410
|
+
if (fm.title && fm.title !== name) {
|
|
411
|
+
html += '<p class="projects-hero-subtitle">' + esc(fm.title) + '</p>';
|
|
412
|
+
}
|
|
413
|
+
if (fm.tagline) {
|
|
414
|
+
html += '<p class="projects-hero-standfirst font-body">' + esc(fm.tagline) + '</p>';
|
|
415
|
+
}
|
|
416
|
+
var links = outboundLinks(fm);
|
|
417
|
+
if (links.length) {
|
|
418
|
+
html += '<div class="projects-hero-actions">' + links.join('') + '</div>';
|
|
419
|
+
}
|
|
420
|
+
html += '</div>';
|
|
421
|
+
var plate = plateHtml(item, 'projects-plate--hero');
|
|
422
|
+
if (plate) html += '<div class="projects-hero-media">' + plate + '</div>';
|
|
423
|
+
html += '</header>';
|
|
424
|
+
detailHero.innerHTML = html;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function dossierRow(term, value) {
|
|
428
|
+
if (!value) return '';
|
|
429
|
+
return '<div class="projects-dossier-row">' +
|
|
430
|
+
'<dt>' + esc(term) + '</dt><dd>' + value + '</dd></div>';
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/* Dossier. The mockup's right-hand spec column — ARCH, STACK, SPEC — with
|
|
434
|
+
the fabricated rows replaced by the four facts a project actually
|
|
435
|
+
declares. `mountArticleToc` appends the contents list to this column's
|
|
436
|
+
first child and inserts the mobile disclosure after `.article-meta`, so
|
|
437
|
+
both hooks below are emitted on purpose. */
|
|
438
|
+
async function renderDossier(item) {
|
|
439
|
+
var fm = item.front_matter || {};
|
|
440
|
+
var period = periodText(fm);
|
|
441
|
+
var rows = '';
|
|
442
|
+
rows += dossierRow('Field', fm.label ? esc(fm.label) : '');
|
|
443
|
+
rows += dossierRow('Period', esc(period));
|
|
444
|
+
if (!period && fm.status) rows += dossierRow('Status', esc(fm.status));
|
|
445
|
+
if (fm.github) {
|
|
446
|
+
rows += dossierRow('Repository',
|
|
447
|
+
'<a href="' + escAttr(fm.github) + '">' + esc(repoPath(fm.github)) + '</a>');
|
|
448
|
+
}
|
|
449
|
+
if (fm.article_url) {
|
|
450
|
+
var articleTitle = await articleTitleFor(fm.article_url);
|
|
451
|
+
rows += dossierRow('Article',
|
|
452
|
+
'<a href="' + escAttr(fm.article_url) + '">' +
|
|
453
|
+
esc(articleTitle || 'Read the article') + '</a>');
|
|
454
|
+
}
|
|
455
|
+
if (fm.external_url) {
|
|
456
|
+
rows += dossierRow('Live', '<a href="' + escAttr(fm.external_url) + '">' +
|
|
457
|
+
esc(String(fm.external_url).replace(/^https?:\/\/(www\.)?/i, '').replace(/\/+$/, '')) +
|
|
458
|
+
'</a>');
|
|
459
|
+
}
|
|
460
|
+
var chips = runtime.renderTagChips(fm.tags, { hrefFor: tagHref });
|
|
461
|
+
if (chips) rows += dossierRow('Tags', chips);
|
|
462
|
+
|
|
463
|
+
var html = '<div class="lg:sticky lg:top-32 projects-dossier">';
|
|
464
|
+
if (rows) {
|
|
465
|
+
html += '<p class="projects-dossier-title">Dossier</p>';
|
|
466
|
+
html += '<dl class="projects-dossier-rows article-meta">' + rows + '</dl>';
|
|
467
|
+
}
|
|
468
|
+
html += runtime.buildDetailActions(fm);
|
|
469
|
+
html += '</div>';
|
|
470
|
+
detailSidebar.innerHTML = html;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function renderProjectDetail(item) {
|
|
474
|
+
var fm = item.front_matter || {};
|
|
475
|
+
if (!fm.layout) fm.layout = 'project';
|
|
476
|
+
/* Projects carry no `description`, so the social card and the meta
|
|
477
|
+
description are built from the tagline. A tagline is a short paragraph
|
|
478
|
+
rather than one sentence, and a search result cut off mid-clause reads
|
|
479
|
+
worse than a shorter one that finishes its thought, so only the
|
|
480
|
+
opening sentence travels. The page still shows the whole thing. */
|
|
481
|
+
if (!fm.description && fm.tagline) fm.description = runtime.leadSentence(fm.tagline);
|
|
482
|
+
|
|
483
|
+
if (listing) listing.classList.add('hidden');
|
|
484
|
+
if (pageHeader) pageHeader.classList.add('hidden');
|
|
485
|
+
if (detail) detail.classList.remove('hidden');
|
|
486
|
+
|
|
487
|
+
renderHero(item);
|
|
488
|
+
await renderDossier(item);
|
|
489
|
+
|
|
490
|
+
var allItems = await runtime.loadWritingIndex(collection);
|
|
491
|
+
var sorted = allItems.slice().sort(bySortOrder);
|
|
492
|
+
var index = sorted.findIndex(function (entry) { return entry.slug === item.slug; });
|
|
493
|
+
|
|
494
|
+
await runtime.renderRuntimeDetailSection({
|
|
495
|
+
detailAttribute: 'data-dynamic-project-detail',
|
|
496
|
+
bodyMarkdown: item.body_markdown,
|
|
497
|
+
frontMatter: fm,
|
|
498
|
+
mainColumn: detailMain,
|
|
499
|
+
sidebarEl: detailSidebar,
|
|
500
|
+
/* The list is sorted ascending by `sort_order`, so "previous" is the
|
|
501
|
+
entry before this one in that order. It used to be the entry after,
|
|
502
|
+
which walked the register backwards. */
|
|
503
|
+
footerNav: {
|
|
504
|
+
lang: 'enus',
|
|
505
|
+
previousUrl: index > 0 ? entryUrl(sorted[index - 1].slug) : null,
|
|
506
|
+
nextUrl: index >= 0 && index < sorted.length - 1 ? entryUrl(sorted[index + 1].slug) : null
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function loadListing() {
|
|
512
|
+
showListing();
|
|
513
|
+
runtime.loadWritingIndex(collection).then(renderListing).catch(function (error) {
|
|
514
|
+
console.error(error);
|
|
515
|
+
state.textContent = 'Projects could not be loaded right now.';
|
|
516
|
+
state.classList.remove('hidden');
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (selectedProject) {
|
|
521
|
+
runtime.loadCollectionItem(collection, selectedProject).then(function (item) {
|
|
522
|
+
return renderProjectDetail(item);
|
|
523
|
+
}).catch(function (error) {
|
|
524
|
+
console.error(error);
|
|
525
|
+
showListing();
|
|
526
|
+
state.textContent = 'This project could not be loaded right now.';
|
|
527
|
+
state.classList.remove('hidden');
|
|
528
|
+
});
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
loadListing();
|
|
533
|
+
})();
|
|
534
|
+
</script>
|