diff --git a/docs/design/artboards/d215/cap/measure_after.js b/docs/design/artboards/d215/cap/measure_after.js index b54c10ee3..37bb4d7f8 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', 'firstrun-welcome', 'firstrun-waiting', 'firstrun-connected', 'firstrun-error', 'connect']; +const SURFACES = ['settings', 'agent-settings', 'agent-memory', 'board', 'connectors', 'chat', 'sidebar', 'byo', 'byo-handoff', 'firstrun-welcome', 'firstrun-waiting', 'firstrun-connected', 'firstrun-error', 'connect', 'team']; // byo-handoff is drawn but can't be captured on prod: reaching it issues a real key. The first-run // dialog is a latch-gated overlay, and the Scout welcome only exists in a brand-new pod, so neither is measured on prod. const PROD_SURFACES = SURFACES.filter(s => s !== 'byo-handoff' && !s.startsWith('firstrun-')); @@ -166,6 +166,7 @@ async function prod(only) { ['connectors', () => '/v2/connectors'], ['byo', () => '/v2/agents/byo'], ['connect', () => '/v2/connect'], + ['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 d40f3a3e1..06cefaa92 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, 28); + assert.strictEqual(Object.keys(t).length, 30); 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, 28); + assert.strictEqual(EXPECTED.length, 30); }); // 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 3f4e0adfb..2743b5b3b 100644 --- a/docs/design/artboards/d215/index.html +++ b/docs/design/artboards/d215/index.html @@ -517,6 +517,55 @@ .app.m .v2-connect .v2-connect__actions { flex-direction: column; } .app.m .v2-connect .v2-connect__cta, .app.m .v2-connect .v2-connect__lane > .v2-connect__secondary, .app.m .v2-connect .v2-connect__secondary { align-self: stretch; height: 44px; font-size: 14px; } +/* 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; } @@ -687,7 +736,7 @@

Same look. Less said.

  • One screen firstRepeated blocks become a table; rare settings collapse.
  • @@ -969,6 +1018,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
    +
    +
    +

    First run

    /v2/pods/:id · Scout welcome + Guide dialog
    helper words welcome – → 16dialog – → 68scroll 390 – → 1.0×
    diff --git a/docs/design/artboards/d215/targets.json b/docs/design/artboards/d215/targets.json index fe89f9530..0afa663f0 100644 --- a/docs/design/artboards/d215/targets.json +++ b/docs/design/artboards/d215/targets.json @@ -79,6 +79,14 @@ "helperWords": 20, "innerRatio": 1.77 }, + "team-1440": { + "helperWords": 56, + "innerRatio": 1 + }, + "team-390": { + "helperWords": 34, + "innerRatio": 1.93 + }, "firstrun-welcome-1440": { "helperWords": 16, "innerRatio": 1 diff --git a/frontend/src/v2/__tests__/signalTokens.test.ts b/frontend/src/v2/__tests__/signalTokens.test.ts index 8b61591fe..bcf13ffb0 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', () => { // 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 addf7a449..df646cfbf 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -174,6 +174,8 @@ describe('v2 layout invariants (CSS rule presence)', () => { const byoCss = read('../components/V2AgentBYO.css'); // R05 lifted the Connect page onto the d215 frame (V2ConnectPage.css). const connectCss = read('../components/V2ConnectPage.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'); @@ -310,15 +312,19 @@ 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'); + // 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)', () => { // 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'); @@ -652,7 +658,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', () => { @@ -1102,8 +1109,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)', () => { @@ -1181,7 +1188,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', () => { @@ -1562,38 +1569,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', () => { @@ -2446,7 +2461,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', () => { @@ -2463,8 +2480,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', () => { @@ -5435,10 +5452,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 = readV2Css(); - 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)', () => { @@ -5593,6 +5610,78 @@ describe('Connect lifted from the d215 frame (R05)', () => { }); }); +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'); + }); + + // 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. + 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}`); + }); +}); + describe('chat sheets lifted out of v2.css (S3)', () => { const chatSheets = ['chat-message', 'chat-pane', 'chat-composer', 'chat-inspector', 'chat-states'] .map((name) => read(`../chat/${name}.css`)); diff --git a/frontend/src/v2/__tests__/v2-type-floors.test.ts b/frontend/src/v2/__tests__/v2-type-floors.test.ts index 11260e233..736ee6d47 100644 --- a/frontend/src/v2/__tests__/v2-type-floors.test.ts +++ b/frontend/src/v2/__tests__/v2-type-floors.test.ts @@ -27,8 +27,8 @@ import { readV2Css } from '../__testkit__/v2Css'; */ // v2.css plus the surface files lifted out of it (D223): the chat sheets V2App // loads, and the per-surface sheets their components import (R04 BYO, S4 board, -// R05 Connect). -const CSS = [readV2Css(), ...['../components/V2AgentBYO.css', '../components/V2PodBoard.css', '../components/V2ConnectPage.css'] +// R05 Connect, R06 Your team). +const CSS = [readV2Css(), ...['../components/V2AgentBYO.css', '../components/V2PodBoard.css', '../components/V2ConnectPage.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..61bc3c78c --- /dev/null +++ b/frontend/src/v2/components/V2YourTeamPage.css @@ -0,0 +1,108 @@ +/* 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; +} +/* 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, +.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 1a18745b8..994aab8a3 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -2153,16 +2153,6 @@ body.modern-ui.v2-canvas { flex-shrink: 0; } -.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__new-pod-text { max-width: 520px; margin-top: 4px; @@ -6393,9 +6383,6 @@ body.modern-ui.v2-canvas { .v2-root:lang(zh) .v2-rail__brand, .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, @@ -7885,129 +7872,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