diff --git a/frontend/design-system/tokens.css b/frontend/design-system/tokens.css index eaf291791b..72d2abc411 100644 --- a/frontend/design-system/tokens.css +++ b/frontend/design-system/tokens.css @@ -103,6 +103,7 @@ --c-key-edge: rgba(15, 23, 42, 0.14); --c-key-inset: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 1px 2px rgba(16, 24, 40, 0.06); --c-shadow-pane: 0 0 0 1px rgba(16, 24, 40, 0.07), 0 1px 2px rgba(16, 24, 40, 0.04), 0 12px 28px -20px rgba(16, 24, 40, 0.18); + --c-ring-card: 0 0 0 1px var(--c-border-soft), 0 1px 2px rgba(16, 24, 40, 0.04); /* card edge: ring + 1px lift */ --c-shadow-pop: 0 0 0 1px rgba(16, 24, 40, 0.08), 0 2px 4px rgba(16, 24, 40, 0.04), 0 18px 36px -14px rgba(16, 24, 40, 0.28); --c-scrim: rgba(16, 24, 40, 0.18); --c-av-ring: rgba(16, 24, 40, 0.07); diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 7b2f3cf60b..72c7e305c5 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -2440,6 +2440,7 @@ } }, "connect": { + "intro": "MCP uses an agent token created during setup.", "cliHeading": "Connect the CLI or MCP", "copy": "Copy", "copied": "Copied", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 81b62352a4..c258761c03 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -2428,6 +2428,7 @@ } }, "connect": { + "intro": "MCP 用的是接入时创建的智能体密钥。", "cliHeading": "连接 CLI 或 MCP", "copy": "复制", "copied": "已复制", diff --git a/frontend/src/v2/V2App.tsx b/frontend/src/v2/V2App.tsx index cb52d50ba1..193dc97b3d 100644 --- a/frontend/src/v2/V2App.tsx +++ b/frontend/src/v2/V2App.tsx @@ -201,7 +201,7 @@ const V2AgentProfileRoute: React.FC = () => { return ( {feature(t('common.pageTitle.agentProfile'), page, false, false)} + ? (page) => {feature(t('common.pageTitle.agentProfile'), page, false, false, 'v2-feature--bar v2-feature--aprofile')} : undefined} /> ); @@ -346,6 +346,7 @@ const V2App: React.FC = () => { /* V2YourTeamPage owns its own header — suppress the generic V2FeaturePage chrome to avoid stacked titles. */ false, + 'v2-feature--bar', )} /> { /> } + element={} /> { /> , false, false, 'v2-feature--run-trace')} + element={feature(t('runTrace.title'), , false, false, 'v2-feature--bar v2-feature--run-trace')} /> { /> , false, false, 'v2-feature--profile')} + element={feature(t('common.pageTitle.profile'), , false, false, 'v2-feature--bar v2-feature--profile')} /> { expect(rd('../agents/v2-agent-profile.css')).toMatch(/\.v2-aprofile__bar\s*\{[^}]*background: var\(--v2-surface\)/); }); test('ink primary buttons take theme tokens, never a literal colour', () => { - // R06 lifted Your team onto the d215 frame: Hire and Answer are its ink - // key, the --v2-fill-* gradient with --v2-on-fill text, in the page's sheet. - const css = rd('../components/V2YourTeamPage.css'); - const rule = css.match(/\n\.v2-team \.v2-team__hire, \.v2-team \.v2-team-card__answer \{[^}]*\}/); + // Rulings 11-04: Your team's Hire and Answer are the v2p ink key + // (.v2p-b-p), the --v2-fill-* gradient with --v2-on-fill text. + expect(rd('../components/V2YourTeamPage.tsx')).toContain('v2p-b v2p-b-p v2p-b-sm v2-team__hire'); + const css = rd('../primitives/primitives.css'); + const rule = css.match(/\n\.v2p-b-p \{[^}]*\}/); expect(rule?.[0]).toContain('background: linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b))'); expect(rule?.[0]).toContain('color: var(--v2-on-fill)'); - expect(rule?.[0]).not.toMatch(/#[0-9a-f]{3,8}\b|rgba?\(/i); + expect(rule?.[0]).not.toMatch(/#[0-9a-f]{3,8}\b/i); + // The fill and text declarations are exactly the two tokenised ones: any added or changed + // background or colour (a literal, a named colour, an override) fails, with no colour list to keep. + const paint = [...(rule?.[0] ?? '').matchAll(/(?:^|[;{\s])(background(?:-color|-image)?|color):\s*([^;}]+)/g)] + .map(([, prop, value]) => [prop, value.trim()]).sort(); + expect(paint).toEqual([ + ['background', 'linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b))'], + ['color', 'var(--v2-on-fill)'], + ]); + // Your team's own sheet adds no colour to the ink keys. + expect(rd('../components/V2YourTeamPage.css')).not.toMatch(/v2-team__hire[^{]*\{[^}]*(?:background|color):/); }); test('the board drawer scrim is --v2-scrim', () => { // S4: the board's rules live in its own sheet; the scrim backs the phone sheet. diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 2109a79751..979ba256e6 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -167,11 +167,14 @@ describe('v2 layout invariants (CSS rule presence)', () => { const phone = css.slice(css.lastIndexOf('@media (max-width: 760px)')); expect(phone).toMatch(/\.v2-aprofile__cta-row \.v2-aprofile__btn \{[^}]*height: 44px/); }); - test('/v2/profile: on a phone the header wraps and its one key takes the full row (d215 s-profile 390)', () => { - const profile = read('../components/V2PersonProfile.css'); - const phone = profile.slice(profile.indexOf('@media (max-width: 760px)')); - expect(phone).toMatch(/\.v2-profile \.v2-profile__head \{[^}]*flex-wrap: wrap/); - expect(phone).toMatch(/\.v2-profile \.v2-profile__follow, \.v2-profile \.v2-profile__edit \{[^}]*flex: 1 0 100%/); + test('/v2/profile: the head is the shared bar, and on a phone its one key takes the full row (rulings 11-04)', () => { + // The page draws V2PageHead with the shared bar class and the wrap variant; + // the full-row key on a phone is the shared rule in v2.css, not a page copy. + expect(read('../components/V2PersonProfile.tsx')).toContain('className="v2-feature__header v2-page-head--wrap v2-profile__head"'); + const v2 = readV2Css(); + const phone = mediaBlockContaining(v2, '.v2-feature__header.v2-page-head--wrap > .v2-page-head__actions'); + expect(ruleBody(v2, '.v2-feature__header.v2-page-head--wrap', { within: phone })).toContain('flex-wrap: wrap'); + expect(ruleBody(v2, '.v2-feature__header.v2-page-head--wrap > .v2-page-head__actions', { within: phone })).toContain('flex: 1 0 100%'); }); test('/v2/activity and /v2/artifacts: the shared phone head bar left v2.css with both pages (d215 builds)', () => { @@ -1730,19 +1733,19 @@ describe('v2 layout invariants (CSS rule presence)', () => { const grid = ruleBody(teamCss, '.v2-team .v2-team__grid'); expect(grid).toContain('grid-template-columns: repeat(3, minmax(0, 1fr))'); expect(grid).toContain('gap: 12px'); - expect(ruleBody(teamCss, '.v2-team .v2-team-card--needsYou')).toContain('border: 2px solid var(--v2-accent)'); + // Rulings 11-04: the needs-you card is the 2px cobalt ring (.v2p-tc.picked), not a border. + expect(ruleBody(teamCss, '.v2-team .v2-team-card--needsYou')).toContain('box-shadow: 0 0 0 2px var(--v2-accent)'); expect(ruleBody(teamCss, '.v2-team .v2-team-card__mark--needsYou')).toContain('var(--v2-accent)'); expect(ruleBody(teamCss, '.v2-team .v2-team-card__mark--working')).toContain('var(--v2-text-primary)'); expect(ruleBody(teamCss, '.v2-team .v2-team-card__mark--idle')).toContain('var(--v2-border-soft)'); - // R06: Hire and Answer share the frame's ink key, the fill gradient - // (--v2-fill-*) with on-fill text, the same key as BYO's submit and - // Connect's CTA; Talk (and Bring your own) is the bordered plain key. - const ink = selectorRuleBody(teamCss, '\n.v2-team .v2-team__hire, .v2-team .v2-team-card__answer {'); - expect(ink).toContain('background: linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b))'); - expect(ink).toContain('color: var(--v2-on-fill)'); - const plain = selectorRuleBody(teamCss, '\n.v2-team .v2-team__byo, .v2-team .v2-team-card__talk {'); - expect(plain).toContain('border: 1px solid var(--v2-key-edge)'); - expect(plain).not.toContain('var(--v2-fill-'); + // Rulings 11-04: Hire and Answer are the v2p ink key (.v2p-b-p), Talk and + // Bring your own the plain key (.v2p-b-s), both at the small size; the page + // sheet no longer draws its own keys. + expect(yourTeamPage).toContain('className="v2p-b v2p-b-p v2p-b-sm v2-team__hire"'); + expect(yourTeamPage).toContain('className="v2p-b v2p-b-p v2p-b-sm v2-team-card__answer"'); + expect(yourTeamPage).toContain('className="v2p-b v2p-b-s v2p-b-sm v2-team-card__talk"'); + expect(yourTeamPage).toContain('className="v2p-b v2p-b-s v2p-b-sm v2-team__byo"'); + expect(teamCss).not.toMatch(/\.v2-team__hire, \.v2-team \.v2-team-card__answer \{/); expect(ruleBody(teamCss, '.v2-team .v2-team-card__status')).toContain('var(--v2-font-mono)'); // ux-lead 66246: the ask wraps, the command wraps at mono 14, the name is display-18, // the mark is 600, and the title stacks over its meta on a phone. @@ -1758,7 +1761,7 @@ describe('v2 layout invariants (CSS rule presence)', () => { // Read the rule inside the team's phone block — a `[\s\S]*?` regex across // the sheet passed with the rule deleted (sprint-review at 58fb4147). const teamPhone = teamPhoneBlock(teamCss); - expect(ruleBody(teamCss, '.v2-team .v2-team__heading', { within: teamPhone })).toContain('flex-direction: column'); + expect(ruleBody(teamCss, '.v2-team .v2-team__head > .v2-page-head__identity', { within: teamPhone })).toContain('flex-direction: column'); expect(ruleBody(teamCss, '.v2-team .v2-team__grid', { within: teamPhone })).toContain('repeat(1, minmax(0, 1fr))'); // The retired surfaces: feature rows, standard cards with icon buttons, quiet rows, green dot. for (const css of [v2, teamCss]) { @@ -3377,13 +3380,14 @@ describe('Bring your own agent onto Signal (TASK-166)', () => { expect(ruleBody(byoCss, '.v2-feature--byo .v2-byo__copy:hover')).toContain('var(--v2-surface-hover)'); expect(ruleBody(byoCss, '.v2-feature--byo select.v2-byo__input:focus-visible')).toContain('var(--v2-focus-ring)'); }); - test('the submit button is the ink key, at radius 10 (item 1)', () => { + test('the submit button is the ink key, at the small key r8 (item 1, rulings 11-04)', () => { const submit = cssDeclarations(ruleBody(byoCss, '.v2-feature--byo .v2-byo__submit')); expect(submit.background).toBe('linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b))'); expect(submit.color).toBe('var(--v2-on-fill)'); expect(submit.border).toBe('1px solid var(--v2-fill-edge)'); const shared = cssDeclarations(selectorRuleBody(byoCss, '.v2-feature--byo .v2-byo__submit, .v2-feature--byo .v2-byo__secondary')); - expect(shared['border-radius']).toBe('10px'); + expect(shared['border-radius']).toBe('var(--v2-r-8)'); + expect(shared.height).toBe('28px'); expect(shared.cursor).toBe('pointer'); // The old rules left v2.css with the lift. expect(v2).not.toContain('button.v2-byo__submit'); @@ -3625,9 +3629,9 @@ describe('the BYO after-submit surfaces onto Signal (TASK-169)', () => { expect(decls(ruleBody(byoCss, '.v2-byo__stat-label')).font).toBe('500 11px/16px var(--v2-font-mono)'); }); - test('item 3: the secondary button is a control, at the frame key radius', () => { + test('item 3: the secondary button is a control, at the small key radius', () => { expect(decls(selectorRuleBody(byoCss, '.v2-feature--byo .v2-byo__submit, .v2-feature--byo .v2-byo__secondary'))['border-radius']) - .toBe('10px'); + .toBe('var(--v2-r-8)'); expect(decls(ruleBody(byoCss, '.v2-feature--byo .v2-byo__secondary')).color).toBe('var(--v2-text-secondary)'); }); @@ -3663,7 +3667,10 @@ describe('the BYO after-submit surfaces onto Signal (TASK-169)', () => { expect(decls(ruleBody(byoCss, '.v2-byo__add-computer-row')).background).toBe('var(--v2-ink)'); expect(decls(ruleBody(byoCss, '.v2-feature--byo .v2-byo__add-computer-row .v2-byo__copy')).position).toBe('static'); const addComputer = decls(ruleBody(byoCss, '.v2-byo__add-computer')); - expect(addComputer['border-radius']).toBe('var(--v2-radius-lg)'); + // Rulings 11-04: a card is the D215 ring and soft shadow at r9, no 1px border. + expect(addComputer['border-radius']).toBe('var(--v2-r-9)'); + expect(addComputer.border).toBeUndefined(); + expect(addComputer['box-shadow']).toBe('var(--v2-ring-card)'); expect(addComputer.background).toBe('var(--v2-bg-subtle)'); // Each snippet is a flat section, not a card. const snippet = decls(selectorRuleBody(byoCss, '.v2-feature--byo .v2-byo__form, .v2-feature--byo .v2-byo__result, .v2-feature--byo .v2-byo__snippet')); @@ -5754,7 +5761,8 @@ describe('Connect lifted from the d215 frame (R05)', () => { const negative = [...connectCss.matchAll(/\n([^\n{}]+) \{[^}]*letter-spacing:\s*-[^;]+;/g)] .flatMap((m) => m[1].split(',').map((sel) => sel.trim())) .filter(Boolean); - expect(negative).toEqual(['.v2-connect .v2-connect__lane h2']); + // The lane label is mono at 0 tracking now (rulings 11-04), so nothing needs a reset. + expect(negative).toEqual([]); for (const sel of negative) { expect(ruleBody(connectCss, `.v2-root:lang(zh) ${sel}`)).toContain('letter-spacing: 0'); } @@ -5779,12 +5787,14 @@ describe('Your team lifted from the d215 frame (R06)', () => { // erase the ring. test('the keyed controls stay class-only so the shared focus ring outranks their shadow', () => { expect(teamCss).not.toMatch(/button\.v2-team(__byo|__hire|-card__answer|-card__talk|-card__mark)/); - expect(selectorRuleBody(teamCss, '\n.v2-team .v2-team__byo, .v2-team .v2-team-card__talk {')).toContain('box-shadow: var(--v2-key-inset)'); + // The keys are the v2p primitives, whose own :focus-visible ring outranks the key shadow. + expect(team).toContain('v2p-b v2p-b-s v2p-b-sm v2-team__byo'); }); test('on a phone, every drawn act and the avatar mark are 44px', () => { + // The acts are v2p keys: the primitives' phone rule sizes them (see the 44 phone-key test). + expect(team).not.toMatch(/className="v2-team(__byo|__hire|-card__answer|-card__talk)"/); const phone = mediaBlockContaining(teamCss, '.v2-team .v2-team__grid'); - expect(selectorRuleBody(phone, '.v2-team .v2-team__byo, .v2-team .v2-team__hire, ')).toContain('height: 44px'); expect(ruleBody(teamCss, '.v2-team .v2-team-card__mark', { within: phone })).toContain('width: 44px'); expect(ruleBody(teamCss, '.v2-team .v2-team-card__mark', { within: phone })).toContain('height: 44px'); }); @@ -5820,12 +5830,12 @@ describe('Your team lifted from the d215 frame (R06)', () => { const negative = [...teamCss.matchAll(/\n([^\n{}]+) \{[^}]*letter-spacing:\s*-[^;]+;/g)] .flatMap((m) => m[1].split(',').map((sel) => sel.trim())) .filter(Boolean); + // The page title is the shared bar's .v2-feature__title, reset in v2.css. expect(negative).toEqual([ - '.v2-team .v2-team__title', '.v2-team .v2-team-card__name', '.v2-team .v2-team__empty-title', ]); - const start = teamCss.indexOf('.v2-root:lang(zh) .v2-team .v2-team__title'); + const start = teamCss.indexOf('.v2-root:lang(zh) .v2-team .v2-team-card__name'); expect(start).toBeGreaterThan(-1); const reset = teamCss.slice(start, teamCss.indexOf('}', start)); expect(reset).toContain('letter-spacing: 0'); @@ -5883,23 +5893,29 @@ describe('chat sheets lifted out of v2.css (S3)', () => { }); }); -// d215 run-trace-after at 390: the page sits in the shared V2FeaturePage body, -// whose phone padding is 12px all round. The frame draws 16px gutters with no -// top padding ("Back to pod" is the first 44px row) and a tighter rhythm; at -// prod 486240fb the 12px body and loose rhythm pushed the foot's run-id line -// under the fixed tab bar. Measured in a browser; jsdom cannot see it. -describe('run trace keeps the frame phone gutters and clears the tab bar', () => { +// d215 run-trace-after at 390. The frame drew 16px gutters with "Back to pod" +// as the first row; at prod 486240fb a loose rhythm pushed the foot's run-id +// line under the fixed tab bar. Rulings 11-04 move it onto the shared head bar +// (back is the bar's lead key) and the 12px phone gutter of .v2-feature__scroll. +// Measured in a browser; jsdom cannot see it. +describe('run trace keeps the 12px phone gutter and clears the tab bar', () => { const rt = read('../components/V2RunTrace.css'); const v2 = readV2Css(); const app = read('../V2App.tsx'); - const phone = mediaBlockContaining(rt, '.v2-feature--run-trace > .v2-feature__body'); + const page = read('../components/V2RunTrace.tsx'); + const phone = mediaBlockContaining(rt, '.v2-feature--run-trace .v2-run-trace p'); - test('the run trace route tags its feature pane', () => { - expect(app).toContain("feature(t('runTrace.title'), , false, false, 'v2-feature--run-trace')"); + test('the run trace route tags its feature pane and draws the shared head bar', () => { + expect(app).toContain("feature(t('runTrace.title'), , false, false, 'v2-feature--bar v2-feature--run-trace')"); + expect(page).toContain('className="v2-feature__header v2-run-trace__head"'); + expect(page).toContain('className="v2p-b v2p-b-g v2p-ib v2-run-trace__back"'); }); - test('at 760 and below the body has 16px side gutters and no top padding', () => { - expect(ruleBody(rt, '.v2-feature--run-trace > .v2-feature__body', { within: phone })).toContain('padding: 0 16px 16px'); + test('at 760 and below the body has the 12px gutter: the shared scroller, no route override', () => { + expect(page).toContain('
'); + expect(ruleBody(v2, '.v2-feature__scroll', { within: mediaBlockContaining(v2, ' .v2-feature__scroll {') })).toContain('padding: 12px 12px 16px'); + expect(rt).not.toMatch(/\.v2-feature__body/); + expect(rt).not.toMatch(/padding: 0 16px/); }); test('the frame rhythm: paragraphs flush, a turn is an 8px grid', () => { @@ -5910,7 +5926,6 @@ describe('run trace keeps the frame phone gutters and clears the tab bar', () => }); test('desktop is untouched: the overrides live only in the phone block', () => { - expect(topLevelRuleBody(rt, '.v2-feature--run-trace > .v2-feature__body')).toBe(''); expect(topLevelRuleBody(rt, '.v2-feature--run-trace .v2-run-trace p')).toBe(''); }); @@ -6011,6 +6026,167 @@ describe('sign-in handoff drawn from the d215 s-handoff frames', () => { }); }); +// ui-audit-1104 findings 2–8, rulings 11-04: the secondary pages take the +// reference surfaces' head bar, keys, edges and labels. jsdom cannot see any of +// this; the browser check measured it, and these pin the rules it rests on. +describe('secondary pages on the reference grammar (ui-audit-1104 findings 2–8)', () => { + const v2 = readV2Css(); + const primitives = read('../primitives/primitives.css'); + const app = read('../V2App.tsx'); + + test('finding 2: the head bar title is Bricolage 24/800 on desktop, not the Plex fallback', () => { + // The desktop rule (line start, outside any media query) names the display face. + const title = ruleBody(v2, '.v2-feature__title'); + expect(title).toContain('font: 800 24px/1 var(--v2-font-display)'); + expect(title).toContain('letter-spacing: -0.03em'); + expect(v2.indexOf('\n.v2-feature__title {')).toBeLessThan(v2.indexOf('@media (max-width: 760px) {\n .v2-feature__header {')); + // The bar: 64px, the title 28px in from the pane's 1px edge, a divider, the count in mono 11.5. + const bar = ruleBody(v2, '.v2-feature__header'); + expect(bar).toContain('min-height: 64px'); + expect(bar).toContain('padding: 0 20px 0 28px'); + expect(bar).toContain('border-bottom: 1px solid var(--v2-border-soft)'); + expect(ruleBody(v2, '.v2-feature__header .v2-page-head__meta')).toContain('font: 500 11.5px/16px var(--v2-font-mono)'); + }); + + test('finding 3: Your team and both profiles draw the shared bar, and no head carries the "i" hint', () => { + expect(read('../components/V2YourTeamPage.tsx')).toContain('className="v2-feature__header v2-page-head--wrap v2-team__head"'); + expect(read('../components/V2PersonProfile.tsx')).toContain('className="v2-feature__header v2-page-head--wrap v2-profile__head"'); + expect(read('../agents/V2AgentProfile.tsx')).toContain("'v2-aprofile__hero v2-page-head v2-feature__header v2-page-head--wrap'"); + // Each of those routes opts the pane out of the body inset so the bar meets the pane edge. + expect(app).toMatch(/'Your Team',[\s\S]{0,300}'v2-feature--bar'/); + expect(app).toContain("'v2-feature--bar v2-feature--profile'"); + expect(app).toContain("'v2-feature--bar v2-feature--aprofile'"); + expect(ruleBody(v2, '.v2-feature--bar > .v2-feature__body')).toContain('padding: 0'); + expect(ruleBody(v2, '.v2-feature__scroll')).toContain('overflow-y: auto'); + // The flex chain that lets the scroller scroll: each link fills and may shrink below its content. + const barBody = ruleBody(v2, '.v2-feature--bar > .v2-feature__body'); + for (const decl of ['display: flex', 'flex-direction: column', 'overflow: hidden']) expect(barBody).toContain(decl); + const legacy = ruleBody(v2, '.v2-feature--bar .v2-feature__legacy'); + for (const decl of ['flex: 1 1 auto', 'min-height: 0', 'display: flex', 'flex-direction: column']) expect(legacy).toContain(decl); + const scroll = ruleBody(v2, '.v2-feature__scroll'); + for (const decl of ['flex: 1 1 auto', 'min-height: 0']) expect(scroll).toContain(decl); + // BYO and Connect moved the hint to an intro line. + expect(app).not.toMatch(/{t('connect.intro')}

"); + }); + + test('finding 5: every v2p key and segment is 44px tall on a phone', () => { + const phone = mediaBlockContaining(primitives, '.v2p-b, .v2p-b-sm {'); + expect(ruleBody(primitives, '.v2p-b, .v2p-b-sm', { within: phone })).toContain('height: 44px'); + const icon = ruleBody(primitives, '.v2p-ib, .v2p-ib.v2p-b-sm', { within: phone }); + expect(icon).toContain('width: 44px'); + expect(icon).toContain('height: 44px'); + expect(ruleBody(primitives, '.v2p-seg button, .v2p-seg--sm button', { within: phone })).toContain('height: 44px'); + // After the desktop sizes, so the phone rule wins at equal specificity. + for (const desktop of ['\n.v2p-b-sm {', '\n.v2p-ib.v2p-b-sm {', '\n.v2p-seg--sm button {']) { + expect(primitives.indexOf(phone)).toBeGreaterThan(primitives.indexOf(desktop)); + } + }); + + test('finding 5: a phone icon key is square, 44x44 from the primitives or the D223 32x32 in chat, never 32x44', () => { + // The primitives' phone icon key grows on both axes together. + const phone = mediaBlockContaining(primitives, '.v2p-b, .v2p-b-sm {'); + const icon = ruleBody(primitives, '.v2p-ib, .v2p-ib.v2p-b-sm', { within: phone }); + expect(icon.match(/width: (\d+)px/)?.[1]).toBe(icon.match(/height: (\d+)px/)?.[1]); + // The chat keys that carry the D223 44px ::after hit area draw at 32x32 on a + // phone. The primitives' 44px height alone would leave them 32 wide and 44 tall. + const sizesOn = (css: string, key: string) => { + const sizes: { width?: string; height?: string } = {}; + const flat = css.replace(/\/\*[\s\S]*?\*\//g, ''); + for (let at = flat.indexOf('@media (max-width: 760px)'); at !== -1; at = flat.indexOf('@media (max-width: 760px)', at + 1)) { + const block = blockAt(flat, at); + const inner = block.slice(block.indexOf('{') + 1, -1); + for (const [, selectors, body] of inner.matchAll(/([^{}]+)\{([^{}]*)\}/g)) { + if (!selectors.split(',').map((one) => one.trim()).includes(`.v2-root button.${key}`)) continue; + const width = /(?:^|[;\s])width:\s*(\d+px)/.exec(body)?.[1]; + const height = /(?:^|[;\s])height:\s*(\d+px)/.exec(body)?.[1]; + if (width) sizes.width = width; + if (height) sizes.height = height; + } + } + return sizes; + }; + const pane = read('../chat/chat-pane.css'); + const composer = read('../chat/chat-composer.css'); + const d223 = [ + [pane, 'v2-thread__back'], + [pane, 'v2-thread__inspector-toggle'], + [pane, 'v2-pod-header__invite'], + [composer, 'v2-composer__plus'], + [composer, 'v2-composer__send'], + ] as const; + for (const [css, key] of d223) { + expect(css).toContain(`.v2-root button.${key}::after`); + expect({ key, ...sizesOn(css, key) }).toEqual({ key, width: '32px', height: '32px' }); + } + }); + + test('the agent profile bar keeps a 44px touch area on the 24px owner avatar control at 390', () => { + const css = read('../agents/v2-agent-profile.css'); + const phone = mediaBlockContaining(css, '.v2-feature--aprofile .v2-aprofile__hero .v2-avatar { width: 24px; height: 24px; }'); + expect(phone).not.toBe(''); + const hit = selectorRuleBody(css, '.v2-feature--aprofile .v2-aprofile__avatar-button::after {'); + // 24 + 2 x 10 = 44, from a positioned button. + expect(hit).toContain('inset: -10px'); + expect(ruleBody(css, '.v2-aprofile .v2-aprofile__avatar-button')).toContain('position: relative'); + expect(css.lastIndexOf('@media (max-width: 760px)')).toBeLessThan(css.indexOf('.v2-feature--aprofile .v2-aprofile__avatar-button::after')); + }); + + test('finding 6: the board header icon keys never shrink on a phone', () => { + const phone = mediaBlockContaining(boardCss, '.v2-feature--board .v2-board__key--icon { flex: none; }'); + expect(ruleBody(boardCss, '.v2-feature--board .v2-board__key--icon', { within: phone })).toContain('flex: none'); + }); + + test('finding 4: the in-app text keys are the 28px small key at r8', () => { + const board = ruleBody(boardCss, '.v2-feature--board .v2-board__key'); + expect(board).toContain('height: 28px'); + expect(board).toContain('border-radius: var(--v2-r-8)'); + const connect = selectorRuleBody(read('../components/V2ConnectPage.css'), '.v2-connect .v2-connect__cta, .v2-connect .v2-connect__secondary {'); + expect(connect).toContain('height: 28px'); + expect(connect).toContain('border-radius: var(--v2-r-8)'); + const aprofile = ruleBody(read('../agents/v2-agent-profile.css'), '.v2-aprofile .v2-aprofile__btn'); + expect(aprofile).toContain('height: 28px'); + expect(aprofile).toContain('border-radius: var(--v2-r-8)'); + }); + + test('finding 7: cards are the D215 ring at r9 and lanes are r12, never a 1px border', () => { + const card = ruleBody(read('../components/V2YourTeamPage.css'), '.v2-team .v2-team-card'); + expect(card).toContain('border-radius: var(--v2-r-9)'); + expect(card).toContain('box-shadow: var(--v2-ring-card)'); + expect(card).not.toMatch(/(^|[\s;{])border:/); + expect(ruleBody(read('../components/V2ConnectPage.css'), '.v2-connect .v2-connect__lane')).toContain('border-radius: var(--v2-r-12)'); + // One card edge, named once in the token sheets, not copied per page. + const ring = '0 0 0 1px var(--v2-border-soft), 0 1px 2px rgba(16, 24, 40, 0.04)'; + expect(v2).toContain(`--v2-ring-card: ${ring};`); + expect(read('../../../design-system/tokens.css')).toMatch(/--c-ring-card:\s+0 0 0 1px var\(--c-border-soft\), 0 1px 2px rgba\(16, 24, 40, 0\.04\);/); + for (const sheet of ['../components/V2YourTeamPage.css', '../components/V2RunTrace.css', '../components/V2AgentBYO.css']) { + expect(read(sheet)).not.toContain(ring); + } + expect(ruleBody(read('../components/V2PersonProfile.css'), '.v2-profile .v2-profile__list')).toContain('border-radius: var(--v2-r-12)'); + expect(ruleBody(read('../agents/v2-agent-profile.css'), '.v2-aprofile .v2-aprofile__card')).toContain('border-radius: var(--v2-r-12)'); + }); + + test('finding 8: section labels are Settings\' mono 11.5/500 muted, and counts are 11.5', () => { + const mono = 'font: 500 11.5px/16px var(--v2-font-mono)'; + expect(ruleBody(read('../components/V2PersonProfile.css'), '.v2-profile .v2-profile__lane h2')).toContain(mono); + expect(ruleBody(read('../agents/v2-agent-profile.css'), '.v2-aprofile .v2-aprofile__card-title')).toContain(mono); + expect(ruleBody(read('../components/V2ConnectPage.css'), '.v2-connect .v2-connect__lane h2')).toContain(mono); + expect(ruleBody(read('../components/V2PersonProfile.css'), '.v2-profile .v2-profile__meta')).toContain(mono); + expect(ruleBody(read('../agents/v2-agent-profile.css'), '.v2-aprofile .v2-aprofile__meta')).toContain('11.5px'); + expect(ruleBody(boardCss, '.v2-feature--board .v2-board__subtitle')).toContain('500 11.5px/1 var(--v2-font-mono)'); + // Run trace: "Turn 1" was Bricolage 20/700. + expect(ruleBody(read('../components/V2RunTrace.css'), '.v2-run-trace h2')).toContain(mono); + }); + + test('finding 7: the run trace summary and tool calls are ringed r9 cards, no 1px border', () => { + const card = ruleBody(read('../components/V2RunTrace.css'), '.v2-run-trace__summary, .v2-run-trace__call'); + expect(card).toContain('border-radius: var(--v2-r-9)'); + expect(card).toContain('box-shadow: var(--v2-ring-card)'); + expect(card).not.toMatch(/(^|[\s;{])border:/); + }); +}); + describe('Activity built to the d215 signal frames (#s-activity-signal)', () => { const css = read('../components/V2ActivityPage.css'); const page = read('../components/V2ActivityPage.tsx'); diff --git a/frontend/src/v2/agents/V2AgentProfile.tsx b/frontend/src/v2/agents/V2AgentProfile.tsx index c7dacc8b78..bb53179ba5 100644 --- a/frontend/src/v2/agents/V2AgentProfile.tsx +++ b/frontend/src/v2/agents/V2AgentProfile.tsx @@ -403,9 +403,13 @@ const V2AgentProfile: React.FC = ({ shell }) => { /> ); + // In the app shell the hero is the shared page head bar (.v2-feature__header, + // the route's v2-feature--bar) and the rest scrolls under it; signed out it + // stays the public page's hero. + const inShell = Boolean(shell); const main = (
-
+
{/* The avatar IS the edit control (2026-09-01). Non-owners get the plain avatar; owners get the same avatar as a button. */} {canEditAvatar ? ( @@ -424,13 +428,13 @@ const V2AgentProfile: React.FC = ({ shell }) => { ) : ( {avatar} )} -
+
-

{agent.displayName}

+

{agent.displayName}

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

{agent.description}

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

{agent.description}

}

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

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

{agent.description}

} {avatarDialogOpen && (

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

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

{pageLead}

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

{t('connect.intro')}

diff --git a/frontend/src/v2/components/V2PageHead.tsx b/frontend/src/v2/components/V2PageHead.tsx index a237b10961..5931489656 100644 --- a/frontend/src/v2/components/V2PageHead.tsx +++ b/frontend/src/v2/components/V2PageHead.tsx @@ -10,14 +10,19 @@ interface V2PageHeadProps { titleClassName?: string; metaClassName?: string; hintLabel?: string; + /** Drawn before the title, inside the bar: a profile's avatar. */ + lead?: React.ReactNode; + /** An id on the h1, for a region labelled by its title. */ + titleId?: string; } const V2PageHead: React.FC = ({ - title, meta, actions, hint, className = '', titleClassName = '', metaClassName = '', hintLabel = 'About this page', + title, meta, actions, hint, className = '', titleClassName = '', metaClassName = '', hintLabel = 'About this page', lead, titleId, }) => (
+ {lead}
-

{title}

+

{title}

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

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

+
+

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

+
); } @@ -162,12 +169,12 @@ const V2PersonProfile: React.FC = () => { let action: React.ReactNode = null; if (isSelf) { - action = {t('profile.editProfile')}; + action = {t('profile.editProfile')}; } else if (!profile.isBot) { action = (
} + {error &&

{error}

} {trace && <>
{agentLabel(trace)}{enumLabel('status', trace.status)} · {triggerPhrase(trace.trigger)} @@ -86,7 +104,7 @@ const V2RunTrace: React.FC = () => { })} {trace.errorKind && {t('runTrace.stopped', { kind: trace.errorKind })}} - {['queued', 'running'].includes(trace.status) &&

{t('runTrace.running')}

} + {['queued', 'running'].includes(trace.status) &&

{t('runTrace.running')}

}
{trace.traceTruncated &&

{t(`runTrace.${limitedKey}`, { turns: trace.omittedTurns, calls: trace.omittedToolCalls })}

} {!trace.turns.length &&

{t('runTrace.empty')}

} @@ -113,6 +131,7 @@ const V2RunTrace: React.FC = () => { {t('runTrace.runId', { id: trace.id })} } +
; }; export default V2RunTrace; diff --git a/frontend/src/v2/components/V2YourTeamPage.css b/frontend/src/v2/components/V2YourTeamPage.css index 61bc3c78c0..6c79b51061 100644 --- a/frontend/src/v2/components/V2YourTeamPage.css +++ b/frontend/src/v2/components/V2YourTeamPage.css @@ -4,20 +4,19 @@ 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); } +/* The head is the shared bar (.v2-feature__header via V2PageHead, the route's + v2-feature--bar), and the keys are the v2p small keys (.v2p-b-sm, 28 at r8, + 44 on phones), so this sheet draws neither (rulings 11-04). The body scrolls + under the bar in a centred 1100 column (.v2-feature__scroll). */ +.v2-team { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; } +.v2-team .v2-team__body { display: flex; flex-direction: column; gap: 20px; } .v2-team .v2-team__grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; } -.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); } +/* A card is the D215 ring and soft shadow at r9 (the primitives' .v2p-tc), not a 1px border. */ +.v2-team .v2-team-card { display: flex; flex-direction: column; gap: 10px; min-width: 0; padding: 16px; border-radius: var(--v2-r-9); background: var(--v2-surface); box-shadow: var(--v2-ring-card); } /* 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--needsYou { box-shadow: 0 0 0 2px var(--v2-accent); } +/* The set-up-your-own card is an add slot, dashed like .v2p-tile.add, so it keeps a border. */ +.v2-team .v2-team-card--own { padding: 15px; border: 1px dashed var(--v2-border); background: transparent; box-shadow: none; } .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; } @@ -48,13 +47,10 @@ .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__body { gap: 14px; } + /* The title never wraps beside its meta: stack them in the bar (66246 (1)). */ + .v2-team .v2-team__head > .v2-page-head__identity { flex-direction: column; align-items: flex-start; justify-content: center; gap: 4px; padding: 8px 0; } + .v2-team .v2-team__head .v2-team__meta { white-space: normal; } .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; } @@ -103,6 +99,5 @@ } /* 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 0b1e575056..6396522071 100644 --- a/frontend/src/v2/components/V2YourTeamPage.tsx +++ b/frontend/src/v2/components/V2YourTeamPage.tsx @@ -7,6 +7,11 @@ import V2Avatar from './V2Avatar'; import './V2YourTeamPage.css'; import { AgentConnection, connectionLabel } from '../utils/agentConnection'; import V2Fold from './V2Fold'; +import V2PageHead from './V2PageHead'; + +// The shared head bar's title and count classes, plus this page's own hook. +const HEAD_TITLE_CLASS = 'v2-feature__title v2-team__title'; +const HEAD_META_CLASS = 'v2-team__meta'; /** * Your Team — direction C, built to `YourTeam.dc.html` (canvas: Workspace in C) @@ -477,18 +482,18 @@ const V2YourTeamPage: React.FC = () => { )}
{needsReconnect && (a.installedBy === String(user?._id) || user?.role === 'admin') ? ( - ) : null} {!needsReconnect && state === 'needsYou' && ( - )} {!needsReconnect && - -
- + {/* The shared page head bar (.v2-feature__header), as Settings and Connectors draw it. */} + + + + + )} + /> +
{!isEntitled && (
@@ -533,10 +541,10 @@ const V2YourTeamPage: React.FC = () => { onChange={(e) => setRedeemCode(e.target.value)} autoFocus /> - - {redeemError && {redeemError}} @@ -576,7 +584,7 @@ const V2YourTeamPage: React.FC = () => {

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

{t('yourTeam.own.command')}
-
@@ -601,6 +609,7 @@ const V2YourTeamPage: React.FC = () => { )} +
); }; diff --git a/frontend/src/v2/primitives/primitives.css b/frontend/src/v2/primitives/primitives.css index 168cc92392..58db464784 100644 --- a/frontend/src/v2/primitives/primitives.css +++ b/frontend/src/v2/primitives/primitives.css @@ -356,3 +356,12 @@ details.v2p-disc > summary .vl { font-size: 13.5px; color: var(--v2-ink-2); min- .v2p-cols { grid-template-columns: minmax(0, 1fr); } .v2p-tile { height: 104px; } } + +/* ---- phones (≤760, rulings 11-04): every key and segment is 44px tall, the + touch floor, so a page never sizes its own phone keys. Icon keys stay + square. Desktop keeps 32 icon / 28 small / 28 segment. ---- */ +@media (max-width: 760px) { + .v2p-b, .v2p-b-sm { height: 44px; } + .v2p-ib, .v2p-ib.v2p-b-sm { width: 44px; height: 44px; } + .v2p-seg button, .v2p-seg--sm button { height: 44px; } +} diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index 9dedb01126..27866a5edb 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -212,6 +212,9 @@ body.modern-ui.v2-canvas { --v2-key-inset: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 1px 2px rgba(16, 24, 40, 0.06); --v2-shadow-pane: 0 0 0 1px rgba(16, 24, 40, 0.07), 0 1px 2px rgba(16, 24, 40, 0.04), 0 12px 28px -20px rgba(16, 24, 40, 0.18); --v2-shadow-pop: 0 0 0 1px rgba(16, 24, 40, 0.08), 0 2px 4px rgba(16, 24, 40, 0.04), 0 18px 36px -14px rgba(16, 24, 40, 0.28); + /* The card edge (D215): a 1px ring plus a 1px lift, no border. Cards that + are not a .v2p-tc take it from here rather than copying the pair. */ + --v2-ring-card: 0 0 0 1px var(--v2-border-soft), 0 1px 2px rgba(16, 24, 40, 0.04); --v2-av-ring: rgba(16, 24, 40, 0.07); --v2-scrim: rgba(16, 24, 40, 0.18); --v2-r-6: 6px; @@ -2501,20 +2504,44 @@ body.modern-ui.v2-canvas { background: var(--v2-surface); /* inside the card the ground is white; the card is the tint step */ } +/* The shared page head bar (V2PageHead with .v2-feature__header): the reference + heads' 64px bar with the title 29px from the pane edge, a divider under it and + the count in mono 11.5. Pages that draw their own head (Your team, the person + and agent profiles) use this class too, with .v2-feature--bar below. */ .v2-feature__header { flex-shrink: 0; - padding: 22px 28px 18px; + box-sizing: border-box; + min-height: 64px; + padding: 0 20px 0 28px; border-bottom: 1px solid var(--v2-border-soft); background: var(--v2-surface); } +.v2-page-head.v2-feature__header { align-items: center; gap: 12px; } +.v2-feature__header > .v2-page-head__identity { flex: 1 1 auto; } +/* The display face is set here, not only in the phone rule: without it the + desktop title fell back to Plex 24/850 (ui-audit-1104 finding 2). */ .v2-feature__title { margin: 0; color: var(--v2-text-primary); - font-size: 24px; - font-weight: 850; + font: 800 24px/1 var(--v2-font-display); letter-spacing: -0.03em; } +.v2-feature__header .v2-page-head__meta { font: 500 11.5px/16px var(--v2-font-mono); } + +/* A page that draws its own head bar inside the feature pane passes + v2-feature--bar: the body stops padding and scrolling, the head sits at the + pane edge, and .v2-feature__scroll under it scrolls and pads the content to + a centred column (--v2-feature-col, 1100 for lists and grids). */ +.v2-feature--bar > .v2-feature__body { padding: 0; display: flex; flex-direction: column; overflow: hidden; } +.v2-feature--bar .v2-feature__legacy { flex: 1 1 auto; width: 100%; max-width: none; min-height: 0; margin: 0; display: flex; flex-direction: column; } +.v2-feature__scroll { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + box-sizing: border-box; + padding: 24px max(28px, calc((100% - var(--v2-feature-col, 1100px)) / 2)) 42px; +} .v2-page-head { display: flex; @@ -4821,6 +4848,12 @@ body.modern-ui.v2-canvas { } .v2-feature__header .v2-page-head__meta { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .v2-feature__body { padding: 12px 12px 16px; } + .v2-feature__scroll { padding: 12px 12px 16px; } + /* A head with actions gives them their own full-width row under the title + (Your team, the profiles): 44px keys never squeeze the title at 390. */ + .v2-feature__header.v2-page-head--wrap { flex-wrap: wrap; row-gap: 0; padding-bottom: 12px; } + .v2-feature__header.v2-page-head--wrap > .v2-page-head__identity { flex: 1 1 auto; min-height: 52px; } + .v2-feature__header.v2-page-head--wrap > .v2-page-head__actions { flex: 1 0 100%; display: grid; grid-auto-columns: minmax(0, 1fr); grid-auto-flow: column; gap: 8px; } } /* ---------- Motion (D220) ---------- */