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 && (
<div
className="absolute right-0 top-full mt-1 w-40 border border-border-default rounded shadow-lg z-50"
style={{ backgroundColor: '#1a1a1a' }}
className="absolute right-0 top-full mt-1 w-40 border border-border-default rounded shadow-lg z-50 bg-bg-tertiary"
>
<div className="py-1">
<button

View File

@ -1,6 +1,6 @@
@import "tailwindcss";
:root {
@theme {
--color-brand: #c20600;
--color-bg-primary: #0a0a0a;
--color-bg-secondary: #121212;
@ -10,8 +10,14 @@
--color-text-secondary: #a3a3a3;
--color-text-muted: #737373;
--color-border-subtle: #1a1a1a;
--color-border-default: #27272a;
--color-border-highlight: #2a2a2a;
--color-border-default: #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 {
margin: 0;
font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif';
font-family: var(--font-sans);
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
line-height: 1.5;
@ -35,12 +41,11 @@ body {
}
a {
color: var(--color-text-secondary);
text-decoration: none;
}
a:hover {
color: var(--color-text-primary);
text-decoration: none;
}
button {

View File

@ -27,7 +27,7 @@ export default function SignIn({ onSuccess }: SignInProps) {
};
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">
<h1 className="text-2xl font-bold text-center mb-6">Sign In</h1>
{error && (
@ -45,7 +45,7 @@ export default function SignIn({ onSuccess }: SignInProps) {
type="email"
value={email}
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
/>
</div>
@ -58,21 +58,21 @@ export default function SignIn({ onSuccess }: SignInProps) {
type="password"
value={password}
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
/>
</div>
<div className="flex gap-4 pt-2">
<Link
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
</Link>
<button
type="submit"
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'}
</button>
@ -80,7 +80,7 @@ export default function SignIn({ onSuccess }: SignInProps) {
</form>
<p className="mt-4 text-center text-text-muted">
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
</Link>
</p>

View File

@ -39,7 +39,7 @@ export default function SignUp({ onSuccess }: SignUpProps) {
};
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">
<h1 className="text-2xl font-bold text-center mb-6">Create Account</h1>
{error && (
@ -57,7 +57,7 @@ export default function SignUp({ onSuccess }: SignUpProps) {
type="email"
value={email}
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
/>
</div>
@ -70,7 +70,7 @@ export default function SignUp({ onSuccess }: SignUpProps) {
type="text"
value={displayName}
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
/>
</div>
@ -83,7 +83,7 @@ export default function SignUp({ onSuccess }: SignUpProps) {
type="password"
value={password}
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
/>
</div>
@ -96,21 +96,21 @@ export default function SignUp({ onSuccess }: SignUpProps) {
type="password"
value={passwordVerify}
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
/>
</div>
<div className="flex gap-4 pt-2">
<Link
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
</Link>
<button
type="submit"
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'}
</button>

View File

@ -14,8 +14,8 @@ export default {
'text-secondary': '#a3a3a3',
'text-muted': '#737373',
'border-subtle': '#1a1a1a',
'border-default': '#27272a',
'border-highlight': '#2a2a2a',
'border-default': '#2a2a2a',
'border-highlight': '#3a3a3a',
},
fontFamily: {
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);
});
});