diff --git a/frontend/design-system/preview/brand-logo.html b/frontend/design-system/preview/brand-logo.html index 5b8588f712..c3a4c8a75d 100644 --- a/frontend/design-system/preview/brand-logo.html +++ b/frontend/design-system/preview/brand-logo.html @@ -89,6 +89,7 @@ } .verdict .dot { width:7px; height:7px; border-radius:50%; background:var(--c-accent); } + diff --git a/frontend/design-system/preview/colors-accent.html b/frontend/design-system/preview/colors-accent.html index 1fcf52fafc..a23b30a40d 100644 --- a/frontend/design-system/preview/colors-accent.html +++ b/frontend/design-system/preview/colors-accent.html @@ -38,6 +38,7 @@ font-size:11px; font-weight:700; letter-spacing:0.01em; } +
diff --git a/frontend/design-system/preview/colors-agent-roles.html b/frontend/design-system/preview/colors-agent-roles.html index 9cf49d705d..2a3ebf658f 100644 --- a/frontend/design-system/preview/colors-agent-roles.html +++ b/frontend/design-system/preview/colors-agent-roles.html @@ -42,6 +42,7 @@ } .chip .dot { width:8px; height:8px; border-radius:50%; } +
diff --git a/frontend/design-system/preview/colors-semantic.html b/frontend/design-system/preview/colors-semantic.html index 3a0350afd2..b864ae103e 100644 --- a/frontend/design-system/preview/colors-semantic.html +++ b/frontend/design-system/preview/colors-semantic.html @@ -29,6 +29,7 @@ .strike { text-decoration:line-through; color:var(--c-fg-tertiary); font-family:var(--c-font-mono); font-size:11px; } +
diff --git a/frontend/design-system/preview/components-buttons.html b/frontend/design-system/preview/components-buttons.html index ca025d3809..b5eaa7b179 100644 --- a/frontend/design-system/preview/components-buttons.html +++ b/frontend/design-system/preview/components-buttons.html @@ -92,6 +92,7 @@ } .chip.is-active .chip__count { opacity:.8; } + - + - - - - + + + + diff --git a/frontend/src/App.css b/frontend/src/App.css index b25711db71..390190cb96 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -162,7 +162,7 @@ body.modern-ui:not(.v2-canvas) input:focus { box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.25); } -body.modern-ui:not(.v2-canvas) button { +body.modern-ui:not(.v2-canvas):not(.guide-canvas) button { background-color: #2563eb; color: #f8fafc; padding: 12px 20px; @@ -174,12 +174,12 @@ body.modern-ui:not(.v2-canvas) button { transition: background-color 0.2s, transform 0.1s; } -body.modern-ui:not(.v2-canvas) button:hover { +body.modern-ui:not(.v2-canvas):not(.guide-canvas) button:hover { background-color: #1d4ed8; transform: translateY(-1px); } -body.modern-ui:not(.v2-canvas) button:active { +body.modern-ui:not(.v2-canvas):not(.guide-canvas) button:active { transform: translateY(1px); } diff --git a/frontend/src/components/landing/GuidePage.tsx b/frontend/src/components/landing/GuidePage.tsx index efab082b34..9e8037a679 100644 --- a/frontend/src/components/landing/GuidePage.tsx +++ b/frontend/src/components/landing/GuidePage.tsx @@ -10,9 +10,8 @@ import { } from '@mui/material'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; -import commonlyLogo from '../../assets/commonly-logo.png'; import guides from '../../content/guides.json'; -import { guidePalette, useGuideCanvas } from './guideShell'; +import { guidePalette, guideTextButtonSx, useGuideCanvas } from './guideShell'; interface GuideStrongParagraph { strong: string; @@ -89,7 +88,7 @@ const GuidePage: React.FC = () => { return ( - @@ -107,11 +106,11 @@ const GuidePage: React.FC = () => { - - Commonly Logo + Commonly Logo Commonly @@ -194,7 +193,7 @@ const GuidePage: React.FC = () => { ))} {section.links && ( - {section.links.map((link) => )} + {section.links.map((link) => )} )} @@ -236,8 +235,8 @@ const GuidePage: React.FC = () => { // Compare is emitted as a static document. React Router would // otherwise hand this path to the V2 catch-all instead of // requesting build/compare/index.html. - ? - : + ? + : ))} diff --git a/frontend/src/components/landing/GuidesIndexPage.tsx b/frontend/src/components/landing/GuidesIndexPage.tsx index fca1e32419..b09a6ff705 100644 --- a/frontend/src/components/landing/GuidesIndexPage.tsx +++ b/frontend/src/components/landing/GuidesIndexPage.tsx @@ -9,9 +9,8 @@ import { } from '@mui/material'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; -import commonlyLogo from '../../assets/commonly-logo.png'; import guides from '../../content/guides.json'; -import { guidePalette, useGuideCanvas } from './guideShell'; +import { guidePalette, guideTextButtonSx, useGuideCanvas } from './guideShell'; interface Guide { eyebrow: string; @@ -29,11 +28,11 @@ const GuidesIndexPage: React.FC = () => { - - Commonly Logo + Commonly Logo Commonly @@ -60,7 +59,7 @@ const GuidesIndexPage: React.FC = () => { {guide.summary} - diff --git a/frontend/src/components/landing/guideShell.ts b/frontend/src/components/landing/guideShell.ts index 2acbfc6cf4..a2cc5b1058 100644 --- a/frontend/src/components/landing/guideShell.ts +++ b/frontend/src/components/landing/guideShell.ts @@ -9,11 +9,17 @@ export const guidePalette = { textPrimary: '#111827', textSecondary: '#4b5563', textTertiary: '#7b8494', - accent: '#2f6feb', - accentStrong: '#1f55c9', - accentSoft: '#e8efff', - accentText: '#2456c8', - accentDeep: '#14306f', + accent: '#1d3fd1', + accentStrong: '#1633a8', + accentSoft: '#e9edfb', + accentText: '#1d3fd1', + accentDeep: '#0f2378', +} as const; + +// Text-button state colors must not fall back to the legacy root MUI theme. +export const guideTextButtonSx = { + color: guidePalette.accentText, + '&:hover': { backgroundColor: guidePalette.accentSoft }, } as const; // Guides render outside V2's component tree, so keep their page canvas light diff --git a/frontend/src/v2/__tests__/signalAccent.test.ts b/frontend/src/v2/__tests__/signalAccent.test.ts new file mode 100644 index 0000000000..c97eb4a176 --- /dev/null +++ b/frontend/src/v2/__tests__/signalAccent.test.ts @@ -0,0 +1,126 @@ +import fs from 'fs'; +import path from 'path'; +import { guidePalette, guideTextButtonSx } from '../../components/landing/guideShell'; + +const frontend = path.resolve(__dirname, '../../..'); +const read = (name: string) => fs.readFileSync(path.join(frontend, name), 'utf8'); +const retiredAccent = /#2f6feb|rgba?\(\s*47\s*,\s*111\s*,\s*235/i; + +// These standalone assets and legacy bridges cannot all inherit .v2-root. +// Historical preview cards and stable avatar colors are outside this sweep. +describe('Signal accent on standalone and bridged surfaces', () => { + test('the standalone favicon paints all four mark shapes cobalt', () => { + const svg = new DOMParser().parseFromString(read('public/favicon.svg'), 'image/svg+xml'); + expect(svg.querySelector('parsererror')).toBeNull(); + const shapes = Array.from(svg.querySelectorAll('path, circle')); + expect(shapes).toHaveLength(4); + shapes.forEach((shape) => { + expect(shape.getAttribute(shape.tagName === 'path' ? 'stroke' : 'fill')).toBe('#1d3fd1'); + }); + }); + + // Structural source guard: these fixtures prove the absence check can detect old colors. + test.each(['color: #2F6FEB;', 'color: rgb(47, 111, 235);', 'background: rgba(47,111,235,.2);'])( + 'the structural retired-accent guard detects %s', (fixture) => { + expect(fixture).toMatch(retiredAccent); + }, + ); + + test.each([ + 'index.html', + 'public/favicon.svg', + 'src/components/landing/guideShell.ts', + 'src/v2/components/V2FeaturePage.tsx', + 'src/v2/components/V2CliAuthorize.css', + 'src/v2/v2.css', + 'design-system/tokens.css', + ])('%s excludes the retired accent and its RGB channels', (name) => { + const source = read(name); + expect(source).not.toMatch(retiredAccent); + }); + + test('the static shell uses Signal for browser chrome and SEO accent states', () => { + const html = read('index.html'); + expect(html).toContain(''); + const expected = [ + ['a', 'color', '#1d3fd1'], ['a:hover', 'color', '#0f2378'], + ['.seo-kicker', 'color', '#1d3fd1'], ['.seo-primary', 'background', '#1d3fd1'], + ['.seo-primary:hover', 'background', '#1633a8'], + ['.seo-band', 'color', '#0f2378'], ['.seo-band', 'background', '#e9edfb'], + ]; + expected.forEach(([selector, property, value]) => { + const rule = html.split(`#seo-page ${selector} {`)[1]?.split('}')[0]; + expect(rule).toContain(`${property}: ${value};`); + }); + }); + + test.each([ + ['src/components/landing/guideShell.ts', /accent:\s*'#1d3fd1'/], + ['src/v2/components/V2FeaturePage.tsx', /accent:\s*'#1d3fd1'/], + ['design-system/tokens.css', /::selection\s*\{\s*background:\s*rgba\(29, 63, 209, 0\.20\)/], + ['src/v2/v2.css', /\.v2-activity__compose:focus-within\s*\{\s*box-shadow: 0 0 0 4px rgba\(29, 63, 209, 0\.10\)/], + ])('%s retains its intended cobalt accent', (name, expected) => { + expect(read(name as string)).toMatch(expected as RegExp); + }); + + test.each([ + ['src/components/landing/guideShell.ts', ['accentStrong', 'accentSoft', 'accentText', 'accentDeep']], + ['src/v2/components/V2FeaturePage.tsx', ['accentStrong', 'accentSoft']], + ])('%s uses Signal accent variants', (name, keys) => { + const source = read(name as string); + const expected: Record = { + accentStrong: '#1633a8', accentSoft: '#e9edfb', accentText: '#1d3fd1', accentDeep: '#0f2378', + }; + (keys as string[]).forEach((key) => { + expect(source).toContain(`${key}: '${expected[key]}'`); + }); + }); + + test('guide buttons are excluded from the legacy blue reset in every interaction state', () => { + const css = read('src/App.css'); + ['', ':hover', ':active'].forEach((state) => { + expect(css).toContain(`body.modern-ui:not(.v2-canvas):not(.guide-canvas) button${state} {`); + expect(css).not.toContain(`body.modern-ui:not(.v2-canvas) button${state} {`); + }); + }); + + test.each(['GuidePage.tsx', 'GuidesIndexPage.tsx'])('%s uses the cobalt mark instead of the legacy PNG', (name) => { + const source = read(`src/components/landing/${name}`); + expect(source).toContain('src="/favicon.svg"'); + expect(source).not.toContain('commonly-logo.png'); + }); + + test('guide text buttons bind their hover tint to Signal instead of the root theme', () => { + expect(guideTextButtonSx.color).toBe('#1d3fd1'); + expect(guideTextButtonSx['&:hover'].backgroundColor).toBe(guidePalette.accentSoft); + expect(guidePalette.accentSoft).toBe('#e9edfb'); + ['GuidePage.tsx', 'GuidesIndexPage.tsx'].forEach((name) => { + const source = read(`src/components/landing/${name}`); + expect(source).toContain('sx={guideTextButtonSx}'); + expect(source).not.toContain('sx={{ color: guidePalette.accentText }}'); + }); + const guidePage = read('src/components/landing/GuidePage.tsx'); + expect(guidePage).toContain("sx={{ ...guideTextButtonSx, alignSelf: 'flex-start' }}"); + expect(guidePage).toContain('href={link.path} sx={guideTextButtonSx}>'); + expect(guidePage).toContain('sx={guideTextButtonSx} onClick={() => navigate(link.path)}>'); + }); + + test('the CLI authorize surface uses shared tokens with standalone fallbacks', () => { + 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))'); + }); + + test.each([ + 'colors-accent.html', + 'colors-semantic.html', + 'colors-agent-roles.html', + 'brand-logo.html', + 'components-buttons.html', + ])('the legacy %s preview identifies its values as historical', (name) => { + const html = read(`design-system/preview/${name}`); + expect(html).toContain('Historical, pre-Signal reference.'); + expect(html).toContain('href="../../../docs/design/signal-identity.md"'); + }); + +}); diff --git a/frontend/src/v2/components/V2CliAuthorize.css b/frontend/src/v2/components/V2CliAuthorize.css index 90ff28b459..d457b39dbd 100644 --- a/frontend/src/v2/components/V2CliAuthorize.css +++ b/frontend/src/v2/components/V2CliAuthorize.css @@ -25,19 +25,19 @@ height: 36px; place-items: center; border-radius: 10px; - background: #2f6feb; + background: var(--v2-accent, #1d3fd1); color: #fff; font-weight: 800; } -.v2-cli-authorize__eyebrow { margin: 16px 0 0 !important; color: #2f6feb !important; font-size: 11px; font-weight: 800; letter-spacing: .11em; } +.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__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__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: #2f6feb; box-shadow: 0 0 0 3px rgba(47, 111, 235, .18); } +.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 #2f6feb; background: #2f6feb; color: #fff; } +.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; } diff --git a/frontend/src/v2/components/V2FeaturePage.tsx b/frontend/src/v2/components/V2FeaturePage.tsx index 1c461843f5..ca1625e908 100644 --- a/frontend/src/v2/components/V2FeaturePage.tsx +++ b/frontend/src/v2/components/V2FeaturePage.tsx @@ -33,9 +33,9 @@ const V2_COLORS = { textPrimary: '#111827', textSecondary: '#4b5563', textMuted: '#6b7280', - accent: '#2f6feb', - accentStrong: '#1f55c9', - accentSoft: '#e8efff', + accent: '#1d3fd1', + accentStrong: '#1633a8', + accentSoft: '#e9edfb', success: '#10b981', warning: '#f4a23a', danger: '#ef4444', @@ -50,12 +50,12 @@ const v2FeatureTheme = createTheme({ mode: 'light', primary: { main: V2_COLORS.accent, - light: '#5b8dff', + light: '#e9edfb', dark: V2_COLORS.accentStrong, contrastText: V2_COLORS.surface, }, secondary: { - main: '#14306f', + main: '#0f2378', contrastText: V2_COLORS.surface, }, success: { main: V2_COLORS.success }, diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index 665496d1d0..1354f28505 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -9677,7 +9677,7 @@ body.modern-ui.v2-canvas { } .v2-activity__compose:focus-within { - box-shadow: 0 0 0 4px rgba(47, 111, 235, 0.10); + box-shadow: 0 0 0 4px rgba(29, 63, 209, 0.10); } .v2-activity__compose-top,