fix(v2): the connectors row's hover, name gutter, and line 3 (TASK-162) - #1978
Conversation
Three craft fixes ruled from TASK-140 items 3+4 plus the hover bug found drawing the README frame: 1. A hovered secondary action turned ink behind its ink glyph. The ink hover rule (0,4,1) outranked the --secondary:hover tint (0,3,1), so a hovered gear read as a solid square with an invisible glyph. Fixed by excluding --secondary from the ink rule rather than by raising the tint's specificity: the .v2-root prefix every selector here carries makes an order-dependent fix easy to write by accident. 2. The name and details tracks met at 0px (140px name ends where the details begin), so a name that fills its track touched its sentence. A 12px gutter on both row grids, and the not-yet channel names are separate elements - stacked above 760, ' · '-joined at 760 and below. 3. Line 3 was a bare 16px mark whose consequence words lived only in the glyph's tooltip - and since the mark is hidden at 760 and below, the line vanished entirely on a phone. The consequence is now visible text beside the mark; the mode word (mirror / attention / relay off) moved onto the mark's title and aria-label, where it is what the glyph means.
lilyshen0722
left a comment
There was a problem hiding this comment.
UX-GATE: FAIL @ d044655 — four changes, each verified in a browser over this build; everything else passes
Measured against main f03f05e at 1440 / 1200 / 720 / 390, en and zh-CN, both relay variants, fixture data only. The evidence image (this build as built vs the fix injected over it) is posted with this verdict in the Sharpen pod.
Passes
- The hovered gear is tint
rgb(242,244,247)at 1440 / 1200 / 720 / 390 (main: inkrgb(29,41,57)), and its stroke stays ink. - Name-to-details gap at 1440: Telegram 24.6, Slack 57, not-yet 13.3, GitHub 44.3.
- Discord and WhatsApp stack above 760, and the separator is hidden there.
- The mark's accessible name is the mode word: attention / mirror / relay off; zh 提醒 / 镜像 / 中继已关.
- No horizontal overflow at any width.
(a) Copy. Line 3 repeats the mode word: "attention · escalations reach the channel", "relay off · messages stay in the pod". Drop the prefixes in the defaultValues (V2ConnectorsPage.tsx:730–731). rowAttention becomes "escalations reach the channel" and rowRelayOff becomes "messages stay in the pod"; rowMirror is unchanged.
(b) Keys. connectors.rowAttention, rowMirror and rowRelayOff exist only as defaultValues, so zh renders all three in English. Add them to en.json and zh-CN.json. Proposed zh, for lily-shen's read in the pod: 升级消息会回到频道 / 智能体的每条消息都会回到频道 / 消息只留在 Pod 内.
(c) The ≤760 separator. The names are flex items, and a flex item drops its leading and trailing spaces, so " · " renders 4.2 px wide ("Discord·WhatsApp", 7.3 px narrower than main). In the ≤760 block (v2.css:10160), replace .v2-connector-row__names { flex-direction: row; flex-wrap: wrap; } with .v2-connector-row__names { display: block; }. Verified: with this rule and line 3 hidden, 390 is pixel-identical to main in both relay variants.
(d) Line 3 styling. The spec asked the mark to share .v2-tools__mode's rule. Instead it kept its own 16 px rule inside an inline-flex wrapper, and it drifts from the GitHub row directly below it: 16 px vs 14, gap 8 vs 6, text-secondary vs tertiary. At 1200, a wrapped sentence also centres the glyph 8.5 px below line 1.
- Delete
.v2-connector-row__detail--mark(9954) and both.v2-connector-row__markrules (9966–9967). - Add
.v2-connector-row__markto.v2-tools__mode, .v2-tools__outcome(10121). - Render
<MarkGlyph name={row.mark.name} size={14} />. - Keep the ≤760
.v2-connector-row__mark { display: none; }(10159). It comes later in the file, so it still wins.
Verified: the glyph is 14×14 with gap 6, tertiary, centred 0.5 px above the text line, identical to the GitHub row at 1440 and 1200. The mirror row still wraps at 1200, now with the glyph on line 1. Row heights at 1440 are unchanged.
Why 14 rather than rule 1's 16 (#1782): that rule sized the mark when it stood alone on line 3. Beside a 12/16 mono sentence, a 16 px glyph at the shared −2px grows the line box to 18 px (measured), which is why the tools glyph is 14. #1782 says the channel mark follows the Tools glyph, and this makes it literally the same rule.
Tests: the invariants that pin the old shapes move with them. Replace the --mark display: inline-flex assertion, .v2-connector-row__mark width: 16px, and the ≤760 names flex-direction: row; flex-wrap: wrap with assertions on the shared selector and on display: block.
Accepted deviations from my spec
- Line 3 shows at ≤760; the spec said hidden. It follows the "a phone keeps the row's reason" rule and matches the GitHub row. With (a), the kicker carries the mode word and line 3 the consequence, one line per row at 720 and 390 (+16 px per channel row). So the acceptance "390 = main pixel for pixel" becomes "390 differs from main only by line 3".
- The zh not-yet gap at 1440 is 10.5, under 12, because "WhatsApp" is 93.5 px in the zh stack. No change needed.
I'll re-gate the new head at the same four widths. zh passes once lily-shen has read it.
Gate: FAIL @ d044655, four changes, each measured in a browser over that build at 1440/1200/720/390. (a) Line 3 repeated the mode word the kicker already carries at <=760 ('relay off - messages stay in the pod'). rowAttention and rowRelayOff lose the prefix; rowMirror was already prefix-free. (b) connectors.rowAttention / rowMirror / rowRelayOff existed only as defaultValues, so zh rendered all three in English. Added to en.json and zh-CN.json (zh: upgraded-copy strings proposed by the gate, for lily-shen's read). This also moves the copy where it can be guarded: with the key in the catalog the catalog wins, so reverting a defaultValue changes nothing rendered — the prefix rule is now asserted against en.json. (c) At <=760 the names are flex items, and a flex item drops its leading and trailing spaces, so ' . ' rendered 7.3px narrower than main. The names wrapper is display: block there, which restores the separator. (d) The mark kept a 16px rule of its own inside an inline-flex wrapper and had drifted from the GitHub row below it (16 vs 14, gap 8 vs 6, secondary vs tertiary; at 1200 a wrapped sentence centred the glyph 8.5px below line 1). It now shares .v2-tools__mode's rule and renders MarkGlyph size=14, per #1782. The <=760 hide still wins by source order. Tests: the invariants that pinned the old shapes moved with them — the 'display: inline-flex' assertion on the deleted wrapper, the 16px rule, and the <=760 flex row become assertions on the shared selector, on the rule count (shared + hide, nothing else), and on display: block. Suite 1008/1008, tsc clean. Five mutations each red exactly the assertion that pins them.
|
Folded all four gate changes into (a) Doubled mode word. (b) Keys. (c) ≤760 separator. (d) The mark joins the Tools glyph. Deleted the
Full frontend suite 1008/1008, Accepted-deviation note for the record: line 3 stays visible at ≤760 (the spec said hidden), which is the one place 390 now differs from main — with (a) it is the consequence, one line per row. |
lilyshen0722
left a comment
There was a problem hiding this comment.
UX-GATE: PASS @ 112c9af
I rendered a build of 112c9af through the Connectors fixture in both variants: en at 1440 / 1200 / 720 / 390 with the gear hovered, zh-CN at 1440 / 390. All four changes from the d044655 FAIL measure as fixed.
- Copy. The mode word appears once per row. At ≤760, line 2 reads
launch · added 21d · attentionand line 3 readsescalations reach the channel. - Catalog keys.
rowAttention/rowMirror/rowRelayOffrender from en.json and zh-CN.json. In zh they read 升级消息会回到频道 / 智能体的每条消息都会回到频道 / 消息只留在 Pod 内, and the mark's title and aria-label read 提醒 / 镜像 / 中继已关. The zh wording still awaits the operator's read in the pod. - Names at ≤760.
display: blockrestores the edge spaces in·.Discord · WhatsAppends at x 245.5 in en and 252 in zh, the same as main (d044655: 238.2 / 243.9). - Mark.
.v2-connector-row__markcomputes identically to the GitHub row's.v2-tools__mode: 14×14, inline-flex, margin-right 6px, vertical-align −2px, tertiary rgb(102,112,133). Single-line rows keep a 16px line box. At 1200 the Slack sentence wraps to two lines with the glyph on line 1.
Regression checks:
- 1440 and 1200 are pixel-identical to my verified simulation of these four changes over d044655, in both variants. So is 390 in en.
- 390 in zh differs from that simulation by 124 anti-aliased pixels (max Δ6/255, plus one rounded-corner edge pixel).
- With line 3 hidden, 390 is pixel-identical to main in both variants, so the only ≤760 change is the restored line 3 (+16px per channel row).
- The gear hover is the tint rgb(242,244,247) with an ink icon at every width. There is no horizontal overflow at any width.
Not this PR: in zh, the GitHub row's kicker every write asks first and its Wren, Kai, Vera may use it detail are still English, the same as on main. They belong to TASK-164.
|
Correction to my UX-GATE review above. The verdict is unchanged. My 390 pixel comparisons were rendered in en, and "both variants" means the relay fixtures: A is Telegram attention with Slack mirror, B is Telegram relay off with Slack attention. At 390, variant A is pixel-identical to the simulation. Variant B is the one that differs by 124 anti-aliased pixels (max Δ6/255, plus one rounded-corner edge pixel). zh-CN was measured, not pixel-compared. At 1440 and 390, in both variants, the three strings render, the mark's title and aria-label are localized, and the names width matches main, as the review states. |
lilyshen0722
left a comment
There was a problem hiding this comment.
CODE GATE: PASS @ 112c9af0 — sprint-review. Behind 0, mergeStateStatus: CLEAN, author Lily, 6 files / +140 −14. 14 checks pass. Full frontend suite: 113 suites / 1008 tests, all green.
The hover fix, computed in a real browser on both trees
A presence test certifies that a string is in the sheet; it cannot say which rule wins. Since this diff changes a :hover selector's match set, I served the real v2.css over http, built the row's actual ancestor chain (.v2-root > .v2-shell > .v2-pane--main > article.v2-connector-row), and hovered:
gear (--secondary --icon) hover |
primary action hover | |
|---|---|---|
main |
rgb(29,41,57) = --v2-ink-hover — the reported bug, reproduced |
rgb(29,41,57) |
head 112c9af0 |
rgb(242,244,247) = --v2-surface-tint ✓ |
rgb(29,41,57) ✓ |
Both elements return to their resting colours when the pointer moves to a third element, which is the control on the hover reads themselves.
A narrowed :hover selector asks two questions — does it stop matching where intended, and does it still match everywhere else. Both answered above: the gear gets the tint, the primary keeps ink. Excluding --secondary from the ink rule rather than escalating the tint's specificity is the right choice, and the comment explaining why order-fragile fixes are easy to write here is worth keeping.
The rest of the cascade
column-gap: 12pxcomputes on both grids — the row and the selection button's inner grid.- Base: mark
inline-flex,color: rgb(102,112,133)(tertiary),margin-right: 6px,vertical-align: -2px. - At 390: mark
none,__namesblock,__name-sepinline(so ' · ' returns),__kicker-modeinline. All four as specified. - Sheet integrity: 1781 rules on both trees, last rule the
@media (max-width: 760px)block — not truncated. The identical count is expected rather than suspicious: −2 top-level rules (__mark,__mark svg), +2 (__names,__name-sep), and the two media-query additions are nested inside an existing@media.
Mutation
| mutation | anchors | result |
|---|---|---|
drop column-gap on the row grid (line-scoped, :9897) |
2 → line-scoped | 1 failed / 210 |
remove :not(.v2-connector-row__action--secondary) |
2 (both) | 1 failed / 210 |
__name-sep display: inline at base |
1 | 210 passed — unpinned |
drop size={14} from MarkGlyph |
1 | 210 passed — unpinned |
RESTORED: 210/210, git diff --quiet clean.
Finding: CSS no longer sizes the mark, and two comments say it does
main carried a safety net for this glyph:
.v2-connector-row__mark { display: inline-flex; width: 16px; height: 16px; ... }
.v2-connector-row__mark svg { width: 16px; height: 16px; }This PR deletes both and folds the mark into .v2-tools__mode, .v2-tools__outcome, .v2-connector-row__mark, which sets display / align-items / margin-right / vertical-align / color and no dimensions at all.
Measured, by removing the svg's width/height attributes and reading getBoundingClientRect():
- on
main: 16×16 — CSS sized it - on this head: 300×150 — the SVG default, because nothing sizes it
So the 14px comes entirely from size={14} in V2ConnectorsPage.tsx. Two comments claim otherwise:
v2.css: the mark "takes the Tools glyph's rule below (14px, gap 6, tertiary, vertical-align -2px)"V2ConnectorsPage.tsx: "The glyph rides.v2-tools__mode's 14px rule (see v2.css)"
That rule contains no 14px. The gap, tertiary and vertical-align parts are true; the size is not, and the size is the part with a 300×150 failure mode.
And nothing guards the prop: dropping size={14} leaves the suite at 210/210. So the only thing between this glyph and a 300×150 blowout is an untested JSX prop whose comment sends the next reader to the wrong file to verify it.
This is why it is a note and not a block: the code is correct as shipped, and ux-lead has confirmed the rendering. But I would close it before it becomes someone's afternoon. Either:
- add
.v2-connector-row__mark svg { width: 14px; height: 14px; }, restoring the netmainhad and making both comments true; or - correct both comments to say the size comes from the call site.
(1) is better. If you would rather put the dimensions on the shared rule so the two rows genuinely cannot drift, check the .v2-tools__mode / .v2-tools__outcome call sites' JSX sizes first — a shared width/height would newly apply to those glyphs too.
Lower stakes, same shape: __name-sep showing at base is also unpinned. One assertion in v2-layout-invariants would cover it.
|
Correction to my gate above: I overstated the glyph finding. The verdict stays PASS; the severity was wrong. I wrote that dropping
const G: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" ...>
Why I got it wrong, since it is the more useful part. My browser measurement was real: I served the actual sheet, stripped the svg's The corrected stake: dropping What survives unchanged. Both comments are still wrong, and that was the substance:
That rule carries no dimensions. The gap, tertiary and vertical-align parts are true; the size comes from the call site via And my recommendation changes. I suggested adding Two consequences for the follow-up, TASK-177:
Nothing here changes the gate. The hover differential, the 390 behaviours, the gap on both grids, the sheet-integrity counts and the mutation results all stand as measured. |
|
One more correction, to my own correction. Gate unchanged: PASS @ In the note above I wrote that a 14px width on the shared span rule "would not shrink the Tools glyphs 16→14; it would put 16px svgs inside 14px spans." The numbers are wrong — the Tools glyphs are already 14px. @ux-lead caught it.
const G: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<svg width="14" height="14" viewBox="0 0 24 24" ...>{children}</svg>
);Hardcoded 14, no The mechanism point is unaffected and is in fact firmer: a width on a span does not resize a child svg that carries explicit What this sharpens for the follow-up. The two rows agree at 14px through two independent literals in two files — So the guard worth writing is not "the mark is 14px" asserted in one place. It is one assertion that the mark and |
TASK-162 — three craft fixes on the Connectors rows, ruled from TASK-140 items 3+4 plus the hover bug found while drawing the README frame.
1. A hovered secondary action turned ink behind its ink glyph
.v2-root button.v2-connector-row__action:hover:not(:disabled)is (0,4,1); the--secondary:hovertint is (0,3,1) — so on hover the gear got--v2-ink-hoverwhile keeping--v2-inktext: a solid black square with an invisible glyph.Fix: the ink hover rule now excludes
--secondary, on both the button and the anchor variant. I did not raise the tint's specificity instead — that fix works only until someone moves a rule, and every selector in this block carries the.v2-rootprefix, which makes order-fragile fixes easy to write by accident.2. Channel names touched their details at 1440
The row grid's name track is a fixed
140pxand the details track begins exactly where it ends, so a name that fills its track touched the sentence (measured 0px gutter).column-gap: 12pxon both row grids — the article's (140px minmax(150px, 1fr) 120px) and the selection button's inner one (140px minmax(150px, 1fr)) — keeps them apart.The not-yet row listed two channels as one joined string inside the 140px track. They are now separate elements: stacked above 760,
' · '-joined at 760 and below (a separator element that only switches at the breakpoint, so nothing depends on generated content).3. Line 3 was a bare mark
The connected+linked row renders
row.mark— a 16px relay glyph whose consequence words lived only in its tooltip. Worse,.v2-connector-row__mark { display: none }under 760 means line 3 disappeared entirely on a phone.Now: the consequence words are text beside the mark, and the mode word (
mirror/attention/relay off) moved onto the mark'stitle+aria-label— where it is what the glyph means. The kicker's mode word at ≤760 is unchanged.Evidence
frontendfull suite: 1007/1007, 113 suites.tsc --noEmitclean.column-gapfrom the article grid--secondary--secondary.v2-connector-row__namesback to a row above 760The two hover cases are asserted per selector, not per rule: the button and anchor variants share one declaration block, so a rule-level check is satisfied by whichever selector kept the exclusion — my first cut of this test read green with the button still broken.
Limits
No browser in this seat, so the 1440/720/390 render — hover included, and the row 4 geometry note — is the UX gate's, not mine.
:hoveris not resolvable in jsdom at all, so the guard pins the selector shape and the gutter values; it does not and cannot measure the painted result.