From d0446557e5be02c8de5617a3e8a448f4f76fe32b Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:31:31 -0700 Subject: [PATCH 1/2] fix(v2): the connectors row's hover, name gutter, and line 3 (TASK-162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../v2/__tests__/V2ConnectorsPage.test.tsx | 24 +++++++++++++-- .../v2/__tests__/v2-layout-invariants.test.ts | 29 +++++++++++++++++++ .../src/v2/components/V2ConnectorsPage.tsx | 18 ++++++++++-- frontend/src/v2/v2.css | 29 +++++++++++++++++-- 4 files changed, 93 insertions(+), 7 deletions(-) diff --git a/frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx b/frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx index c0f1df3e2..725701116 100644 --- a/frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx +++ b/frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx @@ -109,11 +109,28 @@ describe('V2ConnectorsPage', () => { const mark = row?.querySelector('.v2-connector-row__mark'); expect(mark).not.toBeNull(); expect(mark).toHaveAttribute('role', 'img'); - expect(mark?.getAttribute('aria-label')).toMatch(/attention|every agent line|relay off/); + // TASK-162 (3): the glyph carries the MODE WORD (what the mark means); the + // consequence is the visible words beside it, so line 3 is never a bare mark. + expect(mark?.getAttribute('aria-label')).toMatch(/^(attention|mirror|relay off)$/); + expect(mark?.getAttribute('title')).toBe(mark?.getAttribute('aria-label')); + expect(row?.querySelector('.v2-connector-row__mark-text')?.textContent).toMatch(/every agent line|messages stay in the pod/); expect(row?.querySelector('.v2-connector-row__kicker-mode')?.textContent).toMatch(/attention|mirror|relay off/); expect(row?.querySelector('.v2-connector-row__when')).toBeNull(); }); + it('TASK-162: the not-yet channel names are separate elements, with a separator that only shows at ≤760', async () => { + mockGets([]); + const { container } = renderPage(); + await waitFor(() => expect(container.querySelector('.v2-connector-row--not-yet')).not.toBeNull()); + const names = container.querySelector('.v2-connector-row--not-yet .v2-connector-row__names'); + expect(names).not.toBeNull(); + // Separate elements, not one joined string: above 760 they stack in the name + // track, so a joined string would be a single 140px-wide line that runs into + // the details column. + expect(Array.from(names!.querySelectorAll('.v2-connector-row__name-item')).map((item) => item.textContent)).toEqual(['Discord', 'WhatsApp']); + expect(names!.querySelectorAll('.v2-connector-row__name-sep')).toHaveLength(1); + }); + it('TASK-131: relative ages advance in place, and a returning tab re-reads, without a reload', async () => { // Fake the clock so the minute tick is deterministic. The fixture's age // only moves if the page re-renders it, which is the defect being pinned. @@ -145,7 +162,10 @@ describe('V2ConnectorsPage', () => { expect(screen.getByText('Send /commonly-enable in your Telegram chat.')).toBeInTheDocument(); expect(screen.getByText('Code expires in 5 min')).toBeInTheDocument(); expect(screen.getByText('Rewire crew · linked to Ops')).toBeInTheDocument(); - expect(screen.getByText('Discord · WhatsApp')).toBeInTheDocument(); + // TASK-162 (2): the two not-yet names are separate elements that stack in the + // name track above 760 and join with ' · ' at ≤760 — no longer one string. + expect(screen.getByText('Discord')).toBeInTheDocument(); + expect(screen.getByText('WhatsApp')).toBeInTheDocument(); expect(screen.getByText('/commonly-enable abc1 23')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Copy command' })).toBeInTheDocument(); expect(container.querySelectorAll('.v2-connector-row__glyph')).toHaveLength(3); diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 007ab92c9..75e7970e6 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -1822,6 +1822,33 @@ describe('v2 layout invariants (CSS rule presence)', () => { // Direction A (2026-09-19): three tracks — the age moved into the kicker. expect(ruleBody(v2, '.v2-connector-row')).toContain('grid-template-columns: 140px minmax(150px, 1fr) 120px'); expect(ruleBody(v2, '.v2-root button.v2-connector-row__selection')).toContain('grid-template-columns: 140px minmax(150px, 1fr)'); + // TASK-162 (2): the name track ends exactly where the details track begins, so + // a name that fills it touches the sentence. The gutter has to be on BOTH row + // grids — the article's and the selection button's inner one. + expect(ruleBody(v2, '.v2-connector-row')).toContain('column-gap: 12px'); + expect(ruleBody(v2, '.v2-root button.v2-connector-row__selection')).toContain('column-gap: 12px'); + // TASK-162 (1): the ink hover must NOT claim a secondary action, or a hovered + // gear is ink behind an ink glyph. Asserted on the SELECTOR rather than on a + // computed style: jsdom resolves no :hover rules at all, so the cascade this + // bug lived in cannot be measured in-suite — only the shape that fixes it can. + const inkHoverChunks = [...v2.matchAll(/([^{}]+)\{([^{}]*)\}/g)] + .filter(([, , body]) => body.includes('var(--v2-ink-hover)')) + .flatMap(([, selector]) => selector.split(',')) + .map((selector) => selector.trim()) + .filter((selector) => selector.includes('.v2-connector-row__action:hover')); + // Split per SELECTOR, not per rule: both variants share one declaration block, + // so a rule-level check is satisfied by whichever selector kept the exclusion + // and reads green with the button — the gear that actually broke — still + // claiming secondary. Two separate assertions, one per line. + expect(inkHoverChunks).toHaveLength(2); + expect(inkHoverChunks.find((selector) => selector.includes('button.v2-connector-row__action:hover'))).toContain(':not(:disabled)'); + for (const selector of inkHoverChunks) { + expect(selector).toContain(':not(.v2-connector-row__action--secondary)'); + } + // TASK-162 (3): line 3 is the consequence as TEXT beside the mark (the mode + // word moved onto the mark itself), and the not-yet names stack above 760. + expect(ruleBody(v2, '.v2-connector-row__detail--mark')).toContain('display: inline-flex'); + expect(ruleBody(v2, '.v2-connector-row__names')).toContain('flex-direction: column'); // Rule 3: the kicker is mono 11; rule 1: the mark is 16px and the mode word is hidden until 760; rule 2: the gear is 32 (44 on the phone). expect(ruleBody(v2, '.v2-connector-row__kicker')).toContain('var(--v2-font-mono)'); expect(ruleBody(v2, '.v2-connector-row__kicker')).toContain('font-size: 11px'); @@ -1830,6 +1857,8 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(ruleBody(v2, '.v2-root button.v2-connector-row__action--icon')).toContain('width: 32px'); const phone760 = v2.slice(v2.indexOf('@media (max-width: 760px) {\n .v2-connectors {')); expect(phone760).toContain('.v2-connector-row__kicker-mode { display: inline; }'); + expect(phone760).toContain('.v2-connector-row__names { flex-direction: row; flex-wrap: wrap; }'); + expect(phone760).toContain('.v2-connector-row__name-sep { display: inline; }'); expect(phone760).toContain('.v2-root button.v2-connector-row__action--icon { width: 44px; min-height: 44px;'); // TASK-140: the aside's Revoke ✕ carries `--secondary` as well as `--icon`, // and `--secondary` sets `min-height: 36px`. Because min-height beats height, diff --git a/frontend/src/v2/components/V2ConnectorsPage.tsx b/frontend/src/v2/components/V2ConnectorsPage.tsx index ba299b41b..700affb01 100644 --- a/frontend/src/v2/components/V2ConnectorsPage.tsx +++ b/frontend/src/v2/components/V2ConnectorsPage.tsx @@ -1064,8 +1064,13 @@ const V2ConnectorsPage: React.FC = () => { {row.line} {row.mark ? ( - - + // TASK-162 (3): the consequence is the visible words; the mode word + // (mirror / attention / relay off) is what the glyph itself means, + // so it rides title + aria-label rather than being the only thing + // on the line. + + + {row.mark.label} ) : ( {row.detail} @@ -1486,7 +1491,14 @@ const V2ConnectorsPage: React.FC = () => { {t('connectors.notYetKicker', { defaultValue: 'not yet' })} diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index e4e58b14f..a4b8e669f 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -9890,6 +9890,11 @@ body.modern-ui.v2-canvas { /* Direction A (2026-09-19): the age column is gone — it rides the mono kicker above line 1 — so the row is name · details · one act. */ grid-template-columns: 140px minmax(150px, 1fr) 120px; + /* TASK-162 (2): the channel name and its details met at 0px — the 140px name + track ends exactly where the details track begins, so a name that fills its + track touches the sentence. A 12px gutter on BOTH row grids (here and the + selection button's inner grid below) keeps the two columns apart. */ + column-gap: 12px; align-items: center; min-height: 82px; padding: 16px 18px; @@ -9903,6 +9908,7 @@ body.modern-ui.v2-canvas { display: grid; grid-column: 1 / 3; grid-template-columns: 140px minmax(150px, 1fr); + column-gap: 12px; align-items: center; min-width: 0; padding: 0; @@ -9941,6 +9947,16 @@ body.modern-ui.v2-canvas { .v2-connector-row__detail, .v2-connector-row__when { color: var(--v2-text-tertiary); font-family: var(--v2-font-mono); font-size: 12px; line-height: 16px; } /* Meta wraps to a second line; it never ellipsises (lily-shen, 64026). */ .v2-connector-row__detail { overflow-wrap: anywhere; } +/* TASK-162 (3): line 3 used to be the bare 16px mark, its consequence words + reachable only through the glyph's tooltip — and at ≤760 the mark is hidden, + so line 3 disappeared entirely on a phone. The consequence is now TEXT beside + the mark; the mode word moved onto the mark itself (title + aria-label). */ +.v2-connector-row__detail--mark { display: inline-flex; align-items: center; gap: 8px; } +/* TASK-162 (2): the not-yet row lists two channels. Above 760 they stack in the + 140px name track; at ≤760 they join with ' · ' (the separator is the only + element that switches, so nothing here depends on generated content). */ +.v2-connector-row__names { display: flex; flex-direction: column; } +.v2-connector-row__name-sep { display: none; } .v2-connector-row__when { padding-right: 16px; } /* Direction A rule 3: `pod · verb age` in mono 11 above line 1, on every row. */ .v2-connector-row__kicker { color: var(--v2-text-tertiary); font-family: var(--v2-font-mono); font-size: 11px; font-weight: 500; line-height: 16px; text-transform: lowercase; overflow-wrap: anywhere; } @@ -9962,8 +9978,15 @@ body.modern-ui.v2-canvas { text-decoration: none; cursor: pointer; } -.v2-root button.v2-connector-row__action:hover:not(:disabled), -.v2-root a.v2-connector-row__action:hover { background: var(--v2-ink-hover); } +/* TASK-162 (1): a hovered secondary action turned INK behind an ink glyph — + `.v2-root button....__action:hover:not(:disabled)` (0,4,1) outranks the + `--secondary:hover` tint (0,3,1), so the gear read as a solid black square + with an invisible glyph. Excluding secondary from the ink rule fixes it + WITHOUT relying on source order: raising the tint's specificity would work + only until someone moves a rule, and the `.v2-root` prefix that every + selector here carries makes order-fragile fixes easy to write by accident. */ +.v2-root button.v2-connector-row__action:hover:not(:disabled):not(.v2-connector-row__action--secondary), +.v2-root a.v2-connector-row__action:hover:not(.v2-connector-row__action--secondary) { background: var(--v2-ink-hover); } .v2-root button.v2-connector-row__action:disabled { cursor: progress; opacity: 0.65; } .v2-root button.v2-connector-row__action--secondary, .v2-root a.v2-connector-row__action--secondary { border-color: var(--v2-border); background: var(--v2-surface); color: var(--v2-ink); } @@ -10134,6 +10157,8 @@ body.modern-ui.v2-canvas { .v2-root button.v2-connector-row__selection { grid-row: 1; } .v2-connector-row__kicker-mode { display: inline; } .v2-connector-row__mark { display: none; } + .v2-connector-row__names { flex-direction: row; flex-wrap: wrap; } + .v2-connector-row__name-sep { display: inline; } .v2-root button.v2-connector-row__action--icon { width: 44px; min-height: 44px; grid-column: 2; grid-row: 1; justify-self: end; align-self: start; } .v2-root button.v2-connector-row__action:not(.v2-connector-row__action--icon), .v2-root a.v2-connector-row__action { display: inline-flex; grid-column: 1 / -1; grid-row: 3; width: 100%; justify-content: center; min-height: 44px; margin-top: 8px; } /* The gear sits over the selection button's top-right; keep the kicker out from under it. */ From 112c9af00ef69b96a100e75a4ed270037a9412bc Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 27 Sep 2026 12:00:43 -0700 Subject: [PATCH 2/2] fix(v2): fold the TASK-162 UX-gate changes (TASK-162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate: FAIL @ d0446557, 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. --- frontend/src/i18n/locales/en.json | 3 +++ frontend/src/i18n/locales/zh-CN.json | 3 +++ .../v2/__tests__/V2ConnectorsPage.test.tsx | 22 ++++++++++++++++++- .../v2/__tests__/v2-layout-invariants.test.ts | 21 +++++++++++++----- .../src/v2/components/V2ConnectorsPage.tsx | 11 +++++----- frontend/src/v2/v2.css | 16 ++++++++------ 6 files changed, 58 insertions(+), 18 deletions(-) diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 05ed97a7b..a66baa26e 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1874,6 +1874,9 @@ "kickerAttention": "attention", "kickerMirror": "mirror", "kickerOff": "relay off", + "rowAttention": "escalations reach the channel", + "rowMirror": "every agent line reaches the channel", + "rowRelayOff": "messages stay in the pod", "age": { "added": "added {{age}}", "started": "started {{age}}", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 3d861a98a..2c25896f6 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -1866,6 +1866,9 @@ "kickerAttention": "提醒", "kickerMirror": "镜像", "kickerOff": "中继已关", + "rowAttention": "升级消息会回到频道", + "rowMirror": "智能体的每条消息都会回到频道", + "rowRelayOff": "消息只留在 Pod 内", "age": { "added": "{{age}}前添加", "started": "{{age}}前开始", diff --git a/frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx b/frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx index 725701116..94d90b450 100644 --- a/frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx +++ b/frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx @@ -6,6 +6,7 @@ import { act, fireEvent, render, screen, waitFor, within } from '@testing-librar import { MemoryRouter, Route, Routes } from 'react-router-dom'; import V2ConnectorsPage, { INSTALL_LOCK_TTL_MS, installableLifecyclePath } from '../components/V2ConnectorsPage'; import { AuthContext } from '../../context/AuthContext'; +import en from '../../i18n/locales/en.json'; jest.mock('axios', () => { const mock = { @@ -113,11 +114,30 @@ describe('V2ConnectorsPage', () => { // consequence is the visible words beside it, so line 3 is never a bare mark. expect(mark?.getAttribute('aria-label')).toMatch(/^(attention|mirror|relay off)$/); expect(mark?.getAttribute('title')).toBe(mark?.getAttribute('aria-label')); - expect(row?.querySelector('.v2-connector-row__mark-text')?.textContent).toMatch(/every agent line|messages stay in the pod/); + expect(row?.querySelector('.v2-connector-row__mark-text')?.textContent).toBe('messages stay in the pod'); + // The mode word is NOT repeated on line 3: at ≤760 the kicker carries it, so + // 'relay off · messages stay in the pod' said it twice (TASK-162 UX gate). + expect(row?.querySelector('.v2-connector-row__mark-text')?.textContent).not.toMatch(/^(attention|mirror|relay off) ·/); expect(row?.querySelector('.v2-connector-row__kicker-mode')?.textContent).toMatch(/attention|mirror|relay off/); expect(row?.querySelector('.v2-connector-row__when')).toBeNull(); }); + it('TASK-162: line 3 never repeats the mode word the kicker already carries', async () => { + // The prefixes lived in the component's defaultValue, which is exactly where + // they could not be guarded: once the key exists in the catalog the catalog + // wins, so reverting the defaultValue changes nothing rendered. The copy now + // lives in en.json, so that is where the rule is asserted. + const kickerByRow: Array<[keyof typeof en.connectors, string]> = [ + ['rowAttention', 'attention'], + ['rowMirror', 'mirror'], + ['rowRelayOff', 'relay off'], + ]; + for (const [key, modeWord] of kickerByRow) { + expect(en.connectors[key]).toBeTruthy(); + expect(en.connectors[key].startsWith(modeWord)).toBe(false); + } + }); + it('TASK-162: the not-yet channel names are separate elements, with a separator that only shows at ≤760', async () => { mockGets([]); const { container } = renderPage(); diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 75e7970e6..f20f47ed4 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -1846,18 +1846,29 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(selector).toContain(':not(.v2-connector-row__action--secondary)'); } // TASK-162 (3): line 3 is the consequence as TEXT beside the mark (the mode - // word moved onto the mark itself), and the not-yet names stack above 760. - expect(ruleBody(v2, '.v2-connector-row__detail--mark')).toContain('display: inline-flex'); + // word moved onto the mark itself) and the not-yet names stack above 760. expect(ruleBody(v2, '.v2-connector-row__names')).toContain('flex-direction: column'); - // Rule 3: the kicker is mono 11; rule 1: the mark is 16px and the mode word is hidden until 760; rule 2: the gear is 32 (44 on the phone). + // …and the mark SHARES the Tools glyph's rule rather than carrying a second + // rule of its own. The two rows had drifted 16 vs 14, gap 8 vs 6, secondary vs + // tertiary; a rule of its own can always drift again, so the guard counts the + // rules that mention the mark: the shared one and the ≤760 hide, nothing else. + const stripComments = (css: string): string => css.replace(/\/\*[\s\S]*?\*\//g, ''); + const markRuleChunks = [...stripComments(v2).matchAll(/([^{}]+)\{([^{}]*)\}/g)] + .filter(([, selector]) => selector.includes('.v2-connector-row__mark')); + expect(markRuleChunks).toHaveLength(2); + const sharedMarkRule = markRuleChunks.find(([, selector]) => selector.includes('.v2-tools__mode')); + expect(sharedMarkRule?.[2]).toContain('display: inline-flex'); + expect(sharedMarkRule?.[2]).toContain('vertical-align: -2px'); + expect(sharedMarkRule?.[2]).toContain('color: var(--v2-text-tertiary)'); + // Rule 3: the kicker is mono 11; rule 1 (as revised by the TASK-162 gate): the mode word is hidden until 760 and the mark's own 16px rule is GONE — its size now comes from the shared rule above plus the glyph's `size` prop; rule 2: the gear is 32 (44 on the phone). expect(ruleBody(v2, '.v2-connector-row__kicker')).toContain('var(--v2-font-mono)'); expect(ruleBody(v2, '.v2-connector-row__kicker')).toContain('font-size: 11px'); expect(ruleBody(v2, '.v2-connector-row__kicker-mode')).toContain('display: none'); - expect(ruleBody(v2, '.v2-connector-row__mark')).toContain('width: 16px'); + expect(v2).not.toContain('width: 16px; height: 16px; color: var(--v2-text-secondary)'); expect(ruleBody(v2, '.v2-root button.v2-connector-row__action--icon')).toContain('width: 32px'); const phone760 = v2.slice(v2.indexOf('@media (max-width: 760px) {\n .v2-connectors {')); expect(phone760).toContain('.v2-connector-row__kicker-mode { display: inline; }'); - expect(phone760).toContain('.v2-connector-row__names { flex-direction: row; flex-wrap: wrap; }'); + expect(phone760).toContain('.v2-connector-row__names { display: block; }'); expect(phone760).toContain('.v2-connector-row__name-sep { display: inline; }'); expect(phone760).toContain('.v2-root button.v2-connector-row__action--icon { width: 44px; min-height: 44px;'); // TASK-140: the aside's Revoke ✕ carries `--secondary` as well as `--icon`, diff --git a/frontend/src/v2/components/V2ConnectorsPage.tsx b/frontend/src/v2/components/V2ConnectorsPage.tsx index 700affb01..57487b2bb 100644 --- a/frontend/src/v2/components/V2ConnectorsPage.tsx +++ b/frontend/src/v2/components/V2ConnectorsPage.tsx @@ -727,8 +727,8 @@ const V2ConnectorsPage: React.FC = () => { const markLabel = relay ? (mirror ? t('connectors.rowMirror', { defaultValue: 'every agent line reaches the channel' }) - : t('connectors.rowAttention', { defaultValue: 'attention · escalations reach the channel' })) - : t('connectors.rowRelayOff', { defaultValue: 'relay off · messages stay in the pod' }); + : t('connectors.rowAttention', { defaultValue: 'escalations reach the channel' })) + : t('connectors.rowRelayOff', { defaultValue: 'messages stay in the pod' }); return { action: (isTelegram || isSlack) ? 'manage' : null, actionLabel: t('connectors.manage', { defaultValue: 'Manage' }), @@ -1067,9 +1067,10 @@ const V2ConnectorsPage: React.FC = () => { // TASK-162 (3): the consequence is the visible words; the mode word // (mirror / attention / relay off) is what the glyph itself means, // so it rides title + aria-label rather than being the only thing - // on the line. - - + // on the line. The glyph rides .v2-tools__mode's 14px rule (see + // v2.css): #1782 says the channel mark follows the Tools glyph. + + {row.mark.label} ) : ( diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index a4b8e669f..ffeeefe59 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -9950,8 +9950,10 @@ body.modern-ui.v2-canvas { /* TASK-162 (3): line 3 used to be the bare 16px mark, its consequence words reachable only through the glyph's tooltip — and at ≤760 the mark is hidden, so line 3 disappeared entirely on a phone. The consequence is now TEXT beside - the mark; the mode word moved onto the mark itself (title + aria-label). */ -.v2-connector-row__detail--mark { display: inline-flex; align-items: center; gap: 8px; } + the mark; the mode word moved onto the mark itself (title + aria-label), and + the glyph itself follows #1782: it takes the Tools glyph's rule below (14px, + gap 6, tertiary, vertical-align -2px) rather than a 16px rule of its own, + because beside a 12/16 mono sentence a 16px mark grows the line box to 18px. */ /* TASK-162 (2): the not-yet row lists two channels. Above 760 they stack in the 140px name track; at ≤760 they join with ' · ' (the separator is the only element that switches, so nothing here depends on generated content). */ @@ -9962,9 +9964,9 @@ body.modern-ui.v2-canvas { .v2-connector-row__kicker { color: var(--v2-text-tertiary); font-family: var(--v2-font-mono); font-size: 11px; font-weight: 500; line-height: 16px; text-transform: lowercase; overflow-wrap: anywhere; } /* The mode word rides the kicker only where the glyph's tooltip cannot (≤760). */ .v2-connector-row__kicker-mode { display: none; } -/* Direction A rule 1: a category on the row is a 16px mark, the sentence in title + aria-label. */ -.v2-connector-row__mark { display: inline-flex; width: 16px; height: 16px; color: var(--v2-text-secondary); vertical-align: middle; } -.v2-connector-row__mark svg { width: 16px; height: 16px; } +/* Direction A rule 1 (as revised by TASK-162): a category on the row is a mark, + the sentence in title + aria-label — and the mark shares the Tools glyph's rule + (search `.v2-connector-row__mark, .v2-tools__mode`) so the two rows cannot drift. */ .v2-root button.v2-connector-row__action, .v2-root a.v2-connector-row__action { justify-self: end; @@ -10118,7 +10120,7 @@ body.modern-ui.v2-canvas { .v2-tools__trail-line { display: flex; justify-content: space-between; gap: 12px; padding: 6px 0; border-top: 1px solid var(--v2-border-soft); color: var(--v2-text-muted); font-family: var(--v2-font-mono); font-size: 12px; line-height: 16px; } .v2-tools__trail-line > span:first-child { min-width: 0; overflow-wrap: anywhere; } .v2-tools__trail-when { flex: none; } -.v2-tools__mode, .v2-tools__outcome { display: inline-flex; align-items: center; margin-right: 6px; vertical-align: -2px; color: var(--v2-text-tertiary); } +.v2-tools__mode, .v2-tools__outcome, .v2-connector-row__mark { display: inline-flex; align-items: center; margin-right: 6px; vertical-align: -2px; color: var(--v2-text-tertiary); } .v2-tools__form { display: grid; gap: 14px; margin-top: 14px; } .v2-tools__field { display: grid; gap: 6px; margin: 0; padding: 0; border: none; min-width: 0; } .v2-tools__field > span:first-child, .v2-tools__field > legend { color: var(--v2-text-tertiary); font-family: var(--v2-font-mono); font-size: 11px; line-height: 16px; text-transform: lowercase; padding: 0; } @@ -10157,7 +10159,7 @@ body.modern-ui.v2-canvas { .v2-root button.v2-connector-row__selection { grid-row: 1; } .v2-connector-row__kicker-mode { display: inline; } .v2-connector-row__mark { display: none; } - .v2-connector-row__names { flex-direction: row; flex-wrap: wrap; } + .v2-connector-row__names { display: block; } .v2-connector-row__name-sep { display: inline; } .v2-root button.v2-connector-row__action--icon { width: 44px; min-height: 44px; grid-column: 2; grid-row: 1; justify-self: end; align-self: start; } .v2-root button.v2-connector-row__action:not(.v2-connector-row__action--icon), .v2-root a.v2-connector-row__action { display: inline-flex; grid-column: 1 / -1; grid-row: 3; width: 100%; justify-content: center; min-height: 44px; margin-top: 8px; }