mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-05-10 02:41:30 -05:00
* fix(dev-mirror): compute prefix from dev-side depth in rewrite-dev-urls.sh The previous implementation hard-coded PREFIX="../" for any non-root subsite, which silently mis-rewrote every absolute mlsysbook.ai link on the dev preview for nested subsites (vol1, vol2 — they live at /book/vol1/ and /book/vol2/ on dev). The most visible symptom was the navbar title-href landing one level too shallow: clicking the navbar title from inside Vol I went to /book/ instead of the unified landing page at the dev root. Fix: derive PREFIX from the number of path segments in the calling subsite's dev-side path (book/vol1 → 2 hops → '../../') and use the mlsysbook.ai key (not the dev-path) for self-link detection. Add an explicit error if the caller passes a subsite name that is not in the SUBSITES map, instead of silently producing wrong rewrites. Sample rewrites with the fix: vol1 page https://mlsysbook.ai/ → ../../ vol1 page https://mlsysbook.ai/vol2/ → ../../book/vol2/ vol1 page https://mlsysbook.ai/kits/ → ../../kits/ kits page https://mlsysbook.ai/ → ../ kits page https://mlsysbook.ai/vol1/ → ../book/vol1/ Live builds are unaffected — they use the original absolute URLs. * feat(book): per-volume announcement bars (Crimson / ETH-Blue) Split the shared book announcement bar into two volume-scoped files so each volume gets audience-appropriate copy AND inherits the right brand tint. Vol I keeps the Harvard-Crimson tint (its theme accent) and the Foundations-flavored content; Vol II picks up the ETH-Blue tint (its theme accent) and Scale-flavored content that leads with the new volume launch and the cross-ecosystem build path. Files: - announcement-vol1.yml — new, Vol I copy, no hard-coded color (uses `type: primary` so .announcement / .alert-primary get $accent = $brand-crimson via theme-harvard.scss) - announcement-vol2.yml — new, Vol II copy, same pattern but theme feeds $accent = $brand-eth-blue via theme-eth.scss - announcement.yml — emptied to a no-op with a deprecation note; keep for one release cycle to avoid breaking any external metadata reference, then delete The CSS that translates `type: primary` into the per-theme tint already lived in book/quarto/assets/styles/_base-styles.scss (`.announcement { background: linear-gradient(... lighten($accent, 52%) ...) }`). No SCSS changes needed — the previous behavior of a single shared bar just hid that the tint was already theme-driven. Resolves the "Vol II announcement should be ETH-themed" QA note. * feat(theme): cross-site dark-mode persistence + FOUC guard Make dark-mode preference flow seamlessly across every subsite under mlsysbook.ai (Quarto-built and Next.js alike) and eliminate the theme-flash that dark-mode readers see on first paint. Quarto subsites (book / labs / kits / slides / instructors / mlsysim / tinytorch / unified site): - shared/config/site-head.html now inlines a tiny pre-paint script that reads `quarto-color-scheme` from localStorage (or falls back to OS preference) and applies `data-bs-theme`, `data-quarto-color-scheme`, and `style.color-scheme` on <html> BEFORE any other script runs. Eliminates the visible flash that was happening because Quarto's own toggle script runs late. - Listens for `storage` events so a toggle in tab A propagates to tab B without a refresh. - Inlined deliberately: the script is tiny, must be synchronous in <head> to avoid the flash, and inlining sidesteps per-subsite asset path differences. Canonical externalized source kept at shared/scripts/theme-persist.js for documentation/testability — if you change one, mirror to the other. StaffML (Next.js): - public/theme-bootstrap.js now reads the Quarto-side key as a fallback when StaffML has no local preference, so a user toggling dark mode on the book lands here in dark mode on first visit. - components/ThemeProvider.tsx mirrors writes back to `quarto-color-scheme`, so navigating onward to any Quarto subsite inherits StaffML's choice. Both subsystems retain their own keys as primary so each app's behavior is unchanged in isolation. The `quarto-color-scheme` key is the bridge contract — keep it stable across all theme code paths. * test(audit): Playwright site-audit script (sidebar / darkmode / assets) Single Playwright-driven QA script that the release-prep plan needs in three flavors. Implemented as one CLI with three subcommands so the shared boilerplate (browser launch, URL list, output dirs, screenshot naming) lives in one place and the per-site source-of-truth list does too. Subcommands: sidebar Assert every Quarto subsite exposes a populated, visible #quarto-sidebar / .sidebar-navigation. Skips sites that intentionally have no sidebar (landing, slides, StaffML). Catches the regression where Vol I/II builds dropped the sidebar after a config refactor. darkmode Force dark-mode via localStorage + data attributes, scroll top→bottom in 800px chunks (so lazy content renders), and screenshot full-page into _audit/darkmode/<site>.png for eyeball review. Surfaces "half-themed" widgets that CSS linters can't find (announcement bar, footer tiles, code blocks, etc.). assets Listen for failed network requests + 4xx/5xx responses on every site URL. Catches the broken <img> embeds reported during dev-mirror review (TinyTorch big-picture PDF viewer, Vol II cover) before they hit production. Targets dev / live / local with --target. Use --only <substring> to narrow scope. JSON report written to _audit/<cmd>.json for CI ingest. Exits non-zero on issues so it can become a blocking CI check once the baseline is clean. Requires `npm i -D playwright && npx playwright install chromium`.
105 lines
4.5 KiB
JavaScript
105 lines
4.5 KiB
JavaScript
/* =============================================================================
|
|
* theme-persist.js — cross-site dark/light mode persistence + FOUC guard
|
|
* =============================================================================
|
|
*
|
|
* Why this exists
|
|
* ---------------
|
|
* Every subsite under mlsysbook.ai is built by a different engine (Quarto for
|
|
* the book/labs/kits/slides/site/instructors/tinytorch, Next.js for StaffML,
|
|
* etc). Quarto's built-in toggle stores the user's choice in localStorage
|
|
* under the key `quarto-color-scheme`. Because every subsite shares the
|
|
* mlsysbook.ai origin in production, that localStorage entry is *technically*
|
|
* shared — but Quarto's own bridge script runs late, after the page has
|
|
* already painted in the wrong theme. The result is a visible flash when a
|
|
* dark-mode reader navigates from one subsite to another.
|
|
*
|
|
* This script:
|
|
* 1. Runs as the FIRST <script> in <head> (so it executes before render).
|
|
* 2. Reads the user's stored color-scheme preference and applies the
|
|
* corresponding `data-bs-theme` / `data-quarto-color-scheme` attributes
|
|
* on <html> *before* the browser paints, eliminating the flash.
|
|
* 3. Falls back to OS preference (`prefers-color-scheme: dark`) when the
|
|
* user has not yet expressed a preference.
|
|
* 4. Listens for `storage` events so a toggle in tab A is reflected
|
|
* immediately in tab B without a refresh.
|
|
*
|
|
* It deliberately does *not* render any UI: Quarto's existing toggle button
|
|
* stays the source of truth for user-initiated changes; this script only
|
|
* preempts the flash and synchronizes other open tabs.
|
|
*
|
|
* Wiring (Quarto sites): add to include-in-header BEFORE any other script:
|
|
* <script src="/assets/scripts/theme-persist.js"></script>
|
|
*
|
|
* Wiring (non-Quarto sites, e.g. StaffML/Next.js): drop the same file at the
|
|
* same URL and reference it from the document head. The script is framework-
|
|
* agnostic; it only touches `<html>` attributes and a single localStorage key.
|
|
*
|
|
* Canonical source: shared/scripts/theme-persist.js
|
|
* Mirrors: synced via shared/scripts/sync-mirrors.sh (do not hand-edit)
|
|
* ============================================================================= */
|
|
(function () {
|
|
'use strict';
|
|
|
|
// The single key both Quarto and this shim agree on. Do NOT rename without
|
|
// also updating Quarto's expected key — Quarto reads/writes the same value
|
|
// and we explicitly want to interoperate, not shadow it.
|
|
var STORAGE_KEY = 'quarto-color-scheme';
|
|
|
|
function preferredScheme() {
|
|
try {
|
|
var stored = window.localStorage.getItem(STORAGE_KEY);
|
|
if (stored === 'dark' || stored === 'light') {
|
|
return stored;
|
|
}
|
|
} catch (e) {
|
|
// localStorage may be blocked (private mode, sandboxed iframe).
|
|
// Silent fallback to OS preference is the right behavior.
|
|
}
|
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
return 'dark';
|
|
}
|
|
return 'light';
|
|
}
|
|
|
|
function apply(scheme) {
|
|
var html = document.documentElement;
|
|
if (!html) return;
|
|
// Bootstrap 5.3+: data-bs-theme drives semantic colors.
|
|
html.setAttribute('data-bs-theme', scheme);
|
|
// Quarto-internal hook used by some subsite SCSS (kept in sync to avoid
|
|
// half-themed elements during the gap before Quarto's own script runs).
|
|
html.setAttribute('data-quarto-color-scheme', scheme);
|
|
// Hint for any CSS using the @media `prefers-color-scheme` shorthand.
|
|
html.style.colorScheme = scheme;
|
|
}
|
|
|
|
// 1. Apply the initial scheme synchronously (before paint).
|
|
apply(preferredScheme());
|
|
|
|
// 2. Cross-tab sync: react when *another* tab updates the choice.
|
|
window.addEventListener('storage', function (ev) {
|
|
if (ev.key !== STORAGE_KEY) return;
|
|
var next = ev.newValue;
|
|
if (next !== 'dark' && next !== 'light') return;
|
|
apply(next);
|
|
});
|
|
|
|
// 3. OS-preference change: only apply if the user has NOT explicitly
|
|
// chosen via the toggle (i.e. nothing in localStorage).
|
|
if (window.matchMedia) {
|
|
try {
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (ev) {
|
|
try {
|
|
if (window.localStorage.getItem(STORAGE_KEY)) return;
|
|
} catch (e) {
|
|
/* If we can't read storage, treating OS pref as truth is fine. */
|
|
}
|
|
apply(ev.matches ? 'dark' : 'light');
|
|
});
|
|
} catch (e) {
|
|
// Older Safari does not support addEventListener on MediaQueryList.
|
|
// The initial apply() above is sufficient for those browsers.
|
|
}
|
|
}
|
|
})();
|