From 3795db76b005c395e526c50c2e5ee5f45fc1f259 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Mon, 20 Jul 2026 19:56:18 +0200 Subject: [PATCH] Fix calendar activity heatmap in preview and generation --- crates/bds-core/src/engine/preview.rs | 29 ++++++++++++++----- crates/bds-core/src/engine/site_assets.rs | 27 +++++++++++++++++ .../rfc1437-sample/assets/bds.css | 18 ++++++++---- .../rfc1437-sample/assets/calendar-runtime.js | 11 +++---- specs/rendering.allium | 6 ++++ 5 files changed, 71 insertions(+), 20 deletions(-) diff --git a/crates/bds-core/src/engine/preview.rs b/crates/bds-core/src/engine/preview.rs index 95ea447..be15bff 100644 --- a/crates/bds-core/src/engine/preview.rs +++ b/crates/bds-core/src/engine/preview.rs @@ -592,16 +592,16 @@ fn serve_scoped_file( let Some(relative) = path.strip_prefix(prefix) else { return Ok(None); }; + let bundled_path = format!("{scope_dir}/{relative}"); + if let Some(bytes) = crate::engine::site_assets::bundled_site_asset(&bundled_path) { + let mime = guess_content_type(Path::new(&bundled_path)); + return Ok(Some( + (StatusCode::OK, [(header::CONTENT_TYPE, mime)], bytes).into_response(), + )); + } let scope_root = data_dir.join(scope_dir); let candidate = scope_root.join(relative); if !candidate.exists() || !candidate.is_file() { - let bundled_path = format!("{scope_dir}/{relative}"); - if let Some(bytes) = crate::engine::site_assets::bundled_site_asset(&bundled_path) { - let mime = guess_content_type(Path::new(&bundled_path)); - return Ok(Some( - (StatusCode::OK, [(header::CONTENT_TYPE, mime)], bytes).into_response(), - )); - } return Ok(Some(error_response( StatusCode::NOT_FOUND, "preview asset not found", @@ -1167,6 +1167,11 @@ mod tests { std::fs::create_dir_all(dir.path().join("html/pagefind")).unwrap(); std::fs::write(dir.path().join("media/ok.txt"), "ok").unwrap(); std::fs::write(dir.path().join("assets/site.css"), "body { color: red; }").unwrap(); + std::fs::write( + dir.path().join("assets/calendar-runtime.js"), + "window.staleCalendarRuntime = true;", + ) + .unwrap(); std::fs::write(dir.path().join("images/custom.svg"), "").unwrap(); std::fs::write( dir.path().join("html/pagefind/pagefind-ui.js"), @@ -1194,6 +1199,14 @@ mod tests { .unwrap(); let media_body = media.text().unwrap(); let asset_body = asset.text().unwrap(); + let calendar_runtime_body = client + .get(format!( + "http://{PREVIEW_HOST}:{PREVIEW_PORT}/assets/calendar-runtime.js" + )) + .send() + .unwrap() + .text() + .unwrap(); let image_body = client .get(format!( "http://{PREVIEW_HOST}:{PREVIEW_PORT}/images/custom.svg" @@ -1214,6 +1227,8 @@ mod tests { assert_eq!(media_body, "ok"); assert!(asset_body.contains("color: red")); + assert!(calendar_runtime_body.contains("--blog-calendar-heat-strength")); + assert!(!calendar_runtime_body.contains("staleCalendarRuntime")); assert!(image_body.contains("")); assert!(pagefind_body.contains("window.pagefind")); } diff --git a/crates/bds-core/src/engine/site_assets.rs b/crates/bds-core/src/engine/site_assets.rs index 9e25c14..bff768b 100644 --- a/crates/bds-core/src/engine/site_assets.rs +++ b/crates/bds-core/src/engine/site_assets.rs @@ -278,3 +278,30 @@ pub(crate) fn write_bundled_site_assets( } Ok(()) } + +#[cfg(test)] +mod tests { + use super::bundled_site_asset; + + fn bundled_text(path: &str) -> &'static str { + std::str::from_utf8(bundled_site_asset(path).unwrap()).unwrap() + } + + #[test] + fn calendar_heatmap_marks_only_populated_periods_with_one_accent() { + let runtime = bundled_text("assets/calendar-runtime.js"); + let styles = bundled_text("assets/bds.css"); + + assert!(runtime.contains("--blog-calendar-heat-strength")); + assert!(!runtime.contains("--blog-calendar-heat-hue")); + assert!(styles.contains( + "[data-vc-date]:not([data-blog-calendar-has-posts='true']) [data-vc-date-btn]" + )); + assert!(styles.contains("background-color: transparent !important;")); + assert!(styles.contains("border-color: transparent !important;")); + assert!(styles.contains("color-mix(in srgb, var(--pico-primary")); + assert!(styles.contains("var(--blog-calendar-heat-strength")); + assert!(styles.contains("transparent) !important;")); + assert!(!styles.contains("hsl(var(--blog-calendar-heat-hue")); + } +} diff --git a/fixtures/golden-generated-sites/rfc1437-sample/assets/bds.css b/fixtures/golden-generated-sites/rfc1437-sample/assets/bds.css index 6e1ef60..3e4f52a 100644 --- a/fixtures/golden-generated-sites/rfc1437-sample/assets/bds.css +++ b/fixtures/golden-generated-sites/rfc1437-sample/assets/bds.css @@ -49,14 +49,20 @@ main { display: grid; gap: 1rem; } [data-blog-calendar-root] [data-vc-week-day] { font-size: .68rem; line-height: .9rem; min-width: 1.45rem; } [data-blog-calendar-root] [data-vc-date] { padding-top: 0; padding-bottom: 0; } [data-blog-calendar-root] [data-vc-date-btn] { min-height: 1.45rem; min-width: 1.45rem; font-size: .68rem; line-height: .9rem; } -[data-blog-calendar-has-posts='true'] [data-vc-date-btn] { - border-color: hsl(var(--blog-calendar-heat-hue, 210) 85% 42% / .95); - background-color: hsl(var(--blog-calendar-heat-hue, 210) 88% 52% / var(--blog-calendar-heat-alpha, 0)); +[data-blog-calendar-root] [data-vc-date]:not([data-blog-calendar-has-posts='true']) [data-vc-date-btn], +[data-blog-calendar-root] [data-vc-months-month]:not([data-blog-calendar-has-posts='true']), +[data-blog-calendar-root] [data-vc-years-year]:not([data-blog-calendar-has-posts='true']) { + background-color: transparent !important; + border-color: transparent !important; +} +[data-blog-calendar-root] [data-vc-date][data-blog-calendar-has-posts='true'] [data-vc-date-btn] { + background-color: color-mix(in srgb, var(--pico-primary, #0172ad) var(--blog-calendar-heat-strength, 0%), transparent) !important; + border-color: var(--pico-primary, #0172ad) !important; } [data-blog-calendar-root] [data-vc-months-month][data-blog-calendar-has-posts='true'], [data-blog-calendar-root] [data-vc-years-year][data-blog-calendar-has-posts='true'] { - background-color: hsl(var(--blog-calendar-heat-hue, 210) 88% 52% / var(--blog-calendar-heat-alpha, 0)); - border-color: hsl(var(--blog-calendar-heat-hue, 210) 85% 42% / .95); + background-color: color-mix(in srgb, var(--pico-primary, #0172ad) var(--blog-calendar-heat-strength, 0%), transparent) !important; + border-color: var(--pico-primary, #0172ad) !important; } .post { border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); padding: 1rem; background: var(--pico-card-background-color, var(--card-background-color)); min-width: 0; } .post pre { position: relative; overflow-x: auto; max-width: 100%; border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); border-radius: .3rem; margin: .9rem 0; padding: .85rem .9rem; background: var(--pico-code-background-color, rgba(33, 38, 45, .82)); box-sizing: border-box; } @@ -166,4 +172,4 @@ main { display: grid; gap: 1rem; } .blog-search-panel .pagefind-ui__result-excerpt { font-size: .78rem; color: var(--pico-muted-color, var(--muted-color)); } .blog-search-panel .pagefind-ui__result-excerpt mark { background-color: var(--pico-primary-focus, rgba(255,223,0,.35)); color: inherit; } .blog-search-panel .pagefind-ui__button { color: var(--pico-primary, var(--primary)); background: none; border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); border-radius: .2rem; font-size: .78rem; cursor: pointer; } -.blog-search-panel .pagefind-ui__button:hover { border-color: var(--pico-primary, var(--primary)); } \ No newline at end of file +.blog-search-panel .pagefind-ui__button:hover { border-color: var(--pico-primary, var(--primary)); } diff --git a/fixtures/golden-generated-sites/rfc1437-sample/assets/calendar-runtime.js b/fixtures/golden-generated-sites/rfc1437-sample/assets/calendar-runtime.js index de53db2..a7dc94e 100644 --- a/fixtures/golden-generated-sites/rfc1437-sample/assets/calendar-runtime.js +++ b/fixtures/golden-generated-sites/rfc1437-sample/assets/calendar-runtime.js @@ -55,17 +55,14 @@ function applyHeatStyle(target, count, maxCount) { if (!(target instanceof HTMLElement) || !Number.isFinite(count) || count <= 0 || !Number.isFinite(maxCount) || maxCount <= 0) { - target?.style?.setProperty('--blog-calendar-heat-alpha', '0'); - target?.style?.setProperty('--blog-calendar-heat-hue', '210'); + target?.style?.setProperty('--blog-calendar-heat-strength', '0%'); return; } const normalized = Math.min(1, count / maxCount); - const hue = Math.round(210 - (210 * normalized)); - const alpha = (0.30 + normalized * 0.65).toFixed(3); + const strength = Math.round(30 + normalized * 60); - target.style.setProperty('--blog-calendar-heat-hue', String(hue)); - target.style.setProperty('--blog-calendar-heat-alpha', alpha); + target.style.setProperty('--blog-calendar-heat-strength', String(strength) + '%'); } function navigateTo(pathname) { @@ -296,4 +293,4 @@ setPanelOpen(false); }); } -})(); \ No newline at end of file +})(); diff --git a/specs/rendering.allium b/specs/rendering.allium index 28cd08d..09db232 100644 --- a/specs/rendering.allium +++ b/specs/rendering.allium @@ -156,6 +156,12 @@ invariant CompleteStarterNavigation { -- calendar entry, and provides localized search empty-result text. } +invariant CalendarActivityStyling { + -- Calendar days, months, and years with no posts are visually unmarked. + -- Populated periods use one theme-compatible accent whose intensity grows + -- with the relative post count; count differences never change its hue. +} + invariant LabelsUseContentLanguage { -- RenderLabels and the i18n filter resolve against the content/render -- language, consistent with i18n.allium's split-localization rule.