From 30f8e4be95e5a16be275a93f3316dcbcdb695172 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Mon, 11 May 2026 12:59:32 +0300 Subject: [PATCH] =?UTF-8?q?design:=20two-color=20palette=20=E2=80=94=20ren?= =?UTF-8?q?ame=20all=20tokens=20to=20--cream=20/=20--blue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- app/page.tsx | 38 -------- .../experience/ui/ExperienceCard.test.tsx | 4 +- src/entities/experience/ui/ExperienceCard.tsx | 2 +- .../project/ui/DetailedProjectCard.tsx | 4 +- src/entities/project/ui/ProjectCard.test.tsx | 11 +-- src/entities/project/ui/ProjectCard.tsx | 11 +-- src/shared/styles/theme.css | 90 ++++++++++--------- src/shared/ui/Badge/ui/Badge.test.tsx | 8 +- src/shared/ui/Badge/ui/Badge.tsx | 8 +- src/shared/ui/Button/ui/Button.test.tsx | 6 +- src/shared/ui/Button/ui/Button.tsx | 10 +-- src/shared/ui/Card/ui/Card.test.tsx | 14 ++- src/shared/ui/Card/ui/Card.tsx | 11 ++- src/shared/ui/Input/ui/Input.tsx | 10 +-- src/shared/ui/Section/ui/Section.test.tsx | 14 ++- src/shared/ui/Section/ui/Section.tsx | 11 ++- src/shared/ui/TechStack/ui/TechStack.tsx | 2 +- src/widgets/Navigation/ui/MobileNav.tsx | 6 +- src/widgets/Navigation/ui/SidebarNav.tsx | 6 +- src/widgets/Navigation/ui/UtilityBar.test.tsx | 2 +- src/widgets/Navigation/ui/UtilityBar.tsx | 4 +- 21 files changed, 111 insertions(+), 161 deletions(-) delete mode 100644 app/page.tsx diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index 8b4e579..0000000 --- a/app/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { SectionRecord } from '$entities/Section'; -import { getCollection } from '$shared/api'; -import type { NavItem } from '$widgets/Navigation'; -import { MobileNav, SidebarNav } from '$widgets/Navigation'; -import { SectionFactory } from '$widgets/SectionFactory'; -import { SectionsAccordion } from '$widgets/SectionsAccordion'; - -/** - * Portfolio home page. - * - * Fetches all sections at build time (SSG). Renders a fixed sidebar with - * section navigation and a scrollable main column with accordion sections. - */ -export default async function Home() { - const { items: sections } = await getCollection('sections', { - sort: 'order', - }); - - const navItems: NavItem[] = sections.map((s) => ({ - id: s.slug, - label: s.title, - number: s.number, - })); - - return ( -
- -
- - - {sections.map((s) => ( - - ))} - -
-
- ); -} diff --git a/src/entities/experience/ui/ExperienceCard.test.tsx b/src/entities/experience/ui/ExperienceCard.test.tsx index 4f80111..1d14dbf 100644 --- a/src/entities/experience/ui/ExperienceCard.test.tsx +++ b/src/entities/experience/ui/ExperienceCard.test.tsx @@ -37,10 +37,10 @@ describe('ExperienceCard', () => { expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('Senior Developer'); }); - it('period badge has brutal-border, bg-carbon-black, text-ochre-clay, text-sm', () => { + it('period badge has brutal-border, bg-blue, text-cream, text-sm', () => { render(); const badge = screen.getByText('2021 – 2024'); - expect(badge).toHaveClass('brutal-border', 'bg-carbon-black', 'text-ochre-clay', 'text-sm'); + expect(badge).toHaveClass('brutal-border', 'bg-blue', 'text-cream', 'text-sm'); }); it('company paragraph has opacity-80', () => { diff --git a/src/entities/experience/ui/ExperienceCard.tsx b/src/entities/experience/ui/ExperienceCard.tsx index 3a26c32..51a7a32 100644 --- a/src/entities/experience/ui/ExperienceCard.tsx +++ b/src/entities/experience/ui/ExperienceCard.tsx @@ -34,7 +34,7 @@ export function ExperienceCard({ title, company, period, description, className

{title}

{company}

- {period} + {period}

{description}

diff --git a/src/entities/project/ui/DetailedProjectCard.tsx b/src/entities/project/ui/DetailedProjectCard.tsx index ca9a2d9..7f37607 100644 --- a/src/entities/project/ui/DetailedProjectCard.tsx +++ b/src/entities/project/ui/DetailedProjectCard.tsx @@ -49,12 +49,12 @@ export function DetailedProjectCard({ title, year, role, stack, description, det
- +

{title}

{description}

{imageUrl && ( -
+
{title}
)} diff --git a/src/entities/project/ui/ProjectCard.test.tsx b/src/entities/project/ui/ProjectCard.test.tsx index 90af4ef..e42f144 100644 --- a/src/entities/project/ui/ProjectCard.test.tsx +++ b/src/entities/project/ui/ProjectCard.test.tsx @@ -47,20 +47,13 @@ describe('ProjectCard', () => { it('year badge has correct classes', () => { render(); const yearBadge = screen.getByText('2024'); - expect(yearBadge).toHaveClass('brutal-border', 'bg-carbon-black', 'text-ochre-clay', 'text-sm'); + expect(yearBadge).toHaveClass('brutal-border', 'bg-blue', 'text-cream', 'text-sm'); }); it('tags have correct classes', () => { render(); const tag = screen.getByText('React'); - expect(tag).toHaveClass( - 'brutal-border', - 'bg-white', - 'text-carbon-black', - 'text-sm', - 'uppercase', - 'tracking-wide', - ); + expect(tag).toHaveClass('brutal-border', 'bg-cream', 'text-blue', 'text-sm', 'uppercase', 'tracking-wide'); }); }); diff --git a/src/entities/project/ui/ProjectCard.tsx b/src/entities/project/ui/ProjectCard.tsx index 28faa8b..4b1c9f1 100644 --- a/src/entities/project/ui/ProjectCard.tsx +++ b/src/entities/project/ui/ProjectCard.tsx @@ -33,29 +33,26 @@ export function ProjectCard({ title, year, description, tags, imageUrl }: Props)
{title} - {year} + {year}
{description}
{imageUrl && ( -
+
{title}
)} {tags.map((tag) => ( - + {tag} ))} diff --git a/src/shared/styles/theme.css b/src/shared/styles/theme.css index 7a61a82..a00bd21 100644 --- a/src/shared/styles/theme.css +++ b/src/shared/styles/theme.css @@ -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 */ diff --git a/src/shared/ui/Badge/ui/Badge.test.tsx b/src/shared/ui/Badge/ui/Badge.test.tsx index d64295c..aa1469b 100644 --- a/src/shared/ui/Badge/ui/Badge.test.tsx +++ b/src/shared/ui/Badge/ui/Badge.test.tsx @@ -18,17 +18,17 @@ describe('Badge', () => { it('applies default variant classes', () => { render(Tag); 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(Tag); - expect(screen.getByText('Tag')).toHaveClass('bg-burnt-oxide'); + expect(screen.getByText('Tag')).toHaveClass('bg-blue'); }); it('applies secondary variant classes', () => { render(Tag); - 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(Tag); - expect(screen.getByText('Tag')).toHaveClass('bg-carbon-black'); + expect(screen.getByText('Tag')).toHaveClass('bg-blue'); }); }); diff --git a/src/shared/ui/Badge/ui/Badge.tsx b/src/shared/ui/Badge/ui/Badge.tsx index 5e8e037..f8dac9d 100644 --- a/src/shared/ui/Badge/ui/Badge.tsx +++ b/src/shared/ui/Badge/ui/Badge.tsx @@ -20,10 +20,10 @@ interface Props { } const VARIANTS: Record = { - 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', }; /** diff --git a/src/shared/ui/Button/ui/Button.test.tsx b/src/shared/ui/Button/ui/Button.test.tsx index 7ad5775..3f3c125 100644 --- a/src/shared/ui/Button/ui/Button.test.tsx +++ b/src/shared/ui/Button/ui/Button.test.tsx @@ -16,11 +16,11 @@ describe('Button', () => { describe('variants', () => { it('applies primary variant by default', () => { render(); - expect(screen.getByRole('button')).toHaveClass('bg-burnt-oxide'); + expect(screen.getByRole('button')).toHaveClass('bg-blue'); }); it('applies secondary variant', () => { render(); - expect(screen.getByRole('button')).toHaveClass('bg-slate-indigo'); + expect(screen.getByRole('button')).toHaveClass('bg-blue'); }); it('applies outline variant', () => { render(); @@ -28,7 +28,7 @@ describe('Button', () => { }); it('applies ghost variant', () => { render(); - expect(screen.getByRole('button')).toHaveClass('bg-ochre-clay'); + expect(screen.getByRole('button')).toHaveClass('bg-cream'); }); }); describe('sizes', () => { diff --git a/src/shared/ui/Button/ui/Button.tsx b/src/shared/ui/Button/ui/Button.tsx index 2fcabae..6254cc3 100644 --- a/src/shared/ui/Button/ui/Button.tsx +++ b/src/shared/ui/Button/ui/Button.tsx @@ -22,10 +22,10 @@ interface Props extends ButtonHTMLAttributes { } const VARIANTS: Record = { - 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 = { @@ -35,7 +35,7 @@ const SIZES: Record = { }; 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. diff --git a/src/shared/ui/Card/ui/Card.test.tsx b/src/shared/ui/Card/ui/Card.test.tsx index 6ad0e0e..a0ffcc5 100644 --- a/src/shared/ui/Card/ui/Card.test.tsx +++ b/src/shared/ui/Card/ui/Card.test.tsx @@ -13,17 +13,13 @@ describe('Card', () => { }); }); describe('background variants', () => { - it('defaults to ochre background', () => { + it('defaults to cream background', () => { const { container } = render(Content); - expect(container.firstChild).toHaveClass('bg-ochre-clay'); + expect(container.firstChild).toHaveClass('bg-cream'); }); - it('applies slate background', () => { - const { container } = render(Content); - expect(container.firstChild).toHaveClass('bg-slate-indigo'); - }); - it('applies white background', () => { - const { container } = render(Content); - expect(container.firstChild).toHaveClass('bg-white'); + it('applies blue background', () => { + const { container } = render(Content); + expect(container.firstChild).toHaveClass('bg-blue'); }); }); describe('padding', () => { diff --git a/src/shared/ui/Card/ui/Card.tsx b/src/shared/ui/Card/ui/Card.tsx index a637124..f98b940 100644 --- a/src/shared/ui/Card/ui/Card.tsx +++ b/src/shared/ui/Card/ui/Card.tsx @@ -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 = { - 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 (
{children} diff --git a/src/shared/ui/Input/ui/Input.tsx b/src/shared/ui/Input/ui/Input.tsx index 11d51a6..714ceba 100644 --- a/src/shared/ui/Input/ui/Input.tsx +++ b/src/shared/ui/Input/ui/Input.tsx @@ -13,7 +13,7 @@ interface InputProps extends InputHTMLAttributes { } 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 (
{label && ( -