theme enforced and correctly implemented; e2e tests added for theme

This commit is contained in:
Rob Colbert 2026-04-28 17:43:05 -04:00
parent 0bc47e60a5
commit 56ba613cc7
6 changed files with 135 additions and 23 deletions

View File

@ -50,8 +50,7 @@ export default function Header({ user, onSignOut }: HeaderProps) {
{menuOpen && ( {menuOpen && (
<div <div
className="absolute right-0 top-full mt-1 w-40 border border-border-default rounded shadow-lg z-50" className="absolute right-0 top-full mt-1 w-40 border border-border-default rounded shadow-lg z-50 bg-bg-tertiary"
style={{ backgroundColor: '#1a1a1a' }}
> >
<div className="py-1"> <div className="py-1">
<button <button

View File

@ -1,6 +1,6 @@
@import "tailwindcss"; @import "tailwindcss";
:root { @theme {
--color-brand: #c20600; --color-brand: #c20600;
--color-bg-primary: #0a0a0a; --color-bg-primary: #0a0a0a;
--color-bg-secondary: #121212; --color-bg-secondary: #121212;
@ -10,8 +10,14 @@
--color-text-secondary: #a3a3a3; --color-text-secondary: #a3a3a3;
--color-text-muted: #737373; --color-text-muted: #737373;
--color-border-subtle: #1a1a1a; --color-border-subtle: #1a1a1a;
--color-border-default: #27272a; --color-border-default: #2a2a2a;
--color-border-highlight: #2a2a2a; --color-border-highlight: #3a3a3a;
--font-mono: 'Courier New', Courier, monospace;
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--spacing-header: 48px;
--spacing-status: 32px;
} }
* { * {
@ -20,7 +26,7 @@
body { body {
margin: 0; margin: 0;
font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'; font-family: var(--font-sans);
background-color: var(--color-bg-primary); background-color: var(--color-bg-primary);
color: var(--color-text-primary); color: var(--color-text-primary);
line-height: 1.5; line-height: 1.5;
@ -35,12 +41,11 @@ body {
} }
a { a {
color: var(--color-text-secondary);
text-decoration: none; text-decoration: none;
} }
a:hover { a:hover {
color: var(--color-text-primary); text-decoration: none;
} }
button { button {

View File

@ -27,7 +27,7 @@ export default function SignIn({ onSuccess }: SignInProps) {
}; };
return ( return (
<div className="min-h-screen flex items-center justify-center bg-bg"> <div className="min-h-screen flex items-center justify-center bg-bg-primary">
<div className="w-full max-w-md p-8"> <div className="w-full max-w-md p-8">
<h1 className="text-2xl font-bold text-center mb-6">Sign In</h1> <h1 className="text-2xl font-bold text-center mb-6">Sign In</h1>
{error && ( {error && (
@ -45,7 +45,7 @@ export default function SignIn({ onSuccess }: SignInProps) {
type="email" type="email"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-2 bg-bg-secondary border border-border rounded-lg text-text focus:outline-none focus:border-primary" className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary focus:outline-none focus:border-brand"
required required
/> />
</div> </div>
@ -58,21 +58,21 @@ export default function SignIn({ onSuccess }: SignInProps) {
type="password" type="password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-2 bg-bg-secondary border border-border rounded-lg text-text focus:outline-none focus:border-primary" className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary focus:outline-none focus:border-brand"
required required
/> />
</div> </div>
<div className="flex gap-4 pt-2"> <div className="flex gap-4 pt-2">
<Link <Link
to="/" to="/"
className="flex-1 px-4 py-2 text-center border border-border rounded-lg hover:bg-bg-secondary transition-colors" className="flex-1 px-4 py-2 text-center border border-border-default rounded-lg hover:bg-bg-tertiary transition-colors"
> >
Cancel Cancel
</Link> </Link>
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="flex-1 px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg transition-colors disabled:opacity-50" className="flex-1 px-4 py-2 bg-brand hover:bg-red-700 text-white rounded-lg transition-colors disabled:opacity-50"
> >
{loading ? 'Signing in...' : 'Sign In'} {loading ? 'Signing in...' : 'Sign In'}
</button> </button>
@ -80,7 +80,7 @@ export default function SignIn({ onSuccess }: SignInProps) {
</form> </form>
<p className="mt-4 text-center text-text-muted"> <p className="mt-4 text-center text-text-muted">
Don't have an account?{' '} Don't have an account?{' '}
<Link to="/sign-up" className="text-primary hover:underline"> <Link to="/sign-up" className="text-brand hover:underline">
Sign up Sign up
</Link> </Link>
</p> </p>

View File

@ -39,7 +39,7 @@ export default function SignUp({ onSuccess }: SignUpProps) {
}; };
return ( return (
<div className="min-h-screen flex items-center justify-center bg-bg"> <div className="min-h-screen flex items-center justify-center bg-bg-primary">
<div className="w-full max-w-md p-8"> <div className="w-full max-w-md p-8">
<h1 className="text-2xl font-bold text-center mb-6">Create Account</h1> <h1 className="text-2xl font-bold text-center mb-6">Create Account</h1>
{error && ( {error && (
@ -57,7 +57,7 @@ export default function SignUp({ onSuccess }: SignUpProps) {
type="email" type="email"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-2 bg-bg-secondary border border-border rounded-lg text-text focus:outline-none focus:border-primary" className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary focus:outline-none focus:border-brand"
required required
/> />
</div> </div>
@ -70,7 +70,7 @@ export default function SignUp({ onSuccess }: SignUpProps) {
type="text" type="text"
value={displayName} value={displayName}
onChange={(e) => setDisplayName(e.target.value)} onChange={(e) => setDisplayName(e.target.value)}
className="w-full px-4 py-2 bg-bg-secondary border border-border rounded-lg text-text focus:outline-none focus:border-primary" className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary focus:outline-none focus:border-brand"
required required
/> />
</div> </div>
@ -83,7 +83,7 @@ export default function SignUp({ onSuccess }: SignUpProps) {
type="password" type="password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-2 bg-bg-secondary border border-border rounded-lg text-text focus:outline-none focus:border-primary" className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary focus:outline-none focus:border-brand"
required required
/> />
</div> </div>
@ -96,21 +96,21 @@ export default function SignUp({ onSuccess }: SignUpProps) {
type="password" type="password"
value={passwordVerify} value={passwordVerify}
onChange={(e) => setPasswordVerify(e.target.value)} onChange={(e) => setPasswordVerify(e.target.value)}
className="w-full px-4 py-2 bg-bg-secondary border border-border rounded-lg text-text focus:outline-none focus:border-primary" className="w-full px-4 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary focus:outline-none focus:border-brand"
required required
/> />
</div> </div>
<div className="flex gap-4 pt-2"> <div className="flex gap-4 pt-2">
<Link <Link
to="/" to="/"
className="flex-1 px-4 py-2 text-center border border-border rounded-lg hover:bg-bg-secondary transition-colors" className="flex-1 px-4 py-2 text-center border border-border-default rounded-lg hover:bg-bg-tertiary transition-colors"
> >
Cancel Cancel
</Link> </Link>
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="flex-1 px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg transition-colors disabled:opacity-50" className="flex-1 px-4 py-2 bg-brand hover:bg-red-700 text-white rounded-lg transition-colors disabled:opacity-50"
> >
{loading ? 'Creating...' : 'Sign Up'} {loading ? 'Creating...' : 'Sign Up'}
</button> </button>

View File

@ -14,8 +14,8 @@ export default {
'text-secondary': '#a3a3a3', 'text-secondary': '#a3a3a3',
'text-muted': '#737373', 'text-muted': '#737373',
'border-subtle': '#1a1a1a', 'border-subtle': '#1a1a1a',
'border-default': '#27272a', 'border-default': '#2a2a2a',
'border-highlight': '#2a2a2a', 'border-highlight': '#3a3a3a',
}, },
fontFamily: { fontFamily: {
mono: ['Courier New', 'Courier', 'monospace'], mono: ['Courier New', 'Courier', 'monospace'],

View File

@ -0,0 +1,108 @@
import { chromium, test, expect } from '@playwright/test';
const THEME = {
brand: '#c20600',
bgPrimary: '#0a0a0a',
bgSecondary: '#121212',
bgTertiary: '#1a1a1a',
bgElevated: '#202020',
textPrimary: '#d4d4d4',
textSecondary: '#a3a3a3',
textMuted: '#737373',
borderSubtle: '#1a1a1a',
borderDefault: '#2a2a2a',
borderHighlight: '#3a3a3a',
};
function rgbToHex(rgb: string): string {
const match = rgb.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
if (!match) return rgb;
const [, r, g, b] = match.map(Number);
return `#${[r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('')}`;
}
test.describe('Theme Colors', () => {
test('header should have correct border color', async ({ page }) => {
await page.goto('https://code-dev.g4dge7.com:5174/');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
const header = page.locator('header').first();
await expect(header).toBeVisible();
const borderColor = await header.evaluate((el) => {
return window.getComputedStyle(el).borderBottomColor;
});
const actualHex = rgbToHex(borderColor);
const expectedHex = THEME.borderSubtle;
console.log('Header border-bottom-color:', actualHex, '(expected:', expectedHex, ')');
expect(actualHex.toLowerCase()).toBe(expectedHex);
});
test('body should have correct background color', async ({ page }) => {
await page.goto('https://code-dev.g4dge7.com:5174/');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
const bgColor = await page.evaluate(() => {
return window.getComputedStyle(document.body).backgroundColor;
});
const actualHex = rgbToHex(bgColor);
const expectedHex = THEME.bgPrimary;
console.log('Body background-color:', actualHex, '(expected:', expectedHex, ')');
expect(actualHex.toLowerCase()).toBe(expectedHex);
});
test('header text should have correct color', async ({ page }) => {
await page.goto('https://code-dev.g4dge7.com:5174/');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
const header = page.locator('header').first();
const link = header.locator('a').first();
const color = await link.evaluate((el) => {
return window.getComputedStyle(el).color;
});
const actualHex = rgbToHex(color);
const expectedHex = THEME.textPrimary;
console.log('Header link color:', actualHex, '(expected:', expectedHex, ')');
expect(actualHex.toLowerCase()).toBe(expectedHex);
});
test('all theme colors should match spec', async ({ page }) => {
await page.goto('https://code-dev.g4dge7.com:5174/');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
const results: string[] = [];
const header = page.locator('header').first();
const headerBorder = await header.evaluate((el) => {
const c = window.getComputedStyle(el).borderBottomColor;
const m = c.match(/rgb\((\d+),\s*(\d+),\s*(\d+)/);
if (!m) return c;
return '#' + [m[1], m[2], m[3]].map(x => parseInt(x).toString(16).padStart(2, '0')).join('');
});
results.push(`header border: ${headerBorder}`);
const bodyBg = await page.evaluate(() => {
const c = window.getComputedStyle(document.body).backgroundColor;
const m = c.match(/rgb\((\d+),\s*(\d+),\s*(\d+)/);
if (!m) return c;
return '#' + [m[1], m[2], m[3]].map(x => parseInt(x).toString(16).padStart(2, '0')).join('');
});
results.push(`body bg: ${bodyBg}`);
console.log('Theme verification:', results.join(', '));
expect(headerBorder.toLowerCase()).toBe(THEME.borderSubtle);
expect(bodyBg.toLowerCase()).toBe(THEME.bgPrimary);
});
});