diff --git a/assets/kit/interimm.css b/assets/kit/interimm.css index 457c53f..486bc90 100644 --- a/assets/kit/interimm.css +++ b/assets/kit/interimm.css @@ -296,6 +296,36 @@ ul, ol { margin: 0 0 1em; } } @media (max-width: 30rem) { .signal-inner { gap: 0.1rem 1.25rem; font-size: 0.72rem; } } +/* ---------- toolkit: floating button and panel, opt-in with data-toolkit on the header ---------- */ +.toolkit { position: fixed; z-index: 40; right: max(1rem, env(safe-area-inset-right)); bottom: max(1rem, env(safe-area-inset-bottom)); } +.toolkit-button { + display: grid; place-items: center; width: 2.75rem; height: 2.75rem; padding: 0; + border: 1px solid var(--border); border-radius: 50%; background: var(--surface); color: var(--muted); + box-shadow: var(--shadow); cursor: pointer; transition: color 0.15s, border-color 0.15s; +} +.toolkit-button:hover, .toolkit-button[aria-expanded="true"] { color: var(--text); border-color: var(--border-strong); } +.toolkit-panel { + position: absolute; right: 0; bottom: calc(100% + 0.6rem); width: min(19rem, calc(100vw - 2rem)); max-height: min(30rem, 70vh); overflow-y: auto; + padding: 1rem 1.1rem; border: 1px solid var(--border); border-radius: 0.9rem; background: var(--surface); box-shadow: var(--shadow); font-size: 0.9375rem; +} +.toolkit-panel[hidden] { display: none; } +.toolkit-signal { margin: 0; display: grid; gap: 0.35rem; font-family: var(--font-mono); font-size: 0.78rem; font-variant-numeric: tabular-nums; } +.toolkit-signal div { display: flex; justify-content: space-between; gap: 1rem; } +.toolkit-signal dt { color: var(--muted); } +.toolkit-signal dd { margin: 0; color: var(--muted); } +.toolkit-signal b { font-weight: 500; color: var(--text); } +.toolkit-tools { margin-top: 0.85rem; padding-top: 0.75rem; border-top: 1px solid var(--border); display: grid; gap: 0.4rem; } +.toolkit-row { width: 100%; justify-content: space-between; padding: 0.3rem 0; } +.toolkit-row span { color: var(--text); } +.toolkit-langs { margin: 0; display: flex; align-items: center; gap: 1rem; } +.toolkit-langs span { margin-right: auto; } +.toolkit-langs a { color: var(--text); } +.toolkit-links { margin-top: 0.85rem; padding-top: 0.75rem; border-top: 1px solid var(--border); } +.toolkit-links h2 { margin: 0 0 0.4rem; font-family: var(--font-mono); font-size: 0.72rem; font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); } +.toolkit-links ul { margin: 0; padding: 0; list-style: none; display: grid; } +.toolkit-links a { display: block; padding: 0.4rem 0; color: var(--text); text-decoration: none; } +.toolkit-links a:hover { color: var(--accent); } + /* ---------- buttons, chips, links ---------- */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.6ch; @@ -438,6 +468,6 @@ ul, ol { margin: 0 0 1em; } } @media print { - .site-header, .site-footer, .skip-link, .signal { display: none; } + .site-header, .site-footer, .skip-link, .signal, .toolkit { display: none; } body { background: #fff; color: #000; } } diff --git a/assets/kit/interimm.js b/assets/kit/interimm.js index ba6c83c..4aca346 100644 --- a/assets/kit/interimm.js +++ b/assets/kit/interimm.js @@ -20,6 +20,12 @@ // 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. +// 5. A header marked `data-toolkit` gets a small floating button (bottom right) +// that opens a panel with the live signal readings, the light/dark switch +// and the language links. Put an element with `data-toolkit-links` (and an +// optional `data-toolkit-title`) on the page and its links are copied into +// the panel too, e.g. a chapter list. Use it with `data-no-signal` to keep a +// reading page's header slim. (() => { document.documentElement.classList.add('js'); const script = document.currentScript; @@ -153,6 +159,59 @@ syncThemeButtons(); }; + + // ---------- toolkit: floating button with a small panel ---------- + const TOOLKIT_TEXT = { + cn: { toolkit: '工具箱', theme: '外观', language: '语言', links: '目录', close: '关闭', signal: { distance: '地球至火星', delay: '单程信号', mars: '伊希地', sol: '火星日', min: '分', s: '秒' } }, + en: { toolkit: 'Toolkit', theme: 'Appearance', language: 'Language', links: 'Contents', close: 'Close', signal: { distance: 'Earth to Mars', delay: 'One-way signal', mars: 'Isidis', sol: 'Sol', min: 'min', s: 's' } }, + }; + const TOOLKIT_ICON = ''; + const initToolkit = (nav) => { + const header = document.querySelector('[data-interimm-header][data-toolkit]'); + if (!header || document.querySelector('[data-interimm-toolkit]')) return; + const lang = header.dataset.lang || (document.documentElement.lang.toLowerCase().startsWith('zh') ? 'cn' : 'en'); + const t = TOOLKIT_TEXT[lang] || TOOLKIT_TEXT.en; + const sig = (nav && nav.labels && nav.labels.signal) || t.signal; + const languages = nav && nav.languages + ? nav.languages.map((l) => ({ code: l.code, name: l.name, url: langUrl(header, l) })) + : ['cn', 'en'].filter((l) => header.getAttribute(`data-lang-${l}`)).map((l) => ({ code: l === 'cn' ? 'zh' : 'en', name: l === 'cn' ? '中文' : 'English', url: header.getAttribute(`data-lang-${l}`) })); + + const slot = document.querySelector('[data-toolkit-links]'); + const links = slot ? Array.from(slot.querySelectorAll('a[href]')) : []; + const linksTitle = (slot && slot.dataset.toolkitTitle) || t.links; + + const box = document.createElement('div'); + box.className = 'toolkit'; + box.setAttribute('data-interimm-toolkit', ''); + box.innerHTML = ` + `; + document.body.append(box); + + const button = box.querySelector('.toolkit-button'); + const panel = box.querySelector('.toolkit-panel'); + const setOpen = (open, focus) => { + panel.hidden = !open; + button.setAttribute('aria-expanded', String(open)); + if (!open && focus) button.focus(); + }; + button.addEventListener('click', () => setOpen(panel.hidden)); + document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !panel.hidden) setOpen(false, true); }); + document.addEventListener('click', (e) => { if (!panel.hidden && !box.contains(e.target)) setOpen(false); }); + panel.addEventListener('click', (e) => { if (e.target.closest('.toolkit-links a')) setOpen(false); }); + }; + // "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'; @@ -270,18 +329,20 @@ const start = () => { const header = document.querySelector('[data-interimm-header]'); const footer = document.querySelector('[data-interimm-footer]'); - if (!header && !footer) { initThemeToggle(); initMenu(); startSignal(); return; } + if (!header && !footer) { initToolkit(); initThemeToggle(); initMenu(); startSignal(); return; } const host = header || footer; const lang = host.dataset.lang || (document.documentElement.lang.toLowerCase().startsWith('zh') ? 'cn' : 'en'); + let navData = null; fetch(new URL(`nav.${lang}.json`, base)) .then((r) => { if (!r.ok) throw new Error(r.status); return r.json(); }) .then((nav) => { + navData = nav; if (header) header.innerHTML = renderHeader(header, nav, header.dataset.current); if (footer) footer.innerHTML = renderFooter(nav); }) .catch(() => { /* keep the page's own fallback markup */ }) - .finally(() => { initThemeToggle(); initMenu(); startSignal(); }); + .finally(() => { initToolkit(navData); initThemeToggle(); initMenu(); startSignal(); }); }; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); diff --git a/kit/README.md b/kit/README.md index 1d1bb84..a87134c 100644 --- a/kit/README.md +++ b/kit/README.md @@ -64,6 +64,18 @@ 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. +### Toolkit button + +For pages that are mostly reading, put `data-toolkit` on the header (usually with `data-no-signal`). interimm.js then +adds a small round button at the bottom right. It opens a panel with the live signal readings, the light/dark +switch and the language links. To add a list of links to the panel (a chapter list, say), put them in any element +marked `data-toolkit-links`, hidden if you like, and set `data-toolkit-title` for its heading: + +```html + + +``` + ### Light and dark Every page follows the visitor's system setting, and the header has a switch (moon / sun) to pick light or dark