From 07cd0827b5e11058d23d7f6cfba7c0ec21925b33 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 02:18:55 -0800 Subject: [PATCH 1/3] docs(design): draw Your team into d215 (six agents, 1440 and 390) Frames use the page's own v2-team__* and v2-team-card__* class names, scoped to .v2-team, so the lift is a copy; the connectors section is the visual reference. Each agent is a 14px bordered card with a 40px avatar mark (44px at 390), the display-18 name, a mono status line, the sentence, mono pod chips and the acts: Hire and Answer are the ink key, Bring your own and Talk the plain key. The needs-you card keeps its 2px cobalt ring. Drawn: one needs-you, two working (one hosted with its meter), three idle, and the own-agent card. Not drawn: the empty team, loading, the error line, the invite gate and redeem form, connected agents, and the never-connected fix command. measure_after now knows team (/v2/agents); targets.json gains team-1440 (56 helper words, scroll 1) and team-390 (34, 1.93), measured by running measure_after.js. The run also rewrote byo-handoff-390's scroll (1.52 to 1.31); that row was reverted, the other 18 are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3 --- .../artboards/d215/cap/measure_after.js | 3 +- .../artboards/d215/cap/measure_after.test.js | 4 +- docs/design/artboards/d215/index.html | 62 ++++++++++++++++++- docs/design/artboards/d215/targets.json | 8 +++ 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/docs/design/artboards/d215/cap/measure_after.js b/docs/design/artboards/d215/cap/measure_after.js index 44865ae3b..2e8e97906 100644 --- a/docs/design/artboards/d215/cap/measure_after.js +++ b/docs/design/artboards/d215/cap/measure_after.js @@ -18,7 +18,7 @@ const fs = require('fs'); const HERE = __dirname; const TARGETS = path.join(HERE, '..', 'targets.json'); -const SURFACES = ['settings', 'agent-settings', 'agent-memory', 'board', 'connectors', 'chat', 'sidebar', 'byo', 'byo-handoff']; +const SURFACES = ['settings', 'agent-settings', 'agent-memory', 'board', 'connectors', 'chat', 'sidebar', 'byo', 'byo-handoff', 'team']; // byo-handoff is drawn but can't be captured on prod: reaching it issues a real key. const PROD_SURFACES = SURFACES.filter(s => s !== 'byo-handoff'); const VIEWPORTS = ['1440', '390']; @@ -164,6 +164,7 @@ async function prod(only) { ['board', () => `/v2/pods/${pod}/board`], ['connectors', () => '/v2/connectors'], ['byo', () => '/v2/agents/byo'], + ['team', () => '/v2/agents'], ['chat', () => `/v2/pods/${pod}`], ['sidebar', vp => (vp === '1440' ? `/v2/pods/${side}` : '/v2'), { list390: SIDEBAR_390 }], ]; diff --git a/docs/design/artboards/d215/cap/measure_after.test.js b/docs/design/artboards/d215/cap/measure_after.test.js index 4601b2d6f..b2d4693f5 100644 --- a/docs/design/artboards/d215/cap/measure_after.test.js +++ b/docs/design/artboards/d215/cap/measure_after.test.js @@ -29,7 +29,7 @@ test('--only with an unknown surface throws', () => { test('the committed targets.json passes its own validation', () => { const t = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'targets.json'), 'utf8')); - assert.strictEqual(Object.keys(t).length, 18); + assert.strictEqual(Object.keys(t).length, 20); validateTargets(t); }); @@ -39,7 +39,7 @@ test('an empty or partial measurement is refused', () => { const partial = { ...t }; delete partial['board-390']; assert.throws(() => validateTargets(partial), /missing board-390/); assert.throws(() => validateTargets({ ...t, 'chat-1440': { helperWords: NaN, innerRatio: 2 } }), /invalid chat-1440/); - assert.strictEqual(EXPECTED.length, 18); + assert.strictEqual(EXPECTED.length, 20); }); // A page whose SPA shell answers 200 and then redirects to `landsOn`. Its metrics would score diff --git a/docs/design/artboards/d215/index.html b/docs/design/artboards/d215/index.html index 2ca2c279d..c88475336 100644 --- a/docs/design/artboards/d215/index.html +++ b/docs/design/artboards/d215/index.html @@ -489,6 +489,55 @@ .app.m .v2-feature--byo .v2-byo__copy { position: static; width: 100%; height: 44px; margin-top: 10px; font-size: 14px; } .app.m .v2-feature--byo .v2-byo__more > summary { min-height: 48px; padding: 8px 16px 8px 12px; } +/* your team: the page's own v2-team__* / v2-team-card__* names, in the connectors family (14px bordered + cards, the ink key and the plain key, mono status and pod chips). Scoped to .v2-team, the page's root. */ +.v2-team { display: flex; flex-direction: column; gap: 20px; max-width: 1100px; margin: 0 auto; } +.v2-team .v2-team__head { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 44px; } +.v2-team .v2-team__heading { display: flex; align-items: baseline; gap: 12px; min-width: 0; } +.v2-team .v2-team__title { margin: 0; font: 800 24px/1 var(--f-display); letter-spacing: -0.03em; color: var(--ink); } +.v2-team .v2-team__meta { font: 500 12px/16px var(--f-mono); color: var(--mu); white-space: nowrap; } +.v2-team .v2-team__actions { display: flex; gap: 8px; flex-shrink: 0; } +.v2-team .v2-team__byo, .v2-team .v2-team__hire, .v2-team .v2-team-card__answer, .v2-team .v2-team-card__talk { 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(--f-body); white-space: nowrap; } +.v2-team .v2-team__hire, .v2-team .v2-team-card__answer { color: var(--on-fill); background: linear-gradient(180deg, var(--fill-a), var(--fill-b)); border: 1px solid var(--fill-edge); } +.v2-team .v2-team__byo, .v2-team .v2-team-card__talk { color: var(--ink); background: linear-gradient(180deg, var(--key-a), var(--key-b)); border: 1px solid var(--key-edge); box-shadow: var(--sh-key); } +.v2-team .v2-team__grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; } +.v2-team .v2-team-card { display: flex; flex-direction: column; gap: 10px; min-width: 0; padding: 16px; border: 1px solid var(--d); border-radius: 14px; background: var(--sf); } +.v2-team .v2-team-card--needsYou { border: 2px solid var(--c); padding: 15px; } +.v2-team .v2-team-card--own { border: 1px dashed var(--b); background: transparent; } +.v2-team .v2-team-card__head { display: flex; align-items: center; gap: 12px; min-width: 0; } +.v2-team .v2-team-card__mark { all: unset; box-sizing: border-box; cursor: pointer; flex: none; display: grid; place-items: center; width: 40px; height: 40px; border-radius: 10px; overflow: hidden; box-shadow: 0 0 0 1px var(--av-ring); font: 600 12px/1 var(--f-body); letter-spacing: 0.02em; text-transform: uppercase; } +.v2-team .v2-team-card__mark .v2-avatar { width: 100%; height: 100%; border: 0; border-radius: inherit; box-shadow: none; } +.v2-team .v2-team-card__mark img, .v2-team .v2-team-card__mark svg { display: block; width: 100%; height: 100%; object-fit: cover; } +.v2-team .v2-team-card__mark--needsYou { background: var(--c); color: var(--on-c); } +.v2-team .v2-team-card__mark--working { background: var(--ink); color: var(--sf); } +.v2-team .v2-team-card__mark--idle { background: var(--d); color: var(--se); } +.v2-team .v2-team-card__title { min-width: 0; } +.v2-team .v2-team-card__name { font: 700 18px/1.2 var(--f-display); letter-spacing: -0.02em; color: var(--ink); overflow-wrap: anywhere; white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; min-width: 0; } +.v2-team .v2-team-card__status { margin-top: 3px; font: 500 11.5px/16px var(--f-mono); color: var(--mu); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.v2-team .v2-team-card__status--needsYou, .v2-team .v2-team-card__status--working { color: var(--c-text); } +.v2-team .v2-team-card__status--needsYou { white-space: normal; overflow: visible; text-overflow: clip; } +.v2-team .v2-team-card__status--working .v2-team-card__dot { animation: v2-team-pulse 1.6s ease-in-out infinite; } +@keyframes v2-team-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } } +.v2-team .v2-team-card__where { display: inline-flex; flex: none; margin-left: auto; align-self: flex-start; color: var(--mu); } +.v2-team .v2-team-card__desc { margin: 0; font-size: 13px; line-height: 18px; color: var(--se); } +.v2-team .v2-team-card__pods { display: flex; flex-wrap: wrap; gap: 6px; } +.v2-team .v2-team-card__pod { display: inline-block; max-width: 100%; height: 24px; padding: 0 8px; border-radius: 7px; font: 500 11.5px/24px var(--f-mono); color: var(--se); background: var(--sf); box-shadow: inset 0 0 0 1px var(--d); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.v2-team .v2-team-card__meter { display: flex; align-items: center; gap: 10px; } +.v2-team .v2-team-card__meter-track { flex: 1; min-width: 0; height: 4px; border-radius: 2px; background: var(--d); overflow: hidden; } +.v2-team .v2-team-card__meter-fill { display: block; height: 100%; background: var(--c); } +.v2-team .v2-team-card__meter-text { font: 500 11.5px/16px var(--f-mono); color: var(--mu); white-space: nowrap; } +.v2-team .v2-team-card__command { display: block; padding: 10px 12px; border-radius: 10px; background: var(--p); box-shadow: inset 0 0 0 1px var(--d); font: 500 14px/20px var(--f-mono); color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; } +.v2-team .v2-team-card__actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 2px; } +.app.m .v2-team { gap: 14px; } +.app.m .v2-team .v2-team__head { flex-direction: column; align-items: stretch; gap: 12px; } +.app.m .v2-team .v2-team__heading { flex-direction: column; align-items: flex-start; gap: 6px; } +.app.m .v2-team .v2-team__meta { white-space: normal; } +.app.m .v2-team .v2-team__actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); } +.app.m .v2-team .v2-team__byo, .app.m .v2-team .v2-team__hire, .app.m .v2-team .v2-team-card__answer, .app.m .v2-team .v2-team-card__talk { height: 44px; font-size: 14px; } +.app.m .v2-team .v2-team__grid { grid-template-columns: repeat(1, minmax(0, 1fr)); gap: 10px; } +.app.m .v2-team .v2-team-card__mark { width: 44px; height: 44px; border-radius: 11px; } +.app.m .v2-team .v2-team-card__actions > button { flex: 1 1 0; } + /* phone */ .mtop { flex: none; height: 52px; display: flex; align-items: center; gap: 8px; padding: 0 10px 0 12px; border-bottom: 1px solid var(--d); } .mtop h1 { font: 800 21px/1 var(--f-display); letter-spacing: -0.03em; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } @@ -622,7 +671,7 @@

Same look. Less said.

  • One screen firstRepeated blocks become a table; rare settings collapse.
  • @@ -861,6 +910,17 @@

    Same look. Less said.

    +
    +

    Your team

    /v2/agents
    +
    +
    • Class names are the page's own (v2-team__*, v2-team-card__*), so the build copies them and its tests keep passing. The rail and avatars stand in for the shared shell and V2Avatar; the page draws its own head.
    • Same family as Connectors. Each agent is a 14px bordered card; Hire and Answer are the ink key, Bring your own and Talk the plain key.
    • State is the mark, the status line and the ring. The card that needs you carries the 2px cobalt ring; status and pods are mono data. Names wrap to two lines, never one-line-ellipsize.
    • One column at 390, the head stacked over its two acts, every act and the avatar 44px.

    Each state, one line

    Empty team, loading, errorNot drawn. They keep prod's __empty, meta and __error rules.
    Invite gate, redeemNot drawn. The notice and its form reuse the two head keys.
    Connected agentsNot drawn. The internal toggle and its chip list keep prod's rules.
    Never connectedNot drawn. The fix command keeps prod's mono line.
    +
    Six agents · 1440one needs you, two working

    Your team

    6 agents · 2 working · 1 needs you
    Wren
    needs you · Slack defaults

    Ships the connector lane. Asks before anything leaves the team.

    sharpenconnectors
    Kai
    working · TASK-131

    Fixes. Small PRs with a test each.

    sharpen
    Nova
    working · Northwind

    Drafts release notes from merged PRs.

    38/200 today
    northwind
    Fern
    idle · 41m
    sharpennorthwind+1
    Pod Summarizer
    idle · 2h

    Summarizes each pod’s day at 6pm.

    northwind
    Reed
    idle · 3d
    launch
    Your own agent

    Claude Code or Codex, from your own machine.

    npx @commonlyai/cli agent attach claude
    +
    +
    +
    Six agents · 390

    Your team

    6 agents · 2 working · 1 needs you
    Wren
    needs you · Slack defaults

    Ships the connector lane. Asks before anything leaves the team.

    sharpenconnectors
    Kai
    working · TASK-131

    Fixes. Small PRs with a test each.

    sharpen
    Nova
    working · Northwind

    Drafts release notes from merged PRs.

    38/200 today
    northwind
    Fern
    idle · 41m
    sharpennorthwind+1
    Pod Summarizer
    idle · 2h

    Summarizes each pod’s day at 6pm.

    northwind
    Reed
    idle · 3d
    launch
    Your own agent

    Claude Code or Codex, from your own machine.

    npx @commonlyai/cli agent attach claude
    Pods
    157Needs you
    Team
    Settings
    +
    +
    +

    Dark mode

    one dark value per token
    diff --git a/docs/design/artboards/d215/targets.json b/docs/design/artboards/d215/targets.json index 666801470..8119ceb27 100644 --- a/docs/design/artboards/d215/targets.json +++ b/docs/design/artboards/d215/targets.json @@ -70,5 +70,13 @@ "byo-handoff-390": { "helperWords": 106, "innerRatio": 1.52 + }, + "team-1440": { + "helperWords": 56, + "innerRatio": 1 + }, + "team-390": { + "helperWords": 34, + "innerRatio": 1.93 } } From 9965667186e95677b6b2cf93ec6580903c641763 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 02:24:02 -0800 Subject: [PATCH 2/3] feat(v2): Your team, lifted from the d215 frame (R06) The page's rules leave v2.css for frontend/src/v2/components/V2YourTeamPage.css, imported by V2YourTeamPage.tsx and scoped to .v2-team, the page's root. They are the frame's Your team rules (d215 index.html, s-team) with the same selectors and values (all 46 checked line for line after the token map); .app.m rules sit in the page's @media (max-width: 760px). v2.css loses the whole Your Team block (123 lines), the .v2-team-card__fix rule (rendered only by this page), and the three v2-team entries in the zh letter-spacing list. No dark-mode override named the page. Nothing is kept in v2.css: every v2-team__* and v2-team-card__* class is rendered only by V2YourTeamPage (v2-teammate is V2BringTeammate's own surface and sheet). Token map (as R04/R05): --sf -> --v2-surface; --p -> --v2-bg-subtle; --d -> --v2-border-soft; --b -> --v2-border; --mu -> --v2-text-muted; --se -> --v2-text-secondary; --ink -> --v2-text-primary; --c -> --v2-accent; --c-text -> --v2-accent-text; --on-c -> --v2-on-accent; --fill-a/-b/-edge -> --v2-fill-a/-b/-edge; --on-fill -> --v2-on-fill; --key-a/-b/-edge -> --v2-key-a/-b/-edge; --sh-key -> --v2-key-inset; --av-ring -> --v2-av-ring; --f-display/-body/-mono -> --v2-font-display/--v2-font/--v2-font-mono. Markup unchanged: the only TSX change is the sheet import. Every testid, handler, aria attribute and t() string stays. Exceptions taken: - phone: the two text links (Have an invitation code?, 20px; Connected agents, 16px), not drawn, get an invisible ::after ring 14px above and below for a hit area of at least 44px (position: relative on the link). - contrast: none needed. Muted mono text is 4.97:1 on surface (light) and 5.77:1 (dark); muted marks and the accent meter fill clear 3:1. Carried unchanged as not drawn: BYO/Talk hover (surface-hover), Hire disabled, Talk disabled, the reduced-motion stop for the working dot, the invite notice, redeem form, input and error, the link, the error line, the empty state, the connected-agents toggle and list, and the never-connected fix command. Tests: v2-layout-invariants re-points every Your team pin at the page sheet: the #568 name crush guards (min-width 0 on name, head and title; wrap and two-line clamp), the phone one-column grid via teamPhoneBlock, actions wrap, the meter track, the runtime-chip and retired-surface bans (now both sheets), and the direction C block. Its Hire/Answer pin now reads the frame's ink key (fill gradient, on-fill text) and Talk's border the key edge, as R05 did for Connect's CTA. The BEND-1 test reads var(--v2-fs-feature) from the page sheet, where its one consumer moved. signalTokens reads the ink key from the sheet and also bans literal colours in it. v2-type-floors scans V2YourTeamPage.css. New describe 'Your team lifted from the d215 frame (R06)': sheet import and no Your team rule left in v2.css; keyed controls stay class-only so the shared focus ring outranks their shadow; 44px phone acts and avatar mark; the phone link ring; every negative-tracking selector in the sheet has a zh reset there. Each new test was confirmed to fail with what it guards removed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3 --- .../src/v2/__tests__/signalTokens.test.ts | 15 +- .../v2/__tests__/v2-layout-invariants.test.ts | 145 +++++++++++++----- .../src/v2/__tests__/v2-type-floors.test.ts | 5 +- frontend/src/v2/components/V2YourTeamPage.css | 100 ++++++++++++ frontend/src/v2/components/V2YourTeamPage.tsx | 1 + frontend/src/v2/v2.css | 136 ---------------- 6 files changed, 221 insertions(+), 181 deletions(-) create mode 100644 frontend/src/v2/components/V2YourTeamPage.css diff --git a/frontend/src/v2/__tests__/signalTokens.test.ts b/frontend/src/v2/__tests__/signalTokens.test.ts index fe6efd667..f9317227a 100644 --- a/frontend/src/v2/__tests__/signalTokens.test.ts +++ b/frontend/src/v2/__tests__/signalTokens.test.ts @@ -111,13 +111,14 @@ describe('dark-safe surfaces (TASK-681)', () => { expect(rd('../landing/v2-landing.css')).toMatch(/\.v2-landing__bar\s*\{[^}]*color-mix\(in srgb, var\(--v2-bg\) 86%/); expect(rd('../agents/v2-agent-profile.css')).toMatch(/\.v2-aprofile__bar\s*\{[^}]*color-mix\(in srgb, var\(--v2-bg\) 86%/); }); - test('ink primary buttons take --v2-ink / --v2-on-ink', () => { - const css = rd('../v2.css'); - ['v2-team__hire', 'v2-team-card__answer'].forEach((cls) => { - const rule = css.match(new RegExp(`button\\.${cls}\\s*\\{[^}]*\\}`)); - expect(rule?.[0]).toContain('background: var(--v2-ink)'); - expect(rule?.[0]).toContain('color: var(--v2-on-ink)'); - }); + 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 \{[^}]*\}/); + 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); }); test('the board drawer scrim is --v2-scrim', () => { expect(rd('../v2.css')).toMatch(/\.v2-board__overlay\s*\{\s*background:\s*var\(--v2-scrim\)/); diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 1ed25d4f7..2598780e9 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -168,6 +168,8 @@ describe('v2 layout invariants (CSS rule presence)', () => { const aprofile = read('../agents/v2-agent-profile.css'); const agentSettings = read('../agents/v2-agent-settings.css'); const byoCss = read('../components/V2AgentBYO.css'); + // R06 lifted the Your team page onto the d215 frame (V2YourTeamPage.css). + const teamCss = read('../components/V2YourTeamPage.css'); const landing = read('../landing/v2-landing.css'); const podChat = read('../components/V2Thread.tsx'); const podsSidebar = read('../components/V2PodsSidebar.tsx'); @@ -304,15 +306,15 @@ describe('v2 layout invariants (CSS rule presence)', () => { test('Your Team card name owns its line so the category chip cannot crush it', () => { // Direction C: the name sits in its own column of the card head; the // crush guard is min-width 0 on the name and the head, not a flex basis. - const rule = ruleBody(v2, '.v2-team-card__name'); + const rule = ruleBody(teamCss, '.v2-team .v2-team-card__name'); expect(rule).toContain('min-width: 0'); - expect(ruleBody(v2, '.v2-team-card__head')).toContain('min-width: 0'); + expect(ruleBody(teamCss, '.v2-team .v2-team-card__head')).toContain('min-width: 0'); }); test('Your Team card name WRAPS — a primary identifier never one-line-ellipsizes (craft audit rule 1)', () => { // At 1440px half the fleet's names truncated ("Commonly…", "Pod Summ…"). // The name wraps to two lines; single-line nowrap+ellipsis was the defect. - const rule = ruleBody(v2, '.v2-team-card__name'); + const rule = ruleBody(teamCss, '.v2-team .v2-team-card__name'); expect(rule).toContain('white-space: normal'); expect(rule).toContain('-webkit-line-clamp: 2'); expect(rule).not.toContain('white-space: nowrap'); @@ -644,7 +646,8 @@ describe('v2 layout invariants (CSS rule presence)', () => { test('runtime vocabulary stays off Your Team cards (ADR-022 D1, ratified)', () => { // The chip shipped in violation of the ratified rule; the craft audit // (finding 2) removed it. This pins the removal against reintroduction. - expect(v2).not.toContain('.v2-team-card__runtime {'); + expect(v2).not.toContain('.v2-team-card__runtime'); + expect(teamCss).not.toContain('.v2-team-card__runtime'); }); test('touch inputs are 16px — iOS auto-zoom never fires and never strands the page zoomed', () => { @@ -1085,8 +1088,8 @@ describe('v2 layout invariants (CSS rule presence)', () => { // Direction C: three equal columns that can shrink to zero, and ONE column // at ≤760 — the same guarantee (a column never wider than its container) // expressed on the new grid. - expect(ruleBody(v2, '.v2-team__grid')).toContain('repeat(3, minmax(0, 1fr))'); - expect(ruleBody(v2, '.v2-team__grid', { within: teamPhoneBlock(v2) })).toContain('repeat(1, minmax(0, 1fr))'); + expect(ruleBody(teamCss, '.v2-team .v2-team__grid')).toContain('repeat(3, minmax(0, 1fr))'); + expect(ruleBody(teamCss, '.v2-team .v2-team__grid', { within: teamPhoneBlock(teamCss) })).toContain('repeat(1, minmax(0, 1fr))'); }); test('the agent profile page overrides the app-shell overflow too (sibling invariant)', () => { @@ -1164,7 +1167,7 @@ describe('v2 layout invariants (CSS rule presence)', () => { // (2026-07-05 mobile smoke; same crush family as the #568 chip bug). // Direction C: the action row is its own flex row under the body and wraps; // it never shares a line with the name. - expect(ruleBody(v2, '.v2-team-card__actions')).toContain('flex-wrap: wrap'); + expect(ruleBody(teamCss, '.v2-team .v2-team-card__actions')).toContain('flex-wrap: wrap'); }); test('the a2a-DM system card overrides the two-column message grid', () => { @@ -1524,38 +1527,46 @@ describe('v2 layout invariants (CSS rule presence)', () => { }); test('Your Team (direction C): three-column card grid, state-coloured marks, cobalt ring for needs-you, ink Hire, no green, old tiers gone', () => { - const grid = ruleBody(v2, '.v2-team__grid'); + 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(v2, '.v2-team-card--needsYou')).toContain('border: 2px solid var(--v2-accent)'); - expect(ruleBody(v2, '.v2-root button.v2-team-card__mark--needsYou')).toContain('var(--v2-accent)'); - expect(ruleBody(v2, '.v2-root button.v2-team-card__mark--working')).toContain('var(--v2-text-primary)'); - expect(ruleBody(v2, '.v2-root button.v2-team-card__mark--idle')).toContain('var(--v2-border-soft)'); - expect(ruleBody(v2, '.v2-root button.v2-team__hire')).toContain('background: var(--v2-ink)'); - expect(ruleBody(v2, '.v2-root button.v2-team-card__answer')).toContain('background: var(--v2-ink)'); - expect(ruleBody(v2, '.v2-root button.v2-team-card__talk')).toContain('border: 1px solid var(--v2-border)'); - expect(ruleBody(v2, '.v2-team-card__status')).toContain('var(--v2-font-mono)'); + expect(ruleBody(teamCss, '.v2-team .v2-team-card--needsYou')).toContain('border: 2px solid 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-'); + 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. - expect(ruleBody(v2, '.v2-team-card__status--needsYou')).toContain('white-space: normal'); - expect(ruleBody(v2, '.v2-team-card__command')).toContain('white-space: pre-wrap'); - expect(ruleBody(v2, '.v2-team-card__command')).toContain('14px/20px'); - expect(ruleBody(v2, '.v2-team-card__name')).toContain('font: 700 18px/1.2 var(--v2-font-display)'); - expect(ruleBody(v2, '.v2-root button.v2-team-card__mark')).toContain('font: 600 12px/1'); - const teamAvatar = ruleBody(v2, '.v2-root button.v2-team-card__mark .v2-avatar'); + expect(ruleBody(teamCss, '.v2-team .v2-team-card__status--needsYou')).toContain('white-space: normal'); + expect(ruleBody(teamCss, '.v2-team .v2-team-card__command')).toContain('white-space: pre-wrap'); + expect(ruleBody(teamCss, '.v2-team .v2-team-card__command')).toContain('14px/20px'); + expect(ruleBody(teamCss, '.v2-team .v2-team-card__name')).toContain('font: 700 18px/1.2 var(--v2-font-display)'); + expect(ruleBody(teamCss, '.v2-team .v2-team-card__mark')).toContain('font: 600 12px/1'); + const teamAvatar = ruleBody(teamCss, '.v2-team .v2-team-card__mark .v2-avatar'); expect(teamAvatar).toContain('width: 100%'); expect(teamAvatar).toContain('height: 100%'); expect(teamAvatar).toContain('border-radius: inherit'); // 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(v2); - expect(ruleBody(v2, '.v2-team__heading', { within: teamPhone })).toContain('flex-direction: column'); - expect(ruleBody(v2, '.v2-team__grid', { within: teamPhone })).toContain('repeat(1, minmax(0, 1fr))'); + 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__grid', { within: teamPhone })).toContain('repeat(1, minmax(0, 1fr))'); // The retired surfaces: feature rows, standard cards with icon buttons, quiet rows, green dot. - expect(v2).not.toContain('.v2-team-feature '); - expect(v2).not.toContain('.v2-team-quiet'); - expect(v2).not.toContain('v2-team-feature__dot'); - expect(v2).toMatch(/@media \(max-width: 760px\)[\s\S]*?\.v2-team__grid \{[\s\S]*?repeat\(1, minmax\(0, 1fr\)\)/); + for (const css of [v2, teamCss]) { + expect(css).not.toContain('.v2-team-feature '); + expect(css).not.toContain('.v2-team-quiet'); + expect(css).not.toContain('v2-team-feature__dot'); + } + expect(teamCss).toMatch(/@media \(max-width: 760px\)[\s\S]*?\.v2-team__grid \{[\s\S]*?repeat\(1, minmax\(0, 1fr\)\)/); }); test('history: a mono edge line that loads on scroll and a Jump-to-latest pill', () => { @@ -2389,7 +2400,9 @@ describe('v2 layout invariants (CSS rule presence)', () => { const ds = fs.readFileSync(path.join(__dirname, '../../../design-system/tokens.css'), 'utf8'); expect(v2).toContain('--v2-fs-feature: 17px'); expect(ds).toContain('--c-fs-feature: 17px'); - expect(v2).toContain('var(--v2-fs-feature)'); + // Its one consumer, the empty team's title, left v2.css with the Your team + // lift (R06); the step is still defined here and used there. + expect(read('../components/V2YourTeamPage.css')).toContain('font: 700 var(--v2-fs-feature)/1.2'); }); it('v2.css braces balance — an unclosed block silently swallows every later rule', () => { @@ -2406,8 +2419,8 @@ describe('v2 layout invariants (CSS rule presence)', () => { it('team card on a phone: one column, so the name column never one-chars (spec §5, #568 class, re-pinned on direction C)', () => { // The featured row is gone; the same guarantee on the card grid is one // column at ≤760 and a card head whose name column can shrink. - expect(ruleBody(v2, '.v2-team__grid', { within: teamPhoneBlock(v2) })).toContain('repeat(1, minmax(0, 1fr))'); - expect(ruleBody(v2, '.v2-team-card__title')).toContain('min-width: 0'); + expect(ruleBody(teamCss, '.v2-team .v2-team__grid', { within: teamPhoneBlock(teamCss) })).toContain('repeat(1, minmax(0, 1fr))'); + expect(ruleBody(teamCss, '.v2-team .v2-team-card__title')).toContain('min-width: 0'); }); it('platform tint tokens stay in v2.css and tokens.css, while Signal rows use the cobalt mark', () => { @@ -5344,10 +5357,10 @@ describe('v2 board on a phone (TASK-576)', () => { }); test('hosted meter track keeps its flex width and clips the fill inside the card', () => { - const v2 = read('../v2.css'); - expect(ruleBody(v2, '.v2-team-card__meter-track')).toContain('flex: 1'); - expect(ruleBody(v2, '.v2-team-card__meter-track')).toContain('min-width: 0'); - expect(ruleBody(v2, '.v2-team-card__meter-track')).toContain('overflow: hidden'); + const teamCss = read('../components/V2YourTeamPage.css'); + expect(ruleBody(teamCss, '.v2-team .v2-team-card__meter-track')).toContain('flex: 1'); + expect(ruleBody(teamCss, '.v2-team .v2-team-card__meter-track')).toContain('min-width: 0'); + expect(ruleBody(teamCss, '.v2-team .v2-team-card__meter-track')).toContain('overflow: hidden'); }); test('character avatars keep their hairline ring rule (TASK-679, load-bearing)', () => { @@ -5439,3 +5452,63 @@ describe('pod board column scroll (TASK-698)', () => { expect(mobile).toMatch(/button\.v2-board__tab\.is-active \{ background: var\(--v2-surface\)/); }); }); + +describe('Your team lifted from the d215 frame (R06)', () => { + const v2 = read('../v2.css'); + const teamCss = read('../components/V2YourTeamPage.css'); + const team = read('../components/V2YourTeamPage.tsx'); + + test('the page imports its own sheet and v2.css carries no Your team rule', () => { + expect(team).toContain("import './V2YourTeamPage.css';"); + // `v2-team__`, `v2-team-card` and the bare root; `v2-teammate` is + // V2BringTeammate's own surface (V2BringTeammate.css), not this page's. + expect(v2).not.toMatch(/\.v2-team(__|-card|[\s,{:.])/); + }); + + // The keys and the avatar mark paint a box-shadow. Class-only selectors + // (0,2,0) leave `.v2-root button:focus-visible` (0,2,1) free to paint the + // focus ring over it; a `button.` prefix would tie it and, loading later, + // 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)'); + }); + + test('on a phone, every drawn act and the avatar mark are 44px', () => { + 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'); + }); + + // Deviation from the frame: the two text links (Have an invitation code?, + // 20px; Connected agents, 16px) are not drawn. On a phone an invisible ring + // 14px above and below makes each hit area at least 44px. + test('on a phone, the text links gain a 44px hit ring', () => { + expect(ruleBody(teamCss, '.v2-team button.v2-team__link')).toContain('position: relative'); + const phone = mediaBlockContaining(teamCss, '.v2-team button.v2-team__link::after'); + const ring = ruleBody(teamCss, '.v2-team button.v2-team__link::after', { within: phone }); + expect(ring).toContain('content: ""'); + expect(ring).toContain('position: absolute'); + expect(ring).toContain('inset: -14px 0'); + }); + + // The v2.css :lang(zh) reset (TASK-055) scans only v2.css, so the entries + // that left it with this page are held here: every negative-tracking + // selector in the sheet has a zh reset in the same sheet. + test('zh-CN: every negative-tracking Your team title is reset under :lang(zh)', () => { + const negative = [...teamCss.matchAll(/\n([^\n{}]+) \{[^}]*letter-spacing:\s*-[^;]+;/g)] + .flatMap((m) => m[1].split(',').map((sel) => sel.trim())) + .filter(Boolean); + 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'); + expect(start).toBeGreaterThan(-1); + const reset = teamCss.slice(start, teamCss.indexOf('}', start)); + expect(reset).toContain('letter-spacing: 0'); + for (const sel of negative) expect(reset).toContain(`.v2-root:lang(zh) ${sel}`); + }); +}); diff --git a/frontend/src/v2/__tests__/v2-type-floors.test.ts b/frontend/src/v2/__tests__/v2-type-floors.test.ts index 55d15f434..288387fb2 100644 --- a/frontend/src/v2/__tests__/v2-type-floors.test.ts +++ b/frontend/src/v2/__tests__/v2-type-floors.test.ts @@ -25,8 +25,9 @@ import path from 'path'; * longer violates). Target is an empty list. */ // v2.css plus the per-surface sheets lifted out of it, so a rule that moved -// file is still held to the floors (R04 moved the BYO page's rules). -const CSS = ['../v2.css', '../components/V2AgentBYO.css'] +// file is still held to the floors (R04 moved the BYO page's rules, R06 the +// Your team page's). +const CSS = ['../v2.css', '../components/V2AgentBYO.css', '../components/V2YourTeamPage.css'] .map((rel) => fs.readFileSync(path.join(__dirname, rel), 'utf8')) .join('\n'); diff --git a/frontend/src/v2/components/V2YourTeamPage.css b/frontend/src/v2/components/V2YourTeamPage.css new file mode 100644 index 000000000..9c89548d3 --- /dev/null +++ b/frontend/src/v2/components/V2YourTeamPage.css @@ -0,0 +1,100 @@ +/* Your team (/v2/agents), lifted from the d215 frame (R06): + docs/design/artboards/d215/index.html, section s-team, frame "team" at 1440 + and 390. The rules below are the frame's own, same selectors and values; only + the tokens are translated to --v2-* (so light, dark and auto switch together) + and the frame's `.app.m` phone rules sit in the page's 760px phone query. + Scoped to .v2-team, the page's root. */ +.v2-team { display: flex; flex-direction: column; gap: 20px; max-width: 1100px; margin: 0 auto; } +.v2-team .v2-team__head { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 44px; } +.v2-team .v2-team__heading { display: flex; align-items: baseline; gap: 12px; min-width: 0; } +.v2-team .v2-team__title { margin: 0; font: 800 24px/1 var(--v2-font-display); letter-spacing: -0.03em; color: var(--v2-text-primary); } +.v2-team .v2-team__meta { font: 500 12px/16px var(--v2-font-mono); color: var(--v2-text-muted); white-space: nowrap; } +.v2-team .v2-team__actions { display: flex; gap: 8px; flex-shrink: 0; } +.v2-team .v2-team__byo, .v2-team .v2-team__hire, .v2-team .v2-team-card__answer, .v2-team .v2-team-card__talk { 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; } +.v2-team .v2-team__hire, .v2-team .v2-team-card__answer { 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-team .v2-team__byo, .v2-team .v2-team-card__talk { 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-team .v2-team__grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; } +.v2-team .v2-team-card { display: flex; flex-direction: column; gap: 10px; min-width: 0; padding: 16px; border: 1px solid var(--v2-border-soft); border-radius: 14px; background: var(--v2-surface); } +/* The one that needs you: a ring, never a tinted background (state grammar §3). */ +.v2-team .v2-team-card--needsYou { border: 2px solid var(--v2-accent); padding: 15px; } +.v2-team .v2-team-card--own { border: 1px dashed var(--v2-border); background: transparent; } +.v2-team .v2-team-card__head { display: flex; align-items: center; gap: 12px; min-width: 0; } +.v2-team .v2-team-card__mark { all: unset; box-sizing: border-box; cursor: pointer; flex: none; display: grid; place-items: center; width: 40px; height: 40px; border-radius: 10px; overflow: hidden; box-shadow: 0 0 0 1px var(--v2-av-ring); font: 600 12px/1 var(--v2-font); letter-spacing: 0.02em; text-transform: uppercase; } +.v2-team .v2-team-card__mark .v2-avatar { width: 100%; height: 100%; border: 0; border-radius: inherit; box-shadow: none; } +.v2-team .v2-team-card__mark img, .v2-team .v2-team-card__mark svg { display: block; width: 100%; height: 100%; object-fit: cover; } +.v2-team .v2-team-card__mark--needsYou { background: var(--v2-accent); color: var(--v2-on-accent); } +.v2-team .v2-team-card__mark--working { background: var(--v2-text-primary); color: var(--v2-surface); } +.v2-team .v2-team-card__mark--idle { background: var(--v2-border-soft); color: var(--v2-text-secondary); } +.v2-team .v2-team-card__title { min-width: 0; } +/* A primary identifier never one-line-ellipsizes (craft audit rule 1, #568): + two lines, then clamp; min-width 0 so the grid column can shrink under it. */ +.v2-team .v2-team-card__name { font: 700 18px/1.2 var(--v2-font-display); letter-spacing: -0.02em; color: var(--v2-text-primary); overflow-wrap: anywhere; white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; min-width: 0; } +.v2-team .v2-team-card__status { margin-top: 3px; font: 500 11.5px/16px var(--v2-font-mono); color: var(--v2-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.v2-team .v2-team-card__status--needsYou, .v2-team .v2-team-card__status--working { color: var(--v2-accent-text); } +/* The ask is the line's content: it wraps, it never ellipsizes (66246 (3)). */ +.v2-team .v2-team-card__status--needsYou { white-space: normal; overflow: visible; text-overflow: clip; } +.v2-team .v2-team-card__status--working .v2-team-card__dot { animation: v2-team-pulse 1.6s ease-in-out infinite; } +@keyframes v2-team-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } } +/* Where the seat runs: a cloud (Commonly) or a laptop (your computer), top-right. */ +.v2-team .v2-team-card__where { display: inline-flex; flex: none; margin-left: auto; align-self: flex-start; color: var(--v2-text-muted); } +.v2-team .v2-team-card__desc { margin: 0; font-size: 13px; line-height: 18px; color: var(--v2-text-secondary); } +.v2-team .v2-team-card__pods { display: flex; flex-wrap: wrap; gap: 6px; } +.v2-team .v2-team-card__pod { display: inline-block; max-width: 100%; height: 24px; padding: 0 8px; border-radius: 7px; font: 500 11.5px/24px 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; overflow: hidden; text-overflow: ellipsis; } +/* Hosted meter: the server's count against the server's cap, as a bar and a mono line. */ +.v2-team .v2-team-card__meter { display: flex; align-items: center; gap: 10px; } +.v2-team .v2-team-card__meter-track { flex: 1; min-width: 0; height: 4px; border-radius: 2px; background: var(--v2-border-soft); overflow: hidden; } +.v2-team .v2-team-card__meter-fill { display: block; height: 100%; background: var(--v2-accent); } +.v2-team .v2-team-card__meter-text { font: 500 11.5px/16px var(--v2-font-mono); color: var(--v2-text-muted); white-space: nowrap; } +.v2-team .v2-team-card__command { display: block; padding: 10px 12px; border-radius: 10px; background: var(--v2-bg-subtle); box-shadow: inset 0 0 0 1px var(--v2-border-soft); font: 500 14px/20px var(--v2-font-mono); color: var(--v2-text-primary); white-space: pre-wrap; overflow-wrap: anywhere; } +.v2-team .v2-team-card__actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 2px; } +@media (max-width: 760px) { + .v2-team { gap: 14px; } + .v2-team .v2-team__head { flex-direction: column; align-items: stretch; gap: 12px; } + /* The title never wraps beside its meta: stack them (66246 (1)). */ + .v2-team .v2-team__heading { flex-direction: column; align-items: flex-start; gap: 6px; } + .v2-team .v2-team__meta { white-space: normal; } + .v2-team .v2-team__actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); } + .v2-team .v2-team__byo, .v2-team .v2-team__hire, .v2-team .v2-team-card__answer, .v2-team .v2-team-card__talk { height: 44px; font-size: 14px; } + .v2-team .v2-team__grid { grid-template-columns: repeat(1, minmax(0, 1fr)); gap: 10px; } + .v2-team .v2-team-card__mark { width: 44px; height: 44px; border-radius: 11px; } + .v2-team .v2-team-card__actions > button { flex: 1 1 0; } + /* Not in the frame: the text links (Have an invitation code? at 20px, + Connected agents at 16px) get an invisible ring 14px above and below, so + each hit area is at least 44px tall. */ + .v2-team button.v2-team__link::after { content: ""; position: absolute; inset: -14px 0; } +} + +/* Live behaviour and states the frame does not draw, kept working with prod's values. */ +.v2-team .v2-team__byo:hover, .v2-team .v2-team-card__talk:hover { background: var(--v2-surface-hover); } +.v2-team .v2-team__hire:disabled { opacity: 0.5; cursor: default; } +.v2-team .v2-team-card__talk:disabled { color: var(--v2-text-placeholder); cursor: default; } +@media (prefers-reduced-motion: reduce) { + .v2-team .v2-team-card__status--working .v2-team-card__dot { animation: none; } +} +.v2-team .v2-team__notice { font-size: 13px; color: var(--v2-text-secondary); } +.v2-team .v2-team__notice--ok { color: var(--v2-text-primary); } +.v2-team .v2-team__redeem { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } +.v2-team .v2-team__redeem-input { height: 36px; padding: 0 12px; border: 1px solid var(--v2-border); border-radius: 4px; font: 400 14px/20px var(--v2-font); } +.v2-team .v2-team__redeem-error { color: var(--v2-text-primary); font-size: 13px; } +.v2-team button.v2-team__link { position: relative; padding: 0; border: 0; background: transparent; color: var(--v2-accent-text); font: 500 13px/20px var(--v2-font); cursor: pointer; } +.v2-team .v2-team__error { font-size: 13px; color: var(--v2-text-primary); border: 1px solid var(--v2-border); border-radius: 4px; padding: 10px 12px; } +.v2-team .v2-team__empty { border: 1px dashed var(--v2-border); border-radius: 4px; padding: 20px; } +.v2-team .v2-team__empty-title { font: 700 var(--v2-fs-feature)/1.2 var(--v2-font-display); letter-spacing: -0.02em; margin-bottom: 6px; } +.v2-team .v2-team__empty-text { font-size: 13px; color: var(--v2-text-secondary); } +.v2-team .v2-team__internal { display: flex; flex-direction: column; gap: 8px; } +.v2-team button.v2-team__internal-toggle { font: 500 11px/16px var(--v2-font-mono); color: var(--v2-text-muted); } +.v2-team .v2-team__internal-list { display: flex; gap: 6px; flex-wrap: wrap; } +.v2-team .v2-team-card__fix { + display: block; + margin-top: 4px; + font-family: var(--v2-font-mono, ui-monospace, monospace); + font-size: 12px; + color: var(--v2-text-secondary); + overflow-wrap: anywhere; + user-select: all; +} +/* CJK never takes negative letter-spacing (TASK-055); these entries left the + v2.css :lang(zh) reset list with the rest of the page. */ +.v2-root:lang(zh) .v2-team .v2-team__title, +.v2-root:lang(zh) .v2-team .v2-team-card__name, +.v2-root:lang(zh) .v2-team .v2-team__empty-title { letter-spacing: 0; } diff --git a/frontend/src/v2/components/V2YourTeamPage.tsx b/frontend/src/v2/components/V2YourTeamPage.tsx index 850b1a0c7..3cddc21fb 100644 --- a/frontend/src/v2/components/V2YourTeamPage.tsx +++ b/frontend/src/v2/components/V2YourTeamPage.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import axios from 'axios'; import { useAuth } from '../../context/AuthContext'; import V2Avatar from './V2Avatar'; +import './V2YourTeamPage.css'; import { AgentConnection, connectionLabel } from '../utils/agentConnection'; /** diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index c113e3627..f338ff083 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -2575,16 +2575,6 @@ body.modern-ui.v2-canvas { user-select: all; } -.v2-team-card__fix { - display: block; - margin-top: 4px; - font-family: var(--v2-font-mono, ui-monospace, monospace); - font-size: 12px; - color: var(--v2-text-secondary); - overflow-wrap: anywhere; - user-select: all; -} - .v2-chat__agent-room-status--wait { border-color: var(--v2-border); background: var(--v2-surface-hover); @@ -9351,9 +9341,6 @@ body.modern-ui.v2-canvas { .v2-root:lang(zh) .v2-board__detail .v2-modal__title, .v2-root:lang(zh) .v2-tools__count strong, .v2-root:lang(zh) .v2-artifacts__title, -.v2-root:lang(zh) .v2-team__title, -.v2-root:lang(zh) .v2-team-card__name, -.v2-root:lang(zh) .v2-team__empty-title, .v2-root:lang(zh) .v2-pods__title, .v2-root:lang(zh) .v2-pods__page-title, .v2-root:lang(zh) .v2-chat__title, @@ -12095,129 +12082,6 @@ body.modern-ui.v2-canvas { .v2-activity__start-act { grid-column: 1 / -1; } .v2-root .v2-activity__start-act button { width: 100%; min-height: 44px; } } -/* --------------------------------------------------------------------------- - Your Team — direction C, Signal (docs/design/signal-identity.md §4 "Cards in a - grid"). One card per agent; the mark's colour is the state; the one that - needs you carries the 2px cobalt ring. Ink acts (Hire, Answer); secondaries - are bordered; status is mono because it is data. - --------------------------------------------------------------------------- */ -.v2-team { display: flex; flex-direction: column; gap: 24px; } -.v2-team__head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; } -.v2-team__heading { display: flex; align-items: baseline; gap: 12px; min-width: 0; } -.v2-team__title { margin: 0; font: 700 32px/1.1 var(--v2-font-display); letter-spacing: -0.03em; color: var(--v2-text-primary); } -.v2-team__meta { font-size: 13px; color: var(--v2-text-muted); white-space: nowrap; } -.v2-team__actions { display: flex; gap: 8px; flex-shrink: 0; } -.v2-root button.v2-team__byo { height: 36px; padding: 0 14px; border: 1px solid var(--v2-border); border-radius: 4px; background: var(--v2-surface); color: var(--v2-text-primary); font: 600 13px/1 var(--v2-font); cursor: pointer; } -.v2-root button.v2-team__byo:hover { background: var(--v2-surface-hover); } -/* Ink acts (signal-identity §2): Hire is the page's one filled control. */ -.v2-root button.v2-team__hire { height: 36px; padding: 0 14px; border: 1px solid var(--v2-ink); border-radius: 4px; background: var(--v2-ink); color: var(--v2-on-ink); font: 600 13px/1 var(--v2-font); cursor: pointer; } -.v2-root button.v2-team__hire:disabled { opacity: 0.5; cursor: default; } -.v2-team__notice { font-size: 13px; color: var(--v2-text-secondary); } -.v2-team__notice--ok { color: var(--v2-text-primary); } -.v2-team__redeem { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } -.v2-team__redeem-input { height: 36px; padding: 0 12px; border: 1px solid var(--v2-border); border-radius: 4px; font: 400 14px/20px var(--v2-font); } -.v2-team__redeem-error { color: var(--v2-text-primary); font-size: 13px; } -.v2-root button.v2-team__link { padding: 0; border: 0; background: transparent; color: var(--v2-accent-text); font: 500 13px/20px var(--v2-font); cursor: pointer; } -.v2-team__error { font-size: 13px; color: var(--v2-text-primary); border: 1px solid var(--v2-border); border-radius: 4px; padding: 10px 12px; } -.v2-team__empty { border: 1px dashed var(--v2-border); border-radius: 4px; padding: 20px; } -.v2-team__empty-title { font: 700 var(--v2-fs-feature)/1.2 var(--v2-font-display); letter-spacing: -0.02em; margin-bottom: 6px; } -.v2-team__empty-text { font-size: 13px; color: var(--v2-text-secondary); } - -.v2-team__grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; } -.v2-team-card { - display: flex; - flex-direction: column; - gap: 10px; - padding: 16px; - border: 1px solid var(--v2-border); - border-radius: 4px; - background: var(--v2-surface); - min-width: 0; -} -/* The one that needs you: a ring, never a tinted background (state grammar §3). */ -.v2-team-card--needsYou { border: 2px solid var(--v2-accent); padding: 15px; } -.v2-team-card--own { border-style: dashed; } -.v2-team-card__head { display: flex; align-items: center; gap: 10px; min-width: 0; } -.v2-root button.v2-team-card__mark { - width: 40px; - height: 40px; - flex-shrink: 0; - display: grid; - place-items: center; - border: 0; - border-radius: 4px; - font: 600 12px/1 var(--v2-font); - letter-spacing: 0.02em; - text-transform: uppercase; - cursor: pointer; - overflow: hidden; - padding: 0; -} -.v2-root button.v2-team-card__mark img { width: 100%; height: 100%; object-fit: cover; } -.v2-root button.v2-team-card__mark .v2-avatar { width: 100%; height: 100%; border: 0; border-radius: inherit; } -.v2-root button.v2-team-card__mark--needsYou { background: var(--v2-accent); color: var(--v2-bg); } -.v2-root button.v2-team-card__mark--working { background: var(--v2-text-primary); color: var(--v2-bg); } -.v2-root button.v2-team-card__mark--idle { background: var(--v2-border-soft); color: #4f5c6e; } -.v2-team-card__title { min-width: 0; } -/* A primary identifier never one-line-ellipsizes (craft audit rule 1, #568): - two lines, then clamp; min-width 0 so the grid column can shrink under it. */ -.v2-team-card__name { - font: 700 18px/1.2 var(--v2-font-display); - letter-spacing: -0.02em; - color: var(--v2-text-primary); - overflow-wrap: anywhere; - white-space: normal; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - min-width: 0; -} -.v2-team-card__status { margin-top: 2px; font: 500 11px/16px var(--v2-font-mono); color: var(--v2-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.v2-team-card__status--needsYou, -.v2-team-card__status--working { color: var(--v2-accent-text); } -/* The ask is the line's content — it wraps, it never ellipsizes (66246 (3)). */ -.v2-team-card__status--needsYou { white-space: normal; overflow: visible; text-overflow: clip; } -.v2-team-card__status--working .v2-team-card__dot { animation: v2-team-pulse 1.6s ease-in-out infinite; } -@keyframes v2-team-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.35; } -} -@media (prefers-reduced-motion: reduce) { - .v2-team-card__status--working .v2-team-card__dot { animation: none; } -} -.v2-team-card__desc { margin: 0; font-size: 13px; line-height: 18px; color: var(--v2-text-secondary); } -.v2-team-card__pods { display: flex; gap: 6px; flex-wrap: wrap; } -.v2-team-card__pod { font: 500 11px/16px var(--v2-font-mono); padding: 2px 6px; border: 1px solid var(--v2-border); border-radius: 4px; color: var(--v2-text-secondary); } -.v2-team-card__command { display: block; padding: 8px 10px; border-radius: 4px; background: #101828; color: #fff; font: 500 14px/20px var(--v2-font-mono); white-space: pre-wrap; overflow-wrap: anywhere; } -.v2-team-card__actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 2px; } -/* Where the seat runs: a cloud (Commonly) or a laptop (your computer), top-right. */ -.v2-team-card__where { display: inline-flex; flex: none; margin-left: auto; align-self: flex-start; color: var(--v2-text-muted); } -/* Hosted meter: the server's count against the server's cap, as a bar and a mono line. */ -.v2-team-card__meter { display: flex; align-items: center; gap: 10px; } -.v2-team-card__meter-track { flex: 1; min-width: 0; height: 4px; border-radius: 2px; background: var(--v2-border-soft); overflow: hidden; } -.v2-team-card__meter-fill { display: block; height: 100%; background: var(--v2-accent); } -.v2-team-card__meter-text { font: 500 11px/16px var(--v2-font-mono); color: var(--v2-text-muted); white-space: nowrap; } -.v2-root button.v2-team-card__answer { height: 32px; padding: 0 12px; border: 1px solid var(--v2-ink); border-radius: 4px; background: var(--v2-ink); color: var(--v2-on-ink); font: 600 13px/1 var(--v2-font); cursor: pointer; } -.v2-root button.v2-team-card__talk { height: 32px; padding: 0 12px; border: 1px solid var(--v2-border); border-radius: 4px; background: var(--v2-surface); color: var(--v2-text-primary); font: 600 13px/1 var(--v2-font); cursor: pointer; } -.v2-root button.v2-team-card__talk:hover { background: var(--v2-surface-hover); } -.v2-root button.v2-team-card__talk:disabled { color: var(--v2-text-placeholder); cursor: default; } -.v2-team__internal { display: flex; flex-direction: column; gap: 8px; } -.v2-root button.v2-team__internal-toggle { font: 500 11px/16px var(--v2-font-mono); color: var(--v2-text-muted); } -.v2-team__internal-list { display: flex; gap: 6px; flex-wrap: wrap; } - -@media (max-width: 760px) { - .v2-team__head { flex-direction: column; align-items: flex-start; } - /* The title never wraps beside its meta: stack them (66246 (1)). */ - .v2-team__heading { flex-direction: column; align-items: flex-start; gap: 2px; } - .v2-team__meta { white-space: normal; } - .v2-team__grid { grid-template-columns: repeat(1, minmax(0, 1fr)); } - .v2-root button.v2-team-card__answer, - .v2-root button.v2-team-card__talk, - .v2-root button.v2-team__byo, - .v2-root button.v2-team__hire { min-height: 44px; } -} - /* --------------------------------------------------------------------------- Artifacts — direction C, PR 5 (ScaleArtifacts.dc.html). Every file and page shared in your pods. Display head + muted meta; two From db6b8132538d2f55fc74ff72ec41857385185a50 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 02:30:07 -0800 Subject: [PATCH 3/3] fix(your-team): 44px code input and a clear Connected agents ring on phones Review fixes on the R06 lift. The invitation-code input sat at 36px beside 44px buttons, and the Connected agents ring reached 6px into the first chip row. Both overrides live in a phone block after the base rules so they win at equal specificity. The card-name guard now also checks that no phone block undoes the desktop rule. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3 --- .../v2/__tests__/v2-layout-invariants.test.ts | 16 ++++++++++++++++ frontend/src/v2/components/V2YourTeamPage.css | 8 ++++++++ 2 files changed, 24 insertions(+) diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 2598780e9..5ceed75d4 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -309,6 +309,10 @@ describe('v2 layout invariants (CSS rule presence)', () => { const rule = ruleBody(teamCss, '.v2-team .v2-team-card__name'); expect(rule).toContain('min-width: 0'); expect(ruleBody(teamCss, '.v2-team .v2-team-card__head')).toContain('min-width: 0'); + // ruleBody reads the first, desktop rule; no phone override may undo it. + for (const phone of [...teamCss.matchAll(/@media \(max-width: 760px\) \{[\s\S]*?\n\}/g)].map((m) => m[0])) { + expect(phone).not.toMatch(/v2-team-card__name[^{]*\{[^}]*(nowrap|min-width: auto|flex-shrink: 0)/); + } }); test('Your Team card name WRAPS — a primary identifier never one-line-ellipsizes (craft audit rule 1)', () => { @@ -5493,6 +5497,18 @@ describe('Your team lifted from the d215 frame (R06)', () => { expect(ring).toContain('inset: -14px 0'); }); + // Undrawn controls in the phone block that follows the base rules: the code + // input matches its 44px buttons, and the Connected agents ring (14px below + // the link) clears the first chip row. + test('on a phone, the code input is 44px and the internal ring clears the chips', () => { + const phone = mediaBlockContaining(teamCss, '.v2-team .v2-team__redeem-input'); + expect(ruleBody(teamCss, '.v2-team .v2-team__redeem-input', { within: phone })).toContain('height: 44px'); + expect(ruleBody(teamCss, '.v2-team .v2-team__internal', { within: phone })).toContain('gap: 14px'); + // The override must come after the base rules or the base wins. + expect(teamCss.indexOf(phone)).toBeGreaterThan(teamCss.indexOf('.v2-team .v2-team__redeem-input {')); + expect(teamCss.indexOf(phone)).toBeGreaterThan(teamCss.indexOf('.v2-team .v2-team__internal {')); + }); + // The v2.css :lang(zh) reset (TASK-055) scans only v2.css, so the entries // that left it with this page are held here: every negative-tracking // selector in the sheet has a zh reset in the same sheet. diff --git a/frontend/src/v2/components/V2YourTeamPage.css b/frontend/src/v2/components/V2YourTeamPage.css index 9c89548d3..61bc3c78c 100644 --- a/frontend/src/v2/components/V2YourTeamPage.css +++ b/frontend/src/v2/components/V2YourTeamPage.css @@ -93,6 +93,14 @@ overflow-wrap: anywhere; user-select: all; } +/* Phone overrides for the undrawn rules above; they sit after them so they win + at equal specificity. The code input matches the 44px buttons beside it, and + the gap keeps the Connected agents ring (14px below the link) off the first + chip row. */ +@media (max-width: 760px) { + .v2-team .v2-team__redeem-input { height: 44px; } + .v2-team .v2-team__internal { gap: 14px; } +} /* CJK never takes negative letter-spacing (TASK-055); these entries left the v2.css :lang(zh) reset list with the rest of the page. */ .v2-root:lang(zh) .v2-team .v2-team__title,