Skip to content

Commit 09262db

Browse files
The status bar follows the reader's theme, the bookmark chips lose their words (v0.61.1)
- Status bar (PWA): applyTheme rewrote only the FIRST theme-color tag — the light-scoped one. A UA reads the first tag whose media MATCHES, so a dark-mode phone under a light reader theme kept Theme::Dark's paper in its live tag; Chrome chose light status-bar icons for it and drew them over the cream the page paints under its transparent bar — clock and battery washed out. Every tag now carries the resolved paper; the boot script does the same from the cached palette for an explicitly chosen theme (never for "follow the device", whose media pair is already right). Pre-existing, several releases old. E2e: theme-color.spec.ts. - Bookmark chips: NO TEXT — round icon chips (44px floor), the words on aria-label/title and in the tap's toast. Several visible at once, centred while they fit and left-anchored scrolling once they don't, so a chip cut at the edge says "more" (the one-per-page pager hid that there were others). One flag per running plan; "+N more" is gone. plans-today/bookmarks specs read the aria-label; bookmarks.spec pins no-text and several-in-viewport. - Both new regression tests mutation-tested against rebuilt bundles. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent a885867 commit 09262db

11 files changed

Lines changed: 219 additions & 120 deletions

Cargo.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ default-members = [
2121
# shells — About renders it as `engine ` + this number. The workflow refuses a tag
2222
# whose version disagrees with this line (and with apps/web/package.json), so a
2323
# release ships one number, not three.
24-
version = "0.61.0"
24+
version = "0.61.1"
2525
edition = "2021"
2626
license = "MIT"
2727
authors = ["Glendon Klassen <gjklassen@proton.me>"]

apps/web/e2e/bookmarks.spec.ts

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { expect, test, type Page } from "@playwright/test";
22

3-
// The bookmarks strip (maintainer ask, 2026-08-24): the row above the canon
4-
// strip — grown out of the plan chip — carries a swipeable tile per stored
5-
// bookmark: the running plan, then every seating position in `config.slots`
6-
// (Last opened, Sunday morning, Sunday evening, Wednesday evening), each with
7-
// an icon naming its kind. A tap toasts WHICH bookmark it was and where it is
8-
// going, then navigates — four tiles all reading "John 3" are otherwise
9-
// indistinguishable mid-swipe.
3+
// The bookmarks row (maintainer ask, 2026-08-24; icon-only 2026-08-25): the row
4+
// above the canon strip — grown out of the plan chip — carries a round ICON
5+
// chip per stored bookmark: the running plan, then every seating position in
6+
// `config.slots` (Last opened, Sunday morning, Sunday evening, Wednesday
7+
// evening). NO TEXT on the chips: the words ride aria-label/title, and a tap
8+
// toasts WHICH bookmark it was before navigating — with no words on the face,
9+
// the toast is the confirmation. Several chips show at once, so it is plain
10+
// there are more than one and that the row scrolls when they overflow.
1011

1112
async function boot(page: Page): Promise<void> {
1213
await page.setViewportSize({ width: 1100, height: 800 });
@@ -20,7 +21,7 @@ async function boot(page: Page): Promise<void> {
2021
await expect(page.locator(".subtitle")).toHaveText(/\w+ \d+/, { timeout: 90_000 });
2122
}
2223

23-
test("a stored seating rides the strip; a tap says which bookmark and goes there", async ({ page }) => {
24+
test("a stored seating is an icon chip; a tap says which bookmark and goes there", async ({ page }) => {
2425
await boot(page);
2526
// Plant a Sunday-morning seating other than where the reader is.
2627
await page.evaluate(() => {
@@ -29,10 +30,10 @@ test("a stored seating rides the strip; a tap says which bookmark and goes there
2930
});
3031

3132
const tile = page.locator('.bm-tile[data-slot="sunday-morning"]');
32-
await expect(tile).toContainText("Sunday morning");
33-
await expect(tile).toContainText("Psalms 23:4");
34-
// The icon names the tile's kind for the eye the label serves for the reader.
33+
// The words are the accessible name, not the face.
34+
await expect(tile).toHaveAttribute("aria-label", "Sunday morning · Psalms 23:4");
3535
await expect(tile.locator("svg")).toHaveCount(1);
36+
expect((await tile.textContent())?.trim(), "NO TEXT on the chip — the icon is the whole face").toBe("");
3637

3738
await tile.click();
3839
// The toast names the BOOKMARK, plainly — no "going to…" sentence; the
@@ -47,7 +48,29 @@ test("the everyday seating shows as Last opened", async ({ page }) => {
4748
const s = (window as any).__plumbline;
4849
s.config.slots = { other: { book: "Rom", chapter: 8 } };
4950
});
50-
const tile = page.locator('.bm-tile[data-slot="other"]');
51-
await expect(tile).toContainText("Last opened");
52-
await expect(tile).toContainText("Romans 8");
51+
await expect(page.locator('.bm-tile[data-slot="other"]')).toHaveAttribute("aria-label", "Last opened · Romans 8");
52+
});
53+
54+
test("several chips are visible at once, not one page at a time", async ({ page }) => {
55+
await page.setViewportSize({ width: 360, height: 740 }); // a phone, where one-at-a-time hid the rest
56+
await page.goto("/");
57+
const est = page.getByRole("button", { name: "Established believer" });
58+
await expect(est.or(page.locator(".pane canvas").first())).toBeVisible({ timeout: 90_000 });
59+
if (await est.isVisible().catch(() => false)) {
60+
await est.click();
61+
await page.getByRole("button", { name: "Start reading" }).click();
62+
}
63+
await expect(page.locator(".subtitle")).toHaveText(/\w+ \d+/, { timeout: 90_000 });
64+
await page.evaluate(() => {
65+
const s = (window as any).__plumbline;
66+
s.config.slots = {
67+
other: { book: "Rom", chapter: 8 },
68+
"sunday-morning": { book: "Ps", chapter: 23 },
69+
"sunday-evening": { book: "John", chapter: 17 },
70+
"wednesday-evening": { book: "Acts", chapter: 2 },
71+
};
72+
});
73+
const chips = page.locator(".bm-tile");
74+
await expect(chips).toHaveCount(4);
75+
for (let i = 0; i < 4; i++) await expect(chips.nth(i)).toBeInViewport();
5376
});

apps/web/e2e/plans-today.spec.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ test("a running plan rides the reader: the chip goes to today, the navigator lea
3030
await s.author("planStart", "nt-90", new Date().toISOString());
3131
});
3232

33-
// The chip appears with day 1. Its text is the plan's own answer, so pin the
34-
// expected target from the same wire the chip reads.
33+
// The chip appears with day 1. Its label (aria-label — the chip is icon-only)
34+
// is the plan's own answer, so pin the expected target from the same wire
35+
// the chip reads.
3536
const chip = page.locator(".plan-chip-row .plan-chip").first();
3637
await expect(chip).toBeVisible({ timeout: 10_000 });
37-
await expect(chip).toHaveText(/Day 1 · /);
38+
await expect(chip).toHaveAttribute("aria-label", /Day 1 · /);
3839

3940
const first = await page.evaluate(async () => {
4041
const s = (window as any).__plumbline;
@@ -98,7 +99,7 @@ test("the chronological plan is offered, starts, and day 1 begins at Genesis 1",
9899
// lives on the READ screen, and Plans is a destination that replaces the
99100
// reader — so go back the way the ‹ does before looking for it.
100101
await page.evaluate(() => (window as any).__plumbline.goRead());
101-
await expect(page.locator(".plan-chip-row .plan-chip").first()).toHaveText(/Day 1 · /);
102+
await expect(page.locator(".plan-chip-row .plan-chip").first()).toHaveAttribute("aria-label", /Day 1 · /);
102103
});
103104

104105
// The UAT round (2026-08-11). Three separate ways the plans surfaces misled a
@@ -139,8 +140,9 @@ test("finishing a chapter advances the chip and its label", async ({ page }) =>
139140
return plans.running.find((p: any) => p.id === "bible-365").today.chapters[0];
140141
});
141142
const chip = page.locator(".plan-chip-row .plan-chip").first();
142-
await expect(chip).toContainText(first.display);
143-
const labelBefore = await chip.textContent();
143+
// Icon-only chip: its label is the aria-label, not its text.
144+
await expect(chip).toHaveAttribute("aria-label", new RegExp(first.display.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
145+
const labelBefore = await chip.getAttribute("aria-label");
144146

145147
// Read the chapter the way a phone does: dwell ticks into the core's tracker,
146148
// whose banked report lands as `readingWrote` — NOT an authoring write. The
@@ -159,7 +161,7 @@ test("finishing a chapter advances the chip and its label", async ({ page }) =>
159161

160162
// The chip now names what is LEFT — a label that never moves all evening is
161163
// the UAT bug — and sends the reader to the next chapter.
162-
await expect(chip).not.toHaveText(labelBefore!);
164+
await expect(chip).not.toHaveAttribute("aria-label", labelBefore!);
163165
const landed = await page.evaluate(async () => {
164166
const s = (window as any).__plumbline;
165167
document.querySelector<HTMLButtonElement>(".plan-chip-row .plan-chip")!.click();
@@ -212,7 +214,7 @@ test("reading the day's worth advances the chip to the next day", async ({ page
212214
// …and the chip shows DAY 2 — the next portion, ready to be read ahead —
213215
// rather than disappearing until midnight.
214216
await expect(chip).toBeVisible();
215-
await expect(chip).toHaveText(/Day 2 · /);
217+
await expect(chip).toHaveAttribute("aria-label", /Day 2 · /);
216218
});
217219

218220
// Pause sets a plan aside WHOLE: the chip stands down, the card says when the
@@ -247,7 +249,7 @@ test("a paused plan asks nothing until it is resumed", async ({ page }) => {
247249
await card.getByRole("button", { name: "Resume" }).click();
248250
await expect(card.getByRole("button", { name: "Pause" })).toBeVisible();
249251
await page.evaluate(() => (window as any).__plumbline.goRead());
250-
await expect(page.locator(".plan-chip-row .plan-chip").first()).toHaveText(/Day 1 · /);
252+
await expect(page.locator(".plan-chip-row .plan-chip").first()).toHaveAttribute("aria-label", /Day 1 · /);
251253
});
252254

253255
test("no Explore card spills its text past its border, at any text scale", async ({ page }) => {

apps/web/e2e/theme-color.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
// The status bar follows the READER'S theme, whatever the phone's own scheme.
4+
// index.html ships two media-scoped theme-color tags (the pre-script paint —
5+
// manifest.spec.ts pins those); once the palette is known the app rewrites
6+
// them. It rewrote only the FIRST, the light-scoped one — so a dark-mode phone,
7+
// whose UA reads the SECOND, kept Theme::Dark's paper under a light reader
8+
// theme. Chrome then drew light status-bar icons over the cream the page paints
9+
// beneath its transparent bar: the clock and battery washed out (maintainer,
10+
// 2026-08-25). Every tag has to carry the resolved paper.
11+
12+
test("every theme-color tag carries the reader's paper, on a dark-mode device too", async ({ page }) => {
13+
await page.emulateMedia({ colorScheme: "dark" });
14+
await page.goto("/");
15+
const est = page.getByRole("button", { name: "Established believer" });
16+
await expect(est.or(page.locator(".pane canvas").first())).toBeVisible({ timeout: 90_000 });
17+
if (await est.isVisible().catch(() => false)) {
18+
await est.click();
19+
await page.getByRole("button", { name: "Start reading" }).click();
20+
}
21+
await expect(page.locator(".subtitle")).toHaveText(/\w+ \d+/, { timeout: 90_000 });
22+
23+
// A LIGHT reader theme on a DARK device — the mismatch that exposed it.
24+
const paper = await page.evaluate(() => {
25+
const s = (window as any).__plumbline;
26+
s.config.theme = "light";
27+
s.applyTheme();
28+
return s.palette.paper as string;
29+
});
30+
expect(paper.toLowerCase()).toBe("#fcf9f4");
31+
const contents = await page.evaluate(() =>
32+
[...document.querySelectorAll('meta[name="theme-color"]')].map((m) => m.getAttribute("content")),
33+
);
34+
expect(contents.length, "the media-scoped pair is still there to be rewritten").toBe(2);
35+
for (const c of contents) expect(c?.toLowerCase(), "a tag the UA may be reading was left stale").toBe(paper.toLowerCase());
36+
});

apps/web/index.html

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,24 @@
132132
// Every string role, not a hand-picked five: the writer is
133133
// `applyTheme()`, which sets one custom property per string field, and
134134
// reproducing its subset here would be a second list to keep in step.
135-
if (stored)
135+
if (stored) {
136136
for (var role in stored)
137137
if (typeof stored[role] === "string")
138138
document.documentElement.style.setProperty("--" + role, stored[role]);
139+
// The status bar too, and BOTH tags (see session.applyTheme) — but
140+
// ONLY for a theme the reader chose by hand. For "follow the device"
141+
// the media-scoped pair above is already the right pre-script answer
142+
// and last session's paper may not be (the phone's scheme can change
143+
// between sessions); for an explicit theme the cached paper is right
144+
// whatever the device says, and the pair is what is stale — a
145+
// dark-mode phone under a light reader theme otherwise boots with
146+
// light icons over cream until the engine's palette lands.
147+
var choice = localStorage.getItem("plumbline:themeChoice");
148+
if (typeof stored.paper === "string" && choice && choice !== "system") {
149+
var metas = document.querySelectorAll('meta[name="theme-color"]');
150+
for (var i = 0; i < metas.length; i++) metas[i].setAttribute("content", stored.paper);
151+
}
152+
}
139153
} catch (e) {
140154
/* storage blocked or unparseable: the defaults above still paint */
141155
}

apps/web/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "plumbline-web",
33
"private": true,
4-
"version": "0.61.0",
4+
"version": "0.61.1",
55
"type": "module",
66
"scripts": {
77
"dev": "npm run pack:i18n && vite",

0 commit comments

Comments
 (0)