Focus ring clipped under the last field; redundant 'Copied' line on recovery words #290

Open
opened 2026-11-01 15:17:58 +00:00 by kayg · 6 comments
Owner

Owner report (production, first-owner setup, step 2 of 4, "Who are you?"): when the second field ("display name") has focus, the BOTTOM edge of its focus ring is hidden. The username field's ring is fully visible.

Second owner fix, same flow (step 4, recovery words): after pressing copy the button already turns into a tick, yet a line below still says "Copied. Store the words somewhere private." Owner: "there is no need to display the additional message."

Job J66. Branch fix-focus-ring-clip from origin/dev 37bbda455.

Owner report (production, first-owner setup, step 2 of 4, "Who are you?"): when the second field ("display name") has focus, the BOTTOM edge of its focus ring is hidden. The username field's ring is fully visible. Second owner fix, same flow (step 4, recovery words): after pressing copy the button already turns into a tick, yet a line below still says "Copied. Store the words somewhere private." Owner: "there is no need to display the additional message." Job J66. Branch `fix-focus-ring-clip` from origin/dev 37bbda455.
Author
Owner

Start. Branch fix-focus-ring-clip, base origin/dev 37bbda455.

Cause (found by reading, not yet measured). .auth-viewport (apps/tracer/auth.css:174) is overflow: hidden; margin: 0 -6px; padding: 0 6px. The ring is the global outline: 2px + outline-offset: 2px (packages/ui/styles.css:12), so it reaches 4 px outside the field. The 6 px side padding leaves room left/right, but top/bottom have none. The display-name input is the last element in the viewport (the footer is outside it), so its bottom 4 px is clipped. Username has a hint below it, so its ring fits.

Plan (estimate ~2 h):

  1. Failing Playwright test first on a production build: focus every sign-in and setup field, assert the full ring rect is inside every ancestor's clip and not covered (elementFromPoint at the ring's edge midpoints and corners).
  2. Fix: give the shared pane viewport vertical ring room (padding plus compensating negative margin, same trick as the sides) so every position is covered; check the add-host and recovery fields with the same test.
  3. Separate commit: remove the post-copy "Copied. Store the words somewhere private." line on the recovery-words step (the tick on the button is the confirmation). The advice already sits in the step's own lede ("Store them somewhere safe and offline."), so nothing moves. The failure line ("Copy is unavailable here…") stays. Update onboarding.spec.ts.
  4. Docs commit: TASTE.md rows (ring never clipped or covered; confirmation lives on the control).
  5. Screenshots 1440x900 DPR2 and 393x852 DPR3, scripts/verify fast on dev-02, push.
**Start.** Branch `fix-focus-ring-clip`, base origin/dev `37bbda455`. **Cause (found by reading, not yet measured).** `.auth-viewport` (apps/tracer/auth.css:174) is `overflow: hidden; margin: 0 -6px; padding: 0 6px`. The ring is the global `outline: 2px` + `outline-offset: 2px` (packages/ui/styles.css:12), so it reaches 4 px outside the field. The 6 px side padding leaves room left/right, but top/bottom have none. The display-name input is the last element in the viewport (the footer is outside it), so its bottom 4 px is clipped. Username has a hint below it, so its ring fits. **Plan (estimate ~2 h):** 1. Failing Playwright test first on a production build: focus every sign-in and setup field, assert the full ring rect is inside every ancestor's clip and not covered (elementFromPoint at the ring's edge midpoints and corners). 2. Fix: give the shared pane viewport vertical ring room (padding plus compensating negative margin, same trick as the sides) so every position is covered; check the add-host and recovery fields with the same test. 3. Separate commit: remove the post-copy "Copied. Store the words somewhere private." line on the recovery-words step (the tick on the button is the confirmation). The advice already sits in the step's own lede ("Store them somewhere safe and offline."), so nothing moves. The failure line ("Copy is unavailable here…") stays. Update onboarding.spec.ts. 4. Docs commit: TASTE.md rows (ring never clipped or covered; confirmation lives on the control). 5. Screenshots 1440x900 DPR2 and 393x852 DPR3, `scripts/verify fast` on dev-02, push.
Author
Owner

Finding: reproduced, cause confirmed by measurement. New spec apps/tracer/test/focus-ring-clip.spec.ts (Tab-walks every focus stop of setup steps 1-4, returning sign-in and the recovery-key entry at 1440x900 and 393x852 on the production build) run against unmodified code (branch head 2eb065190, dev-02):

Expected: []
Received: [
  "setup-2-identity-desktop #setup-display-name: bottom edge clipped by div.auth-viewport",
  "setup-2-identity-phone #setup-display-name: bottom edge clipped by div.auth-viewport",
]

Every other stop passes (username, token, buttons, checkbox, recovery words). .auth-viewport is overflow: hidden; margin: 0 -6px; padding: 0 6px: room at the sides, none above or below. The ring reaches 4 px (2 px outline + 2 px offset). Display name is the last element in the viewport, so its bottom 4 px is cut; username has its hint below it.

Fix: give the viewport the same 6 px of room on all four sides (padding with a compensating negative margin, so layout does not move), tied to a ring-room token next to the ring definition in packages/ui. recovery.ts reads the viewport's border-box height back as a content-box height; that one line changes to read the computed height so the padding is not counted twice.

**Finding: reproduced, cause confirmed by measurement.** New spec `apps/tracer/test/focus-ring-clip.spec.ts` (Tab-walks every focus stop of setup steps 1-4, returning sign-in and the recovery-key entry at 1440x900 and 393x852 on the production build) run against unmodified code (branch head 2eb065190, dev-02): ``` Expected: [] Received: [ "setup-2-identity-desktop #setup-display-name: bottom edge clipped by div.auth-viewport", "setup-2-identity-phone #setup-display-name: bottom edge clipped by div.auth-viewport", ] ``` Every other stop passes (username, token, buttons, checkbox, recovery words). `.auth-viewport` is `overflow: hidden; margin: 0 -6px; padding: 0 6px`: room at the sides, none above or below. The ring reaches 4 px (2 px outline + 2 px offset). Display name is the last element in the viewport, so its bottom 4 px is cut; username has its hint below it. Fix: give the viewport the same 6 px of room on all four sides (padding with a compensating negative margin, so layout does not move), tied to a ring-room token next to the ring definition in packages/ui. `recovery.ts` reads the viewport's border-box height back as a content-box height; that one line changes to read the computed height so the padding is not counted twice.
Author
Owner

Finding + correction. The ring fix works for the reported field: with it, setup-2-identity no longer fails (before: #setup-display-name bottom edge, ring 628.0 > clip 624.5 desktop, 616.2 > 612.5 phone).

My first geometry attempt was wrong and is fixed in the branch: the page is border-box and the script sets the viewport height from the pane, so adding padding made the content box 12 px short (footer moved up 12 px; caught by the gap assertion I added). The viewport is now content-box; gaps 30 px above and 26.3 px below match origin/dev.

The spec used to stop at the first failing step, so it hid two more clipped rings. With it now collecting every step, the SAME two show on unmodified origin/dev and on my branch (not caused by my change):

setup-4-recovery-desktop #download-recovery: right edge clipped by div.auth-viewport (ring 919.0 > clip 913.0)
setup-4-recovery-phone   #download-recovery: right edge clipped by div.auth-viewport (ring 362.0 > clip 356.0)
setup-4-recovery-phone   #setup-back: bottom edge clipped by the page (ring 855.8 > clip 852.0)

Cause: the actions row is margin-right: -8px so its icon sits on the content edge, which puts the button's ring 12 px past the content, beyond the viewport's 6 px; and Tab scrolls the phone page so the back link is flush with the bottom edge. Fixing both in this branch (same family, same spec), then the three polish items (#291).

**Finding + correction.** The ring fix works for the reported field: with it, `setup-2-identity` no longer fails (before: `#setup-display-name` bottom edge, ring 628.0 > clip 624.5 desktop, 616.2 > 612.5 phone). My first geometry attempt was wrong and is fixed in the branch: the page is border-box and the script sets the viewport `height` from the pane, so adding padding made the content box 12 px short (footer moved up 12 px; caught by the gap assertion I added). The viewport is now content-box; gaps 30 px above and 26.3 px below match origin/dev. The spec used to stop at the first failing step, so it hid two more clipped rings. With it now collecting every step, the SAME two show on unmodified origin/dev and on my branch (not caused by my change): ``` setup-4-recovery-desktop #download-recovery: right edge clipped by div.auth-viewport (ring 919.0 > clip 913.0) setup-4-recovery-phone #download-recovery: right edge clipped by div.auth-viewport (ring 362.0 > clip 356.0) setup-4-recovery-phone #setup-back: bottom edge clipped by the page (ring 855.8 > clip 852.0) ``` Cause: the actions row is `margin-right: -8px` so its icon sits on the content edge, which puts the button's ring 12 px past the content, beyond the viewport's 6 px; and Tab scrolls the phone page so the back link is flush with the bottom edge. Fixing both in this branch (same family, same spec), then the three polish items (#291).
Author
Owner

Revised plan (owner polish items A/B/C from #291 added; estimate +2.5 h; original ring work is past its 2 h estimate because the first geometry attempt was wrong, see above).

Commits, in order: recovery-words (done) -> ring fix (done) -> ring room for the actions row and the phone page edge (the two pre-existing clips) -> (A) -> (B) -> (C) -> one docs commit with every TASTE row.

  • (A) Machine details card. The "action" is a secondary .ui-button whose text is the server's registry summary, centred and wrapping. It becomes a row built from the existing settings row (.ui-sp-row) with a danger tone: short label "Remove host", the registry sentence as its quiet hint, left-aligned like the key/value rows. Blocked in part: TASTE (line 176, #242) says a destructive row asks in its own row, but no in-row confirmation component exists on origin/dev. I will not build a second one (one behaviour, one component); the row keeps today's anchored confirmation until #242's component is merged, then swaps. Needs your call if you want it earlier.
  • (B) Option lists with hints. Today the hint is right-aligned (margin-right: auto on the label), so its start x follows each label's length. Choice: one aligned hint column per list (width of the widest label), because stacking every hint would double the height of the Theme/Font lists (TASTE: rows 28-32 px); the existing narrow-width stacking fallback (TASTE "Text never collides") stays. Playwright check, +-0.5 px.
  • (C) Section heading gap. .ui-ts-title has padding: 6px 0 2px (2 px above the first row). One token --ui-heading-gap in packages/ui, used by every section heading there. Spacing assertion.
  • Production-build screenshots before/after (1440x900 DPR2, 393x852 DPR3), attached here.
**Revised plan (owner polish items A/B/C from #291 added; estimate +2.5 h; original ring work is past its 2 h estimate because the first geometry attempt was wrong, see above).** Commits, in order: recovery-words (done) -> ring fix (done) -> ring room for the actions row and the phone page edge (the two pre-existing clips) -> (A) -> (B) -> (C) -> one docs commit with every TASTE row. - **(A) Machine details card.** The "action" is a secondary `.ui-button` whose text is the server's registry summary, centred and wrapping. It becomes a row built from the existing settings row (`.ui-sp-row`) with a danger tone: short label "Remove host", the registry sentence as its quiet hint, left-aligned like the key/value rows. **Blocked in part:** TASTE (line 176, #242) says a destructive row asks in its own row, but no in-row confirmation component exists on origin/dev. I will not build a second one (one behaviour, one component); the row keeps today's anchored confirmation until #242's component is merged, then swaps. Needs your call if you want it earlier. - **(B) Option lists with hints.** Today the hint is right-aligned (`margin-right: auto` on the label), so its start x follows each label's length. Choice: one aligned hint column per list (width of the widest label), because stacking every hint would double the height of the Theme/Font lists (TASTE: rows 28-32 px); the existing narrow-width stacking fallback (TASTE "Text never collides") stays. Playwright check, +-0.5 px. - **(C) Section heading gap.** `.ui-ts-title` has `padding: 6px 0 2px` (2 px above the first row). One token `--ui-heading-gap` in packages/ui, used by every section heading there. Spacing assertion. - Production-build screenshots before/after (1440x900 DPR2, 393x852 DPR3), attached here.
Author
Owner

Screenshots, production build (dev-02), before = unmodified origin/dev 37bbda455, after = branch head e66b2a21e. 1440x900 at DPR 2 and 393x852 at DPR 3, ink theme except the machine card (paper).

  • (B) Settings -> Window Sizes: before-*-window-sizes.png / after-*-window-sizes.png. The Desktop list's descriptions were right-aligned at ragged x; now one left edge at the widest name + 10 px (0.5 px checked in settings-rows.spec.ts). The Phones list stacks in this narrow window, as before, each description on its name's edge.
  • (C) heading gap: before-*-hosts-details-heading.png / after-*-hosts-details-heading.png (DETAILS and HOST CONNECTIONS) and the same in the window-sizes pair (PHONES, DESKTOP). Gap under a heading 2 px -> 8 px (--ui-heading-gap). 8 px is my first pick; it is one number to change.
  • (A) machine details card: after-*-machine-details.png. There is no "before" from the mock: the prototype never showed the removal action (the product only shows it for a real second host), so the "before" is the owner's production screenshot. The row is shown with ?removal=1 on the windows prototype (a no-op action).
**Screenshots, production build (dev-02), before = unmodified origin/dev `37bbda455`, after = branch head `e66b2a21e`.** 1440x900 at DPR 2 and 393x852 at DPR 3, ink theme except the machine card (paper). - **(B) Settings -> Window Sizes:** `before-*-window-sizes.png` / `after-*-window-sizes.png`. The Desktop list's descriptions were right-aligned at ragged x; now one left edge at the widest name + 10 px (0.5 px checked in `settings-rows.spec.ts`). The Phones list stacks in this narrow window, as before, each description on its name's edge. - **(C) heading gap:** `before-*-hosts-details-heading.png` / `after-*-hosts-details-heading.png` (DETAILS and HOST CONNECTIONS) and the same in the window-sizes pair (PHONES, DESKTOP). Gap under a heading 2 px -> 8 px (`--ui-heading-gap`). 8 px is my first pick; it is one number to change. - **(A) machine details card:** `after-*-machine-details.png`. There is no "before" from the mock: the prototype never showed the removal action (the product only shows it for a real second host), so the "before" is the owner's production screenshot. The row is shown with `?removal=1` on the windows prototype (a no-op action).
Author
Owner

Finish. Branch fix-focus-ring-clip, head e66b2a21e, base origin/dev 37bbda455. Not merged, not deployed.

Commits (atomic, oldest first):

  1. 342a530fc recovery words: the copy tick is the only confirmation
  2. 4642d22f8 ui: the focus ring is never clipped; the pane viewport leaves it room (+ the two other clipped rings the spec found)
  3. ddb6987c6 (A) the machine details card's destructive action is a danger row
  4. 3b03c35ba (B) an option list's descriptions share one left edge
  5. fbe5eb081 (C) section headings share one gap
  6. e66b2a21e taste rows (one docs commit)

Focus ring screenshots (attached): ring-before-* / ring-after-*, setup step 2, display name focused, 1440x900 DPR 2 and 393x852 DPR 3. Before: bottom edge missing. After: whole.

scripts/verify fast on dev-02 at e66b2a21e, verbatim:

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| fmt                       | PASS         |
| VT native fmt             | PASS         |
| CLI plugin compile-out    | PASS         |
| clippy                    | PASS         |
| SSH runtime test helper   | PASS         |
| real-binary fixture paths | PASS         |
| test                      | FAIL (101)   |
| SSH inode reuse fixture   | PASS         |
| shim process-per-user     | PASS         |
| cargo-deny                | PASS         |
| gate architecture         | PASS         |
| static builds             | SKIP (x86)   |
+---------------------------+--------------+
VERIFY_EXIT=1

==> Repository artifact size gate
repository artifact gate: PASS (13 changed path(s), merge base 37bbda45554f)

test FAIL (101) is not from this branch: five restart::tests in crates/infraternal (no Rust touched here). The same five fail on unmodified origin/dev 37bbda455 on dev-02: cargo test -p infraternal restart::tests -> 13 passed; 5 failed, exit 101. Static builds SKIP on arm as expected. verify fast runs no TypeScript, so separately: bun x tsc --project tsconfig.json -> TSC_OK.

Browser specs on a production build (dev-02): focus-ring-clip (setup steps 1-4, returning sign-in, recovery entry, both viewports) pass; settings-rows (6 size/theme runs + heading test), details-removal-row (2), details, machine-sections, onboarding, focus-ring, recovery, phone-window-width, font-switch pass. sign-out.spec.ts: 2 of 13 fail (palette sign-out, D245 storage); identical on unmodified origin/dev (same 2, same timings) and at my head. In one big batch 3 more sign-out tests timed out at 31-39 s each while the machine was loaded; rerun alone at my head they pass.

Not done / needs a decision:

  • (A) The in-row confirmation (TASTE, #242) is not implemented on dev, so the row keeps the anchored confirmation; swap when #242's component lands. Danger-colour contrast per theme not measured.
  • With a build that enables the unapproved recovery link (INFRATERNAL_RECOVERY_UI=1, D162), "use recovery key" sits flush under the white passkey button and its ring's top edge lies on that fill (spec reports it covered). Not changed: unapproved presentation behind a build flag.
  • Heading gap 8 px (C) is my first pick.
  • Recovery-words: the success line also used to be the role=status announcement; the button's aria-label change is now the only signal and the button is blurred right after. No screen reader was run.
  • The shared .ui-pane-viewport (add-host wizard) clips while a step moves; not measured, not changed.
**Finish. Branch `fix-focus-ring-clip`, head `e66b2a21e`, base origin/dev `37bbda455`. Not merged, not deployed.** Commits (atomic, oldest first): 1. `342a530fc` recovery words: the copy tick is the only confirmation 2. `4642d22f8` ui: the focus ring is never clipped; the pane viewport leaves it room (+ the two other clipped rings the spec found) 3. `ddb6987c6` (A) the machine details card's destructive action is a danger row 4. `3b03c35ba` (B) an option list's descriptions share one left edge 5. `fbe5eb081` (C) section headings share one gap 6. `e66b2a21e` taste rows (one docs commit) **Focus ring screenshots (attached): `ring-before-*` / `ring-after-*`**, setup step 2, display name focused, 1440x900 DPR 2 and 393x852 DPR 3. Before: bottom edge missing. After: whole. **`scripts/verify fast` on dev-02 at `e66b2a21e`, verbatim:** ``` +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | fmt | PASS | | VT native fmt | PASS | | CLI plugin compile-out | PASS | | clippy | PASS | | SSH runtime test helper | PASS | | real-binary fixture paths | PASS | | test | FAIL (101) | | SSH inode reuse fixture | PASS | | shim process-per-user | PASS | | cargo-deny | PASS | | gate architecture | PASS | | static builds | SKIP (x86) | +---------------------------+--------------+ VERIFY_EXIT=1 ==> Repository artifact size gate repository artifact gate: PASS (13 changed path(s), merge base 37bbda45554f) ``` `test` FAIL (101) is not from this branch: five `restart::tests` in `crates/infraternal` (no Rust touched here). The same five fail on unmodified origin/dev `37bbda455` on dev-02: `cargo test -p infraternal restart::tests` -> `13 passed; 5 failed`, exit 101. Static builds SKIP on arm as expected. `verify fast` runs no TypeScript, so separately: `bun x tsc --project tsconfig.json` -> TSC_OK. **Browser specs on a production build (dev-02):** `focus-ring-clip` (setup steps 1-4, returning sign-in, recovery entry, both viewports) pass; `settings-rows` (6 size/theme runs + heading test), `details-removal-row` (2), `details`, `machine-sections`, `onboarding`, `focus-ring`, `recovery`, `phone-window-width`, `font-switch` pass. `sign-out.spec.ts`: 2 of 13 fail (palette sign-out, D245 storage); identical on unmodified origin/dev (same 2, same timings) and at my head. In one big batch 3 more sign-out tests timed out at 31-39 s each while the machine was loaded; rerun alone at my head they pass. **Not done / needs a decision:** - (A) The in-row confirmation (TASTE, #242) is not implemented on dev, so the row keeps the anchored confirmation; swap when #242's component lands. Danger-colour contrast per theme not measured. - With a build that enables the unapproved recovery link (`INFRATERNAL_RECOVERY_UI=1`, D162), "use recovery key" sits flush under the white passkey button and its ring's top edge lies on that fill (spec reports it covered). Not changed: unapproved presentation behind a build flag. - Heading gap 8 px (C) is my first pick. - Recovery-words: the success line also used to be the `role=status` announcement; the button's aria-label change is now the only signal and the button is blurred right after. No screen reader was run. - The shared `.ui-pane-viewport` (add-host wizard) clips while a step moves; not measured, not changed.
Sign in to join this conversation.
No labels
launch-qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/infraternal#290
No description provided.