feat(desk): the Desk row, booth dates, and the theme toggle (r2b merge 2: D1 + D1b + D3)

Operator rulings, 2026-09-23.

D1, the Desk row:
- Kept vs ephemeral reads at a glance: an always-visible lifetime pill in
  the right column (sage ★ kept, amber held, ◷ counting down).
- The facts line is facts only.
- zip / keep|release / wipe are one cluster, with zip out of the middle.
  Where a real hover exists it floats over the preview strip (covering
  pictures, never information), appears on hover or keyboard focus, and
  takes no room. Anywhere else (touch, any coarse pointer) it is the
  row's last line, visible, with 32px controls. × hides too (the operator
  answered yes).
D1b: "created 12 Sep" (filesystem birth time; nothing when unknown) and
  "updated 5d ago" (the content clock), as <time> facts on the row and in
  the booth header, from one macro and one clock per page.
D3, the theme toggle: System · Light · Dark in the top bar.
- Stored in localStorage and applied in <head> before any stylesheet.
- System removes data-theme, so the OS query follows the OS live, with
  no listener.
- The token sheet is re-vendored at the same SVOS SHA with a scoping-only
  transform (155 declarations, the same set, both directions), so forced
  themes win over the OS and high contrast follows the theme in effect.
- The ask chrome inside verbatim pages follows the choice through
  data-bk-theme on our own fragments, live across tabs. The host page's
  <html> is never touched.

Declared test changes:
- two row tests replaced;
- the wipe-dialog test hovers first;
- four r2_flow rows retired, with successors in r2b.toml (45/45).
785 passed.
This commit is contained in:
vh
2026-09-23 19:25:32 -07:00
parent ca0641f55b
commit 436d234ca0
12 changed files with 868 additions and 185 deletions
+111
View File
@@ -997,3 +997,114 @@ def test_the_fog_landing_is_built_from_the_ring_never_echoed(tmp_path):
assert loc("gone.png") == "/b/g/"
assert loc("n.md") == "/b/g/"
assert loc("") == "/b/g/"
def _css_blocks(css: str) -> list[tuple[str, list[str]]]:
"""[(media prelude | selector, declarations)] in source order. Enough of a
parser for the vendored sheet: blocks are one level deep, or one inside a
single @media."""
out, media = [], ""
for m in re.finditer(r'(@media[^{]+)\{|([^{}@]+?)\s*\{([^{}]*)\}|\}', css):
if m.group(1):
media = " ".join(m.group(1).split())
elif m.group(2) is not None:
key = (media + " | " + " ".join(m.group(2).split()).split("*/")[-1].strip()).strip()
body = re.sub(r"/\*.*?\*/", "", m.group(3), flags=re.S) # a comment hides the declaration after it
decls = [d.strip() for d in body.split(";") if d.strip()]
out.append((key, decls))
else:
media = ""
return out
def test_a_forced_theme_and_the_os_theme_are_the_same_declarations():
"""r2b D3: light is written twice — under the OS query for
`:root:not([data-theme="dark"])`, and bare for `:root[data-theme="light"]`
— and light-hc twice likewise. The two copies of each must be the same
declarations, value for value, or forcing a theme would give a different
theme than the OS asking for it.
And each must declare exactly the property set of the block it overrides,
read from the UNMOVED dark block (SVOS's light and dark declare the same
properties, as do its two high-contrast blocks). A check that only compared
the re-scoped copies with each other would pass a transform that dropped
the same declaration from both (heid contract panel, 4/4)."""
css = (pathlib.Path(__file__).parent.parent / "booth/templates/_svos_tokens.css").read_text()
blocks = _css_blocks(css)
by = {}
for key, decls in blocks:
by.setdefault(key, []).append(decls)
props = lambda decls: {d.split(":", 1)[0].strip() for d in decls}
dark = by["| :root"][1] # primitives, DARK, art layer
art = by["| :root"][2]
[os_light] = by['@media (prefers-color-scheme: light) | :root:not([data-theme="dark"])']
[forced_light] = by['| :root[data-theme="light"]']
[dhc] = by["@media (prefers-contrast: more) | :root"]
[os_lhc] = by['@media (prefers-contrast: more) and (prefers-color-scheme: light) | :root:not([data-theme="dark"])']
[forced_lhc] = by['@media (prefers-contrast: more) | :root[data-theme="light"]']
assert "color-scheme: dark" in dark and len(dark) > 30 # the parser found the real dark block
assert os_light == forced_light
assert os_lhc == forced_lhc
assert props(forced_light) == props(dark) | (props(forced_light) & props(art))
assert props(dark) <= props(forced_light)
assert props(forced_lhc) == props(dhc)
def _row(body: str, name: str) -> str:
return re.search(r'<article class="desk-row[^"]*" data-booth="%s".*?</article>' % re.escape(name), body, re.S).group(0)
def test_the_lifetime_pill_class_is_kept_held_or_counting(tmp_path):
"""r2b D1 (operator: 'make it obvious which are kept and which are
ephemeral'): the lifetime is a pill in the right column, its CLASS chosen
by state and its words exactly the lifetime macro's."""
from booth.marks import declare_pick
k = _booth(tmp_path, "kept1", {"a.png": PNG})
(k / ".forever").write_bytes(b"")
h = _booth(tmp_path, "held", {"a.png": PNG})
declare_pick(h, "q", {"prompt": "Which?", "options": ["x", "y"]})
_booth(tmp_path, "loose", {"a.png": PNG})
body = _client(tmp_path).get("/").text
pill = lambda n: re.search(r'<span class="life (life-\w+)"[^>]*>(.*?)</span>\s*</div>', _row(body, n), re.S)
assert pill("kept1").group(1) == "life-kept" and "kept" in pill("kept1").group(2)
assert pill("held").group(1) == "life-held" and "held until answered" in pill("held").group(2)
assert pill("loose").group(1) == "life-count" and "expires in" in pill("loose").group(2)
# and the lifetime left the facts line, which is facts only
assert "expires in" not in re.search(r'class="desk-facts".*?</div>', _row(body, "loose"), re.S).group(0)
def test_created_and_updated_are_dated_facts_and_none_says_nothing(tmp_path, monkeypatch):
"""r2b D1b (operator: 'creation and update dates on the booths'): created is
a date, updated an age, each a <time> with its full stamp as the title, on
the Desk row's facts line and in the booth header. A birth time the
filesystem cannot give renders NOTHING — never a guess (booth-dev)."""
import os
import booth.app as app_mod
t0 = time.time() - 5 * 86400
b = _booth(tmp_path, "g", {"a.png": PNG})
os.utime(b / "a.png", (t0, t0))
_booth(tmp_path, "nobirth", {"a.png": PNG})
real = app_mod.birth_time
monkeypatch.setattr(app_mod, "birth_time", lambda p: None if p.name == "nobirth" else t0 - 86400)
c = _client(tmp_path)
facts = lambda n: re.search(r'class="desk-facts".*?</div>', _row(c.get("/").text, n), re.S).group(0)
g = facts("g")
assert re.search(r'<time[^>]*datetime="\d{4}-\d\d-\d\dT[^"]+"[^>]*title="created [^"]+"[^>]*>created [^<]+</time>', g)
assert re.search(r'<time[^>]*title="updated [^"]+"[^>]*>updated 5d ago</time>', g)
assert "created" not in facts("nobirth")
head = c.get("/b/g/").text
assert re.search(r'<time[^>]*>created [^<]+</time>', head) and re.search(r'<time[^>]*>updated 5d ago</time>', head)
monkeypatch.setattr(app_mod, "birth_time", real)
def test_a_forced_theme_is_applied_before_first_paint(tmp_path):
"""r2b D3 / INV-6: the script that applies a stored theme runs in <head>
BEFORE any stylesheet, so a forced theme never flashes the other one. The
toggle is markup on every page, always `hidden` until the script shows it."""
_booth(tmp_path, "g", {"a.png": PNG})
c = _client(tmp_path)
for url in ("/", "/b/g/"):
page = c.get(url).text
head = page[:page.index("</head>")]
assert head.index("booth.theme") < head.index("<style>"), url
assert re.search(r'<div class="theme"[^>]*hidden', page), url