Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,11 @@
@media print {
.instrument-section { display: none; }
}

/* Dark: the plan is ink on white paper, so show it as white ink on the dark page. */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .imagemap-canvas { background: #201f1b; }
:root:not([data-theme="light"]) .imagemap-canvas .leaflet-image-layer { filter: invert(0.93) hue-rotate(180deg); }
}
:root[data-theme="dark"] .imagemap-canvas { background: #201f1b; }
:root[data-theme="dark"] .imagemap-canvas .leaflet-image-layer { filter: invert(0.93) hue-rotate(180deg); }
44 changes: 41 additions & 3 deletions assets/kit/interimm.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
Center. Paper and ink, serif headings, thin rules, one Mars-rust colour, and
one live element (the signal line under the header) that always knows how far
away Mars is. Colours are tokens on :root and follow the visitor's light or
dark setting. Keep changes additive: other sites depend on these class names. */
dark setting, or the theme they picked with the header switch. Keep changes additive: other sites depend on these class names. */

/* ---------- fonts (self-hosted, SIL OFL; see static/kit/fonts) ---------- */
@font-face {
Expand Down Expand Up @@ -83,8 +83,13 @@
--font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, var(--cjk), monospace;
}

/* Dark: follows the visitor's system setting unless they picked a theme with the
header switch, which interimm.js stores as data-theme="light" or "dark" on <html>.
Site styles use the same two selectors for their own dark colours:
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { ... } }
:root[data-theme="dark"] { ... } */
@media (prefers-color-scheme: dark) {
:root {
:root:not([data-theme="light"]) {
--bg: #131315;
--bg-alt: #19191c;
--surface: #1b1b1e;
Expand All @@ -101,8 +106,28 @@
--accent-soft: rgb(226 122 80 / 0.12);
--shadow: 0 1px 2px rgb(0 0 0 / 0.4), 0 16px 40px rgb(0 0 0 / 0.3);
}
.brand-mark { filter: invert(1); }
:root:not([data-theme="light"]) .brand-mark { filter: invert(1); }
}
:root[data-theme="dark"] {
color-scheme: dark;
--bg: #131315;
--bg-alt: #19191c;
--surface: #1b1b1e;
--surface-2: #232327;
--text: #ebeae4;
--muted: #9b9a93;
--border: #2d2d31;
--border-strong: #ebeae4;
--link: #ebeae4;
--focus: #e27a50;
--accent: #e27a50;
--accent-hover: #ec916c;
--accent-text: #1c0700;
--accent-soft: rgb(226 122 80 / 0.12);
--shadow: 0 1px 2px rgb(0 0 0 / 0.4), 0 16px 40px rgb(0 0 0 / 0.3);
}
:root[data-theme="dark"] .brand-mark { filter: invert(1); }
:root[data-theme="light"] { color-scheme: light; }

/* `.space` used to be an always-dark starfield band. It is now a quiet header
band on paper, kept so pages that use it still get the right spacing. */
Expand Down Expand Up @@ -203,6 +228,18 @@ ul, ol { margin: 0 0 1em; }
.lang-link { color: var(--text); }
.nav-tools .nav-link svg { width: 1rem; height: 1rem; }

/* light/dark switch, added to .nav-tools by interimm.js (or any [data-theme-toggle] button) */
.theme-toggle {
display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.35rem 0;
border: 0; background: none; color: var(--muted); font: inherit; font-size: 0.9375rem; line-height: inherit; cursor: pointer;
transition: color 0.15s;
}
.theme-toggle:hover { color: var(--text); }
.theme-toggle svg { width: 1rem; height: 1rem; }
.theme-toggle .icon-sun { display: none; }
:root[data-theme-now="dark"] .theme-toggle .icon-sun { display: inline; }
:root[data-theme-now="dark"] .theme-toggle .icon-moon { display: none; }

.dropdown { position: relative; }
.dropdown > summary { list-style: none; }
.dropdown > summary::-webkit-details-marker { display: none; }
Expand Down Expand Up @@ -241,6 +278,7 @@ ul, ol { margin: 0 0 1em; }
.nav-link, .dropdown > summary { padding: 0.55rem 0; font-size: 1rem; border-bottom: 1px solid var(--border); width: 100%; }
.nav-tools { flex-direction: row; flex-wrap: wrap; gap: 0 1.5rem; padding-top: 0.25rem; }
.nav-tools .nav-link { width: auto; border-bottom: 0; }
.nav-tools .theme-toggle { padding: 0.55rem 0; font-size: 1rem; }
.dropdown-menu { position: static; min-width: 0; margin: 0 0 0.5rem; padding: 0.25rem 0; border: 0; box-shadow: none; background: transparent; }
.dropdown-menu a { padding-inline: 1rem 0; }
}
Expand Down
88 changes: 86 additions & 2 deletions assets/kit/interimm.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
// distance, the one-way signal delay, Mars time (MTC) and the sol. Any
// element with data-signal-au, -delay, -mtc or -sol is filled in. The same
// calculations are exposed as window.InterImm.astro for page scripts.
// 4. It adds a light/dark switch to the header. The choice is kept in a cookie
// on interimm.org, so it holds across interimm.org and its subdomains, and is
// set as data-theme="light" or "dark" on <html>; with no choice the page
// follows the system setting. data-theme-now always holds the theme in use.
// Page scripts can read and change it through window.InterImm.theme and
// listen for the `interimm:theme` event on document.
(() => {
document.documentElement.classList.add('js');
const script = document.currentScript;
Expand Down Expand Up @@ -69,6 +75,84 @@
const astro = { msd, mtc, position, distance, AU_KM, AU_LIGHT_S };
window.InterImm = Object.assign(window.InterImm || {}, { astro });

// ---------- theme ----------
const root = document.documentElement;
const THEME_COOKIE = 'interimm-theme';
const darkQuery = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
const systemTheme = () => (darkQuery && darkQuery.matches ? 'dark' : 'light');
const savedTheme = () => {
const m = document.cookie.match(/(?:^|; )interimm-theme=(light|dark)/);
return m ? m[1] : null;
};
const saveTheme = (t) => {
const domain = /(^|\.)interimm\.org$/.test(location.hostname) ? '; domain=interimm.org' : '';
document.cookie = t
? `${THEME_COOKIE}=${t}; path=/; max-age=31536000; SameSite=Lax${domain}`
: `${THEME_COOKIE}=; path=/; max-age=0; SameSite=Lax${domain}`;
};
const themeNow = () => root.dataset.theme || systemTheme();
const themeLabels = () => (document.documentElement.lang.toLowerCase().startsWith('zh')
? { dark: '切换到深色', light: '切换到浅色' }
: { dark: 'Switch to dark theme', light: 'Switch to light theme' });
// The browser bar colour: follow the page background when a theme is picked.
const syncThemeColor = () => {
let meta = document.querySelector('meta[name="theme-color"][data-interimm]');
if (!root.dataset.theme) { if (meta) meta.remove(); return; }
if (!meta) {
meta = document.createElement('meta');
meta.name = 'theme-color';
meta.setAttribute('data-interimm', '');
document.head.prepend(meta);
}
meta.content = getComputedStyle(root).getPropertyValue('--bg').trim() || (root.dataset.theme === 'dark' ? '#131315' : '#f5f5f1');
};
const syncThemeButtons = () => {
const next = themeNow() === 'dark' ? 'light' : 'dark';
const label = themeLabels()[next];
document.querySelectorAll('[data-theme-toggle]').forEach((b) => {
b.setAttribute('aria-label', label);
b.title = label;
});
};
const applyTheme = (t) => {
if (t === 'light' || t === 'dark') root.dataset.theme = t;
else delete root.dataset.theme;
const now = themeNow();
const changed = root.dataset.themeNow !== now;
root.dataset.themeNow = now;
syncThemeColor();
syncThemeButtons();
if (changed) document.dispatchEvent(new CustomEvent('interimm:theme', { detail: { theme: now } }));
};
// Picking the theme the system already uses clears the choice, so the page
// follows the system again from then on.
const setTheme = (t) => {
const pick = t === systemTheme() ? null : t;
saveTheme(pick);
applyTheme(pick);
};
applyTheme(savedTheme());
if (darkQuery && darkQuery.addEventListener) darkQuery.addEventListener('change', () => applyTheme(root.dataset.theme));
window.InterImm.theme = { get: themeNow, set: setTheme, toggle: () => setTheme(themeNow() === 'dark' ? 'light' : 'dark') };

const SUN_ICON = '<svg class="icon-sun" aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="18" height="18"><g fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="12" cy="12" r="4.2"/><path d="M12 2.5v2.2M12 19.3v2.2M2.5 12h2.2M19.3 12h2.2M5.3 5.3l1.6 1.6M17.1 17.1l1.6 1.6M5.3 18.7l1.6-1.6M17.1 6.9l1.6-1.6"/></g></svg>';
const MOON_ICON = '<svg class="icon-moon" aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="18" height="18"><path d="M20 14.5A8.5 8.5 0 0 1 9.5 4a8.5 8.5 0 1 0 10.5 10.5z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg>';
const initThemeToggle = () => {
const tools = document.querySelector('.site-header .nav-tools');
if (tools && !document.querySelector('.site-header [data-theme-toggle]')) {
const li = document.createElement('li');
li.className = 'nav-item';
li.innerHTML = `<button class="theme-toggle" type="button" data-theme-toggle>${MOON_ICON}${SUN_ICON}</button>`;
tools.prepend(li);
}
document.querySelectorAll('[data-theme-toggle]').forEach((b) => {
if (b.dataset.themeBound) return;
b.dataset.themeBound = '1';
b.addEventListener('click', () => window.InterImm.theme.toggle());
});
syncThemeButtons();
};

// "13 min 42 s" / "13 分 42 秒", from the page's own data-unit-min / data-unit-s, else English.
const duration = (seconds, el) => {
const min = el.dataset.unitMin || 'min';
Expand Down Expand Up @@ -186,7 +270,7 @@
const start = () => {
const header = document.querySelector('[data-interimm-header]');
const footer = document.querySelector('[data-interimm-footer]');
if (!header && !footer) { initMenu(); startSignal(); return; }
if (!header && !footer) { initThemeToggle(); initMenu(); startSignal(); return; }

const host = header || footer;
const lang = host.dataset.lang || (document.documentElement.lang.toLowerCase().startsWith('zh') ? 'cn' : 'en');
Expand All @@ -197,7 +281,7 @@
if (footer) footer.innerHTML = renderFooter(nav);
})
.catch(() => { /* keep the page's own fallback markup */ })
.finally(() => { initMenu(); startSignal(); });
.finally(() => { initThemeToggle(); initMenu(); startSignal(); });
};

if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
Expand Down
27 changes: 27 additions & 0 deletions kit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,33 @@ The header's language link (中文 / English) goes to the interimm.org home page
that has its own page in another language says where it is with `data-lang-cn` or `data-lang-en` on the header,
for example `data-lang-cn="https://cities.interimm.org/?lang=cn"`, and the link goes there instead.

### Light and dark

Every page follows the visitor's system setting, and the header has a switch (moon / sun) to pick light or dark
by hand. The choice is a cookie on `interimm.org`, so it holds on interimm.org, its project pages
(`/interplanetary-logistics/`, `/mars-map/`, ...) and its subdomains (cities.interimm.org, book.interimm.org).
interimm.js adds the switch to `.nav-tools` in the header; put `data-theme-toggle` on any other button to make
it a switch too. A picked theme is set as `data-theme="light"` or `data-theme="dark"` on `<html>`, and
`data-theme-now` always says which one is showing.

To avoid a flash of the wrong theme while the deferred script loads, put this in `<head>`, before the stylesheets:

```html
<script>(function(m){if(m)document.documentElement.dataset.theme=m[1]})(document.cookie.match(/(?:^|; )interimm-theme=(light|dark)/))</script>
```

Site styles write their own dark colours with the same two selectors the kit uses, so both the system setting
and the switch work:

```css
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --plot-line: #e27a50; } }
:root[data-theme="dark"] { --plot-line: #e27a50; }
```

A plain `@media (prefers-color-scheme: dark)` block still works for visitors who never touch the switch, but
it ignores the switch. Scripts that draw in colour (canvas, WebGL) read `InterImm.theme.get()` and redraw on the
`interimm:theme` event, which fires on `document` whenever the theme in use changes, by switch or by system.

### Caching

GitHub Pages lets browsers cache files for about 10 minutes, so a kit change reaches other sites within that
Expand Down
2 changes: 1 addition & 1 deletion layouts/partials/head.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
{{- $image := "" -}}
{{- with .Params.images }}{{ $image = index . 0 }}{{ else }}{{ with site.Params.images }}{{ $image = index . 0 }}{{ end }}{{ end -}}
<meta charset="utf-8">
<script>document.documentElement.classList.add("js")</script>
<script>document.documentElement.classList.add("js");(function(m){if(m)document.documentElement.dataset.theme=m[1]})(document.cookie.match(/(?:^|; )interimm-theme=(light|dark)/))</script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#131315" media="(prefers-color-scheme: dark)">
Expand Down
Loading