diff --git a/frontend/src/v2/__tests__/signalAccent.test.ts b/frontend/src/v2/__tests__/signalAccent.test.ts index 0d98c1a283..f68489943e 100644 --- a/frontend/src/v2/__tests__/signalAccent.test.ts +++ b/frontend/src/v2/__tests__/signalAccent.test.ts @@ -106,10 +106,29 @@ describe('Signal accent on standalone and bridged surfaces', () => { expect(guidePage).toContain('sx={guideTextButtonSx} onClick={() => navigate(link.path)}>'); }); - test('the CLI authorize surface uses shared tokens with standalone fallbacks', () => { + test('the CLI authorize surface mounts in .v2-root and reads tokens only', () => { + // TASK-682: wrapped in .v2-root so the tokens resolve and the dark theme reaches it. + const tsx = read('src/v2/components/V2CliAuthorize.tsx'); + expect(tsx).toContain('
'); const css = read('src/v2/components/V2CliAuthorize.css'); - expect(css).toContain('var(--v2-accent, #1d3fd1)'); - expect(css).toContain('var(--v2-focus-ring, 0 0 0 3px rgba(29, 63, 209, .18))'); + expect(css).toContain('background: var(--v2-accent-fill)'); + expect(css).toContain('box-shadow: var(--v2-focus-ring)'); + // No fallbacks inside var() (it merges after #3058), and no card shadow. + expect(css).not.toMatch(/var\(--v2-[a-z-]+,/); + expect(css).not.toContain('0 12px 32px'); + // The only hexes are the warning-ink token's light and dark values. + expect(css.replace(/\/\*[\s\S]*?\*\//g, '').replace(/var\(--v2-[a-z-]+, #[0-9a-f]+\)/gi, '').match(/#[0-9a-f]{3,8}\b/gi)).toEqual(['#7c4a03', '#f4c27a', '#f4c27a']); + // The input must outrank `body.modern-ui:not(.v2-canvas) input` (0,2,2). + expect(css).toContain('.v2-root.v2-cli-authorize .v2-cli-authorize__field input {'); + expect(css).toContain('color: var(--v2-warning-ink) !important'); + expect(css).toContain(':root[data-v2-theme="dark"] .v2-cli-authorize { --v2-warning-ink: #f4c27a; }'); + expect(css).toContain(':root:not([data-v2-theme="light"]) .v2-cli-authorize { --v2-warning-ink: #f4c27a; }'); + // App.css's legacy button rule is (0,3,2), its :hover (0,4,2): the controls must out-specify both. + expect(css).toContain('.v2-root.v2-cli-authorize .v2-cli-authorize__card .v2-cli-authorize__primary {'); + expect(css).toContain('.v2-root.v2-cli-authorize .v2-cli-authorize__card .v2-cli-authorize__primary:hover'); + expect(css).toContain('.v2-root.v2-cli-authorize .v2-cli-authorize__card .v2-cli-authorize__secondary {'); + // Inputs set their own ground, or dark leaves them browser white. + expect(css).toMatch(/\.v2-cli-authorize__field input \{[^}]*background: var\(--v2-surface\)/); }); test.each([ diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index c296ed9a4b..a473122600 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -147,6 +147,19 @@ const bareElementSelectors = (css: string): string[] => { }; describe('v2 layout invariants (CSS rule presence)', () => { + test('/cli/authorize scrolls inside the .v2-root shell on a short viewport (TASK-682)', () => { + // .v2-root is height 100vh + overflow hidden; the page must override it or + // the lower controls (Deny / Authorize) are unreachable on a short screen. + const cli = fs.readFileSync(path.join(__dirname, '../components/V2CliAuthorize.css'), 'utf8'); + const root = /^\.v2-root\.v2-cli-authorize \{([^}]*)\}/m.exec(cli)?.[1] ?? ''; + expect(root).toContain('overflow-y: auto'); + expect(root).toContain('display: grid'); + expect(root).not.toContain('place-items: center'); + expect(/^\.v2-cli-authorize__card \{([^}]*)\}/m.exec(cli)?.[1]).toContain('margin: auto'); + // The legacy V1 form rule (0,2,2) would paint the form #111827. + expect(cli).toContain('.v2-root.v2-cli-authorize .v2-cli-authorize__card form {'); + }); + const v2 = read('../v2.css'); const tokens = read('../../../design-system/tokens.css'); const aprofile = read('../agents/v2-agent-profile.css'); @@ -1013,8 +1026,8 @@ describe('v2 layout invariants (CSS rule presence)', () => { // The footer CTA sits on the accent-deep band, so it keeps main's white fill. // An ink fill there reads as a dark hole (review 5673841827). const footerPrimary = ruleBody(aprofile, '.v2-root .v2-aprofile__footer-cta .v2-aprofile__btn--primary'); - expect(footerPrimary).toContain('background: #fff'); - expect(footerPrimary).toContain('color: var(--v2-accent)'); + expect(footerPrimary).toContain('background: var(--v2-on-accent, #fff)'); + expect(footerPrimary).toContain('color: var(--v2-accent-fill, var(--v2-accent))'); const avatarFocus = ruleBody(aprofile, '.v2-root button.v2-aprofile__avatar-button:focus-visible'); expect(avatarFocus).toContain('outline: none'); @@ -4144,18 +4157,18 @@ describe('TASK-167 row A — the landing hero onto Signal', () => { expect(bar).not.toContain('; the override has to carry the // .v2-root prefix plus the band class to outrank the rail pin. - expect(landing).toContain('.v2-root .v2-landing__bar--band button.v2-lang-switch__trigger { color: #ffffff; }'); + expect(landing).toContain('.v2-root .v2-landing__bar--band button.v2-lang-switch__trigger { color: var(--v2-on-accent, #ffffff); }'); }); test('the rotator is white on the band — the accent is cobalt and would vanish', () => { const rotator = selectorRuleBody(landing, '.v2-landing__rotator {'); - expect(rotator).toContain('color: #ffffff'); + expect(rotator).toContain('color: var(--v2-on-accent, #ffffff)'); expect(rotator).not.toContain('var(--v2-accent)'); }); test('the demo caption is on the band, above the frame, and the old lift is gone', () => { const cap = selectorRuleBody(landing, '.v2-landing__demo-cap {'); - expect(cap).toContain('color: #ffffff'); + expect(cap).toContain('color: var(--v2-on-accent, #ffffff)'); expect(cap).toContain('margin: 44px 0 0'); expect(cap).not.toContain('var(--v2-font-mono)'); expect(mediaAt(landing, '@media (max-width: 760px) {')).toContain('.v2-landing__demo-cap { margin: 28px 0 0; }'); @@ -4419,8 +4432,24 @@ describe('TASK-167 row C — How it works, Why open source and What you get onto expect(adapter).not.toContain('background'); expect(adapter).not.toContain('border'); const code = topLevelRuleBody(landing, '.v2-landing__code'); - expect(code).toContain('background: var(--v2-text-primary)'); - expect(code).toContain('color: #ffffff'); + // Code tokens, not text-primary or ink: both flip light in dark (TASK-682). + expect(code).toContain('background: var(--v2-code-bg)'); + expect(code).toContain('color: var(--v2-code-text)'); + expect(code).toContain('outline: 1px solid var(--v2-code-edge)'); + expect(code).toContain('outline-offset: -1px'); + expect(topLevelRuleBody(landing, '.v2-landing__code-comment')).toContain('color: var(--v2-code-muted)'); + // Light keeps today's slab exactly; dark is a dark slab with an edge. + const lightCode = /^\.v2-landing \{([^}]*--v2-code-bg[^}]*)\}/m.exec(landing)?.[1] ?? ''; + expect(lightCode).toContain('--v2-code-bg: #101828'); + expect(lightCode).toContain('--v2-code-text: #ffffff'); + expect(lightCode).toContain('--v2-code-muted: #98a2b3'); + expect(lightCode).toContain('--v2-code-edge: var(--v2-code-bg)'); + const darkCode = topLevelRuleBody(landing, ':root[data-v2-theme="dark"] .v2-landing'); + expect(darkCode).toContain('--v2-code-bg: #0b0e13'); + expect(darkCode).toContain('--v2-code-text: #e8ebf1'); + expect(darkCode).toContain('--v2-code-muted: #8b94a5'); + expect(darkCode).toContain('--v2-code-edge: #364051'); + expect(landing).toContain(':root:not([data-v2-theme="light"]) .v2-landing {'); expect(code).toContain('font-size: 14px'); expect(code).toContain('line-height: 22px'); // 1 1 auto, so the three blocks share the row's height instead of each @@ -4430,7 +4459,7 @@ describe('TASK-167 row C — How it works, Why open source and What you get onto expect(code).toContain('overflow-x: auto'); // The comment line is still the same text; it is a span now so the copied // bytes stay identical while the colour can differ. - expect(topLevelRuleBody(landing, '.v2-landing__code-comment')).toContain('#98a2b3'); + expect(topLevelRuleBody(landing, '.v2-landing__code-comment')).toContain('var(--v2-code-muted)'); expect(landingTsx).toContain('v2-landing__code-comment'); expect(landingTsx).toContain('commonly agent run my-agent'); expect(landingTsx).toContain('# joins pods, replies to @mentions'); diff --git a/frontend/src/v2/agents/v2-agent-profile.css b/frontend/src/v2/agents/v2-agent-profile.css index 810243c253..a3e09e4615 100644 --- a/frontend/src/v2/agents/v2-agent-profile.css +++ b/frontend/src/v2/agents/v2-agent-profile.css @@ -198,7 +198,7 @@ background: var(--v2-accent-deep); border-radius: var(--v2-radius-lg); color: rgba(255, 255, 255, 0.85); font-size: 15px; } -.v2-root .v2-aprofile__footer-cta .v2-aprofile__btn--primary { background: #fff; color: var(--v2-accent); border-color: #fff; } +.v2-root .v2-aprofile__footer-cta .v2-aprofile__btn--primary { background: var(--v2-on-accent, #fff); color: var(--v2-accent-fill, var(--v2-accent)); border-color: var(--v2-on-accent, #fff); } .v2-root .v2-aprofile__footer-cta .v2-aprofile__btn--primary:hover { background: rgba(255,255,255,0.9); } @media (max-width: 760px) { diff --git a/frontend/src/v2/agents/v2-persona-catalog.css b/frontend/src/v2/agents/v2-persona-catalog.css index 78fcbef94a..150f4a764b 100644 --- a/frontend/src/v2/agents/v2-persona-catalog.css +++ b/frontend/src/v2/agents/v2-persona-catalog.css @@ -180,8 +180,8 @@ padding: 7px 16px; font-size: 13px; font-weight: 600; - color: #fff; - background: var(--v2-accent); + color: var(--v2-on-accent, #fff); + background: var(--v2-accent-fill, var(--v2-accent)); border: none; border-radius: var(--v2-radius-md, 8px); cursor: pointer; diff --git a/frontend/src/v2/components/V2CliAuthorize.css b/frontend/src/v2/components/V2CliAuthorize.css index d457b39dbd..4dd546e290 100644 --- a/frontend/src/v2/components/V2CliAuthorize.css +++ b/frontend/src/v2/components/V2CliAuthorize.css @@ -1,54 +1,80 @@ -.v2-cli-authorize { - min-height: 100vh; +/* Mounted inside .v2-root (TASK-682), so every value below is a token and + follows the dark theme. Two sets of resets compete with these rules: + .v2-root's button/link/input resets at (0,1,0)-(0,1,1), and App.css's + legacy `body.modern-ui:not(.v2-canvas) input` (0,2,2) and + `...:not(.guide-canvas) button` (0,3,2) with :hover at (0,4,2). The + controls carry .v2-root and .v2-cli-authorize (plus __card on buttons and + the hover rules) so they win on specificity, not on stylesheet order. + No fallbacks inside var(): this file merges strictly after #3058, which + defines --v2-accent-fill and --v2-on-accent (UX Lead, msg 120679). */ +/* Warning ink (UX Lead, pod msg 120615): #7c4a03 vanishes on dark + warning-soft. Scoped here with the same dark selectors as v2.css until the + token has a second consumer. */ +.v2-cli-authorize { --v2-warning-ink: #7c4a03; } +:root[data-v2-theme="dark"] .v2-cli-authorize { --v2-warning-ink: #f4c27a; } +@media (prefers-color-scheme: dark) { + :root:not([data-v2-theme="light"]) .v2-cli-authorize { --v2-warning-ink: #f4c27a; } +} + +/* .v2-root is the app shell: height 100vh, overflow hidden, flex column. + This route is a page, not a shell, so it scrolls itself; the card centres + with margin:auto, which never pushes it past the top edge on a short + viewport the way place-items:center would. */ +.v2-root.v2-cli-authorize { + height: 100vh; + overflow-y: auto; display: grid; - place-items: center; padding: 24px; - background: #f8f8fb; - color: #111827; - font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: var(--v2-bg-subtle); + color: var(--v2-text-primary); + font-family: var(--v2-font); } .v2-cli-authorize__card { width: min(100%, 460px); + margin: auto; padding: 32px; - border: 1px solid #e5e7eb; - border-radius: 18px; - background: #fff; - box-shadow: 0 12px 32px rgba(17, 24, 39, 0.08); + border: 1px solid var(--v2-border); + border-radius: var(--v2-radius-lg); + background: var(--v2-surface); } -.v2-cli-authorize__card form { margin: 0; padding: 0; border: 0; background: transparent; box-shadow: none; } +/* Outranks App.css's legacy `body.modern-ui:not(.v2-canvas) form` (0,2,2). */ +.v2-root.v2-cli-authorize .v2-cli-authorize__card form { max-width: none; margin: 0; padding: 0; border: 0; border-radius: 0; background: transparent; box-shadow: none; } .v2-cli-authorize__mark { display: grid; width: 36px; height: 36px; place-items: center; - border-radius: 10px; - background: var(--v2-accent, #1d3fd1); - color: #fff; + border-radius: var(--v2-radius-lg); + background: var(--v2-accent-fill); + color: var(--v2-on-accent); font-weight: 800; } -.v2-cli-authorize__eyebrow { margin: 16px 0 0 !important; color: var(--v2-accent, #1d3fd1) !important; font-size: 11px; font-weight: 800; letter-spacing: .11em; } +.v2-cli-authorize__eyebrow { margin: 16px 0 0 !important; color: var(--v2-accent-text) !important; font-size: 11px; font-weight: 800; letter-spacing: .11em; } .v2-cli-authorize__card h1 { margin: 18px 0 8px; font-size: 24px; } -.v2-cli-authorize__card p { margin: 0 0 20px; color: #4b5563; line-height: 1.5; } +.v2-cli-authorize__card p { margin: 0 0 20px; color: var(--v2-text-secondary); line-height: 1.5; } .v2-cli-authorize__field { display: grid; gap: 7px; margin-bottom: 18px; font-size: 13px; font-weight: 700; } -.v2-cli-authorize__field input { width: 100%; box-sizing: border-box; padding: 12px; border: 1px solid #d7dce7; border-radius: 9px; font: 600 18px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .08em; } -.v2-cli-authorize__field input:focus { outline: none; border-color: var(--v2-accent, #1d3fd1); box-shadow: var(--v2-focus-ring, 0 0 0 3px rgba(29, 63, 209, .18)); } -.v2-cli-authorize__primary, .v2-cli-authorize__secondary { display: inline-flex; justify-content: center; align-items: center; min-height: 42px; padding: 0 16px; border-radius: 9px; font: inherit; font-weight: 700; text-decoration: none; cursor: pointer; } -.v2-cli-authorize__primary { border: 1px solid var(--v2-accent, #1d3fd1); background: var(--v2-accent, #1d3fd1); color: #fff; } -.v2-cli-authorize__primary:disabled { cursor: wait; opacity: .65; } -.v2-cli-authorize__secondary { border: 1px solid #d7dce7; background: #fff; color: #374151; } -.v2-cli-authorize__facts { margin: 0 0 18px; padding: 14px; border-radius: 10px; background: #f7f7fa; } +.v2-root.v2-cli-authorize .v2-cli-authorize__field input { width: 100%; box-sizing: border-box; margin: 0; padding: 12px; border: 1px solid var(--v2-border-strong); border-radius: var(--v2-radius); background: var(--v2-surface); color: var(--v2-text-primary); font: 600 18px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .08em; } +.v2-root.v2-cli-authorize .v2-cli-authorize__field input:focus { outline: none; border-color: var(--v2-accent); box-shadow: var(--v2-focus-ring); } +.v2-root.v2-cli-authorize .v2-cli-authorize__card .v2-cli-authorize__primary, .v2-root.v2-cli-authorize .v2-cli-authorize__card .v2-cli-authorize__secondary { display: inline-flex; justify-content: center; align-items: center; min-height: 42px; padding: 0 16px; border-radius: var(--v2-radius); font: inherit; font-weight: 700; text-decoration: none; cursor: pointer; } +.v2-root.v2-cli-authorize .v2-cli-authorize__card .v2-cli-authorize__primary { border: 1px solid var(--v2-accent-fill); background: var(--v2-accent-fill); color: var(--v2-on-accent); } +.v2-root.v2-cli-authorize .v2-cli-authorize__card .v2-cli-authorize__primary:disabled { cursor: wait; opacity: .65; } +.v2-root.v2-cli-authorize .v2-cli-authorize__card .v2-cli-authorize__secondary { border: 1px solid var(--v2-border-strong); background: var(--v2-surface); color: var(--v2-text-secondary); } +.v2-root.v2-cli-authorize .v2-cli-authorize__card .v2-cli-authorize__primary:hover, .v2-root.v2-cli-authorize .v2-cli-authorize__card .v2-cli-authorize__primary:active { background: var(--v2-accent-fill); transform: none; } +.v2-root.v2-cli-authorize .v2-cli-authorize__card .v2-cli-authorize__secondary:hover, .v2-root.v2-cli-authorize .v2-cli-authorize__card .v2-cli-authorize__secondary:active { background: var(--v2-surface-hover); transform: none; } +.v2-cli-authorize__facts { margin: 0 0 18px; padding: 14px; border-radius: var(--v2-radius-lg); background: var(--v2-bg-subtle); } .v2-cli-authorize__facts div { display: flex; justify-content: space-between; gap: 18px; padding: 5px 0; } -.v2-cli-authorize__facts dt { color: #6b7280; } +.v2-cli-authorize__facts dt { color: var(--v2-text-muted); } .v2-cli-authorize__facts dd { margin: 0; font-weight: 600; text-align: right; } -.v2-cli-authorize__warning { padding: 10px 12px; border-radius: 8px; background: #fdefdc; color: #7c4a03 !important; font-size: 13px; } +.v2-cli-authorize__warning { padding: 10px 12px; border-radius: var(--v2-radius); background: var(--v2-warning-soft); color: var(--v2-warning-ink) !important; font-size: 13px; } .v2-cli-authorize__actions { display: flex; justify-content: flex-end; gap: 10px; } @media (max-width: 480px) { - .v2-cli-authorize { padding: 16px; align-items: start; } + .v2-root.v2-cli-authorize { padding: 16px; } + .v2-cli-authorize__card { margin: 0 auto auto; } .v2-cli-authorize__card { padding: 24px 20px; } .v2-cli-authorize__facts div { display: grid; gap: 2px; } .v2-cli-authorize__facts dd { text-align: left; } diff --git a/frontend/src/v2/components/V2CliAuthorize.tsx b/frontend/src/v2/components/V2CliAuthorize.tsx index de73c02f09..1cf1472776 100644 --- a/frontend/src/v2/components/V2CliAuthorize.tsx +++ b/frontend/src/v2/components/V2CliAuthorize.tsx @@ -93,7 +93,7 @@ const V2CliAuthorize: React.FC = () => { const next = `/cli/authorize${code ? `?code=${encodeURIComponent(code)}` : ''}`; return ( -
+

CLI SIGN-IN

diff --git a/frontend/src/v2/landing/__tests__/landingInstallCopy.test.tsx b/frontend/src/v2/landing/__tests__/landingInstallCopy.test.tsx index c55bd4e466..2fd54e6d15 100644 --- a/frontend/src/v2/landing/__tests__/landingInstallCopy.test.tsx +++ b/frontend/src/v2/landing/__tests__/landingInstallCopy.test.tsx @@ -179,12 +179,12 @@ describe('landing install line copy control (TASK-154)', () => { // than anything drawn by the pill itself. expect(pill).toContain('background: transparent'); expect(pill).toContain('border: 1px solid transparent'); - expect(pill).toContain('color: #ffffff'); + expect(pill).toContain('color: var(--v2-on-accent, #ffffff)'); // The hover state needs the same tier: at (0,2,0) it lost to the (0,2,1) reset. expect(ruleBody(`${PILL_SELECTOR}:hover`)).toContain('background: rgba(255, 255, 255, 0.12)'); // Focus is invisible against cobalt without an explicit ring, and the // band's own hairline is too faint to serve as one. - expect(ruleBody(`${PILL_SELECTOR}:focus-visible`)).toContain('outline: 2px solid #ffffff'); + expect(ruleBody(`${PILL_SELECTOR}:focus-visible`)).toContain('outline: 2px solid var(--v2-on-accent, #ffffff)'); }); it('keeps the line 46px tall and gives the pill a 44px target on phones', () => { diff --git a/frontend/src/v2/landing/demo-workspace.css b/frontend/src/v2/landing/demo-workspace.css index a1d997deae..9a7d5f8d03 100644 --- a/frontend/src/v2/landing/demo-workspace.css +++ b/frontend/src/v2/landing/demo-workspace.css @@ -95,8 +95,8 @@ display: grid; place-items: center; border-radius: var(--v2-radius-sm); - background: var(--v2-accent); - color: #ffffff; + background: var(--v2-accent-fill, var(--v2-accent)); + color: var(--v2-on-accent, #ffffff); font-family: var(--v2-font-display); font-weight: 800; font-size: 18px; diff --git a/frontend/src/v2/landing/v2-landing.css b/frontend/src/v2/landing/v2-landing.css index e98cd3a81d..cfaf93edae 100644 --- a/frontend/src/v2/landing/v2-landing.css +++ b/frontend/src/v2/landing/v2-landing.css @@ -94,6 +94,9 @@ V2LandingPage), and the class starts ON so first paint is the band state and there is no white flash before the observer runs. */ .v2-landing__bar--band { + /* Brand block, fixed cobalt in both themes (Lumen, #3063 HOLD): the band's + translucent white text is tuned on #1d3fd1 and drops below AA on dark + --v2-accent-fill (#3d5ef2). */ background: #1d3fd1; backdrop-filter: none; border-bottom-color: rgba(255, 255, 255, 0.22); @@ -117,14 +120,14 @@ transition: color 80ms ease; } .v2-landing__navlink:hover { color: var(--v2-text-primary); } -.v2-landing__bar--band .v2-landing__brand-name { color: #ffffff; } +.v2-landing__bar--band .v2-landing__brand-name { color: var(--v2-on-accent, #ffffff); } .v2-landing__bar--band .v2-landing__navlink { color: rgba(255, 255, 255, 0.86); } -.v2-landing__bar--band .v2-landing__navlink:hover { color: #ffffff; } +.v2-landing__bar--band .v2-landing__navlink:hover { color: var(--v2-on-accent, #ffffff); } /* The language trigger is a