From fedf91db3478f20a1c63927dcab0d61c1beec4c1 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 4 Nov 2026 00:30:41 -0800 Subject: [PATCH 1/9] fix(v2): one shared head bar, 44px phone keys in the primitives, and board keys that hold their size - .v2-feature__title names the display face on desktop: BYO, Connect and the admin/dev heads drew Plex 24/850 there (ui-audit-1104 finding 2). - .v2-feature__header is the reference 64px bar (title 28px in, divider, mono 11.5 count); V2PageHead gains a lead slot (a profile's avatar), and .v2-feature--bar / .v2-feature__scroll let a page draw that bar at the pane edge with a centred column under it (finding 3). - primitives.css: every v2p key and segment is 44px tall at <=760 (finding 5). - Board: text keys take the v2p small-key size (28 at r8), the subtitle is mono 11.5, and phone icon keys are flex: none so back, search and New task stop shrinking to 23x32 (findings 4, 6, 8). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MDs9DT6tbhKm6UhsmFhLk9 --- frontend/src/v2/components/V2PageHead.tsx | 5 +++- frontend/src/v2/components/V2PodBoard.css | 8 +++-- frontend/src/v2/primitives/primitives.css | 9 ++++++ frontend/src/v2/v2.css | 36 +++++++++++++++++++++-- 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/frontend/src/v2/components/V2PageHead.tsx b/frontend/src/v2/components/V2PageHead.tsx index a237b10961..3b33f0340b 100644 --- a/frontend/src/v2/components/V2PageHead.tsx +++ b/frontend/src/v2/components/V2PageHead.tsx @@ -10,12 +10,15 @@ interface V2PageHeadProps { titleClassName?: string; metaClassName?: string; hintLabel?: string; + /** Drawn before the title, inside the bar: a profile's avatar. */ + lead?: React.ReactNode; } const V2PageHead: React.FC = ({ - title, meta, actions, hint, className = '', titleClassName = '', metaClassName = '', hintLabel = 'About this page', + title, meta, actions, hint, className = '', titleClassName = '', metaClassName = '', hintLabel = 'About this page', lead, }) => (
+ {lead}

{title}

{hint != null && {hint}} diff --git a/frontend/src/v2/components/V2PodBoard.css b/frontend/src/v2/components/V2PodBoard.css index d8c810634a..0aa6d5bbea 100644 --- a/frontend/src/v2/components/V2PodBoard.css +++ b/frontend/src/v2/components/V2PodBoard.css @@ -20,7 +20,8 @@ .v2-feature--board .v2-board { background: var(--v2-surface); } /* keys (.b, .b-g, .b-p, .b-s, .b-sm, .ib) */ -.v2-feature--board .v2-board__key { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 7px; height: 34px; padding: 0 13px; border-radius: 10px; font: 600 13px/1 var(--v2-font); cursor: pointer; white-space: nowrap; transition: transform var(--v2-dur-fast) var(--v2-ease); } +/* A text key is the v2p small key (28 at r8, rulings 11-04); the icon keys below are 32 at r8. */ +.v2-feature--board .v2-board__key { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 7px; height: 28px; padding: 0 10px; border-radius: var(--v2-r-8); font: 600 12.5px/1 var(--v2-font); cursor: pointer; white-space: nowrap; transition: transform var(--v2-dur-fast) var(--v2-ease); } .v2-feature--board .v2-board__key svg { width: 16px; height: 16px; flex: none; } .v2-feature--board .v2-board__key:active { transform: translateY(1px); } .v2-feature--board .v2-board__key:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--v2-accent-soft), 0 0 0 1px var(--v2-accent); } @@ -39,7 +40,7 @@ .v2-feature--board .v2-board__head { flex: none; height: 64px; display: flex; align-items: center; gap: 10px; padding: 0 20px; border-bottom: 1px solid var(--v2-border-soft); } .v2-feature--board .v2-board__title-wrap { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 10px; } .v2-feature--board .v2-board__title { margin: 0; min-width: 0; font: 700 18px/20px var(--v2-font-display); letter-spacing: -0.025em; color: var(--v2-text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.v2-feature--board .v2-board__subtitle { font: 500 12px/1 var(--v2-font-mono); color: var(--v2-text-muted); white-space: nowrap; } +.v2-feature--board .v2-board__subtitle { font: 500 11.5px/1 var(--v2-font-mono); color: var(--v2-text-muted); white-space: nowrap; } .v2-feature--board .v2-board__tools, .v2-feature--board .v2-board__filters { display: flex; gap: 6px; align-items: center; } .v2-feature--board .v2-board__chip { position: relative; display: inline-flex; align-items: center; gap: 5px; height: 24px; padding: 0 8px; border: 0; border-radius: 7px; font: 500 11.5px/1 var(--v2-font-mono); color: var(--v2-text-secondary); background: var(--v2-surface); box-shadow: inset 0 0 0 1px var(--v2-border-soft); white-space: nowrap; cursor: pointer; } @@ -165,6 +166,9 @@ /* Exception: 44px phone hit areas on the frame's 32px top-bar keys and 24px chips (chat precedent). The key's 1px border sits inside, so the inset is -7px. */ .v2-feature--board .v2-board__head > .v2-board__key { position: relative; z-index: 1; } + /* An icon key never shrinks: in the 52px bar the title and search take the + slack, and back, search and New task stay 32 square (they measured 23x32). */ + .v2-feature--board .v2-board__key--icon { flex: none; } .v2-feature--board .v2-board__head > .v2-board__key::after { content: ''; position: absolute; inset: -7px; } /* The 28px tabs reach 44px up into the gap (14px) and down to the track's edge (2px), where the chips' areas begin. Search and New task sit 12px apart so their areas meet, never overlap. */ diff --git a/frontend/src/v2/primitives/primitives.css b/frontend/src/v2/primitives/primitives.css index 168cc92392..58db464784 100644 --- a/frontend/src/v2/primitives/primitives.css +++ b/frontend/src/v2/primitives/primitives.css @@ -356,3 +356,12 @@ details.v2p-disc > summary .vl { font-size: 13.5px; color: var(--v2-ink-2); min- .v2p-cols { grid-template-columns: minmax(0, 1fr); } .v2p-tile { height: 104px; } } + +/* ---- phones (≤760, rulings 11-04): every key and segment is 44px tall, the + touch floor, so a page never sizes its own phone keys. Icon keys stay + square. Desktop keeps 32 icon / 28 small / 28 segment. ---- */ +@media (max-width: 760px) { + .v2p-b, .v2p-b-sm { height: 44px; } + .v2p-ib, .v2p-ib.v2p-b-sm { width: 44px; height: 44px; } + .v2p-seg button, .v2p-seg--sm button { height: 44px; } +} diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index 9dedb01126..2cae11db4b 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -2501,20 +2501,44 @@ body.modern-ui.v2-canvas { background: var(--v2-surface); /* inside the card the ground is white; the card is the tint step */ } +/* The shared page head bar (V2PageHead with .v2-feature__header): the reference + heads' 64px bar with the title 29px from the pane edge, a divider under it and + the count in mono 11.5. Pages that draw their own head (Your team, the person + and agent profiles) use this class too, with .v2-feature--bar below. */ .v2-feature__header { flex-shrink: 0; - padding: 22px 28px 18px; + box-sizing: border-box; + min-height: 64px; + padding: 0 20px 0 28px; border-bottom: 1px solid var(--v2-border-soft); background: var(--v2-surface); } +.v2-page-head.v2-feature__header { align-items: center; gap: 12px; } +.v2-feature__header > .v2-page-head__identity { flex: 1 1 auto; } +/* The display face is set here, not only in the phone rule: without it the + desktop title fell back to Plex 24/850 (ui-audit-1104 finding 2). */ .v2-feature__title { margin: 0; color: var(--v2-text-primary); - font-size: 24px; - font-weight: 850; + font: 800 24px/1 var(--v2-font-display); letter-spacing: -0.03em; } +.v2-feature__header .v2-page-head__meta { font: 500 11.5px/16px var(--v2-font-mono); } + +/* A page that draws its own head bar inside the feature pane passes + v2-feature--bar: the body stops padding and scrolling, the head sits at the + pane edge, and .v2-feature__scroll under it scrolls and pads the content to + a centred column (--v2-feature-col, 1100 for lists and grids). */ +.v2-feature--bar > .v2-feature__body { padding: 0; display: flex; flex-direction: column; overflow: hidden; } +.v2-feature--bar .v2-feature__legacy { flex: 1 1 auto; width: 100%; max-width: none; min-height: 0; margin: 0; display: flex; flex-direction: column; } +.v2-feature__scroll { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + box-sizing: border-box; + padding: 24px max(28px, calc((100% - var(--v2-feature-col, 1100px)) / 2)) 42px; +} .v2-page-head { display: flex; @@ -4821,6 +4845,12 @@ body.modern-ui.v2-canvas { } .v2-feature__header .v2-page-head__meta { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .v2-feature__body { padding: 12px 12px 16px; } + .v2-feature__scroll { padding: 12px 12px 16px; } + /* A head with actions gives them their own full-width row under the title + (Your team, the profiles): 44px keys never squeeze the title at 390. */ + .v2-feature__header.v2-page-head--wrap { flex-wrap: wrap; row-gap: 0; padding-bottom: 12px; } + .v2-feature__header.v2-page-head--wrap > .v2-page-head__identity { flex: 1 1 auto; min-height: 52px; } + .v2-feature__header.v2-page-head--wrap > .v2-page-head__actions { flex: 1 0 100%; display: grid; grid-auto-columns: minmax(0, 1fr); grid-auto-flow: column; gap: 8px; } } /* ---------- Motion (D220) ---------- */ From d3945c868198b0a5fd6f99fb192c9b97c1beb484 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 4 Nov 2026 00:30:41 -0800 Subject: [PATCH 2/9] fix(v2): Your team, the profiles, BYO and Connect take the reference head, keys, edges and labels - Your team, Person profile and Agent profile (in the shell) draw the shared head bar at the pane edge; their bodies scroll under it in a centred column (1100 for the grid, 760 for the profiles). Follow counts and the agent description move to the intro line under the bar. - BYO and Connect lose the head's "i" hint; its text is the page's intro line (Connect's was a hard-coded English string, now connect.intro in en/zh-CN). - Keys: Team and Profile adopt the v2p small keys; Agent profile, Connect and BYO keys take the same 28/r8 size. Phones get 44 from the primitives. - Edges: Team cards and BYO add-computer are the D215 ring at r9, the lanes and sections r12. Section labels are mono 11.5/500 muted; counts 11.5. - Tests: the guards that pinned the old frame values now pin the new ones, plus a describe for findings 2-8. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MDs9DT6tbhKm6UhsmFhLk9 --- frontend/src/i18n/locales/en.json | 1 + frontend/src/i18n/locales/zh-CN.json | 1 + frontend/src/v2/V2App.tsx | 7 +- .../src/v2/__tests__/signalTokens.test.ts | 11 +- .../v2/__tests__/v2-layout-invariants.test.ts | 149 +++++++++++++++--- frontend/src/v2/agents/V2AgentProfile.tsx | 18 ++- frontend/src/v2/agents/v2-agent-profile.css | 44 ++++-- frontend/src/v2/components/V2AgentBYO.css | 14 +- frontend/src/v2/components/V2AgentBYO.tsx | 15 +- frontend/src/v2/components/V2ConnectPage.css | 13 +- frontend/src/v2/components/V2ConnectPage.tsx | 2 + .../src/v2/components/V2PersonProfile.css | 46 +++--- .../src/v2/components/V2PersonProfile.tsx | 72 +++++---- frontend/src/v2/components/V2YourTeamPage.css | 35 ++-- frontend/src/v2/components/V2YourTeamPage.tsx | 53 ++++--- 15 files changed, 319 insertions(+), 162 deletions(-) diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 7b2f3cf60b..72c7e305c5 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -2440,6 +2440,7 @@ } }, "connect": { + "intro": "MCP uses an agent token created during setup.", "cliHeading": "Connect the CLI or MCP", "copy": "Copy", "copied": "Copied", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 81b62352a4..c9c3121f99 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -2428,6 +2428,7 @@ } }, "connect": { + "intro": "MCP 使用接入时创建的智能体密钥。", "cliHeading": "连接 CLI 或 MCP", "copy": "复制", "copied": "已复制", diff --git a/frontend/src/v2/V2App.tsx b/frontend/src/v2/V2App.tsx index cb52d50ba1..65dfcbec96 100644 --- a/frontend/src/v2/V2App.tsx +++ b/frontend/src/v2/V2App.tsx @@ -201,7 +201,7 @@ const V2AgentProfileRoute: React.FC = () => { return ( {feature(t('common.pageTitle.agentProfile'), page, false, false)} + ? (page) => {feature(t('common.pageTitle.agentProfile'), page, false, false, 'v2-feature--bar v2-feature--aprofile')} : undefined} /> ); @@ -346,6 +346,7 @@ const V2App: React.FC = () => { /* V2YourTeamPage owns its own header — suppress the generic V2FeaturePage chrome to avoid stacked titles. */ false, + 'v2-feature--bar', )} /> { /> } + element={} /> { /> , false, false, 'v2-feature--profile')} + element={feature(t('common.pageTitle.profile'), , false, false, 'v2-feature--bar v2-feature--profile')} /> { expect(rd('../agents/v2-agent-profile.css')).toMatch(/\.v2-aprofile__bar\s*\{[^}]*background: var\(--v2-surface\)/); }); test('ink primary buttons take theme tokens, never a literal colour', () => { - // R06 lifted Your team onto the d215 frame: Hire and Answer are its ink - // key, the --v2-fill-* gradient with --v2-on-fill text, in the page's sheet. - const css = rd('../components/V2YourTeamPage.css'); - const rule = css.match(/\n\.v2-team \.v2-team__hire, \.v2-team \.v2-team-card__answer \{[^}]*\}/); + // Rulings 11-04: Your team's Hire and Answer are the v2p ink key + // (.v2p-b-p), the --v2-fill-* gradient with --v2-on-fill text. + expect(rd('../components/V2YourTeamPage.tsx')).toContain('v2p-b v2p-b-p v2p-b-sm v2-team__hire'); + const css = rd('../primitives/primitives.css'); + const rule = css.match(/\n\.v2p-b-p \{[^}]*\}/); expect(rule?.[0]).toContain('background: linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b))'); expect(rule?.[0]).toContain('color: var(--v2-on-fill)'); - expect(rule?.[0]).not.toMatch(/#[0-9a-f]{3,8}\b|rgba?\(/i); + expect(rule?.[0]).not.toMatch(/#[0-9a-f]{3,8}\b/i); }); test('the board drawer scrim is --v2-scrim', () => { // S4: the board's rules live in its own sheet; the scrim backs the phone sheet. diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 2109a79751..a951bb5457 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -167,11 +167,14 @@ describe('v2 layout invariants (CSS rule presence)', () => { const phone = css.slice(css.lastIndexOf('@media (max-width: 760px)')); expect(phone).toMatch(/\.v2-aprofile__cta-row \.v2-aprofile__btn \{[^}]*height: 44px/); }); - test('/v2/profile: on a phone the header wraps and its one key takes the full row (d215 s-profile 390)', () => { - const profile = read('../components/V2PersonProfile.css'); - const phone = profile.slice(profile.indexOf('@media (max-width: 760px)')); - expect(phone).toMatch(/\.v2-profile \.v2-profile__head \{[^}]*flex-wrap: wrap/); - expect(phone).toMatch(/\.v2-profile \.v2-profile__follow, \.v2-profile \.v2-profile__edit \{[^}]*flex: 1 0 100%/); + test('/v2/profile: the head is the shared bar, and on a phone its one key takes the full row (rulings 11-04)', () => { + // The page draws V2PageHead with the shared bar class and the wrap variant; + // the full-row key on a phone is the shared rule in v2.css, not a page copy. + expect(read('../components/V2PersonProfile.tsx')).toContain('className="v2-feature__header v2-page-head--wrap v2-profile__head"'); + const v2 = readV2Css(); + const phone = mediaBlockContaining(v2, '.v2-feature__header.v2-page-head--wrap > .v2-page-head__actions'); + expect(ruleBody(v2, '.v2-feature__header.v2-page-head--wrap', { within: phone })).toContain('flex-wrap: wrap'); + expect(ruleBody(v2, '.v2-feature__header.v2-page-head--wrap > .v2-page-head__actions', { within: phone })).toContain('flex: 1 0 100%'); }); test('/v2/activity and /v2/artifacts: the shared phone head bar left v2.css with both pages (d215 builds)', () => { @@ -1730,19 +1733,19 @@ describe('v2 layout invariants (CSS rule presence)', () => { const grid = ruleBody(teamCss, '.v2-team .v2-team__grid'); expect(grid).toContain('grid-template-columns: repeat(3, minmax(0, 1fr))'); expect(grid).toContain('gap: 12px'); - expect(ruleBody(teamCss, '.v2-team .v2-team-card--needsYou')).toContain('border: 2px solid var(--v2-accent)'); + // Rulings 11-04: the needs-you card is the 2px cobalt ring (.v2p-tc.picked), not a border. + expect(ruleBody(teamCss, '.v2-team .v2-team-card--needsYou')).toContain('box-shadow: 0 0 0 2px var(--v2-accent)'); expect(ruleBody(teamCss, '.v2-team .v2-team-card__mark--needsYou')).toContain('var(--v2-accent)'); expect(ruleBody(teamCss, '.v2-team .v2-team-card__mark--working')).toContain('var(--v2-text-primary)'); expect(ruleBody(teamCss, '.v2-team .v2-team-card__mark--idle')).toContain('var(--v2-border-soft)'); - // R06: Hire and Answer share the frame's ink key, the fill gradient - // (--v2-fill-*) with on-fill text, the same key as BYO's submit and - // Connect's CTA; Talk (and Bring your own) is the bordered plain key. - const ink = selectorRuleBody(teamCss, '\n.v2-team .v2-team__hire, .v2-team .v2-team-card__answer {'); - expect(ink).toContain('background: linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b))'); - expect(ink).toContain('color: var(--v2-on-fill)'); - const plain = selectorRuleBody(teamCss, '\n.v2-team .v2-team__byo, .v2-team .v2-team-card__talk {'); - expect(plain).toContain('border: 1px solid var(--v2-key-edge)'); - expect(plain).not.toContain('var(--v2-fill-'); + // Rulings 11-04: Hire and Answer are the v2p ink key (.v2p-b-p), Talk and + // Bring your own the plain key (.v2p-b-s), both at the small size; the page + // sheet no longer draws its own keys. + expect(yourTeamPage).toContain('className="v2p-b v2p-b-p v2p-b-sm v2-team__hire"'); + expect(yourTeamPage).toContain('className="v2p-b v2p-b-p v2p-b-sm v2-team-card__answer"'); + expect(yourTeamPage).toContain('className="v2p-b v2p-b-s v2p-b-sm v2-team-card__talk"'); + expect(yourTeamPage).toContain('className="v2p-b v2p-b-s v2p-b-sm v2-team__byo"'); + expect(teamCss).not.toMatch(/\.v2-team__hire, \.v2-team \.v2-team-card__answer \{/); expect(ruleBody(teamCss, '.v2-team .v2-team-card__status')).toContain('var(--v2-font-mono)'); // ux-lead 66246: the ask wraps, the command wraps at mono 14, the name is display-18, // the mark is 600, and the title stacks over its meta on a phone. @@ -1758,7 +1761,7 @@ describe('v2 layout invariants (CSS rule presence)', () => { // Read the rule inside the team's phone block — a `[\s\S]*?` regex across // the sheet passed with the rule deleted (sprint-review at 58fb4147). const teamPhone = teamPhoneBlock(teamCss); - expect(ruleBody(teamCss, '.v2-team .v2-team__heading', { within: teamPhone })).toContain('flex-direction: column'); + expect(ruleBody(teamCss, '.v2-team .v2-team__head > .v2-page-head__identity', { within: teamPhone })).toContain('flex-direction: column'); expect(ruleBody(teamCss, '.v2-team .v2-team__grid', { within: teamPhone })).toContain('repeat(1, minmax(0, 1fr))'); // The retired surfaces: feature rows, standard cards with icon buttons, quiet rows, green dot. for (const css of [v2, teamCss]) { @@ -3377,13 +3380,14 @@ describe('Bring your own agent onto Signal (TASK-166)', () => { expect(ruleBody(byoCss, '.v2-feature--byo .v2-byo__copy:hover')).toContain('var(--v2-surface-hover)'); expect(ruleBody(byoCss, '.v2-feature--byo select.v2-byo__input:focus-visible')).toContain('var(--v2-focus-ring)'); }); - test('the submit button is the ink key, at radius 10 (item 1)', () => { + test('the submit button is the ink key, at the small key r8 (item 1, rulings 11-04)', () => { const submit = cssDeclarations(ruleBody(byoCss, '.v2-feature--byo .v2-byo__submit')); expect(submit.background).toBe('linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b))'); expect(submit.color).toBe('var(--v2-on-fill)'); expect(submit.border).toBe('1px solid var(--v2-fill-edge)'); const shared = cssDeclarations(selectorRuleBody(byoCss, '.v2-feature--byo .v2-byo__submit, .v2-feature--byo .v2-byo__secondary')); - expect(shared['border-radius']).toBe('10px'); + expect(shared['border-radius']).toBe('var(--v2-r-8)'); + expect(shared.height).toBe('28px'); expect(shared.cursor).toBe('pointer'); // The old rules left v2.css with the lift. expect(v2).not.toContain('button.v2-byo__submit'); @@ -3625,9 +3629,9 @@ describe('the BYO after-submit surfaces onto Signal (TASK-169)', () => { expect(decls(ruleBody(byoCss, '.v2-byo__stat-label')).font).toBe('500 11px/16px var(--v2-font-mono)'); }); - test('item 3: the secondary button is a control, at the frame key radius', () => { + test('item 3: the secondary button is a control, at the small key radius', () => { expect(decls(selectorRuleBody(byoCss, '.v2-feature--byo .v2-byo__submit, .v2-feature--byo .v2-byo__secondary'))['border-radius']) - .toBe('10px'); + .toBe('var(--v2-r-8)'); expect(decls(ruleBody(byoCss, '.v2-feature--byo .v2-byo__secondary')).color).toBe('var(--v2-text-secondary)'); }); @@ -3663,7 +3667,10 @@ describe('the BYO after-submit surfaces onto Signal (TASK-169)', () => { expect(decls(ruleBody(byoCss, '.v2-byo__add-computer-row')).background).toBe('var(--v2-ink)'); expect(decls(ruleBody(byoCss, '.v2-feature--byo .v2-byo__add-computer-row .v2-byo__copy')).position).toBe('static'); const addComputer = decls(ruleBody(byoCss, '.v2-byo__add-computer')); - expect(addComputer['border-radius']).toBe('var(--v2-radius-lg)'); + // Rulings 11-04: a card is the D215 ring and soft shadow at r9, no 1px border. + expect(addComputer['border-radius']).toBe('var(--v2-r-9)'); + expect(addComputer.border).toBeUndefined(); + expect(addComputer['box-shadow']).toContain('0 0 0 1px var(--v2-border-soft)'); expect(addComputer.background).toBe('var(--v2-bg-subtle)'); // Each snippet is a flat section, not a card. const snippet = decls(selectorRuleBody(byoCss, '.v2-feature--byo .v2-byo__form, .v2-feature--byo .v2-byo__result, .v2-feature--byo .v2-byo__snippet')); @@ -5754,7 +5761,8 @@ describe('Connect lifted from the d215 frame (R05)', () => { const negative = [...connectCss.matchAll(/\n([^\n{}]+) \{[^}]*letter-spacing:\s*-[^;]+;/g)] .flatMap((m) => m[1].split(',').map((sel) => sel.trim())) .filter(Boolean); - expect(negative).toEqual(['.v2-connect .v2-connect__lane h2']); + // The lane label is mono at 0 tracking now (rulings 11-04), so nothing needs a reset. + expect(negative).toEqual([]); for (const sel of negative) { expect(ruleBody(connectCss, `.v2-root:lang(zh) ${sel}`)).toContain('letter-spacing: 0'); } @@ -5779,12 +5787,14 @@ describe('Your team lifted from the d215 frame (R06)', () => { // erase the ring. test('the keyed controls stay class-only so the shared focus ring outranks their shadow', () => { expect(teamCss).not.toMatch(/button\.v2-team(__byo|__hire|-card__answer|-card__talk|-card__mark)/); - expect(selectorRuleBody(teamCss, '\n.v2-team .v2-team__byo, .v2-team .v2-team-card__talk {')).toContain('box-shadow: var(--v2-key-inset)'); + // The keys are the v2p primitives, whose own :focus-visible ring outranks the key shadow. + expect(team).toContain('v2p-b v2p-b-s v2p-b-sm v2-team__byo'); }); test('on a phone, every drawn act and the avatar mark are 44px', () => { + // The acts are v2p keys: the primitives' phone rule sizes them (see the 44 phone-key test). + expect(team).not.toMatch(/className="v2-team(__byo|__hire|-card__answer|-card__talk)"/); const phone = mediaBlockContaining(teamCss, '.v2-team .v2-team__grid'); - expect(selectorRuleBody(phone, '.v2-team .v2-team__byo, .v2-team .v2-team__hire, ')).toContain('height: 44px'); expect(ruleBody(teamCss, '.v2-team .v2-team-card__mark', { within: phone })).toContain('width: 44px'); expect(ruleBody(teamCss, '.v2-team .v2-team-card__mark', { within: phone })).toContain('height: 44px'); }); @@ -5820,12 +5830,12 @@ describe('Your team lifted from the d215 frame (R06)', () => { const negative = [...teamCss.matchAll(/\n([^\n{}]+) \{[^}]*letter-spacing:\s*-[^;]+;/g)] .flatMap((m) => m[1].split(',').map((sel) => sel.trim())) .filter(Boolean); + // The page title is the shared bar's .v2-feature__title, reset in v2.css. expect(negative).toEqual([ - '.v2-team .v2-team__title', '.v2-team .v2-team-card__name', '.v2-team .v2-team__empty-title', ]); - const start = teamCss.indexOf('.v2-root:lang(zh) .v2-team .v2-team__title'); + const start = teamCss.indexOf('.v2-root:lang(zh) .v2-team .v2-team-card__name'); expect(start).toBeGreaterThan(-1); const reset = teamCss.slice(start, teamCss.indexOf('}', start)); expect(reset).toContain('letter-spacing: 0'); @@ -6008,6 +6018,93 @@ describe('sign-in handoff drawn from the d215 s-handoff frames', () => { // The backend redirects to /discord/*; without this mapping the card renders outside .v2-root, unstyled. expect(read('../../utils/legacyV2Path.ts')).toContain("if (pathname.startsWith('/discord/')) return `/v2${pathname}${search}`;"); expect(read('../../index.tsx')).toContain('/^\\/discord\\/(?:callback|success|error)\\/?$/.test(bootPath)'); + +// ui-audit-1104 findings 2–8, rulings 11-04: the secondary pages take the +// reference surfaces' head bar, keys, edges and labels. jsdom cannot see any of +// this; the browser check measured it, and these pin the rules it rests on. +describe('secondary pages on the reference grammar (ui-audit-1104 findings 2–8)', () => { + const v2 = readV2Css(); + const primitives = read('../primitives/primitives.css'); + const app = read('../V2App.tsx'); + + test('finding 2: the head bar title is Bricolage 24/800 on desktop, not the Plex fallback', () => { + // The desktop rule (line start, outside any media query) names the display face. + const title = ruleBody(v2, '.v2-feature__title'); + expect(title).toContain('font: 800 24px/1 var(--v2-font-display)'); + expect(title).toContain('letter-spacing: -0.03em'); + expect(v2.indexOf('\n.v2-feature__title {')).toBeLessThan(v2.indexOf('@media (max-width: 760px) {\n .v2-feature__header {')); + // The bar: 64px, the title 28px in from the pane's 1px edge, a divider, the count in mono 11.5. + const bar = ruleBody(v2, '.v2-feature__header'); + expect(bar).toContain('min-height: 64px'); + expect(bar).toContain('padding: 0 20px 0 28px'); + expect(bar).toContain('border-bottom: 1px solid var(--v2-border-soft)'); + expect(ruleBody(v2, '.v2-feature__header .v2-page-head__meta')).toContain('font: 500 11.5px/16px var(--v2-font-mono)'); + }); + + test('finding 3: Your team and both profiles draw the shared bar, and no head carries the "i" hint', () => { + expect(read('../components/V2YourTeamPage.tsx')).toContain('className="v2-feature__header v2-page-head--wrap v2-team__head"'); + expect(read('../components/V2PersonProfile.tsx')).toContain('className="v2-feature__header v2-page-head--wrap v2-profile__head"'); + expect(read('../agents/V2AgentProfile.tsx')).toContain("'v2-aprofile__hero v2-page-head v2-feature__header v2-page-head--wrap'"); + // Each of those routes opts the pane out of the body inset so the bar meets the pane edge. + expect(app).toMatch(/'Your Team',[\s\S]{0,300}'v2-feature--bar'/); + expect(app).toContain("'v2-feature--bar v2-feature--profile'"); + expect(app).toContain("'v2-feature--bar v2-feature--aprofile'"); + expect(ruleBody(v2, '.v2-feature--bar > .v2-feature__body')).toContain('padding: 0'); + expect(ruleBody(v2, '.v2-feature__scroll')).toContain('overflow-y: auto'); + // BYO and Connect moved the hint to an intro line. + expect(app).not.toMatch(/{t('connect.intro')}

"); + }); + + test('finding 5: every v2p key and segment is 44px tall on a phone', () => { + const phone = mediaBlockContaining(primitives, '.v2p-b, .v2p-b-sm {'); + expect(ruleBody(primitives, '.v2p-b, .v2p-b-sm', { within: phone })).toContain('height: 44px'); + const icon = ruleBody(primitives, '.v2p-ib, .v2p-ib.v2p-b-sm', { within: phone }); + expect(icon).toContain('width: 44px'); + expect(icon).toContain('height: 44px'); + expect(ruleBody(primitives, '.v2p-seg button, .v2p-seg--sm button', { within: phone })).toContain('height: 44px'); + // After the desktop sizes, so the phone rule wins at equal specificity. + for (const desktop of ['\n.v2p-b-sm {', '\n.v2p-ib.v2p-b-sm {', '\n.v2p-seg--sm button {']) { + expect(primitives.indexOf(phone)).toBeGreaterThan(primitives.indexOf(desktop)); + } + }); + + test('finding 6: the board header icon keys never shrink on a phone', () => { + const phone = mediaBlockContaining(boardCss, '.v2-feature--board .v2-board__key--icon { flex: none; }'); + expect(ruleBody(boardCss, '.v2-feature--board .v2-board__key--icon', { within: phone })).toContain('flex: none'); + }); + + test('finding 4: the in-app text keys are the 28px small key at r8', () => { + const board = ruleBody(boardCss, '.v2-feature--board .v2-board__key'); + expect(board).toContain('height: 28px'); + expect(board).toContain('border-radius: var(--v2-r-8)'); + const connect = selectorRuleBody(read('../components/V2ConnectPage.css'), '.v2-connect .v2-connect__cta, .v2-connect .v2-connect__secondary {'); + expect(connect).toContain('height: 28px'); + expect(connect).toContain('border-radius: var(--v2-r-8)'); + const aprofile = ruleBody(read('../agents/v2-agent-profile.css'), '.v2-aprofile .v2-aprofile__btn'); + expect(aprofile).toContain('height: 28px'); + expect(aprofile).toContain('border-radius: var(--v2-r-8)'); + }); + + test('finding 7: cards are the D215 ring at r9 and lanes are r12, never a 1px border', () => { + const card = ruleBody(read('../components/V2YourTeamPage.css'), '.v2-team .v2-team-card'); + expect(card).toContain('border-radius: var(--v2-r-9)'); + expect(card).toContain('box-shadow: 0 0 0 1px var(--v2-border-soft)'); + expect(card).not.toMatch(/(^|[\s;{])border:/); + expect(ruleBody(read('../components/V2ConnectPage.css'), '.v2-connect .v2-connect__lane')).toContain('border-radius: var(--v2-r-12)'); + expect(ruleBody(read('../components/V2PersonProfile.css'), '.v2-profile .v2-profile__list')).toContain('border-radius: var(--v2-r-12)'); + expect(ruleBody(read('../agents/v2-agent-profile.css'), '.v2-aprofile .v2-aprofile__card')).toContain('border-radius: var(--v2-r-12)'); + }); + + test('finding 8: section labels are Settings\' mono 11.5/500 muted, and counts are 11.5', () => { + const mono = 'font: 500 11.5px/16px var(--v2-font-mono)'; + expect(ruleBody(read('../components/V2PersonProfile.css'), '.v2-profile .v2-profile__lane h2')).toContain(mono); + expect(ruleBody(read('../agents/v2-agent-profile.css'), '.v2-aprofile .v2-aprofile__card-title')).toContain(mono); + expect(ruleBody(read('../components/V2ConnectPage.css'), '.v2-connect .v2-connect__lane h2')).toContain(mono); + expect(ruleBody(read('../components/V2PersonProfile.css'), '.v2-profile .v2-profile__meta')).toContain(mono); + expect(ruleBody(read('../agents/v2-agent-profile.css'), '.v2-aprofile .v2-aprofile__meta')).toContain('11.5px'); + expect(ruleBody(boardCss, '.v2-feature--board .v2-board__subtitle')).toContain('500 11.5px/1 var(--v2-font-mono)'); }); }); diff --git a/frontend/src/v2/agents/V2AgentProfile.tsx b/frontend/src/v2/agents/V2AgentProfile.tsx index c7dacc8b78..bb53179ba5 100644 --- a/frontend/src/v2/agents/V2AgentProfile.tsx +++ b/frontend/src/v2/agents/V2AgentProfile.tsx @@ -403,9 +403,13 @@ const V2AgentProfile: React.FC = ({ shell }) => { /> ); + // In the app shell the hero is the shared page head bar (.v2-feature__header, + // the route's v2-feature--bar) and the rest scrolls under it; signed out it + // stays the public page's hero. + const inShell = Boolean(shell); const main = (
-
+
{/* The avatar IS the edit control (2026-09-01). Non-owners get the plain avatar; owners get the same avatar as a button. */} {canEditAvatar ? ( @@ -424,13 +428,13 @@ const V2AgentProfile: React.FC = ({ shell }) => { ) : ( {avatar} )} -
+
-

{agent.displayName}

+

{agent.displayName}

{runtimeLabel && {runtimeLabel}} {agent.officialAgent && {t('agentProfile.hero.official')}}
- {agent.description &&

{agent.description}

} + {agent.description && !inShell &&

{agent.description}

}

{t('agentProfile.hero.activeInPods', { count: pods.count })} {memory.has && ` · ${t('agentProfile.hero.memories', { count: memory.entryCount })}`} @@ -441,13 +445,16 @@ const V2AgentProfile: React.FC = ({ shell }) => { )}

-
+
{isOwner && signedIn && ( {t('agentSettings.settings')} )} {primaryKey}
+
+ {/* In the bar there is room for the name and one meta line; the description is the intro line under it. */} + {agent.description && inShell &&

{agent.description}

} {avatarDialogOpen && (

{t('agentProfile.avatar.title')}

@@ -612,6 +619,7 @@ const V2AgentProfile: React.FC = ({ shell }) => {
)} +
); diff --git a/frontend/src/v2/agents/v2-agent-profile.css b/frontend/src/v2/agents/v2-agent-profile.css index 3075e51b69..00721c63a1 100644 --- a/frontend/src/v2/agents/v2-agent-profile.css +++ b/frontend/src/v2/agents/v2-agent-profile.css @@ -20,22 +20,26 @@ .v2-aprofile .v2-aprofile__brand .v2-rail__brand-icon svg { width: 16px; height: 16px; } .v2-aprofile .v2-aprofile__nav { display: flex; align-items: center; gap: 18px; } .v2-aprofile .v2-aprofile__navlink { font-size: 13.5px; font-weight: 500; color: var(--v2-text-secondary); text-decoration: none; } -.v2-aprofile .v2-aprofile__btn { box-sizing: border-box; cursor: pointer; flex: none; display: inline-flex; align-items: center; justify-content: center; height: 34px; padding: 0 14px; border-radius: 10px; font: 600 13px/1 var(--v2-font); white-space: nowrap; text-decoration: none; } +/* The v2p small key's size (.v2p-b-sm: 28 at r8, rulings 11-04); 44 on phones below. */ +.v2-aprofile .v2-aprofile__btn { box-sizing: border-box; cursor: pointer; flex: none; display: inline-flex; align-items: center; justify-content: center; height: 28px; padding: 0 10px; border-radius: var(--v2-r-8); font: 600 12.5px/1 var(--v2-font); white-space: nowrap; text-decoration: none; } .v2-aprofile .v2-aprofile__btn--primary { color: var(--v2-on-fill); background: linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b)); border: 1px solid var(--v2-fill-edge); } .v2-aprofile .v2-aprofile__btn--ghost { color: var(--v2-text-primary); background: linear-gradient(180deg, var(--v2-key-a), var(--v2-key-b)); border: 1px solid var(--v2-key-edge); box-shadow: var(--v2-key-inset); } .v2-aprofile .v2-aprofile__hero { display: flex; align-items: flex-start; gap: 16px; } .v2-aprofile .v2-aprofile__avatar { flex: none; display: inline-flex; position: relative; } .v2-aprofile .v2-aprofile__avatar-button { all: unset; cursor: pointer; flex: none; display: inline-flex; position: relative; border-radius: 14px; } -.v2-aprofile .v2-aprofile__avatar .v2-avatar, -.v2-aprofile .v2-aprofile__avatar-button .v2-avatar { width: 56px; height: 56px; border-radius: 14px; font-size: 18px; } +/* The public hero's 56px face; in the app shell the bar draws memory's 34 (below). */ +.v2-aprofile--public .v2-aprofile__avatar .v2-avatar, +.v2-aprofile--public .v2-aprofile__avatar-button .v2-avatar { width: 56px; height: 56px; border-radius: 14px; font-size: 18px; } .v2-aprofile .v2-aprofile__avatar-hint { position: absolute; right: -4px; bottom: -4px; height: 18px; padding: 0 6px; border-radius: 9px; background: var(--v2-surface); box-shadow: inset 0 0 0 1px var(--v2-border-soft); font: 600 12px/18px var(--v2-font); color: var(--v2-text-secondary); } .v2-aprofile .v2-aprofile__hero-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1; } .v2-aprofile .v2-aprofile__name-row { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 8px; } -.v2-aprofile .v2-aprofile__name { margin: 0; font: 800 24px/1.15 var(--v2-font-display); letter-spacing: -0.03em; color: var(--v2-text-primary); overflow-wrap: anywhere; } +.v2-aprofile .v2-aprofile__name { margin: 0; color: var(--v2-text-primary); overflow-wrap: anywhere; } +/* Signed out the hero sets its own title; in the shell it is the bar's .v2-feature__title. */ +.v2-aprofile--public .v2-aprofile__name { font: 800 24px/1.15 var(--v2-font-display); letter-spacing: -0.03em; } .v2-aprofile .v2-aprofile__runtime { font: 600 11px/18px var(--v2-font-mono); letter-spacing: .04em; text-transform: uppercase; color: var(--v2-text-secondary); background: var(--v2-surface-tint); padding: 0 7px; border-radius: 5px; } .v2-aprofile .v2-aprofile__badge { font: 600 12px/18px var(--v2-font); color: var(--v2-accent-text); background: var(--v2-accent-soft); padding: 0 8px; border-radius: 9px; } .v2-aprofile .v2-aprofile__desc { margin: 2px 0 0; font-size: 14px; line-height: 20px; color: var(--v2-text-secondary); } -.v2-aprofile .v2-aprofile__meta { margin: 2px 0 0; font: 500 12px/16px var(--v2-font-mono); color: var(--v2-text-muted); } +.v2-aprofile .v2-aprofile__meta { margin: 2px 0 0; font: 500 11.5px/16px var(--v2-font-mono); color: var(--v2-text-muted); } .v2-aprofile .v2-aprofile__keys { flex: none; display: flex; gap: 8px; } /* A long agent name truncates inside its key instead of overflowing it. */ .v2-aprofile .v2-aprofile__keys .v2-aprofile__btn { min-width: 0; max-width: 100%; } @@ -43,9 +47,10 @@ .v2-aprofile .v2-agent-settings__tabs { display: flex; gap: 24px; margin: 0; border-bottom: 0; box-shadow: inset 0 -1px 0 var(--v2-border-soft); } .v2-aprofile .v2-agent-settings__tabs > * { padding: 10px 2px; font-size: 14px; color: var(--v2-text-secondary); text-decoration: none; } .v2-aprofile .v2-agent-settings__tabs [aria-current="page"] { color: var(--v2-text-primary); font-weight: 600; border-bottom: 0; box-shadow: inset 0 -2px 0 var(--v2-accent); } -.v2-aprofile .v2-aprofile__card { display: flex; flex-direction: column; gap: 8px; padding: 16px 20px; border-radius: 14px; background: var(--v2-surface); box-shadow: inset 0 0 0 1px var(--v2-border-soft); } -.v2-aprofile .v2-aprofile__card-title { display: flex; align-items: baseline; gap: 8px; margin: 0; font: 800 16px/1.25 var(--v2-font-display); letter-spacing: -0.02em; color: var(--v2-text-primary); } -.v2-aprofile .v2-aprofile__count { font: 500 12px/16px var(--v2-font-mono); letter-spacing: 0; color: var(--v2-text-muted); } +/* A section is a ringed list at r12 with Settings' mono 11.5 label (rulings 11-04). */ +.v2-aprofile .v2-aprofile__card { display: flex; flex-direction: column; gap: 8px; padding: 16px 20px; border-radius: var(--v2-r-12); background: var(--v2-surface); box-shadow: inset 0 0 0 1px var(--v2-border-soft); } +.v2-aprofile .v2-aprofile__card-title { display: flex; align-items: baseline; gap: 8px; margin: 0; font: 500 11.5px/16px var(--v2-font-mono); color: var(--v2-text-muted); } +.v2-aprofile .v2-aprofile__count { font: 500 11.5px/16px var(--v2-font-mono); letter-spacing: 0; color: var(--v2-text-muted); } .v2-aprofile .v2-aprofile__owner-tag { margin-left: auto; font: 500 11.5px/16px var(--v2-font-mono); letter-spacing: 0; color: var(--v2-text-muted); } .v2-aprofile .v2-aprofile__list { list-style: none; margin: 0; padding: 0; } .v2-aprofile .v2-aprofile__list li + li { border-top: 1px solid var(--v2-border-soft); } @@ -106,8 +111,8 @@ /* The avatar picker opens in the page flow under the header it edits, so it never covers the profile and fits a phone without a sideways scroll. */ -.v2-aprofile .v2-aprofile__avatar-picker { padding: 16px 20px; border-radius: 14px; background: var(--v2-surface); box-shadow: inset 0 0 0 1px var(--v2-border-soft); } -.v2-aprofile .v2-aprofile__avatar-title { margin: 0 0 12px; font: 800 16px/1.25 var(--v2-font-display); letter-spacing: -0.02em; color: var(--v2-text-primary); } +.v2-aprofile .v2-aprofile__avatar-picker { padding: 16px 20px; border-radius: var(--v2-r-12); background: var(--v2-surface); box-shadow: inset 0 0 0 1px var(--v2-border-soft); } +.v2-aprofile .v2-aprofile__avatar-title { margin: 0 0 12px; font: 500 11.5px/16px var(--v2-font-mono); color: var(--v2-text-muted); } .v2-aprofile .v2-aprofile__avatar-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(52px, 1fr)); gap: 8px; max-width: 560px; } .v2-aprofile .v2-aprofile__avatar-choice { all: unset; box-sizing: border-box; cursor: pointer; aspect-ratio: 1; padding: 2px; border: 2px solid transparent; border-radius: 50%; line-height: 0; } .v2-aprofile .v2-aprofile__avatar-choice img { width: 100%; height: 100%; border-radius: 50%; } @@ -120,10 +125,27 @@ .v2-aprofile .v2-aprofile__avatar-cancel { all: unset; box-sizing: border-box; cursor: pointer; display: inline-flex; align-items: center; margin: 12px 0 0; height: 34px; padding: 0 14px; border-radius: 10px; font: 600 13px/1 var(--v2-font); color: var(--v2-text-primary); background: linear-gradient(180deg, var(--v2-key-a), var(--v2-key-b)); border: 1px solid var(--v2-key-edge); box-shadow: var(--v2-key-inset); } /* CJK never takes negative letter-spacing (TASK-055). */ .v2-root:lang(zh) .v2-aprofile .v2-aprofile__name, -.v2-root:lang(zh) .v2-aprofile .v2-aprofile__card-title, .v2-root:lang(zh) .v2-aprofile .v2-aprofile__empty-title, .v2-root:lang(zh) .v2-aprofile .v2-aprofile__brand { letter-spacing: 0; } +/* In the app shell (the route passes v2-feature--bar v2-feature--aprofile) the + hero is the shared page head bar, .v2-feature__header: avatar, name and one + meta line, the keys at the end, as agent memory draws it. The rest scrolls + under it in a centred 760 column. Signed out, the public layout above stands. */ +.v2-feature--aprofile .v2-aprofile { flex: 1 1 auto; } +.v2-feature--aprofile .v2-aprofile__main { max-width: none; flex: 1 1 auto; min-height: 0; gap: 0; } +.v2-feature--aprofile .v2-aprofile__hero { min-height: 76px; align-items: center; gap: 12px; } +.v2-feature--aprofile .v2-aprofile__hero-text { gap: 5px; } +.v2-feature--aprofile .v2-aprofile__body { --v2-feature-col: 760px; display: flex; flex-direction: column; gap: 16px; } +.v2-aprofile--public .v2-aprofile__body { display: contents; } +.v2-aprofile .v2-aprofile__intro { margin: 0 4px; font-size: 13.5px; } +@media (max-width: 760px) { + .v2-feature--aprofile .v2-aprofile__hero { min-height: 52px; gap: 0 10px; } + .v2-feature--aprofile .v2-aprofile__hero .v2-avatar { width: 24px; height: 24px; } + .v2-feature--aprofile .v2-aprofile__hero-text { gap: 3px; padding: 8px 0; } + .v2-feature--aprofile .v2-aprofile__body { gap: 10px; } +} + /* At 390 every control is at least 44px to touch, including the ones the frame draws smaller. Last in the file so it outranks the resets above. */ @media (max-width: 760px) { diff --git a/frontend/src/v2/components/V2AgentBYO.css b/frontend/src/v2/components/V2AgentBYO.css index 7ea5507716..08aaa8ff1c 100644 --- a/frontend/src/v2/components/V2AgentBYO.css +++ b/frontend/src/v2/components/V2AgentBYO.css @@ -6,6 +6,8 @@ the page's 760px phone query. Scoped to .v2-feature--byo, the page's wrapper. */ .v2-feature--byo .v2-byo__layout { display: grid; grid-template-columns: minmax(0, 1fr) 280px; gap: 28px; max-width: 1040px; margin: 0 auto; align-items: start; } .v2-feature--byo .v2-byo__main { min-width: 0; } +/* The page's lead line: the head carries no "i" hint (rulings 11-04). */ +.v2-feature--byo .v2-byo__intro { margin: 0 4px 14px; font-size: 13.5px; line-height: 20px; color: var(--v2-text-secondary); } .v2-feature--byo .v2-byo__form, .v2-feature--byo .v2-byo__result, .v2-feature--byo .v2-byo__snippet { display: flex; flex-direction: column; gap: 10px; } .v2-feature--byo .v2-byo__modes-question, .v2-feature--byo .v2-byo__preview-label, .v2-feature--byo .v2-byo__snippet-head { margin: 0 4px; font: 500 11.5px/1 var(--v2-font-mono); color: var(--v2-text-muted); } .v2-feature--byo .v2-byo__modes, .v2-feature--byo .v2-byo__os { margin: 0; display: inline-flex; align-self: flex-start; gap: 2px; padding: 2px; border-radius: 9px; background: var(--v2-surface-tint); box-shadow: inset 0 0 0 1px var(--v2-border-soft); } @@ -21,7 +23,8 @@ /* the page shows a field's hint only while it has focus or an error */ .v2-feature--byo .v2-byo__field > .v2-byo__hint { display: none; } .v2-feature--byo .v2-byo__field:focus-within > .v2-byo__hint, .v2-feature--byo .v2-byo__field--error > .v2-byo__hint { display: block; } -.v2-feature--byo .v2-byo__submit, .v2-feature--byo .v2-byo__secondary { all: unset; box-sizing: border-box; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; height: 34px; padding: 0 13px; border-radius: 10px; font: 600 13px/1 var(--v2-font); white-space: nowrap; } +/* The v2p small key's size (.v2p-b-sm: 28 at r8, rulings 11-04); 44 on phones below. */ +.v2-feature--byo .v2-byo__submit, .v2-feature--byo .v2-byo__secondary { all: unset; box-sizing: border-box; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; height: 28px; padding: 0 10px; border-radius: var(--v2-r-8); font: 600 12.5px/1 var(--v2-font); white-space: nowrap; } .v2-feature--byo .v2-byo__submit { align-self: flex-start; margin-top: 6px; color: var(--v2-on-fill); background: linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b)); border: 1px solid var(--v2-fill-edge); } .v2-feature--byo .v2-byo__secondary { color: var(--v2-text-secondary); } .v2-feature--byo .v2-byo__link { all: unset; align-self: flex-start; cursor: pointer; color: var(--v2-accent-text); font-weight: 600; } @@ -131,8 +134,8 @@ } .v2-root .v2-byo__hosted-offer button.v2-byo__hosted-offer-action { margin-top: 0; - height: 40px; - padding: 0 16px; + height: 28px; /* the small key, as the submit it extends; 44 on phones */ + padding: 0 10px; align-self: center; } .v2-byo__hosted-offer .v2-byo__error { @@ -336,14 +339,15 @@ } .v2-root a.v2-byo__cursor-link:hover { text-decoration: underline; } /* ADR-026 D1: one-paste computer setup, shown only while no machine exists. */ +/* A card: the D215 ring and soft shadow at r9, not a 1px border at r6 (rulings 11-04). */ .v2-byo__add-computer { display: flex; flex-direction: column; gap: 10px; padding: 18px; - border: 1px solid var(--v2-border); - border-radius: var(--v2-radius-lg); + border-radius: var(--v2-r-9); background: var(--v2-bg-subtle); + box-shadow: 0 0 0 1px var(--v2-border-soft), 0 1px 2px rgba(16, 24, 40, 0.04); } .v2-byo__add-computer > p { margin: 0; } .v2-byo__add-computer-row { diff --git a/frontend/src/v2/components/V2AgentBYO.tsx b/frontend/src/v2/components/V2AgentBYO.tsx index b97e2011b8..d5cc8a5552 100644 --- a/frontend/src/v2/components/V2AgentBYO.tsx +++ b/frontend/src/v2/components/V2AgentBYO.tsx @@ -1188,6 +1188,13 @@ const V2AgentBYO: React.FC = () => { ) : null; + const pageLead = (() => { + if (issued) return undefined; + if (showCompactHosted && !hosted && hosting?.configured) return t('agentByo.compact.lead',{ agents: hosting?.caps.agentsPerUser, turns: hosting?.caps.turnsPerDay }); + if (showCompactByo) return t('agentByo.compactByo.lead'); + return t('agentByo.description'); + })(); + return ( { @@ -1200,17 +1207,13 @@ const V2AgentBYO: React.FC = () => { } return t('agentByo.title'); })()} - hint={(() => { - if (issued) return undefined; - if (showCompactHosted && !hosted && hosting?.configured) return t('agentByo.compact.lead',{ agents: hosting?.caps.agentsPerUser, turns: hosting?.caps.turnsPerDay }); - if (showCompactByo) return t('agentByo.compactByo.lead'); - return t('agentByo.description'); - })()} showPodsSidebar={false} className="v2-feature--byo" >
+ {/* The page's one-line lead: it was the head's "i" hint, which no head carries now. */} + {pageLead &&

{pageLead}

} {!issued && !hosted && personaCard && (
diff --git a/frontend/src/v2/components/V2ConnectPage.css b/frontend/src/v2/components/V2ConnectPage.css index 2d3b818761..2dfccc0207 100644 --- a/frontend/src/v2/components/V2ConnectPage.css +++ b/frontend/src/v2/components/V2ConnectPage.css @@ -5,10 +5,13 @@ together) and the frame's `.app.m` phone rules sit in the page's 760px phone query. Scoped to .v2-connect, the page's root. */ .v2-connect { display: flex; flex-direction: column; gap: 16px; max-width: 960px; margin: 0 auto; } -.v2-connect .v2-connect__lane { display: flex; flex-direction: column; gap: 14px; padding: 20px; border-radius: 14px; background: var(--v2-surface); box-shadow: inset 0 0 0 1px var(--v2-border-soft); } +.v2-connect .v2-connect__intro { margin: 0 4px; font-size: 13.5px; line-height: 20px; color: var(--v2-text-secondary); } +/* A lane is a list surface: the D215 ring at r12 (rulings 11-04), not a pane's r14. */ +.v2-connect .v2-connect__lane { display: flex; flex-direction: column; gap: 14px; padding: 20px; border-radius: var(--v2-r-12); background: var(--v2-surface); box-shadow: inset 0 0 0 1px var(--v2-border-soft); } .v2-connect .v2-connect__lane-heading { display: flex; align-items: baseline; gap: 10px; } .v2-connect .v2-connect__number { flex: none; font: 500 11.5px/1 var(--v2-font-mono); color: var(--v2-accent-text); } -.v2-connect .v2-connect__lane h2 { margin: 0; font: 800 16px/1.25 var(--v2-font-display); letter-spacing: -0.02em; color: var(--v2-text-primary); } +/* Section labels are Settings' mono 11.5/500 muted (rulings 11-04), not a display heading. */ +.v2-connect .v2-connect__lane h2 { margin: 0; font: 500 11.5px/16px var(--v2-font-mono); color: var(--v2-text-muted); } .v2-connect .v2-connect__code-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } .v2-connect .v2-connect__code-card { min-width: 0; overflow: hidden; border-radius: 12px; background: var(--v2-bg-subtle); box-shadow: inset 0 0 0 1px var(--v2-border-soft); } .v2-connect .v2-connect__code-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 44px; padding: 8px 8px 8px 14px; border-bottom: 1px solid var(--v2-border-soft); font: 500 11.5px/16px var(--v2-font-mono); color: var(--v2-text-muted); } @@ -20,7 +23,8 @@ .v2-connect .v2-connect__link { display: inline-flex; align-items: center; min-height: 32px; font: 600 13px/20px var(--v2-font); color: var(--v2-accent-text); text-decoration: none; } .v2-connect .v2-connect__lane > .v2-connect__link, .v2-connect .v2-connect__lane > .v2-connect__secondary { align-self: flex-start; } .v2-connect .v2-connect__actions { display: flex; flex-wrap: wrap; gap: 8px; } -.v2-connect .v2-connect__cta, .v2-connect .v2-connect__secondary { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; height: 34px; padding: 0 13px; border-radius: 10px; font: 600 13px/1 var(--v2-font); white-space: nowrap; text-decoration: none; cursor: pointer; } +/* The v2p small key's size (.v2p-b-sm, 28 at r8); 44 on phones below. */ +.v2-connect .v2-connect__cta, .v2-connect .v2-connect__secondary { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; height: 28px; padding: 0 10px; border-radius: var(--v2-r-8); font: 600 12.5px/1 var(--v2-font); white-space: nowrap; text-decoration: none; cursor: pointer; } .v2-connect .v2-connect__cta { color: var(--v2-on-fill); background: linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b)); border: 1px solid var(--v2-fill-edge); } .v2-connect .v2-connect__secondary { color: var(--v2-text-primary); background: linear-gradient(180deg, var(--v2-key-a), var(--v2-key-b)); border: 1px solid var(--v2-key-edge); box-shadow: var(--v2-key-inset); } @media (max-width: 760px) { @@ -47,6 +51,3 @@ background: var(--v2-ink-hover); } .v2-connect .v2-connect__link:hover { text-decoration: underline; } -/* CJK never takes negative letter-spacing (TASK-055); this entry left the - v2.css :lang(zh) reset list with the rest of the page. */ -.v2-root:lang(zh) .v2-connect .v2-connect__lane h2 { letter-spacing: 0; } diff --git a/frontend/src/v2/components/V2ConnectPage.tsx b/frontend/src/v2/components/V2ConnectPage.tsx index 8631946914..c84bf2b14c 100644 --- a/frontend/src/v2/components/V2ConnectPage.tsx +++ b/frontend/src/v2/components/V2ConnectPage.tsx @@ -65,6 +65,8 @@ const V2ConnectPage: React.FC = () => { return (
+ {/* The page's one-line lead: it was the head's "i" hint, which no head carries now. */} +

{t('connect.intro')}

diff --git a/frontend/src/v2/components/V2PersonProfile.css b/frontend/src/v2/components/V2PersonProfile.css index 0a1ae38b38..89b40f4292 100644 --- a/frontend/src/v2/components/V2PersonProfile.css +++ b/frontend/src/v2/components/V2PersonProfile.css @@ -4,37 +4,42 @@ translated to --v2-* (so light, dark and auto switch together) and the frame's `.app.m` phone rules sit in the 760px phone query. Scoped to .v2-profile, the page's root. */ -.v2-profile { display: flex; flex-direction: column; gap: 16px; max-width: 760px; margin: 0 auto; } -.v2-profile .v2-profile__head { display: flex; align-items: center; gap: 16px; } +/* The head is the shared bar (V2PageHead, .v2-feature__header, the route's + v2-feature--bar) with the avatar as its lead, as agent memory draws it; the + body scrolls under it in a centred 760 column. Keys are the v2p small keys + (28 at r8, 44 on phones). Rulings 11-04. */ +.v2-profile { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; } +.v2-profile .v2-profile__head { min-height: 76px; } +.v2-profile .v2-profile__head > .v2-page-head__identity { flex-direction: column; align-items: flex-start; gap: 5px; min-width: 0; } +.v2-profile .v2-profile__head .v2-profile__name { max-width: 100%; overflow-wrap: anywhere; } +.v2-profile .v2-profile__body { --v2-feature-col: 760px; display: flex; flex-direction: column; gap: 16px; } .v2-profile .v2-profile__avatar { flex: none; display: inline-flex; } -.v2-profile .v2-profile__avatar .v2-avatar { width: 56px; height: 56px; border-radius: 14px; font-size: 18px; } -.v2-profile .v2-profile__id { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1; } -.v2-profile .v2-profile__name { margin: 0; font: 800 24px/1.15 var(--v2-font-display); letter-spacing: -0.03em; color: var(--v2-text-primary); overflow-wrap: anywhere; } -.v2-profile .v2-profile__meta { font: 500 12px/16px var(--v2-font-mono); color: var(--v2-text-muted); } -.v2-profile .v2-profile__counts { font-size: 13px; line-height: 18px; color: var(--v2-text-secondary); } -.v2-profile .v2-profile__follow, .v2-profile .v2-profile__edit { all: unset; box-sizing: border-box; cursor: pointer; flex: none; display: inline-flex; align-items: center; justify-content: center; height: 34px; padding: 0 14px; border-radius: 10px; font: 600 13px/1 var(--v2-font); white-space: nowrap; text-decoration: none; } -.v2-profile .v2-profile__follow { color: var(--v2-on-fill); background: linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b)); border: 1px solid var(--v2-fill-edge); } -.v2-profile .v2-profile__follow--on, .v2-profile .v2-profile__edit { color: var(--v2-text-primary); background: linear-gradient(180deg, var(--v2-key-a), var(--v2-key-b)); border: 1px solid var(--v2-key-edge); box-shadow: var(--v2-key-inset); } -.v2-profile .v2-profile__lane { display: flex; flex-direction: column; gap: 10px; padding: 18px 20px; border-radius: 14px; background: var(--v2-surface); box-shadow: inset 0 0 0 1px var(--v2-border-soft); } -.v2-profile .v2-profile__lane h2 { margin: 0; font: 800 16px/1.25 var(--v2-font-display); letter-spacing: -0.02em; color: var(--v2-text-primary); } -.v2-profile .v2-profile__list { list-style: none; margin: 0; padding: 0; } +.v2-profile .v2-profile__meta { font: 500 11.5px/16px var(--v2-font-mono); color: var(--v2-text-muted); } +.v2-profile .v2-profile__counts { margin: 0 4px; font-size: 13.5px; line-height: 20px; color: var(--v2-text-secondary); } +.v2-profile .v2-profile__follow, .v2-profile .v2-profile__edit { flex: none; text-decoration: none; } +/* A section is Settings' grammar: a mono label over a ringed r12 list (rulings 11-04). */ +.v2-profile .v2-profile__lane { display: flex; flex-direction: column; min-width: 0; } +.v2-profile .v2-profile__lane h2 { margin: 0 4px 8px; font: 500 11.5px/16px var(--v2-font-mono); color: var(--v2-text-muted); } +.v2-profile .v2-profile__list { list-style: none; margin: 0; padding: 0 16px; border-radius: var(--v2-r-12); background: var(--v2-surface); box-shadow: inset 0 0 0 1px var(--v2-border-soft); } .v2-profile .v2-profile__list li + li { border-top: 1px solid var(--v2-border-soft); } -.v2-profile .v2-profile__row { display: flex; align-items: center; justify-content: space-between; gap: 14px; min-height: 44px; padding: 6px 0; color: var(--v2-text-primary); text-decoration: none; } +.v2-profile .v2-profile__row { display: flex; align-items: center; justify-content: space-between; gap: 14px; min-height: 48px; padding: 6px 0; color: var(--v2-text-primary); text-decoration: none; } .v2-profile .v2-profile__row-title { font-size: 14px; font-weight: 600; color: var(--v2-text-primary); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .v2-profile .v2-profile__row-text { font-size: 13.5px; line-height: 19px; color: var(--v2-ink-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .v2-profile .v2-profile__row-meta { flex: none; font: 500 11.5px/16px var(--v2-font-mono); color: var(--v2-text-muted); } .v2-profile .v2-profile__note { margin: 0; font-size: 13.5px; line-height: 20px; color: var(--v2-text-secondary); } .v2-profile .v2-profile__note a { color: var(--v2-accent-text); font-weight: 600; text-decoration: none; } @media (max-width: 760px) { - .v2-profile { gap: 10px; } - .v2-profile .v2-profile__head { flex-wrap: wrap; gap: 12px 14px; } - .v2-profile .v2-profile__follow, .v2-profile .v2-profile__edit { flex: 1 0 100%; height: 44px; font-size: 14px; } - .v2-profile .v2-profile__lane { padding: 14px 16px; } + .v2-profile .v2-profile__body { gap: 10px; } + /* The bar's 52px row: 24px avatar, the name, the handle; the act takes the row under it. */ + .v2-profile .v2-profile__head { min-height: 52px; gap: 0 10px; } + .v2-profile .v2-profile__head .v2-avatar { width: 24px; height: 24px; } + .v2-profile .v2-profile__head > .v2-page-head__identity { gap: 3px; padding: 8px 0; } + .v2-profile .v2-profile__list { padding: 0 12px; } .v2-profile .v2-profile__row-text { white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } } /* Live behaviour the frame does not draw. */ -.v2-profile .v2-profile__follow:hover:not(:disabled) { border-color: var(--v2-ink-hover); background: var(--v2-ink-hover); } +.v2-profile .v2-profile__follow:not(.v2-profile__follow--on):hover:not(:disabled) { border-color: var(--v2-ink-hover); background: var(--v2-ink-hover); } .v2-profile .v2-profile__follow--on:hover:not(:disabled), .v2-profile .v2-profile__edit:hover { border-color: var(--v2-border-strong); background: var(--v2-surface-hover); color: var(--v2-text-primary); } .v2-profile .v2-profile__follow:disabled { cursor: default; opacity: 0.7; } @@ -44,5 +49,4 @@ .v2-profile a.v2-profile__row:hover .v2-profile__row-title { color: var(--v2-accent-text); } .v2-profile .v2-profile__note a:hover { text-decoration: underline; } /* CJK never takes negative letter-spacing (TASK-055). */ -.v2-root:lang(zh) .v2-profile .v2-profile__name, -.v2-root:lang(zh) .v2-profile .v2-profile__lane h2 { letter-spacing: 0; } +.v2-root:lang(zh) .v2-profile .v2-profile__name { letter-spacing: 0; } diff --git a/frontend/src/v2/components/V2PersonProfile.tsx b/frontend/src/v2/components/V2PersonProfile.tsx index 59f923acab..01cb710ae6 100644 --- a/frontend/src/v2/components/V2PersonProfile.tsx +++ b/frontend/src/v2/components/V2PersonProfile.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { useAuth } from '../../context/AuthContext'; import { useV2Api } from '../hooks/useV2Api'; import V2Avatar from './V2Avatar'; +import V2PageHead from './V2PageHead'; import './V2PersonProfile.css'; /* @@ -116,10 +117,12 @@ const V2PersonProfile: React.FC = () => { if (state !== 'ready' || !profile) { return (
-

- {state === 'notFound' ? t('profile.notFound') : t('profile.loadError')}{' '} - {t('profile.backHome')} -

+
+

+ {state === 'notFound' ? t('profile.notFound') : t('profile.loadError')}{' '} + {t('profile.backHome')} +

+
); } @@ -162,12 +165,12 @@ const V2PersonProfile: React.FC = () => { let action: React.ReactNode = null; if (isSelf) { - action = {t('profile.editProfile')}; + action = {t('profile.editProfile')}; } else if (!profile.isBot) { action = ( ) : null} {!needsReconnect && state === 'needsYou' && ( - )} {!needsReconnect && - -
-
+ {/* The shared page head bar (.v2-feature__header), as Settings and Connectors draw it. */} + + + + + )} + /> +
{!isEntitled && (
@@ -533,10 +537,10 @@ const V2YourTeamPage: React.FC = () => { onChange={(e) => setRedeemCode(e.target.value)} autoFocus /> - - {redeemError && {redeemError}} @@ -576,7 +580,7 @@ const V2YourTeamPage: React.FC = () => {

{t('yourTeam.own.text')}

{t('yourTeam.own.command')}
-
@@ -601,6 +605,7 @@ const V2YourTeamPage: React.FC = () => { )} +
); }; From ae3efe1b1c10600c93ccc1e8ab4a72dc4657eca9 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 4 Nov 2026 00:33:54 -0800 Subject: [PATCH 3/9] fix(v2): the run trace takes the shared head bar, the 12px phone gutter, ringed cards and mono turn labels - The run trace draws V2PageHead with .v2-feature__header (the route passes v2-feature--bar); "Back to pod" is the bar's lead ghost icon key, and the h1 is the shared 24/800 title instead of 32/700 that wrapped at 390. - The body is .v2-feature__scroll in a 760 column, so the phone gutter is the ruled 12px (was a 16px route override); the gutter guard moves to 12. - Summary and tool calls are the D215 ring at r9 (were 1px at r6); "Turn N" is the mono 11.5 label (was Bricolage 20/700); Retry and Refresh are v2p small keys. V2PageHead gains titleId for the region's aria-labelledby. - Lint: the Team and Profile head class strings move out of JSX (i18next/no-literal-string), and the invariants file gets back the two closing braces the rebase conflict resolution dropped. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MDs9DT6tbhKm6UhsmFhLk9 --- frontend/src/v2/V2App.tsx | 2 +- .../v2/__tests__/v2-layout-invariants.test.ts | 40 +++++++++++++------ frontend/src/v2/components/V2PageHead.tsx | 6 ++- .../src/v2/components/V2PersonProfile.tsx | 12 ++++-- frontend/src/v2/components/V2RunTrace.css | 36 ++++++++++------- frontend/src/v2/components/V2RunTrace.tsx | 27 +++++++++++-- frontend/src/v2/components/V2YourTeamPage.tsx | 8 +++- 7 files changed, 91 insertions(+), 40 deletions(-) diff --git a/frontend/src/v2/V2App.tsx b/frontend/src/v2/V2App.tsx index 65dfcbec96..193dc97b3d 100644 --- a/frontend/src/v2/V2App.tsx +++ b/frontend/src/v2/V2App.tsx @@ -386,7 +386,7 @@ const V2App: React.FC = () => { /> , false, false, 'v2-feature--run-trace')} + element={feature(t('runTrace.title'), , false, false, 'v2-feature--bar v2-feature--run-trace')} /> { }); }); -// d215 run-trace-after at 390: the page sits in the shared V2FeaturePage body, -// whose phone padding is 12px all round. The frame draws 16px gutters with no -// top padding ("Back to pod" is the first 44px row) and a tighter rhythm; at -// prod 486240fb the 12px body and loose rhythm pushed the foot's run-id line -// under the fixed tab bar. Measured in a browser; jsdom cannot see it. -describe('run trace keeps the frame phone gutters and clears the tab bar', () => { +// d215 run-trace-after at 390. The frame drew 16px gutters with "Back to pod" +// as the first row; at prod 486240fb a loose rhythm pushed the foot's run-id +// line under the fixed tab bar. Rulings 11-04 move it onto the shared head bar +// (back is the bar's lead key) and the 12px phone gutter of .v2-feature__scroll. +// Measured in a browser; jsdom cannot see it. +describe('run trace keeps the 12px phone gutter and clears the tab bar', () => { const rt = read('../components/V2RunTrace.css'); const v2 = readV2Css(); const app = read('../V2App.tsx'); - const phone = mediaBlockContaining(rt, '.v2-feature--run-trace > .v2-feature__body'); + const page = read('../components/V2RunTrace.tsx'); + const phone = mediaBlockContaining(rt, '.v2-feature--run-trace .v2-run-trace p'); - test('the run trace route tags its feature pane', () => { - expect(app).toContain("feature(t('runTrace.title'), , false, false, 'v2-feature--run-trace')"); + test('the run trace route tags its feature pane and draws the shared head bar', () => { + expect(app).toContain("feature(t('runTrace.title'), , false, false, 'v2-feature--bar v2-feature--run-trace')"); + expect(page).toContain('className="v2-feature__header v2-run-trace__head"'); + expect(page).toContain('className="v2p-b v2p-b-g v2p-ib v2-run-trace__back"'); }); - test('at 760 and below the body has 16px side gutters and no top padding', () => { - expect(ruleBody(rt, '.v2-feature--run-trace > .v2-feature__body', { within: phone })).toContain('padding: 0 16px 16px'); + test('at 760 and below the body has the 12px gutter: the shared scroller, no route override', () => { + expect(page).toContain('
'); + expect(ruleBody(v2, '.v2-feature__scroll', { within: mediaBlockContaining(v2, ' .v2-feature__scroll {') })).toContain('padding: 12px 12px 16px'); + expect(rt).not.toMatch(/\.v2-feature__body/); + expect(rt).not.toMatch(/padding: 0 16px/); }); test('the frame rhythm: paragraphs flush, a turn is an 8px grid', () => { @@ -5920,7 +5926,6 @@ describe('run trace keeps the frame phone gutters and clears the tab bar', () => }); test('desktop is untouched: the overrides live only in the phone block', () => { - expect(topLevelRuleBody(rt, '.v2-feature--run-trace > .v2-feature__body')).toBe(''); expect(topLevelRuleBody(rt, '.v2-feature--run-trace .v2-run-trace p')).toBe(''); }); @@ -6018,6 +6023,8 @@ describe('sign-in handoff drawn from the d215 s-handoff frames', () => { // The backend redirects to /discord/*; without this mapping the card renders outside .v2-root, unstyled. expect(read('../../utils/legacyV2Path.ts')).toContain("if (pathname.startsWith('/discord/')) return `/v2${pathname}${search}`;"); expect(read('../../index.tsx')).toContain('/^\\/discord\\/(?:callback|success|error)\\/?$/.test(bootPath)'); + }); +}); // ui-audit-1104 findings 2–8, rulings 11-04: the secondary pages take the // reference surfaces' head bar, keys, edges and labels. jsdom cannot see any of @@ -6105,6 +6112,15 @@ describe('secondary pages on the reference grammar (ui-audit-1104 findings 2–8 expect(ruleBody(read('../components/V2PersonProfile.css'), '.v2-profile .v2-profile__meta')).toContain(mono); expect(ruleBody(read('../agents/v2-agent-profile.css'), '.v2-aprofile .v2-aprofile__meta')).toContain('11.5px'); expect(ruleBody(boardCss, '.v2-feature--board .v2-board__subtitle')).toContain('500 11.5px/1 var(--v2-font-mono)'); + // Run trace: "Turn 1" was Bricolage 20/700. + expect(ruleBody(read('../components/V2RunTrace.css'), '.v2-run-trace h2')).toContain(mono); + }); + + test('finding 7: the run trace summary and tool calls are ringed r9 cards, no 1px border', () => { + const card = ruleBody(read('../components/V2RunTrace.css'), '.v2-run-trace__summary, .v2-run-trace__call'); + expect(card).toContain('border-radius: var(--v2-r-9)'); + expect(card).toContain('box-shadow: 0 0 0 1px var(--v2-border-soft)'); + expect(card).not.toMatch(/(^|[\s;{])border:/); }); }); diff --git a/frontend/src/v2/components/V2PageHead.tsx b/frontend/src/v2/components/V2PageHead.tsx index 3b33f0340b..5931489656 100644 --- a/frontend/src/v2/components/V2PageHead.tsx +++ b/frontend/src/v2/components/V2PageHead.tsx @@ -12,15 +12,17 @@ interface V2PageHeadProps { hintLabel?: string; /** Drawn before the title, inside the bar: a profile's avatar. */ lead?: React.ReactNode; + /** An id on the h1, for a region labelled by its title. */ + titleId?: string; } const V2PageHead: React.FC = ({ - title, meta, actions, hint, className = '', titleClassName = '', metaClassName = '', hintLabel = 'About this page', lead, + title, meta, actions, hint, className = '', titleClassName = '', metaClassName = '', hintLabel = 'About this page', lead, titleId, }) => (
{lead}
-

{title}

+

{title}

{hint != null && {hint}} {meta != null && {meta}}
diff --git a/frontend/src/v2/components/V2PersonProfile.tsx b/frontend/src/v2/components/V2PersonProfile.tsx index 01cb710ae6..44c7204289 100644 --- a/frontend/src/v2/components/V2PersonProfile.tsx +++ b/frontend/src/v2/components/V2PersonProfile.tsx @@ -7,11 +7,15 @@ import V2Avatar from './V2Avatar'; import V2PageHead from './V2PageHead'; import './V2PersonProfile.css'; +// The shared head bar's title and count classes, plus this page's own hook. +const HEAD_TITLE_CLASS = 'v2-feature__title v2-profile__name'; +const HEAD_META_CLASS = 'v2-profile__meta'; + /* * A person's page at /v2/profile/:id, drawn as d215 section s-profile * (docs/design/artboards/d215/index.html). Header (avatar, name, handle and - * join date, counts, Follow), then "Pods in common" and "Recent posts" as - * bordered lanes. A lane with no rows is omitted, never an empty box. + * join date, Follow) in the shared head bar, the counts as the intro line, + * then "Pods in common" and "Recent posts" as ringed lists. A lane with no rows is omitted, never an empty box. */ interface SocialProfile { @@ -185,7 +189,7 @@ const V2PersonProfile: React.FC = () => { {/* The shared page head bar (.v2-feature__header): avatar, name and handle, the act at the end. */} { )} title={name} - metaClassName="v2-profile__meta" + metaClassName={HEAD_META_CLASS} meta={( <> {profile.username ? `@${profile.username}` : null} diff --git a/frontend/src/v2/components/V2RunTrace.css b/frontend/src/v2/components/V2RunTrace.css index 9c203a797d..e8bf98f364 100644 --- a/frontend/src/v2/components/V2RunTrace.css +++ b/frontend/src/v2/components/V2RunTrace.css @@ -1,12 +1,21 @@ -.v2-run-trace { max-width: 880px; min-width: 0; color: var(--v2-text-primary); } -.v2-run-trace h1 { font-family: var(--v2-font-display); font-size: 32px; line-height: 1.1; letter-spacing: -0.03em; margin: 16px 0; overflow-wrap: anywhere; } -.v2-run-trace h2 { font-family: var(--v2-font-display); font-size: 20px; margin: 0; } -.v2-run-trace__back { color: var(--v2-text-primary); min-height: 32px; display: inline-flex; align-items: center; } +/* Run trace (/v2/pods/:podId/runs/:runId), on the reference grammar (rulings + 11-04): the shared page head bar (.v2-feature__header, the route's + v2-feature--bar) with back as its lead key, the body scrolling under it in a + 760 column (.v2-feature__scroll, 12px phone gutter), cards on the D215 ring, + turn labels in mono 11.5. */ +.v2-run-trace { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; min-width: 0; color: var(--v2-text-primary); } +.v2-run-trace .v2-run-trace__body { --v2-feature-col: 760px; } +.v2-run-trace__head .v2-run-trace__back { flex: none; color: var(--v2-text-secondary); text-decoration: none; } +/* A turn's label is Settings' section label, not a display heading. */ +.v2-run-trace h2 { margin: 0; font: 500 11.5px/16px var(--v2-font-mono); color: var(--v2-text-muted); } /* Under an agent reply: as quiet as a timestamp (d215 s-chat, trace link). */ .v2-run-trace-link a { color: var(--v2-text-secondary); font-size: 12.5px; text-decoration: none; min-height: 32px; display: inline-flex; align-items: center; } .v2-run-trace-link a:hover { color: var(--v2-text-primary); text-decoration: underline; } -.v2-run-trace__summary, .v2-run-trace__call { display: grid; gap: 8px; border: 1px solid var(--v2-border); border-radius: var(--v2-radius-lg); padding: 16px; margin: 12px 0; min-width: 0; } -.v2-run-trace__meta, .v2-run-trace code, .v2-run-trace dd { font-family: var(--v2-font-mono); font-size: 12px; } +/* A card is the D215 ring and soft shadow at r9, not a 1px border at r6. */ +.v2-run-trace__summary, .v2-run-trace__call { display: grid; gap: 8px; border-radius: var(--v2-r-9); background: var(--v2-surface); box-shadow: 0 0 0 1px var(--v2-border-soft), 0 1px 2px rgba(16, 24, 40, 0.04); padding: 16px; margin: 12px 0; min-width: 0; } +.v2-run-trace__summary { margin-top: 0; } +.v2-run-trace__meta { font: 500 11.5px/16px var(--v2-font-mono); color: var(--v2-text-muted); } +.v2-run-trace code, .v2-run-trace dd { font-family: var(--v2-font-mono); font-size: 12px; } .v2-run-trace code, .v2-run-trace dd, .v2-run-trace__call strong { overflow-wrap: anywhere; } .v2-run-trace__turns { list-style: none; margin: 24px 0; padding: 0; } .v2-run-trace__foot { display: grid; gap: 4px; border-top: 1px solid var(--v2-border-soft); padding-top: 12px; margin-top: 24px; color: var(--v2-text-muted); font-size: 12px; line-height: 16px; } @@ -15,22 +24,19 @@ .v2-run-trace__turns > li { border-top: 1px solid var(--v2-border-soft); padding-top: 16px; margin-top: 16px; } .v2-run-trace dl { display: grid; grid-template-columns: 140px minmax(0, 1fr); gap: 8px; margin: 8px 0; } .v2-run-trace dd { margin: 0; } -.v2-run-trace button { min-height: 32px; padding: 0 12px; border: 1px solid var(--v2-border); border-radius: var(--v2-radius-sm); background: var(--v2-surface); color: var(--v2-text-primary); cursor: pointer; } -.v2-run-trace button:focus-visible, .v2-run-trace a:focus-visible, .v2-run-trace-link a:focus-visible { outline: 2px solid var(--v2-accent); outline-offset: 2px; } +.v2-run-trace__body a:focus-visible, .v2-run-trace-link a:focus-visible { outline: 2px solid var(--v2-accent); outline-offset: 2px; } .v2-run-trace-link { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } @media (max-width: 760px) { - .v2-run-trace__back, .v2-run-trace-link a, .v2-run-trace button { min-height: 44px; } + .v2-run-trace-link a { min-height: 44px; } .v2-run-trace dl { grid-template-columns: minmax(0, 1fr); gap: 4px; } .v2-run-trace dd { margin-bottom: 8px; } .v2-run-trace-link { display: grid; gap: 0; } } -/* d215 run-trace-after 390 (.mbody2--trace): 16px side gutters and no top - padding, so "Back to pod" is the first 44px row; replaces the shared 12px - feature body for this route only. The frame's tighter rhythm (paragraphs - flush, a turn's parts 8px apart) keeps the foot clear of the tab bar, - whose 58px .v2-feature already reserves. Desktop is unchanged. */ +/* d215 run-trace-after 390: a tighter rhythm (paragraphs flush, a turn's parts + 8px apart) keeps the foot clear of the tab bar, whose 58px .v2-feature + already reserves. The gutter is the shared 12px of .v2-feature__scroll + (rulings 11-04); the back row moved into the head bar. Desktop is unchanged. */ @media (max-width: 760px) { - .v2-feature--run-trace > .v2-feature__body { padding: 0 16px 16px; } .v2-feature--run-trace .v2-run-trace p { margin: 0; } .v2-feature--run-trace .v2-run-trace__turns > li { display: grid; gap: 8px; } } diff --git a/frontend/src/v2/components/V2RunTrace.tsx b/frontend/src/v2/components/V2RunTrace.tsx index 1a9bf58327..17b57c4a76 100644 --- a/frontend/src/v2/components/V2RunTrace.tsx +++ b/frontend/src/v2/components/V2RunTrace.tsx @@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next'; import { Link, useParams } from 'react-router-dom'; import { useV2Api } from '../hooks/useV2Api'; import { agentKeyFor } from '../utils/agentKey'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import V2PageHead from './V2PageHead'; import './V2RunTrace.css'; export interface RunTrace { @@ -73,10 +75,26 @@ const V2RunTrace: React.FC = () => { : trace?.omittedTurns ? 'limitedTurns' : trace?.omittedToolCalls ? 'limitedCalls' : 'limitedContent'; return
- {t('runTrace.back')} -

{t('runTrace.title')}

+ {/* The shared page head bar, back as its lead key (as the board and memory draw it). */} + +
; }; export default V2RunTrace; diff --git a/frontend/src/v2/components/V2YourTeamPage.tsx b/frontend/src/v2/components/V2YourTeamPage.tsx index c974c42533..6396522071 100644 --- a/frontend/src/v2/components/V2YourTeamPage.tsx +++ b/frontend/src/v2/components/V2YourTeamPage.tsx @@ -9,6 +9,10 @@ import { AgentConnection, connectionLabel } from '../utils/agentConnection'; import V2Fold from './V2Fold'; import V2PageHead from './V2PageHead'; +// The shared head bar's title and count classes, plus this page's own hook. +const HEAD_TITLE_CLASS = 'v2-feature__title v2-team__title'; +const HEAD_META_CLASS = 'v2-team__meta'; + /** * Your Team — direction C, built to `YourTeam.dc.html` (canvas: Workspace in C) * under the Signal identity (`docs/design/signal-identity.md`). @@ -506,9 +510,9 @@ const V2YourTeamPage: React.FC = () => { {/* The shared page head bar (.v2-feature__header), as Settings and Connectors draw it. */} Date: Wed, 4 Nov 2026 00:46:01 -0800 Subject: [PATCH 4/9] fix(v2): the agent profile bar keeps its name left-aligned on a phone and drops the Edit badge The shared phone rule centres a head's identity, which in this bar is a column, so the name and meta sat centred at 390. In the shell bar the 18px "Edit" badge covered the 34px (24 on a phone) face; the avatar stays the edit control, named by its aria-label and title. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MDs9DT6tbhKm6UhsmFhLk9 --- frontend/src/v2/agents/v2-agent-profile.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/v2/agents/v2-agent-profile.css b/frontend/src/v2/agents/v2-agent-profile.css index 00721c63a1..2b59f37318 100644 --- a/frontend/src/v2/agents/v2-agent-profile.css +++ b/frontend/src/v2/agents/v2-agent-profile.css @@ -135,14 +135,17 @@ .v2-feature--aprofile .v2-aprofile { flex: 1 1 auto; } .v2-feature--aprofile .v2-aprofile__main { max-width: none; flex: 1 1 auto; min-height: 0; gap: 0; } .v2-feature--aprofile .v2-aprofile__hero { min-height: 76px; align-items: center; gap: 12px; } -.v2-feature--aprofile .v2-aprofile__hero-text { gap: 5px; } +.v2-feature--aprofile .v2-aprofile__hero-text { gap: 5px; align-items: flex-start; } +/* The 18px "Edit" badge covered the bar's 34px face; the avatar stays the edit + control, named by its aria-label and title. */ +.v2-feature--aprofile .v2-aprofile__avatar-hint { display: none; } .v2-feature--aprofile .v2-aprofile__body { --v2-feature-col: 760px; display: flex; flex-direction: column; gap: 16px; } .v2-aprofile--public .v2-aprofile__body { display: contents; } .v2-aprofile .v2-aprofile__intro { margin: 0 4px; font-size: 13.5px; } @media (max-width: 760px) { .v2-feature--aprofile .v2-aprofile__hero { min-height: 52px; gap: 0 10px; } .v2-feature--aprofile .v2-aprofile__hero .v2-avatar { width: 24px; height: 24px; } - .v2-feature--aprofile .v2-aprofile__hero-text { gap: 3px; padding: 8px 0; } + .v2-feature--aprofile .v2-aprofile__hero-text { gap: 3px; padding: 8px 0; align-items: flex-start; } .v2-feature--aprofile .v2-aprofile__body { gap: 10px; } } From c95ecc10b6ff7559f23c0a287e17bba6b63e2e88 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 4 Nov 2026 01:11:07 -0800 Subject: [PATCH 5/9] fix(v2): chat's D223 keys stay 32x32 drawn on a phone, out of the primitives' 44px rule The 44px phone rule on .v2p-b/.v2p-ib reached the pod head (back, inspector toggle) and the composer (+ and Send), which draw at 32 and take touches through a 44px ::after. Back and the toggle came out 32x44 and the head grew to 64; Send was a 32x44 oblong. The chat sheets now restore 32x32 at <=760, so the head, composer and hit areas match main again. A guard holds every phone icon key square: 44x44 from the primitives, or 32x32 on the D223 keys. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MDs9DT6tbhKm6UhsmFhLk9 --- .../v2/__tests__/v2-layout-invariants.test.ts | 38 +++++++++++++++++++ frontend/src/v2/chat/chat-composer.css | 3 ++ frontend/src/v2/chat/chat-pane.css | 4 ++ 3 files changed, 45 insertions(+) diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 9bae1355e3..89b4095435 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -6077,6 +6077,44 @@ describe('secondary pages on the reference grammar (ui-audit-1104 findings 2–8 } }); + test('finding 5: a phone icon key is square, 44x44 from the primitives or the D223 32x32 in chat, never 32x44', () => { + // The primitives' phone icon key grows on both axes together. + const phone = mediaBlockContaining(primitives, '.v2p-b, .v2p-b-sm {'); + const icon = ruleBody(primitives, '.v2p-ib, .v2p-ib.v2p-b-sm', { within: phone }); + expect(icon.match(/width: (\d+)px/)?.[1]).toBe(icon.match(/height: (\d+)px/)?.[1]); + // The chat keys that carry the D223 44px ::after hit area draw at 32x32 on a + // phone. The primitives' 44px height alone would leave them 32 wide and 44 tall. + const sizesOn = (css: string, key: string) => { + const sizes: { width?: string; height?: string } = {}; + const flat = css.replace(/\/\*[\s\S]*?\*\//g, ''); + for (let at = flat.indexOf('@media (max-width: 760px)'); at !== -1; at = flat.indexOf('@media (max-width: 760px)', at + 1)) { + const block = blockAt(flat, at); + const inner = block.slice(block.indexOf('{') + 1, -1); + for (const [, selectors, body] of inner.matchAll(/([^{}]+)\{([^{}]*)\}/g)) { + if (!selectors.split(',').map((one) => one.trim()).includes(`.v2-root button.${key}`)) continue; + const width = /(?:^|[;\s])width:\s*(\d+px)/.exec(body)?.[1]; + const height = /(?:^|[;\s])height:\s*(\d+px)/.exec(body)?.[1]; + if (width) sizes.width = width; + if (height) sizes.height = height; + } + } + return sizes; + }; + const pane = read('../chat/chat-pane.css'); + const composer = read('../chat/chat-composer.css'); + const d223 = [ + [pane, 'v2-thread__back'], + [pane, 'v2-thread__inspector-toggle'], + [pane, 'v2-pod-header__invite'], + [composer, 'v2-composer__plus'], + [composer, 'v2-composer__send'], + ] as const; + for (const [css, key] of d223) { + expect(css).toContain(`.v2-root button.${key}::after`); + expect({ key, ...sizesOn(css, key) }).toEqual({ key, width: '32px', height: '32px' }); + } + }); + test('finding 6: the board header icon keys never shrink on a phone', () => { const phone = mediaBlockContaining(boardCss, '.v2-feature--board .v2-board__key--icon { flex: none; }'); expect(ruleBody(boardCss, '.v2-feature--board .v2-board__key--icon', { within: phone })).toContain('flex: none'); diff --git a/frontend/src/v2/chat/chat-composer.css b/frontend/src/v2/chat/chat-composer.css index b16f020a11..decdd4c15d 100644 --- a/frontend/src/v2/chat/chat-composer.css +++ b/frontend/src/v2/chat/chat-composer.css @@ -110,6 +110,9 @@ z-index keeps it above later positioned siblings such as the transcript. */ .v2-root button.v2-composer__plus, .v2-root button.v2-composer__send { position: relative; z-index: 1; } + /* The drawn 32x32 stays (D223); the primitives' 44px phone size does not apply. */ + .v2-root button.v2-composer__plus, + .v2-root button.v2-composer__send { width: 32px; height: 32px; } .v2-root button.v2-composer__plus::after, .v2-root button.v2-composer__send::after { content: ''; diff --git a/frontend/src/v2/chat/chat-pane.css b/frontend/src/v2/chat/chat-pane.css index 08f68a92c2..ee97709f6a 100644 --- a/frontend/src/v2/chat/chat-pane.css +++ b/frontend/src/v2/chat/chat-pane.css @@ -553,6 +553,10 @@ .v2-root button.v2-thread__back, .v2-root button.v2-pod-header__invite, .v2-root button.v2-thread__inspector-toggle { position: relative; z-index: 1; } + /* D223 standing rule: these keys draw at 32x32 and take touches at 44 through + the ::after below, so they opt out of the primitives' 44px phone size. */ + .v2-root button.v2-thread__back, + .v2-root button.v2-thread__inspector-toggle { width: 32px; height: 32px; } .v2-root button.v2-thread__back::after, .v2-root button.v2-pod-header__invite::after, .v2-root button.v2-thread__inspector-toggle::after { From 6cb16cd88762cb4b5ab002fa6b46c2f16553eac8 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 4 Nov 2026 01:11:07 -0800 Subject: [PATCH 6/9] fix(v2): the card edge is one token, --v2-ring-card, not a copy per page Your team, the run trace and BYO's add-computer card each carried the D215 ring literal. It now lives once in v2.css (and as --c-ring-card in the design system tokens), and the guards read the token. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MDs9DT6tbhKm6UhsmFhLk9 --- frontend/design-system/tokens.css | 1 + .../src/v2/__tests__/v2-layout-invariants.test.ts | 13 ++++++++++--- frontend/src/v2/components/V2AgentBYO.css | 2 +- frontend/src/v2/components/V2RunTrace.css | 2 +- frontend/src/v2/components/V2YourTeamPage.css | 2 +- frontend/src/v2/v2.css | 3 +++ 6 files changed, 17 insertions(+), 6 deletions(-) diff --git a/frontend/design-system/tokens.css b/frontend/design-system/tokens.css index eaf291791b..72d2abc411 100644 --- a/frontend/design-system/tokens.css +++ b/frontend/design-system/tokens.css @@ -103,6 +103,7 @@ --c-key-edge: rgba(15, 23, 42, 0.14); --c-key-inset: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 1px 2px rgba(16, 24, 40, 0.06); --c-shadow-pane: 0 0 0 1px rgba(16, 24, 40, 0.07), 0 1px 2px rgba(16, 24, 40, 0.04), 0 12px 28px -20px rgba(16, 24, 40, 0.18); + --c-ring-card: 0 0 0 1px var(--c-border-soft), 0 1px 2px rgba(16, 24, 40, 0.04); /* card edge: ring + 1px lift */ --c-shadow-pop: 0 0 0 1px rgba(16, 24, 40, 0.08), 0 2px 4px rgba(16, 24, 40, 0.04), 0 18px 36px -14px rgba(16, 24, 40, 0.28); --c-scrim: rgba(16, 24, 40, 0.18); --c-av-ring: rgba(16, 24, 40, 0.07); diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 89b4095435..a817b82f23 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -3670,7 +3670,7 @@ describe('the BYO after-submit surfaces onto Signal (TASK-169)', () => { // Rulings 11-04: a card is the D215 ring and soft shadow at r9, no 1px border. expect(addComputer['border-radius']).toBe('var(--v2-r-9)'); expect(addComputer.border).toBeUndefined(); - expect(addComputer['box-shadow']).toContain('0 0 0 1px var(--v2-border-soft)'); + expect(addComputer['box-shadow']).toBe('var(--v2-ring-card)'); expect(addComputer.background).toBe('var(--v2-bg-subtle)'); // Each snippet is a flat section, not a card. const snippet = decls(selectorRuleBody(byoCss, '.v2-feature--byo .v2-byo__form, .v2-feature--byo .v2-byo__result, .v2-feature--byo .v2-byo__snippet')); @@ -6135,9 +6135,16 @@ describe('secondary pages on the reference grammar (ui-audit-1104 findings 2–8 test('finding 7: cards are the D215 ring at r9 and lanes are r12, never a 1px border', () => { const card = ruleBody(read('../components/V2YourTeamPage.css'), '.v2-team .v2-team-card'); expect(card).toContain('border-radius: var(--v2-r-9)'); - expect(card).toContain('box-shadow: 0 0 0 1px var(--v2-border-soft)'); + expect(card).toContain('box-shadow: var(--v2-ring-card)'); expect(card).not.toMatch(/(^|[\s;{])border:/); expect(ruleBody(read('../components/V2ConnectPage.css'), '.v2-connect .v2-connect__lane')).toContain('border-radius: var(--v2-r-12)'); + // One card edge, named once in the token sheets, not copied per page. + const ring = '0 0 0 1px var(--v2-border-soft), 0 1px 2px rgba(16, 24, 40, 0.04)'; + expect(v2).toContain(`--v2-ring-card: ${ring};`); + expect(read('../../../design-system/tokens.css')).toMatch(/--c-ring-card:\s+0 0 0 1px var\(--c-border-soft\), 0 1px 2px rgba\(16, 24, 40, 0\.04\);/); + for (const sheet of ['../components/V2YourTeamPage.css', '../components/V2RunTrace.css', '../components/V2AgentBYO.css']) { + expect(read(sheet)).not.toContain(ring); + } expect(ruleBody(read('../components/V2PersonProfile.css'), '.v2-profile .v2-profile__list')).toContain('border-radius: var(--v2-r-12)'); expect(ruleBody(read('../agents/v2-agent-profile.css'), '.v2-aprofile .v2-aprofile__card')).toContain('border-radius: var(--v2-r-12)'); }); @@ -6157,7 +6164,7 @@ describe('secondary pages on the reference grammar (ui-audit-1104 findings 2–8 test('finding 7: the run trace summary and tool calls are ringed r9 cards, no 1px border', () => { const card = ruleBody(read('../components/V2RunTrace.css'), '.v2-run-trace__summary, .v2-run-trace__call'); expect(card).toContain('border-radius: var(--v2-r-9)'); - expect(card).toContain('box-shadow: 0 0 0 1px var(--v2-border-soft)'); + expect(card).toContain('box-shadow: var(--v2-ring-card)'); expect(card).not.toMatch(/(^|[\s;{])border:/); }); }); diff --git a/frontend/src/v2/components/V2AgentBYO.css b/frontend/src/v2/components/V2AgentBYO.css index 08aaa8ff1c..53e13e5ec8 100644 --- a/frontend/src/v2/components/V2AgentBYO.css +++ b/frontend/src/v2/components/V2AgentBYO.css @@ -347,7 +347,7 @@ padding: 18px; border-radius: var(--v2-r-9); background: var(--v2-bg-subtle); - box-shadow: 0 0 0 1px var(--v2-border-soft), 0 1px 2px rgba(16, 24, 40, 0.04); + box-shadow: var(--v2-ring-card); } .v2-byo__add-computer > p { margin: 0; } .v2-byo__add-computer-row { diff --git a/frontend/src/v2/components/V2RunTrace.css b/frontend/src/v2/components/V2RunTrace.css index e8bf98f364..4d375ef174 100644 --- a/frontend/src/v2/components/V2RunTrace.css +++ b/frontend/src/v2/components/V2RunTrace.css @@ -12,7 +12,7 @@ .v2-run-trace-link a { color: var(--v2-text-secondary); font-size: 12.5px; text-decoration: none; min-height: 32px; display: inline-flex; align-items: center; } .v2-run-trace-link a:hover { color: var(--v2-text-primary); text-decoration: underline; } /* A card is the D215 ring and soft shadow at r9, not a 1px border at r6. */ -.v2-run-trace__summary, .v2-run-trace__call { display: grid; gap: 8px; border-radius: var(--v2-r-9); background: var(--v2-surface); box-shadow: 0 0 0 1px var(--v2-border-soft), 0 1px 2px rgba(16, 24, 40, 0.04); padding: 16px; margin: 12px 0; min-width: 0; } +.v2-run-trace__summary, .v2-run-trace__call { display: grid; gap: 8px; border-radius: var(--v2-r-9); background: var(--v2-surface); box-shadow: var(--v2-ring-card); padding: 16px; margin: 12px 0; min-width: 0; } .v2-run-trace__summary { margin-top: 0; } .v2-run-trace__meta { font: 500 11.5px/16px var(--v2-font-mono); color: var(--v2-text-muted); } .v2-run-trace code, .v2-run-trace dd { font-family: var(--v2-font-mono); font-size: 12px; } diff --git a/frontend/src/v2/components/V2YourTeamPage.css b/frontend/src/v2/components/V2YourTeamPage.css index ac3788967d..6c79b51061 100644 --- a/frontend/src/v2/components/V2YourTeamPage.css +++ b/frontend/src/v2/components/V2YourTeamPage.css @@ -12,7 +12,7 @@ .v2-team .v2-team__body { display: flex; flex-direction: column; gap: 20px; } .v2-team .v2-team__grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; } /* A card is the D215 ring and soft shadow at r9 (the primitives' .v2p-tc), not a 1px border. */ -.v2-team .v2-team-card { display: flex; flex-direction: column; gap: 10px; min-width: 0; padding: 16px; border-radius: var(--v2-r-9); background: var(--v2-surface); box-shadow: 0 0 0 1px var(--v2-border-soft), 0 1px 2px rgba(16, 24, 40, 0.04); } +.v2-team .v2-team-card { display: flex; flex-direction: column; gap: 10px; min-width: 0; padding: 16px; border-radius: var(--v2-r-9); background: var(--v2-surface); box-shadow: var(--v2-ring-card); } /* The one that needs you: a ring, never a tinted background (state grammar §3). */ .v2-team .v2-team-card--needsYou { box-shadow: 0 0 0 2px var(--v2-accent); } /* The set-up-your-own card is an add slot, dashed like .v2p-tile.add, so it keeps a border. */ diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index 2cae11db4b..27866a5edb 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -212,6 +212,9 @@ body.modern-ui.v2-canvas { --v2-key-inset: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 1px 2px rgba(16, 24, 40, 0.06); --v2-shadow-pane: 0 0 0 1px rgba(16, 24, 40, 0.07), 0 1px 2px rgba(16, 24, 40, 0.04), 0 12px 28px -20px rgba(16, 24, 40, 0.18); --v2-shadow-pop: 0 0 0 1px rgba(16, 24, 40, 0.08), 0 2px 4px rgba(16, 24, 40, 0.04), 0 18px 36px -14px rgba(16, 24, 40, 0.28); + /* The card edge (D215): a 1px ring plus a 1px lift, no border. Cards that + are not a .v2p-tc take it from here rather than copying the pair. */ + --v2-ring-card: 0 0 0 1px var(--v2-border-soft), 0 1px 2px rgba(16, 24, 40, 0.04); --v2-av-ring: rgba(16, 24, 40, 0.07); --v2-scrim: rgba(16, 24, 40, 0.18); --v2-r-6: 6px; From 1ecf6daa5efab7f1b4e28b8b1988dc11028f20cd Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 4 Nov 2026 01:11:07 -0800 Subject: [PATCH 7/9] =?UTF-8?q?fix(i18n):=20Connect's=20zh=20intro=20reads?= =?UTF-8?q?=20'MCP=20=E7=94=A8=E7=9A=84=E6=98=AF=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E6=97=B6=E5=88=9B=E5=BB=BA=E7=9A=84=E6=99=BA=E8=83=BD=E4=BD=93?= =?UTF-8?q?=E5=AF=86=E9=92=A5=E3=80=82'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MDs9DT6tbhKm6UhsmFhLk9 --- frontend/src/i18n/locales/zh-CN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index c9c3121f99..c258761c03 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -2428,7 +2428,7 @@ } }, "connect": { - "intro": "MCP 使用接入时创建的智能体密钥。", + "intro": "MCP 用的是接入时创建的智能体密钥。", "cliHeading": "连接 CLI 或 MCP", "copy": "复制", "copied": "已复制", From 419fdb77ba566935b2459787e9dbcf69157c85ce Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 4 Nov 2026 01:11:07 -0800 Subject: [PATCH 8/9] fix(v2): the agent profile bar's owner avatar keeps a 44px touch area, and two guards tighten On a phone the bar draws the owner's avatar control at 24px (main drew it at 56); a ::after brings the touch area back to 44. The bar layout's flex chain (body, legacy wrapper, scroller with min-height 0) gets a presence guard, and the ink-key guard now rejects any literal fill or text colour, not only hex. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MDs9DT6tbhKm6UhsmFhLk9 --- frontend/src/v2/__tests__/signalTokens.test.ts | 8 ++++++++ .../v2/__tests__/v2-layout-invariants.test.ts | 18 ++++++++++++++++++ frontend/src/v2/agents/v2-agent-profile.css | 2 ++ 3 files changed, 28 insertions(+) diff --git a/frontend/src/v2/__tests__/signalTokens.test.ts b/frontend/src/v2/__tests__/signalTokens.test.ts index 39f52beac4..70d20cad15 100644 --- a/frontend/src/v2/__tests__/signalTokens.test.ts +++ b/frontend/src/v2/__tests__/signalTokens.test.ts @@ -149,6 +149,14 @@ describe('dark-safe surfaces (TASK-681)', () => { expect(rule?.[0]).toContain('background: linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b))'); expect(rule?.[0]).toContain('color: var(--v2-on-fill)'); expect(rule?.[0]).not.toMatch(/#[0-9a-f]{3,8}\b/i); + // Every fill and text declaration resolves through a token: no hex, rgb(), hsl() or named colour. + const paint = [...(rule?.[0] ?? '').matchAll(/(?:^|[;{\s])(background(?:-color|-image)?|color):\s*([^;}]+)/g)]; + expect(paint.map(([, prop]) => prop)).toEqual(expect.arrayContaining(['background', 'color'])); + for (const [, prop, value] of paint) { + expect({ prop, literal: value.replace(/var\([^)]*\)/g, '').match(/#[0-9a-f]{3,8}\b|\b(?:rgba?|hsla?)\(|\b(?:white|black|red|blue|green|gray|grey)\b/i)?.[0] ?? null }).toEqual({ prop, literal: null }); + } + // Your team's own sheet adds no colour to the ink keys. + expect(rd('../components/V2YourTeamPage.css')).not.toMatch(/v2-team__hire[^{]*\{[^}]*(?:background|color):/); }); test('the board drawer scrim is --v2-scrim', () => { // S4: the board's rules live in its own sheet; the scrim backs the phone sheet. diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index a817b82f23..979ba256e6 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -6058,6 +6058,13 @@ describe('secondary pages on the reference grammar (ui-audit-1104 findings 2–8 expect(app).toContain("'v2-feature--bar v2-feature--aprofile'"); expect(ruleBody(v2, '.v2-feature--bar > .v2-feature__body')).toContain('padding: 0'); expect(ruleBody(v2, '.v2-feature__scroll')).toContain('overflow-y: auto'); + // The flex chain that lets the scroller scroll: each link fills and may shrink below its content. + const barBody = ruleBody(v2, '.v2-feature--bar > .v2-feature__body'); + for (const decl of ['display: flex', 'flex-direction: column', 'overflow: hidden']) expect(barBody).toContain(decl); + const legacy = ruleBody(v2, '.v2-feature--bar .v2-feature__legacy'); + for (const decl of ['flex: 1 1 auto', 'min-height: 0', 'display: flex', 'flex-direction: column']) expect(legacy).toContain(decl); + const scroll = ruleBody(v2, '.v2-feature__scroll'); + for (const decl of ['flex: 1 1 auto', 'min-height: 0']) expect(scroll).toContain(decl); // BYO and Connect moved the hint to an intro line. expect(app).not.toMatch(/ { + const css = read('../agents/v2-agent-profile.css'); + const phone = mediaBlockContaining(css, '.v2-feature--aprofile .v2-aprofile__hero .v2-avatar { width: 24px; height: 24px; }'); + expect(phone).not.toBe(''); + const hit = selectorRuleBody(css, '.v2-feature--aprofile .v2-aprofile__avatar-button::after {'); + // 24 + 2 x 10 = 44, from a positioned button. + expect(hit).toContain('inset: -10px'); + expect(ruleBody(css, '.v2-aprofile .v2-aprofile__avatar-button')).toContain('position: relative'); + expect(css.lastIndexOf('@media (max-width: 760px)')).toBeLessThan(css.indexOf('.v2-feature--aprofile .v2-aprofile__avatar-button::after')); + }); + test('finding 6: the board header icon keys never shrink on a phone', () => { const phone = mediaBlockContaining(boardCss, '.v2-feature--board .v2-board__key--icon { flex: none; }'); expect(ruleBody(boardCss, '.v2-feature--board .v2-board__key--icon', { within: phone })).toContain('flex: none'); diff --git a/frontend/src/v2/agents/v2-agent-profile.css b/frontend/src/v2/agents/v2-agent-profile.css index 2b59f37318..4b78418d93 100644 --- a/frontend/src/v2/agents/v2-agent-profile.css +++ b/frontend/src/v2/agents/v2-agent-profile.css @@ -162,4 +162,6 @@ .v2-aprofile button.v2-aprofile__more { display: inline-flex; align-items: center; min-height: 44px; } .v2-aprofile .v2-aprofile__avatar-cancel { min-height: 44px; } .v2-aprofile .v2-aprofile__cta-row .v2-aprofile__btn { height: 44px; font-size: 14px; } + /* The bar draws the owner's avatar control at 24px; the touch area stays 44. */ + .v2-feature--aprofile .v2-aprofile__avatar-button::after { content: ''; position: absolute; inset: -10px; } } From cb6f3f879ad2c04a6e79d02549bb528ed20ff4fb Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 4 Nov 2026 01:26:37 -0800 Subject: [PATCH 9/9] test(v2): the ink-key guard asserts the exact tokenised paint, not a colour list From CodeRabbit on #3373: `background: orange` and `color: orange` passed, because the named-colour list was incomplete. The guard now requires the rule's background and colour declarations to be exactly the two tokenised ones. A `color: orange` override fails it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MDs9DT6tbhKm6UhsmFhLk9 --- frontend/src/v2/__tests__/signalTokens.test.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/frontend/src/v2/__tests__/signalTokens.test.ts b/frontend/src/v2/__tests__/signalTokens.test.ts index 70d20cad15..1465944b63 100644 --- a/frontend/src/v2/__tests__/signalTokens.test.ts +++ b/frontend/src/v2/__tests__/signalTokens.test.ts @@ -149,12 +149,14 @@ describe('dark-safe surfaces (TASK-681)', () => { expect(rule?.[0]).toContain('background: linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b))'); expect(rule?.[0]).toContain('color: var(--v2-on-fill)'); expect(rule?.[0]).not.toMatch(/#[0-9a-f]{3,8}\b/i); - // Every fill and text declaration resolves through a token: no hex, rgb(), hsl() or named colour. - const paint = [...(rule?.[0] ?? '').matchAll(/(?:^|[;{\s])(background(?:-color|-image)?|color):\s*([^;}]+)/g)]; - expect(paint.map(([, prop]) => prop)).toEqual(expect.arrayContaining(['background', 'color'])); - for (const [, prop, value] of paint) { - expect({ prop, literal: value.replace(/var\([^)]*\)/g, '').match(/#[0-9a-f]{3,8}\b|\b(?:rgba?|hsla?)\(|\b(?:white|black|red|blue|green|gray|grey)\b/i)?.[0] ?? null }).toEqual({ prop, literal: null }); - } + // The fill and text declarations are exactly the two tokenised ones: any added or changed + // background or colour (a literal, a named colour, an override) fails, with no colour list to keep. + const paint = [...(rule?.[0] ?? '').matchAll(/(?:^|[;{\s])(background(?:-color|-image)?|color):\s*([^;}]+)/g)] + .map(([, prop, value]) => [prop, value.trim()]).sort(); + expect(paint).toEqual([ + ['background', 'linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b))'], + ['color', 'var(--v2-on-fill)'], + ]); // Your team's own sheet adds no colour to the ink keys. expect(rd('../components/V2YourTeamPage.css')).not.toMatch(/v2-team__hire[^{]*\{[^}]*(?:background|color):/); });