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
32 changes: 31 additions & 1 deletion assets/kit/interimm.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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; }
}
65 changes: 63 additions & 2 deletions assets/kit/interimm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = '<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="20" height="20"><g fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M4 7h9M17 7h3M4 17h3M11 17h9"/><circle cx="15" cy="7" r="2"/><circle cx="9" cy="17" r="2"/></g></svg>';
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 = `<button class="toolkit-button" type="button" aria-expanded="false" aria-controls="toolkit-panel" aria-label="${esc(t.toolkit)}" title="${esc(t.toolkit)}">${TOOLKIT_ICON}</button>
<div class="toolkit-panel" id="toolkit-panel" role="dialog" aria-label="${esc(t.toolkit)}" hidden>
<dl class="toolkit-signal">
<div><dt>${esc(sig.distance)}</dt><dd><b data-signal-au>–</b> AU</dd></div>
<div><dt>${esc(sig.delay)}</dt><dd><b data-signal-delay data-unit-min="${esc(sig.min)}" data-unit-s="${esc(sig.s)}">–</b></dd></div>
<div><dt>${esc(sig.mars)}</dt><dd><b data-signal-mtc>–</b> MTC</dd></div>
<div><dt>${esc(sig.sol)}</dt><dd><b data-signal-sol>–</b></dd></div>
</dl>
<div class="toolkit-tools">
<button class="toolkit-row theme-toggle" type="button" data-theme-toggle><span>${esc(t.theme)}</span>${MOON_ICON}${SUN_ICON}</button>
${languages.length ? `<p class="toolkit-langs"><span>${esc(t.language)}</span>${languages.map((l) => `<a href="${esc(l.url)}" hreflang="${esc(l.code)}" lang="${esc(l.code)}">${esc(l.name)}</a>`).join('')}</p>` : ''}
</div>
${links.length ? `<nav class="toolkit-links" aria-label="${esc(linksTitle)}"><h2>${esc(linksTitle)}</h2><ul>${links.map((a) => `<li><a href="${esc(a.getAttribute('href'))}">${esc(a.textContent.trim())}</a></li>`).join('')}</ul></nav>` : ''}
</div>`;
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';
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 12 additions & 0 deletions kit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<header class="site-header" data-interimm-header data-no-signal data-toolkit data-lang="en">...</header>
<nav hidden data-toolkit-links data-toolkit-title="Chapters"><a href="/history/">History</a> ...</nav>
```

### Light and dark

Every page follows the visitor's system setting, and the header has a switch (moon / sun) to pick light or dark
Expand Down
Loading