design: two-color palette — rename all tokens to --cream / --blue

Replace ochre-clay, carbon-black, burnt-oxide, slate-indigo with clean
two-color system: --cream (#f4f0e8) and --blue (#041cf3). Update every
component, utility class, and test assertion.
This commit is contained in:
Ilia Mashkov
2026-05-11 12:59:32 +03:00
parent fed9c97ddb
commit 30f8e4be95
21 changed files with 111 additions and 161 deletions
+49 -41
View File
@@ -28,28 +28,26 @@
--fraunces-wonk: 1;
--fraunces-soft: 0;
/* === COLOR PALETTE === */
--vibrant-blue: #041cf3;
--paper-white: #ffffff;
--carbon-black: #121212;
--structure-gray: #f2f2f2;
/* === COLOR PALETTE: 2-color system === */
--cream: #f4f0e8;
--blue: #041cf3;
/* === SEMANTIC COLORS === */
--background: var(--paper-white);
--foreground: var(--carbon-black);
--card: var(--paper-white);
--card-foreground: var(--carbon-black);
--primary: var(--vibrant-blue);
--primary-foreground: var(--paper-white);
--secondary: var(--structure-gray);
--secondary-foreground: var(--carbon-black);
--muted: var(--structure-gray);
--muted-foreground: #666666;
--accent: var(--vibrant-blue);
--accent-foreground: var(--paper-white);
--destructive: #d4183d;
--border: var(--carbon-black);
--ring: var(--vibrant-blue);
--background: var(--cream);
--foreground: var(--blue);
--card: var(--cream);
--card-foreground: var(--blue);
--primary: var(--blue);
--primary-foreground: var(--cream);
--secondary: var(--cream);
--secondary-foreground: var(--blue);
--muted: var(--cream);
--muted-foreground: rgba(4, 28, 243, 0.5);
--accent: var(--blue);
--accent-foreground: var(--cream);
--destructive: var(--blue);
--border: var(--blue);
--ring: var(--blue);
/* === SPACING (8pt Linear Scale) === */
--space-0: 0;
@@ -71,9 +69,9 @@
--radius: 0px;
/* === BRUTALIST SHADOWS === */
--shadow-brutal: 8px 8px 0 var(--carbon-black);
--shadow-brutal-sm: 4px 4px 0 var(--carbon-black);
--shadow-brutal-lg: 12px 12px 0 var(--carbon-black);
--shadow-brutal: 8px 8px 0 var(--blue);
--shadow-brutal-sm: 4px 4px 0 var(--blue);
--shadow-brutal-lg: 12px 12px 0 var(--blue);
/* === GRID === */
--grid-gap: var(--space-3);
@@ -83,10 +81,8 @@
--font-heading: var(--font-fraunces);
--font-body: var(--font-public-sans);
--color-ochre-clay: var(--ochre-clay);
--color-slate-indigo: var(--slate-indigo);
--color-burnt-oxide: var(--burnt-oxide);
--color-carbon-black: var(--carbon-black);
--color-cream: var(--cream);
--color-blue: var(--blue);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
@@ -124,15 +120,27 @@
overflow-x: hidden;
}
/* Paper grain texture */
/* Subtle blue-tinted grain on parchment */
body::before {
content: "";
position: fixed;
inset: 0;
background-image:
repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0, 0, 0, 0.02) 2px, rgba(0, 0, 0, 0.02) 4px),
repeating-linear-gradient(90deg, transparent, transparent 2px, rgba(0, 0, 0, 0.02) 2px, rgba(0, 0, 0, 0.02) 4px);
opacity: 0.4;
repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(4, 28, 243, 0.015) 2px,
rgba(4, 28, 243, 0.015) 4px
),
repeating-linear-gradient(
90deg,
transparent,
transparent 2px,
rgba(4, 28, 243, 0.015) 2px,
rgba(4, 28, 243, 0.015) 4px
);
opacity: 0.6;
display: block;
pointer-events: none;
z-index: 1;
@@ -150,7 +158,7 @@
font-variation-settings:
"WONK" var(--fraunces-wonk),
"SOFT" var(--fraunces-soft);
color: var(--carbon-black);
color: var(--blue);
}
h1 {
@@ -173,13 +181,13 @@
font-family: var(--font-body);
font-size: var(--text-base);
font-weight: var(--font-weight-body);
color: var(--carbon-black);
color: var(--blue);
}
a {
color: var(--burnt-oxide);
color: var(--blue);
text-decoration: none;
border-bottom: 2px solid var(--carbon-black);
border-bottom: 2px solid var(--blue);
transition: all 0.2s;
}
@@ -190,7 +198,7 @@
blockquote {
font-family: var(--font-heading);
font-size: var(--text-xl);
border-left: var(--border-width) solid var(--carbon-black);
border-left: var(--border-width) solid var(--blue);
padding-left: var(--space-4);
margin: var(--space-6) 0;
}
@@ -207,19 +215,19 @@
box-shadow: var(--shadow-brutal-lg);
}
.brutal-border {
border: var(--border-width) solid var(--carbon-black);
border: var(--border-width) solid var(--blue);
}
.brutal-border-top {
border-top: var(--border-width) solid var(--carbon-black);
border-top: var(--border-width) solid var(--blue);
}
.brutal-border-bottom {
border-bottom: var(--border-width) solid var(--carbon-black);
border-bottom: var(--border-width) solid var(--blue);
}
.brutal-border-left {
border-left: var(--border-width) solid var(--carbon-black);
border-left: var(--border-width) solid var(--blue);
}
.brutal-border-right {
border-right: var(--border-width) solid var(--carbon-black);
border-right: var(--border-width) solid var(--blue);
}
/* Animations */
+4 -4
View File
@@ -18,17 +18,17 @@ describe('Badge', () => {
it('applies default variant classes', () => {
render(<Badge variant="default">Tag</Badge>);
const el = screen.getByText('Tag');
expect(el).toHaveClass('bg-carbon-black', 'text-ochre-clay');
expect(el).toHaveClass('bg-blue', 'text-cream');
});
it('applies primary variant classes', () => {
render(<Badge variant="primary">Tag</Badge>);
expect(screen.getByText('Tag')).toHaveClass('bg-burnt-oxide');
expect(screen.getByText('Tag')).toHaveClass('bg-blue');
});
it('applies secondary variant classes', () => {
render(<Badge variant="secondary">Tag</Badge>);
expect(screen.getByText('Tag')).toHaveClass('bg-slate-indigo');
expect(screen.getByText('Tag')).toHaveClass('bg-blue');
});
it('applies outline variant classes', () => {
@@ -38,7 +38,7 @@ describe('Badge', () => {
it('defaults to default variant when unspecified', () => {
render(<Badge>Tag</Badge>);
expect(screen.getByText('Tag')).toHaveClass('bg-carbon-black');
expect(screen.getByText('Tag')).toHaveClass('bg-blue');
});
});
+4 -4
View File
@@ -20,10 +20,10 @@ interface Props {
}
const VARIANTS: Record<BadgeVariant, string> = {
default: 'brutal-border bg-carbon-black text-ochre-clay',
primary: 'brutal-border bg-burnt-oxide text-ochre-clay',
secondary: 'brutal-border bg-slate-indigo text-ochre-clay',
outline: 'brutal-border bg-transparent text-carbon-black',
default: 'brutal-border bg-blue text-cream',
primary: 'brutal-border bg-blue text-cream',
secondary: 'brutal-border bg-blue text-cream',
outline: 'brutal-border bg-transparent text-blue',
};
/**
+3 -3
View File
@@ -16,11 +16,11 @@ describe('Button', () => {
describe('variants', () => {
it('applies primary variant by default', () => {
render(<Button>Go</Button>);
expect(screen.getByRole('button')).toHaveClass('bg-burnt-oxide');
expect(screen.getByRole('button')).toHaveClass('bg-blue');
});
it('applies secondary variant', () => {
render(<Button variant="secondary">Go</Button>);
expect(screen.getByRole('button')).toHaveClass('bg-slate-indigo');
expect(screen.getByRole('button')).toHaveClass('bg-blue');
});
it('applies outline variant', () => {
render(<Button variant="outline">Go</Button>);
@@ -28,7 +28,7 @@ describe('Button', () => {
});
it('applies ghost variant', () => {
render(<Button variant="ghost">Go</Button>);
expect(screen.getByRole('button')).toHaveClass('bg-ochre-clay');
expect(screen.getByRole('button')).toHaveClass('bg-cream');
});
});
describe('sizes', () => {
+5 -5
View File
@@ -22,10 +22,10 @@ interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
}
const VARIANTS: Record<ButtonVariant, string> = {
primary: 'bg-burnt-oxide text-ochre-clay',
secondary: 'bg-slate-indigo text-ochre-clay',
outline: 'bg-transparent text-carbon-black border-carbon-black',
ghost: 'bg-ochre-clay text-carbon-black border-carbon-black',
primary: 'bg-blue text-cream',
secondary: 'bg-blue text-cream',
outline: 'bg-transparent text-blue border-blue',
ghost: 'bg-cream text-blue border-blue',
};
const SIZES: Record<ButtonSize, string> = {
@@ -35,7 +35,7 @@ const SIZES: Record<ButtonSize, string> = {
};
const BASE =
'brutal-border brutal-shadow transition-all duration-200 hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[6px_6px_0_var(--carbon-black)] active:translate-x-[4px] active:translate-y-[4px] active:shadow-[4px_4px_0_var(--carbon-black)] uppercase tracking-wider';
'brutal-border brutal-shadow transition-all duration-200 hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[6px_6px_0_var(--blue)] active:translate-x-[4px] active:translate-y-[4px] active:shadow-[4px_4px_0_var(--blue)] uppercase tracking-wider';
/**
* Brutalist button with variants and sizes.
+5 -9
View File
@@ -13,17 +13,13 @@ describe('Card', () => {
});
});
describe('background variants', () => {
it('defaults to ochre background', () => {
it('defaults to cream background', () => {
const { container } = render(<Card>Content</Card>);
expect(container.firstChild).toHaveClass('bg-ochre-clay');
expect(container.firstChild).toHaveClass('bg-cream');
});
it('applies slate background', () => {
const { container } = render(<Card background="slate">Content</Card>);
expect(container.firstChild).toHaveClass('bg-slate-indigo');
});
it('applies white background', () => {
const { container } = render(<Card background="white">Content</Card>);
expect(container.firstChild).toHaveClass('bg-white');
it('applies blue background', () => {
const { container } = render(<Card background="blue">Content</Card>);
expect(container.firstChild).toHaveClass('bg-blue');
});
});
describe('padding', () => {
+5 -6
View File
@@ -1,7 +1,7 @@
import type { ReactNode } from 'react';
import { cn } from '$shared/lib';
export type CardBackground = 'ochre' | 'slate' | 'white';
export type CardBackground = 'cream' | 'blue';
interface CardProps {
/**
@@ -14,7 +14,7 @@ interface CardProps {
className?: string;
/**
* Background color preset
* @default 'ochre'
* @default 'cream'
*/
background?: CardBackground;
/**
@@ -25,15 +25,14 @@ interface CardProps {
}
const BG: Record<CardBackground, string> = {
ochre: 'bg-ochre-clay',
slate: 'bg-slate-indigo text-ochre-clay',
white: 'bg-white',
cream: 'bg-cream',
blue: 'bg-blue text-cream',
};
/**
* Brutalist card container with background and padding variants.
*/
export function Card({ children, className, background = 'ochre', noPadding = false }: CardProps) {
export function Card({ children, className, background = 'cream', noPadding = false }: CardProps) {
return (
<div className={cn('brutal-border brutal-shadow', BG[background], !noPadding && 'p-6 md:p-8', className)}>
{children}
+5 -5
View File
@@ -13,7 +13,7 @@ interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
}
const INPUT_BASE =
'brutal-border bg-white px-4 py-3 text-carbon-black focus:outline-none focus:ring-2 focus:ring-burnt-oxide focus:ring-offset-2 focus:ring-offset-ochre-clay transition-all';
'brutal-border bg-cream px-4 py-3 text-blue focus:outline-none focus:ring-2 focus:ring-blue focus:ring-offset-2 focus:ring-offset-cream transition-all';
/**
* Text input with optional label and error state.
@@ -26,7 +26,7 @@ export function Input({ label, error, className, id, ...props }: InputProps) {
return (
<div className="flex flex-col gap-2">
{label && (
<label htmlFor={inputId} className="text-carbon-black">
<label htmlFor={inputId} className="text-blue">
{label}
</label>
)}
@@ -37,7 +37,7 @@ export function Input({ label, error, className, id, ...props }: InputProps) {
{...props}
/>
{error && (
<span id={errorId} className="text-sm text-burnt-oxide">
<span id={errorId} className="text-sm text-blue">
{error}
</span>
)}
@@ -72,7 +72,7 @@ export function Textarea({ label, error, rows = 4, className, id, ...props }: Te
return (
<div className="flex flex-col gap-2">
{label && (
<label htmlFor={textareaId} className="text-carbon-black">
<label htmlFor={textareaId} className="text-blue">
{label}
</label>
)}
@@ -84,7 +84,7 @@ export function Textarea({ label, error, rows = 4, className, id, ...props }: Te
{...props}
/>
{error && (
<span id={errorId} className="text-sm text-burnt-oxide">
<span id={errorId} className="text-sm text-blue">
{error}
</span>
)}
+5 -9
View File
@@ -18,17 +18,13 @@ describe('Section', () => {
});
describe('background variants', () => {
it('defaults to ochre background', () => {
it('defaults to cream background', () => {
const { container } = render(<Section>x</Section>);
expect(container.querySelector('section')).toHaveClass('bg-ochre-clay', 'text-carbon-black');
expect(container.querySelector('section')).toHaveClass('bg-cream', 'text-blue');
});
it('applies slate background', () => {
const { container } = render(<Section background="slate">x</Section>);
expect(container.querySelector('section')).toHaveClass('bg-slate-indigo', 'text-ochre-clay');
});
it('applies white background', () => {
const { container } = render(<Section background="white">x</Section>);
expect(container.querySelector('section')).toHaveClass('bg-white', 'text-carbon-black');
it('applies blue background', () => {
const { container } = render(<Section background="blue">x</Section>);
expect(container.querySelector('section')).toHaveClass('bg-blue', 'text-cream');
});
});
+5 -6
View File
@@ -1,7 +1,7 @@
import type { ReactNode } from 'react';
import { cn } from '$shared/lib';
export type SectionBackground = 'ochre' | 'slate' | 'white';
export type SectionBackground = 'cream' | 'blue';
export type ContainerSize = 'default' | 'wide' | 'ultra-wide';
interface SectionProps {
@@ -11,7 +11,7 @@ interface SectionProps {
children: ReactNode;
/**
* Background color variant
* @default 'ochre'
* @default 'cream'
*/
background?: SectionBackground;
/**
@@ -26,15 +26,14 @@ interface SectionProps {
}
const BACKGROUNDS: Record<SectionBackground, string> = {
ochre: 'bg-ochre-clay text-carbon-black',
slate: 'bg-slate-indigo text-ochre-clay',
white: 'bg-white text-carbon-black',
cream: 'bg-cream text-blue',
blue: 'bg-blue text-cream',
};
/**
* Full-width page section with background and optional borders.
*/
export function Section({ children, background = 'ochre', bordered = false, className }: SectionProps) {
export function Section({ children, background = 'cream', bordered = false, className }: SectionProps) {
return (
<section className={cn(BACKGROUNDS[background], bordered && 'brutal-border-top brutal-border-bottom', className)}>
{children}
+1 -1
View File
@@ -18,7 +18,7 @@ export function TechStackBrick({ name, className }: TechStackBrickProps) {
return (
<div
className={cn(
'brutal-border brutal-shadow bg-white px-4 py-3 text-center',
'brutal-border brutal-shadow bg-cream px-4 py-3 text-center',
'transition-all duration-200 hover:shadow-none hover:translate-x-[2px] hover:translate-y-[2px]',
className,
)}