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
+43
View File
@@ -253,6 +253,41 @@ def human_dur(seconds: float) -> str:
return "<1m"
# r2b D1b — a booth's dates as the operator reads them. Created is a DATE
# (the day it began); updated is an AGE (how recently it moved). Each goes in a
# <time> whose `datetime` is `iso` and whose title is the full `stamp`, so the
# short form never hides the exact one. Local time, the box's own zone: the
# Booth has one viewer, on this box.
_MONTHS = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
def date_iso(epoch: float) -> str:
return datetime.fromtimestamp(epoch, tz=timezone.utc).isoformat(timespec="seconds")
def date_stamp(epoch: float) -> str:
return time.strftime("%Y-%m-%d %H:%M", time.localtime(epoch))
def date_day(epoch: float, now: float | None = None) -> str:
"""'12 Sep', with the year only when it is not this year's."""
t, n = time.localtime(epoch), time.localtime(time.time() if now is None else now)
day = f"{t.tm_mday} {_MONTHS[t.tm_mon - 1]}"
return day if t.tm_year == n.tm_year else f"{day} {t.tm_year}"
def date_ago(seconds: float) -> str:
"""An age in its largest whole unit: 'just now', '12m ago', '5h ago', '3d ago'."""
s = int(seconds)
if s < 60:
return "just now"
if s < 3600:
return f"{s // 60}m ago"
if s < 2 * 86400:
return f"{s // 3600}h ago"
return f"{s // 86400}d ago"
def _newest_mtime(path: Path) -> float:
"""Newest mtime among a folder and everything under it — OUR LOCKS EXCEPT.
@@ -1011,6 +1046,10 @@ def create_app(
auto_reload=False,
)
env.filters["dur"] = human_dur
env.filters["iso"] = date_iso
env.filters["stamp"] = date_stamp
env.filters["day"] = date_day
env.filters["ago"] = date_ago
templates = Jinja2Templates(env=env)
# embed.js IS READ ONCE, HERE, for exactly the reason above. It is the third
@@ -1133,6 +1172,9 @@ def create_app(
"bookmarks": bookmarks[:BOOKMARKS_SHOWN],
"bookmarks_total": len(bookmarks),
"board_url": f"/b/{quote(links_board, safe='')}/",
# r2b D1b: ONE clock for the page, so every row's "updated ...
# ago" is measured from the same instant.
"now": time.time(),
},
)
@@ -1301,6 +1343,7 @@ def create_app(
# handed to the operator lands HERE and not on the index.
"created_at": birth_time(booth),
"landed_at": _content_mtime(booth),
"now": time.time(),
# The lifetime line, same three states as the index card: a
# booth URL handed to the operator lands HERE, not on the index,
# so "why is this not counting down" has to be answerable here.