From 584447fc2e09f6434398ef3a289c4906c4ef8089 Mon Sep 17 00:00:00 2001 From: Elmehdi Aitbrahim Date: Mon, 7 Sep 2026 13:11:59 -0400 Subject: [PATCH] feat(web): the deployment card -- the badge's tooltip, reachable without a pointer (#755) `modeBadge` writes the identifying `--db`/`--config` pair into a `title`, and `title` is a HOVER tooltip. There is no hover on a phone, which is the device #648/#656 exist for -- so the half of the badge that actually identifies a deployment was unreachable exactly where the terminal is least available. The mode WORD identifies nothing on its own: two paper deployments both say `paper`, and a live sandbox and a paper-equities profile can both say `confirm`. A NATIVE DISCLOSURE, WHICH IS WHAT MAKES IT SAFE The badge is now the `` of a `
`. Keyboard-operable, touch-operable and announced, with no handler to bind and no button to build -- so `deploymentCard` JOINS `sessionChip` and `paperBanner` in `test_neither_the_chip_nor_the_banner_builds_anything_clickable` rather than being exempted from it. `venueCard` already uses the element the same way. That matters more than convenience. This is the surface where a reader looks for the control, which makes it the likeliest place for one to appear later. A "switch to live" affordance arriving through a door marked accessibility is still the growth funnel `sessionChip`'s docstring refuses, so the card is held to the stricter standard on the way in. The card says so in words, too. Without the sentence it reads as an unfinished settings panel; with it the refusal is legible where someone would look for the control. The test pins the constant's CONTENT, not just its placement, and refuses four invitation wordings -- a reference-only assertion would pass over a note reworded into a CTA. TAGS CHANGED, LAYOUT DID NOT `` and `` are DIVs now: `
` is flow content and a `` may hold only phrasing content. Both already set their own `display: inline-flex` in `keel.css`, so nothing moves. The card is positioned out of flow -- opening it must not reflow the header, which on a narrow screen would push the nav under the reader's finger mid-tap. `title` is KEPT, per the issue: it works for keyboard and pointer and costs nothing. This supplements it rather than replacing it. TWO SCANS CAUGHT ME `SWITCHING_NOTE` was spelled with `+`, which `test_render_contains_no_arithmetic` forbids in `render.js` -- string concatenation is `.concat` throughout that file precisely so the derivation-free scan can stay simple. And the header test pins the badge's TAG, so changing it to `` failed there; updated with the reason, since what it protects (the badge exists, carrying that id and both classes, which `modeBadge` and `header .mode:empty` key on) is unchanged. Registered in `_CONFIG_READERS` in the same commit that adds it, which is the rule that table states: a new consumer arrives with the check covering it. Four config keys, and a rename on either side would otherwise leave the card rendering em-dashes forever. 6,319 passed / 3 skipped, ruff clean. Three mutants killed: the card built but never mounted, the refusal reworded as an invitation, and the db path dropped. Not proven here and unchanged: that the popover is legible on a real phone, or that a tap opens it. Hand checks against a running `keel serve`. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KZZxmspQXe5qJ9FAsG13s6 --- keel/web/static/css/keel.css | 42 +++++++++++ keel/web/static/index.html | 29 ++++++-- keel/web/static/js/main.js | 7 ++ keel/web/static/js/render.js | 51 ++++++++++++++ tests/web/test_client_assets.py | 13 +++- tests/web/test_deployment_card.py | 112 ++++++++++++++++++++++++++++++ 6 files changed, 247 insertions(+), 7 deletions(-) create mode 100644 tests/web/test_deployment_card.py diff --git a/keel/web/static/css/keel.css b/keel/web/static/css/keel.css index 7cd0770..a3daee6 100644 --- a/keel/web/static/css/keel.css +++ b/keel/web/static/css/keel.css @@ -245,6 +245,48 @@ header .mode { * pill is a stray mark, and an unreadable config should cost the badge rather than guess a * mode. Same pairing as `#build` in the footer, which is also empty until the same read. */ header .mode:empty { display: none; } + +/* ── the deployment card (#755) ─────────────────────────────────────────────────────────────── + * + * The badge is the `` of a native disclosure, so the identifying `--db`/`--config` pair + * -- which `modeBadge` also writes into a `title`, and `title` is hover-only -- is reachable by + * tap on a device that has no pointer. + * + * The whole disclosure hides when the badge is empty, matching `header .mode:empty`: a config + * that could not be read is not a deployment called "unknown", and an empty pill with a + * disclosure triangle beside it would be worse than nothing. */ +header #deployment:has(> summary:empty) { display: none; } + +header #deployment { position: relative; } + +/* The default triangle is removed on BOTH spellings -- `::-webkit-details-marker` for older + * WebKit, `list-style` for everything current -- because the badge is a pill and a marker glued + * to it reads as a chevron on a button, which is the dropdown this is deliberately not. */ +header #deployment > summary { cursor: pointer; list-style: none; } +header #deployment > summary::-webkit-details-marker { display: none; } + +/* Anchored to the badge and OUT of flow: opening it must not reflow the header, which on a narrow + * screen would push the nav around under the reader's finger mid-tap. */ +header #deployment > #deployment-card { + position: absolute; + right: 0; + top: calc(100% + 0.4rem); + z-index: 2; + min-width: min(22rem, 88vw); + background: var(--bg); + border: 1px solid var(--control-line); + border-radius: 6px; + padding: 0.6rem; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18); + text-align: left; +} + +/* The paths are the point of the card and they are long. Wrapping beats a horizontal scrollbar + * inside a popover no wider than a phone. */ +header #deployment > #deployment-card .v { + overflow-wrap: anywhere; + font-size: 0.85em; +} /* The emphasis table `render.js::MODE_CLASS` places: paper is the quiet one (muted, like any * label), confirm and live carry the accent, because a deployment that can place orders should * sit up in the header. NOT `--good`/`--bad` -- those are `state`'s vocabulary and there is no diff --git a/keel/web/static/index.html b/keel/web/static/index.html index 09b3273..eaebd03 100644 --- a/keel/web/static/index.html +++ b/keel/web/static/index.html @@ -178,7 +178,7 @@ (`:empty` in `keel.css`), because an empty pill is a stray mark, and an unreadable config should cost the badge rather than guess a mode. --> - +
- + +
- +
+ +
+
- +