From 2e5532ac5460b4d78aa3c67beb7ec30c39066639 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 02:05:39 -0800 Subject: [PATCH 1/3] docs(design): draw Connect into d215 (member view, 1440 and 390) Frames use the page's own v2-connect__* class names, scoped to .v2-connect, so the lift is a copy; the connectors section is the visual reference. Lanes are bordered panels, each code block a panel like BYO's command, Copy the same key, the agent lane's acts the ink key and a plain key. The admin lane and the Copied label are not drawn; they share the drawn classes. measure_after now knows connect (/v2/connect); targets.json gains its two rows, the existing 18 unchanged. The two connect rows were computed by hand from the frame's box model (no browser was run for this commit): 20 helper words at both widths, scroll 1.08 at 1440 and 1.77 at 390. Re-run `node measure_after.js` to replace them with measured values. 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 | 73 ++++++++++++++++++- docs/design/artboards/d215/targets.json | 8 ++ 4 files changed, 84 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..3ee6bbe5d 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', 'connect']; // 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'], + ['connect', () => '/v2/connect'], ['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..af6486d89 100644 --- a/docs/design/artboards/d215/index.html +++ b/docs/design/artboards/d215/index.html @@ -489,6 +489,34 @@ .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; } +/* connect: the page's own v2-connect__* names, in the connectors family (key-gradient Copy, panel code + blocks like BYO's command, mono labels). Scoped to .v2-connect, the page's root. */ +.v2-connect { display: flex; flex-direction: column; gap: 16px; max-width: 960px; margin: 0 auto; } +.v2-connect .v2-connect__lane { display: flex; flex-direction: column; gap: 14px; padding: 20px; border-radius: 14px; background: var(--sf); box-shadow: inset 0 0 0 1px var(--d); } +.v2-connect .v2-connect__lane-heading { display: flex; align-items: baseline; gap: 10px; } +.v2-connect .v2-connect__number { flex: none; font: 500 11.5px/1 var(--f-mono); color: var(--c-text); } +.v2-connect .v2-connect__lane h2 { margin: 0; font: 800 16px/1.25 var(--f-display); letter-spacing: -0.02em; color: var(--ink); } +.v2-connect .v2-connect__code-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } +.v2-connect .v2-connect__code-card { min-width: 0; overflow: hidden; border-radius: 12px; background: var(--p); box-shadow: inset 0 0 0 1px var(--d); } +.v2-connect .v2-connect__code-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 44px; padding: 8px 8px 8px 14px; border-bottom: 1px solid var(--d); font: 500 11.5px/16px var(--f-mono); color: var(--mu); } +.v2-connect .v2-connect__code-card pre { margin: 0; padding: 12px 14px; font: 500 12.5px/21px var(--f-mono); color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; } +.v2-connect .v2-connect__copy { all: unset; box-sizing: border-box; cursor: pointer; position: relative; flex: none; display: inline-flex; align-items: center; justify-content: center; height: 28px; padding: 0 12px; border-radius: 8px; font: 600 12.5px/1 var(--f-body); 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-connect .v2-connect__rule .v2-connect__code-head span { min-width: 0; } +.v2-connect .v2-connect__rule .v2-connect__code-head code { font: 500 11.5px/16px var(--f-mono); color: var(--ink); overflow-wrap: anywhere; } +.v2-connect .v2-connect__cursor { display: flex; flex-wrap: wrap; align-items: center; gap: 0 12px; font-size: 13px; line-height: 20px; color: var(--se); } +.v2-connect .v2-connect__link { display: inline-flex; align-items: center; min-height: 32px; font: 600 13px/20px var(--f-body); color: var(--c-text); text-decoration: none; } +.v2-connect .v2-connect__lane > .v2-connect__link, .v2-connect .v2-connect__lane > .v2-connect__secondary { align-self: flex-start; } +.v2-connect .v2-connect__actions { display: flex; flex-wrap: wrap; gap: 8px; } +.v2-connect .v2-connect__cta, .v2-connect .v2-connect__secondary { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; height: 34px; padding: 0 13px; border-radius: 10px; font: 600 13px/1 var(--f-body); white-space: nowrap; text-decoration: none; cursor: pointer; } +.v2-connect .v2-connect__cta { color: var(--on-fill); background: linear-gradient(180deg, var(--fill-a), var(--fill-b)); border: 1px solid var(--fill-edge); } +.v2-connect .v2-connect__secondary { color: var(--ink); background: linear-gradient(180deg, var(--key-a), var(--key-b)); border: 1px solid var(--key-edge); box-shadow: var(--sh-key); } +.app.m .v2-connect { gap: 10px; } +.app.m .v2-connect .v2-connect__lane { padding: 16px; } +.app.m .v2-connect .v2-connect__code-grid { grid-template-columns: minmax(0, 1fr); } +.app.m .v2-connect .v2-connect__link { min-height: 44px; } +.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; } + /* 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 +650,7 @@

Same look. Less said.

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

    Same look. Less said.

    +
    +

    Connect

    /v2/connect
    +
    +
    • Class names are the page's own (v2-connect__*), so the build copies them and its tests keep passing. The rail, page head and (i) stand in for the shared shell and V2Hint.
    • Same family as Connectors. Lanes are bordered panels, each code block is a panel like BYO's command, and Copy is the same key.
    • Links and acts are controls. Links sit in a 32px row at 1440 and a 44px row at 390; the agent lane's two acts are the ink key and a plain key, full width at 390.

    Each state, one line

    MemberDrawn. Lanes 01 and 02.
    AdminNot drawn. Lane 03, Instance keys, is a third lane with one plain key.
    CopiedNot drawn. The key's label reads Copied for 1.5s.
    +
    Member · 1440

    Connect

    Connect the CLI or MCP

    Commonly CLI
    npm install -g @commonlyai/cli
    +commonly login --instance https://api.commonly.me
    Claude Code MCP
    claude mcp add commonly \
    +  -e COMMONLY_API_URL=https://api.commonly.me \
    +  -e COMMONLY_AGENT_TOKEN=cm_agent_… \
    +  -- npx -y @commonlyai/mcp
    Add Commonly to CursorAdd your agent key in Cursor’s MCP settings.
    Cursor rule · save as .cursor/rules/commonly.mdc in your project
    ---
    +description: Use the team's Commonly pod for shared context and handoffs
    +alwaysApply: true
    +---
    +
    +When a task names a Commonly pod, call `commonly_get_context` with that pod's id before you start, so you have the team's current priorities and recent messages. Use `commonly_get_messages` when you need more history.
    +
    +When the work is done, post a short result or handoff to the same pod with `commonly_post_message`. Don't post step-by-step progress.
    +
    +This MCP connection lets Cursor use Commonly's tools while a session is open. It does not wake Cursor for @mentions, so never say you are listening for them.
    +
    +Keep agent keys out of pod messages, URLs, logs and committed files.
    +
    Manage connected CLI devices

    Connect an agent you run

    +
    +
    +
    Member · 390
    Connect

    Connect the CLI or MCP

    Commonly CLI
    npm install -g @commonlyai/cli
    +commonly login --instance https://api.commonly.me
    Claude Code MCP
    claude mcp add commonly \
    +  -e COMMONLY_API_URL=https://api.commonly.me \
    +  -e COMMONLY_AGENT_TOKEN=cm_agent_… \
    +  -- npx -y @commonlyai/mcp
    Add Commonly to CursorAdd your agent key in Cursor’s MCP settings.
    Cursor rule · save as .cursor/rules/commonly.mdc in your project
    ---
    +description: Use the team's Commonly pod for shared context and handoffs
    +alwaysApply: true
    +---
    +
    +When a task names a Commonly pod, call `commonly_get_context` with that pod's id before you start, so you have the team's current priorities and recent messages. Use `commonly_get_messages` when you need more history.
    +
    +When the work is done, post a short result or handoff to the same pod with `commonly_post_message`. Don't post step-by-step progress.
    +
    +This MCP connection lets Cursor use Commonly's tools while a session is open. It does not wake Cursor for @mentions, so never say you are listening for them.
    +
    +Keep agent keys out of pod messages, URLs, logs and committed files.
    +
    Manage connected CLI devices

    Connect an agent you run

    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..004085e04 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 + }, + "connect-1440": { + "helperWords": 20, + "innerRatio": 1.08 + }, + "connect-390": { + "helperWords": 20, + "innerRatio": 1.77 } } From 01dcf188c8e6ccce0f9c79ed399a9c28db3027ca Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 02:08:43 -0800 Subject: [PATCH 2/3] feat(v2): Connect, lifted from the d215 frame (R05) The page's rules leave v2.css for frontend/src/v2/components/V2ConnectPage.css, imported by V2ConnectPage.tsx and scoped to .v2-connect, the page's root. They are the frame's Connect rules (d215 index.html, s-connect) with the same selectors and values; .app.m rules sit in the page's @media (max-width: 760px). v2.css loses the whole Connect block (182 lines) and the one v2-connect__ entry in the zh letter-spacing list. No dark-mode override named Connect. Token map (as R04): --sf -> --v2-surface; --p -> --v2-bg-subtle; --d -> --v2-border-soft; --mu -> --v2-text-muted; --se -> --v2-text-secondary; --ink -> --v2-text-primary; --c-text -> --v2-accent-text; --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; --f-display/-body/-mono -> --v2-font-display/--v2-font/--v2-font-mono. Markup unchanged: every handler, aria attribute and string stays (the page has no testids or t() strings). Exceptions taken: - phone: Copy (a 28px key with a 1px border) gets an invisible ::after ring, inset -9px from its 26px padding box, for a 44px hit area. - contrast: none needed. Muted mono labels on the code panel are 4.76:1 light and 5.5:1 dark. Carried unchanged as not drawn: Copy/secondary hover (border-strong, surface-hover, text-primary), CTA hover (ink-hover), link hover underline, and the zh letter-spacing reset for the lane h2, now in the page's sheet. Dropped as dead: .v2-connect__lane-heading p and .v2-connect__note (+ its code rule), which no markup renders (V2ConnectPage.test asserts none exist), and .v2-connect__cta:disabled (the CTA is a link). Tests: v2-layout-invariants re-points the Cursor rule header pins (span min-width 0, code overflow-wrap anywhere) and the B3 primary-act pin at V2ConnectPage.css; the latter now reads the frame's ink key (fill gradient, on-fill text). New describe 'Connect lifted from the d215 frame (R05)': sheet import + no Connect rule left in v2.css; the phone Copy ring; 44px phone links and acts; every negative-tracking selector in the sheet has a zh reset there (the v2.css zh test scans only v2.css). v2-type-floors scans V2ConnectPage.css; .v2-connect__number leaves its allowlist (now mono 11.5px). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3 --- .../v2/__tests__/v2-layout-invariants.test.ts | 60 +++++- .../src/v2/__tests__/v2-type-floors.test.ts | 8 +- frontend/src/v2/components/V2ConnectPage.css | 52 +++++ frontend/src/v2/components/V2ConnectPage.tsx | 1 + frontend/src/v2/v2.css | 185 +----------------- 5 files changed, 114 insertions(+), 192 deletions(-) create mode 100644 frontend/src/v2/components/V2ConnectPage.css diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 1ed25d4f7..f5c8bf422 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'); + // R05 lifted the Connect page onto the d215 frame (V2ConnectPage.css). + const connectCss = read('../components/V2ConnectPage.css'); const landing = read('../landing/v2-landing.css'); const podChat = read('../components/V2Thread.tsx'); const podsSidebar = read('../components/V2PodsSidebar.tsx'); @@ -297,8 +299,8 @@ describe('v2 layout invariants (CSS rule presence)', () => { // #2670: the Cursor rule card's header names an unbroken save path; without // these two declarations it pushes the Copy button out of the card at 390px. test('Cursor rule card header wraps its save path inside the card', () => { - expect(ruleBody(v2, '.v2-connect__rule .v2-connect__code-head span')).toContain('min-width: 0'); - expect(ruleBody(v2, '.v2-connect__rule .v2-connect__code-head code')).toContain('overflow-wrap: anywhere'); + expect(ruleBody(connectCss, '.v2-connect .v2-connect__rule .v2-connect__code-head span')).toContain('min-width: 0'); + expect(ruleBody(connectCss, '.v2-connect .v2-connect__rule .v2-connect__code-head code')).toContain('overflow-wrap: anywhere'); }); test('Your Team card name owns its line so the category chip cannot crush it', () => { @@ -1420,9 +1422,11 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(columnCount).not.toContain('border-radius:'); expect(columnCount).not.toContain('padding:'); - const connectCta = lastRuleBody(v2, '.v2-root .v2-connect__cta'); - expect(connectCta).toContain('background: var(--v2-ink)'); - expect(connectCta).toContain('color: var(--v2-on-ink)'); + // R05: Connect's primary act is the frame's ink key, the fill gradient + // (--v2-fill-*) with on-fill text, the same key as BYO's submit. + const connectCta = lastRuleBody(connectCss, '.v2-connect .v2-connect__cta'); + expect(connectCta).toContain('background: linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b))'); + expect(connectCta).toContain('color: var(--v2-on-fill)'); }); test('the board craft-pass buttons use icon components instead of unicode glyphs', () => { @@ -5439,3 +5443,49 @@ describe('pod board column scroll (TASK-698)', () => { expect(mobile).toMatch(/button\.v2-board__tab\.is-active \{ background: var\(--v2-surface\)/); }); }); + +describe('Connect lifted from the d215 frame (R05)', () => { + const v2 = read('../v2.css'); + const connectCss = read('../components/V2ConnectPage.css'); + const connect = read('../components/V2ConnectPage.tsx'); + + test('the page imports its own sheet and v2.css carries no Connect rule', () => { + expect(connect).toContain("import './V2ConnectPage.css';"); + // `v2-connect__` and the bare root; `v2-connector*` is a different surface. + expect(v2).not.toMatch(/\.v2-connect(__|[\s,{:])/); + }); + + // Deviation from the frame: Copy is a 28px key, so on a phone an invisible + // ::after ring makes its hit area 44px. With the key's 1px border, `inset` + // measures from the 26px padding box, so -9px is what reaches 44. + test('on a phone, Copy keeps its 28px key and gains a 44px hit ring', () => { + const key = ruleBody(connectCss, '.v2-connect .v2-connect__copy'); + expect(key).toContain('height: 28px'); + expect(key).toContain('border: 1px solid var(--v2-key-edge)'); + expect(key).toContain('position: relative'); + const phone = mediaBlockContaining(connectCss, '.v2-connect .v2-connect__copy::after'); + const ring = ruleBody(connectCss, '.v2-connect .v2-connect__copy::after', { within: phone }); + expect(ring).toContain('content: ""'); + expect(ring).toContain('position: absolute'); + expect(ring).toContain('inset: -9px'); + }); + + test('on a phone, links and the agent lane acts are 44px tall', () => { + const phone = mediaBlockContaining(connectCss, '.v2-connect .v2-connect__link'); + expect(ruleBody(connectCss, '.v2-connect .v2-connect__link', { within: phone })).toContain('min-height: 44px'); + expect(selectorRuleBody(phone, '.v2-connect .v2-connect__cta, ')).toContain('height: 44px'); + }); + + // The v2.css :lang(zh) reset (TASK-055) scans only v2.css, so the entry that + // left it with this page is held here: every negative-tracking selector in + // the sheet has a zh reset in the same sheet. + test('zh-CN: every negative-tracking Connect title is reset under :lang(zh)', () => { + const negative = [...connectCss.matchAll(/\n([^\n{}]+) \{[^}]*letter-spacing:\s*-[^;]+;/g)] + .flatMap((m) => m[1].split(',').map((sel) => sel.trim())) + .filter(Boolean); + expect(negative).toEqual(['.v2-connect .v2-connect__lane h2']); + for (const sel of negative) { + expect(ruleBody(connectCss, `.v2-root:lang(zh) ${sel}`)).toContain('letter-spacing: 0'); + } + }); +}); diff --git a/frontend/src/v2/__tests__/v2-type-floors.test.ts b/frontend/src/v2/__tests__/v2-type-floors.test.ts index 55d15f434..5c2152da2 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, R05 the +// Connect page's). +const CSS = ['../v2.css', '../components/V2AgentBYO.css', '../components/V2ConnectPage.css'] .map((rel) => fs.readFileSync(path.join(__dirname, rel), 'utf8')) .join('\n'); @@ -148,11 +149,12 @@ const ALLOWLIST: string[] = [ '.v2-runtime-row__byo', // 9.0px — no-mono-family '.v2-runtime-row__label', // 10.5px — no-mono-family // unassigned + // R05 took `.v2-connect__number` (11.0px, no family) to mono 500 11.5px + // with the Connect lift, so its entry left this list. '.v2-approval__badge', // 10.5px — mono-below-floor '.v2-approval__time', // 11.5px — no-mono-family '.v2-board__card-id', // 10.5px — no-mono-family '.v2-board__detail-update-author', // 11.5px — no-mono-family - '.v2-connect__number', // 11.0px — no-mono-family '.v2-filter-count', // 11.0px — no-mono-family '.v2-invite-card__meta', // 11.0px — no-mono-family '.v2-invite-link', // 11.0px — no-mono-family diff --git a/frontend/src/v2/components/V2ConnectPage.css b/frontend/src/v2/components/V2ConnectPage.css new file mode 100644 index 000000000..2d3b81876 --- /dev/null +++ b/frontend/src/v2/components/V2ConnectPage.css @@ -0,0 +1,52 @@ +/* Connect (/v2/connect), lifted from the d215 frame (R05): + docs/design/artboards/d215/index.html, section s-connect, frame "connect" 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-connect, the page's root. */ +.v2-connect { display: flex; flex-direction: column; gap: 16px; max-width: 960px; margin: 0 auto; } +.v2-connect .v2-connect__lane { display: flex; flex-direction: column; gap: 14px; padding: 20px; border-radius: 14px; background: var(--v2-surface); box-shadow: inset 0 0 0 1px var(--v2-border-soft); } +.v2-connect .v2-connect__lane-heading { display: flex; align-items: baseline; gap: 10px; } +.v2-connect .v2-connect__number { flex: none; font: 500 11.5px/1 var(--v2-font-mono); color: var(--v2-accent-text); } +.v2-connect .v2-connect__lane h2 { margin: 0; font: 800 16px/1.25 var(--v2-font-display); letter-spacing: -0.02em; color: var(--v2-text-primary); } +.v2-connect .v2-connect__code-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } +.v2-connect .v2-connect__code-card { min-width: 0; overflow: hidden; border-radius: 12px; background: var(--v2-bg-subtle); box-shadow: inset 0 0 0 1px var(--v2-border-soft); } +.v2-connect .v2-connect__code-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 44px; padding: 8px 8px 8px 14px; border-bottom: 1px solid var(--v2-border-soft); font: 500 11.5px/16px var(--v2-font-mono); color: var(--v2-text-muted); } +.v2-connect .v2-connect__code-card pre { margin: 0; padding: 12px 14px; font: 500 12.5px/21px var(--v2-font-mono); color: var(--v2-text-primary); white-space: pre-wrap; overflow-wrap: anywhere; } +.v2-connect .v2-connect__copy { all: unset; box-sizing: border-box; cursor: pointer; position: relative; flex: none; display: inline-flex; align-items: center; justify-content: center; height: 28px; padding: 0 12px; border-radius: 8px; font: 600 12.5px/1 var(--v2-font); color: var(--v2-text-primary); background: linear-gradient(180deg, var(--v2-key-a), var(--v2-key-b)); border: 1px solid var(--v2-key-edge); box-shadow: var(--v2-key-inset); } +.v2-connect .v2-connect__rule .v2-connect__code-head span { min-width: 0; } +.v2-connect .v2-connect__rule .v2-connect__code-head code { font: 500 11.5px/16px var(--v2-font-mono); color: var(--v2-text-primary); overflow-wrap: anywhere; } +.v2-connect .v2-connect__cursor { display: flex; flex-wrap: wrap; align-items: center; gap: 0 12px; font-size: 13px; line-height: 20px; color: var(--v2-text-secondary); } +.v2-connect .v2-connect__link { display: inline-flex; align-items: center; min-height: 32px; font: 600 13px/20px var(--v2-font); color: var(--v2-accent-text); text-decoration: none; } +.v2-connect .v2-connect__lane > .v2-connect__link, .v2-connect .v2-connect__lane > .v2-connect__secondary { align-self: flex-start; } +.v2-connect .v2-connect__actions { display: flex; flex-wrap: wrap; gap: 8px; } +.v2-connect .v2-connect__cta, .v2-connect .v2-connect__secondary { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; height: 34px; padding: 0 13px; border-radius: 10px; font: 600 13px/1 var(--v2-font); white-space: nowrap; text-decoration: none; cursor: pointer; } +.v2-connect .v2-connect__cta { color: var(--v2-on-fill); background: linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b)); border: 1px solid var(--v2-fill-edge); } +.v2-connect .v2-connect__secondary { color: var(--v2-text-primary); background: linear-gradient(180deg, var(--v2-key-a), var(--v2-key-b)); border: 1px solid var(--v2-key-edge); box-shadow: var(--v2-key-inset); } +@media (max-width: 760px) { + .v2-connect { gap: 10px; } + .v2-connect .v2-connect__lane { padding: 16px; } + .v2-connect .v2-connect__code-grid { grid-template-columns: minmax(0, 1fr); } + .v2-connect .v2-connect__link { min-height: 44px; } + .v2-connect .v2-connect__actions { flex-direction: column; } + .v2-connect .v2-connect__cta, .v2-connect .v2-connect__lane > .v2-connect__secondary, .v2-connect .v2-connect__secondary { align-self: stretch; height: 44px; font-size: 14px; } + /* Not in the frame: a 44px hit area around the 28px Copy key. With its 1px + border, `inset` measures from the padding box (26px), so -9px makes 44. */ + .v2-connect .v2-connect__copy::after { content: ""; position: absolute; inset: -9px; } +} + +/* Live behaviour the frame does not draw, kept working with prod's values. */ +.v2-connect .v2-connect__copy:hover, +.v2-connect .v2-connect__secondary:hover { + border-color: var(--v2-border-strong); + background: var(--v2-surface-hover); + color: var(--v2-text-primary); +} +.v2-connect .v2-connect__cta:hover { + border-color: var(--v2-ink-hover); + background: var(--v2-ink-hover); +} +.v2-connect .v2-connect__link:hover { text-decoration: underline; } +/* CJK never takes negative letter-spacing (TASK-055); this entry left the + v2.css :lang(zh) reset list with the rest of the page. */ +.v2-root:lang(zh) .v2-connect .v2-connect__lane h2 { letter-spacing: 0; } diff --git a/frontend/src/v2/components/V2ConnectPage.tsx b/frontend/src/v2/components/V2ConnectPage.tsx index 5f8abfe35..9fa883c5d 100644 --- a/frontend/src/v2/components/V2ConnectPage.tsx +++ b/frontend/src/v2/components/V2ConnectPage.tsx @@ -4,6 +4,7 @@ import getApiBaseUrl from '../../utils/apiBaseUrl'; import { useAuth } from '../../context/AuthContext'; import cursorMcpInstallLink from '../utils/cursorMcpInstallLink'; import { CURSOR_PROJECT_RULE, CURSOR_PROJECT_RULE_PATH } from '../utils/cursorProjectRule'; +import './V2ConnectPage.css'; type CopyKey = 'cli' | 'mcp' | 'cursorRule'; diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index c113e3627..2c65806b2 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -6336,188 +6336,6 @@ body.modern-ui.v2-canvas { border: 1px solid var(--v2-ink); } -/* ---------- Connect — CLI, MCP, and agent runtime entry points ---------- */ - -.v2-connect { - display: flex; - flex-direction: column; - gap: 16px; - max-width: 960px; - margin: 0 auto; -} - -.v2-connect__lane { - padding: 20px; - border: 1px solid var(--v2-border); - border-radius: var(--v2-radius-lg); - background: var(--v2-surface); -} - -.v2-connect__lane-heading { - display: flex; - align-items: flex-start; - gap: 12px; -} - -.v2-connect__number { - flex: 0 0 auto; - padding-top: 3px; - color: var(--v2-accent-text); - font-family: "SF Mono", ui-monospace, SFMono-Regular, Menlo, monospace; - font-size: 11px; - font-weight: 700; -} - -.v2-connect__lane h2 { - margin: 0; - color: var(--v2-text-primary); - font-size: 16px; - font-weight: 700; - letter-spacing: -0.01em; -} - -.v2-connect__lane-heading p, -.v2-connect__note { - margin: 5px 0 0; - color: var(--v2-text-secondary); - font-size: 13px; - line-height: 1.5; -} - -.v2-connect__code-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; - margin-top: 18px; -} - -.v2-connect__code-card { - overflow: hidden; - border: 1px solid var(--v2-border-soft); - border-radius: var(--v2-radius); - background: var(--v2-bg-subtle); -} - -.v2-connect__code-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 8px 10px; - border-bottom: 1px solid var(--v2-border-soft); - color: var(--v2-text-secondary); - font-size: 12px; - font-weight: 650; -} - -.v2-connect__code-card pre { - min-height: 92px; - margin: 0; - padding: 12px; - overflow-x: auto; - color: var(--v2-text-primary); - font-family: "SF Mono", ui-monospace, SFMono-Regular, Menlo, monospace; - font-size: 12px; - line-height: 1.55; - white-space: pre-wrap; - word-break: break-word; -} - -.v2-root button.v2-connect__copy, -.v2-root .v2-connect__secondary, -.v2-root .v2-connect__cta { - display: inline-flex; - align-items: center; - justify-content: center; - min-height: 34px; - padding: 7px 11px; - border-radius: var(--v2-radius-sm); - font-size: 13px; - font-weight: 650; - text-decoration: none; - cursor: pointer; -} - -.v2-root button.v2-connect__copy, -.v2-root .v2-connect__secondary { - border: 1px solid var(--v2-border); - background: var(--v2-surface); - color: var(--v2-text-secondary); -} - -.v2-root button.v2-connect__copy:hover, -.v2-root .v2-connect__secondary:hover { - border-color: var(--v2-border-strong); - background: var(--v2-surface-hover); - color: var(--v2-text-primary); -} - -.v2-root .v2-connect__cta { - border: 1px solid var(--v2-ink); - background: var(--v2-ink); - color: var(--v2-on-ink); -} - -.v2-root .v2-connect__cta:hover:not(:disabled) { - border-color: var(--v2-ink-hover); - background: var(--v2-ink-hover); -} - -.v2-root .v2-connect__cta:disabled { - cursor: default; - opacity: 0.6; -} - -.v2-connect__note { - margin-top: 12px; -} - -.v2-connect__note code { - font-family: "SF Mono", ui-monospace, SFMono-Regular, Menlo, monospace; -} - -.v2-root a.v2-connect__link { - display: inline-block; - margin-top: 12px; - color: var(--v2-accent-text); - font-size: 13px; - font-weight: 650; - text-decoration: none; -} - -.v2-root a.v2-connect__link:hover { - text-decoration: underline; -} - -.v2-connect__cursor { - display: flex; - flex-wrap: wrap; - align-items: baseline; - gap: 4px 12px; - margin-top: 12px; - color: var(--v2-text-tertiary); - font: 400 13px/20px var(--v2-font); -} -.v2-root .v2-connect__cursor .v2-connect__link { margin-top: 0; } -.v2-connect__rule { margin-top: 12px; } -.v2-connect__rule .v2-connect__code-head span { min-width: 0; } -.v2-connect__rule .v2-connect__code-head code { - font-family: "SF Mono", ui-monospace, SFMono-Regular, Menlo, monospace; - overflow-wrap: anywhere; -} - -.v2-connect__actions { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 16px; -} - -@media (max-width: 760px) { - .v2-connect__lane { padding: 16px; } - .v2-connect__code-grid { grid-template-columns: 1fr; } -} - /* ---------- Admin · Users (waitlist + invitation codes) ---------- */ .v2-admin-users { display: flex; @@ -9379,8 +9197,7 @@ body.modern-ui.v2-canvas { .v2-root:lang(zh) .v2-activity__title, .v2-root:lang(zh) .v2-activity__section h2, .v2-root:lang(zh) .v2-activity__agent-card h3, -.v2-root:lang(zh) .v2-activity__board-row h3, -.v2-root:lang(zh) .v2-connect__lane h2 { +.v2-root:lang(zh) .v2-activity__board-row h3 { letter-spacing: 0; } From 34ae6687225ebe3e912ce88fe092995aaf7f96c8 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 02:12:09 -0800 Subject: [PATCH 3/3] test(v2): measure the connect targets; pin the phone key rule's full selector (R05) - targets.json: the connect rows are now measured from the artboard (measure_after.js): 20 helper words at both widths, scroll 1.09 at 1440 (hand estimate 1.08) and 1.77 at 390. - The phone 44px rule is pinned with its whole selector list, so it fails if the secondary key drops out of it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3 --- docs/design/artboards/d215/targets.json | 2 +- frontend/src/v2/__tests__/v2-layout-invariants.test.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/design/artboards/d215/targets.json b/docs/design/artboards/d215/targets.json index 004085e04..6bb5d9ecb 100644 --- a/docs/design/artboards/d215/targets.json +++ b/docs/design/artboards/d215/targets.json @@ -73,7 +73,7 @@ }, "connect-1440": { "helperWords": 20, - "innerRatio": 1.08 + "innerRatio": 1.09 }, "connect-390": { "helperWords": 20, diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index f5c8bf422..1b5e515e6 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -5474,6 +5474,8 @@ describe('Connect lifted from the d215 frame (R05)', () => { const phone = mediaBlockContaining(connectCss, '.v2-connect .v2-connect__link'); expect(ruleBody(connectCss, '.v2-connect .v2-connect__link', { within: phone })).toContain('min-height: 44px'); expect(selectorRuleBody(phone, '.v2-connect .v2-connect__cta, ')).toContain('height: 44px'); + // The same rule must size both keys, the CTA and the secondary, not the CTA alone. + expect(phone).toContain('.v2-connect .v2-connect__cta, .v2-connect .v2-connect__lane > .v2-connect__secondary, .v2-connect .v2-connect__secondary { align-self: stretch; height: 44px;'); }); // The v2.css :lang(zh) reset (TASK-055) scans only v2.css, so the entry that