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
10 changes: 7 additions & 3 deletions blocks/footer.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* ─── prose footer · v3 · {{variant}} ───────────────────────────────────
/* ─── prose footer · v4 · {{variant}} ───────────────────────────────────
Generated from @robertblust/design — edit it there and run `npm run design`.

Two forms, and the difference is not styling. companygraph.io and
Expand All @@ -10,8 +10,12 @@
The row is mono because the row is data: a repository URL and a license.
The credit leaves that mono deliberately, because a brand is not a record
value. Both facts belong here rather than in sixteen page comments. A link
in the row is the family's link, from the `link` block, with nothing of its own. */
footer{padding:0 0 2rem; display:flex; flex-wrap:wrap; gap:.9rem 2.6rem; align-items:center;
in the row is the family's link, from the `link` block, with nothing of its own.

The row's floor adds the phone's bottom inset. A page that declares `viewport-fit=cover`
draws under Android's and iOS's bottom bars, and the last row would sit under them;
everywhere else the inset is 0 and the floor is the 2rem it always was. */
footer{padding:0 0 calc(2rem + env(safe-area-inset-bottom)); display:flex; flex-wrap:wrap; gap:.9rem 2.6rem; align-items:center;
font-family:"Plex Mono",monospace; font-size:.76rem; letter-spacing:.08em; color:var(--dim)}
{{credit}} footer a[aria-current="page"]{color:var(--c-firm)}
/* ─── end prose footer ──────────────────────────────────────────────────*/
22 changes: 21 additions & 1 deletion blocks/theme.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* ─── theme · v5 · {{variant}} ───────────────────────────────────────
/* ─── theme · v6 · {{variant}} ───────────────────────────────────────
One theme across three domains, and where it is remembered. Generated from
@robertblust/design — editing it here does nothing.

Expand Down Expand Up @@ -66,4 +66,24 @@
if (d) d.setAttribute("aria-pressed", theme === "dark" ? "true" : "false");
}
function setTheme(v){ theme = v; themeRemember(v); applyTheme(); }

/* A page opened from a phone's home screen has Android paint its system bars from the page's
`theme-color`. A page declares that meta with dark's ground, the default, and this keeps it
on the ground of the theme the visitor actually sees. It watches `data-theme` rather than
hooking `applyTheme`, because a page may switch the theme with a copy of its own — blust.ch's
pages do — and the attribute is the one thing every switch sets. The color is read from the
tokens rather than restated here, so a re-picked ground moves the bars with it. A page
without the meta is left as it is. */
function themeColor(){
try {
var tc = document.querySelector('meta[name="theme-color"]');
if (!tc) return;
var g = getComputedStyle(document.documentElement).getPropertyValue("--ground").trim();
if (g) tc.setAttribute("content", g);
} catch (e) {}
}
themeColor();
try {
new MutationObserver(themeColor).observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
} catch (e) {}
/* ─── end theme ──────────────────────────────────────────────────────── */
4 changes: 2 additions & 2 deletions docs/design-system/blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ The lines a prose page lifts out of its paragraphs, each drawn in a color role f

## Footer

The row at the foot of every prose page: the repository and the license, and on companygraph.io and guestgraph.io the credit to blust.ch. A site names its form in `design.config.json` as `"footer": "credit"` or `"plain"`. Contract: [blocks/footer.css](../../blocks/footer.css), and the credit's rules in [blocks/footer-credit.css](../../blocks/footer-credit.css).
The row at the foot of every prose page: the repository and the license, and on companygraph.io and guestgraph.io the credit to blust.ch. A site names its form in `design.config.json` as `"footer": "credit"` or `"plain"`. On a phone the row keeps clear of the system's bottom bar. Contract: [blocks/footer.css](../../blocks/footer.css), and the credit's rules in [blocks/footer-credit.css](../../blocks/footer-credit.css).

```text
credit
Expand Down Expand Up @@ -200,6 +200,6 @@ These blocks draw nothing of their own and have no picture.
| Block | What it does | Contract |
| --- | --- | --- |
| Theme boot | Sets the theme before the first paint | [blocks/theme-boot.js](../../blocks/theme-boot.js) |
| Theme | Switches and remembers light and dark across the family's domains | [blocks/theme.js](../../blocks/theme.js) |
| Theme | Switches and remembers light and dark across the family's domains, and keeps a page's `theme-color` on the ground of the theme shown | [blocks/theme.js](../../blocks/theme.js) |
| Language | Decides and remembers English or German across the family's domains | [blocks/lang.js](../../blocks/lang.js) |
| Nav fit | Measures whether the header row fits | [blocks/nav-fit.js](../../blocks/nav-fit.js) |
58 changes: 58 additions & 0 deletions test/theme.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -920,3 +920,61 @@ test("--c-sum, the conclusion line, clears 3:1 against its own ground, in both t
assert.ok(r >= 3, `${name}: --c-sum is ${r.toFixed(2)}:1 on --ground, needs 3`);
}
});

// The theme-color hook is run for real, as carryTheme is above: a regex over the source would
// pass a block that names `theme-color` and `--ground` in a comment while never setting the meta.
// The page switches the theme through `documentElement` alone, the way a page carrying its own
// copy of the switch does, so the hook is shown to follow the attribute, not `setTheme`.
function runThemeColor({ meta, grounds, start = null }) {
const js = blockFor("theme", "page");
let dataTheme = start;
const observers = [];
const documentElement = {
getAttribute: (n) => (n === "data-theme" ? dataTheme : null),
setAttribute(n, v) { if (n === "data-theme") { dataTheme = v; observers.forEach((f) => f()); } },
removeAttribute(n) { if (n === "data-theme") { dataTheme = null; observers.forEach((f) => f()); } },
};
const sandbox = {
console, URL,
localStorage: { getItem: () => null, setItem() {} },
location: { href: "https://blust.ch/", origin: "https://blust.ch", search: "", pathname: "/", hash: "" },
history: { replaceState() {} },
document: {
addEventListener() {}, documentElement, getElementById: () => null,
querySelector: (sel) => (sel === 'meta[name="theme-color"]' ? meta : null),
},
getComputedStyle: () => ({ getPropertyValue: (n) => (n === "--ground" ? grounds[dataTheme === "light" ? "light" : "dark"] : "") }),
MutationObserver: class {
constructor(f) { this.f = f; }
observe(target, opts) { if (target === documentElement && opts.attributeFilter.includes("data-theme")) observers.push(this.f); }
},
};
vm.createContext(sandbox);
vm.runInContext(js, sandbox);
return { documentElement };
}

test("theme-color follows data-theme, whichever switch sets it", () => {
// Android paints a home-screen page's system bars from `theme-color`; a light visitor whose
// bars stayed dark's ground would see a dark band under a light page.
const content = { value: "#0C0E13" };
const meta = { setAttribute(n, v) { if (n === "content") content.value = v; } };
const { documentElement } = runThemeColor({ meta, grounds: { dark: " #0C0E13", light: " #FAF9F5" } });
documentElement.setAttribute("data-theme", "light");
assert.equal(content.value, "#FAF9F5", "light did not move theme-color to light's ground");
documentElement.removeAttribute("data-theme");
assert.equal(content.value, "#0C0E13", "dark did not move theme-color back to dark's ground");
});

test("theme-color takes the theme a page loads in", () => {
// `theme boot` has already set light before this block runs; the bars must not wait for a click.
const content = { value: "#0C0E13" };
const meta = { setAttribute(n, v) { if (n === "content") content.value = v; } };
runThemeColor({ meta, grounds: { dark: "#0C0E13", light: "#FAF9F5" }, start: "light" });
assert.equal(content.value, "#FAF9F5");
});

test("a page without a theme-color meta is left alone", () => {
const { documentElement } = runThemeColor({ meta: null, grounds: { dark: "#0C0E13", light: "#FAF9F5" } });
assert.doesNotThrow(() => documentElement.setAttribute("data-theme", "light"));
});
4 changes: 2 additions & 2 deletions versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
"title": "v2",
"reset": "v3",
"lang": "v5",
"footer": "v3",
"footer": "v4",
"transport": "v11",
"lockup": "v1",
"fit": "v1",
"rests": "v1",
"runtime": "v10",
"themeBoot": "v6",
"theme": "v5",
"theme": "v6",
"navFit": "v3",
"processes": "v1",
"surfaces": "v2",
Expand Down
Loading