feat(ui): establish theme tokens and global base styles

This commit is contained in:
Paul Huliganga 2026-03-26 16:24:40 -04:00
parent 83e2b95501
commit f42bd53cff
3 changed files with 209 additions and 87 deletions

View File

@ -8,7 +8,6 @@ import { ErrorBoundary } from './components/ErrorBoundary';
import { ToastContainer } from './components/Toast'; import { ToastContainer } from './components/Toast';
import { useToast } from './hooks/useToast'; import { useToast } from './hooks/useToast';
import { createContext, useContext } from 'react'; import { createContext, useContext } from 'react';
import { colors, radius } from './theme';
// Create toast context to share toast functionality across the app // Create toast context to share toast functionality across the app
interface ToastContextType { interface ToastContextType {
@ -48,10 +47,10 @@ function App() {
return ( return (
<ErrorBoundary> <ErrorBoundary>
<ToastContext.Provider value={toast}> <ToastContext.Provider value={toast}>
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen">
<ToastContainer messages={toast.messages} onClose={toast.removeToast} /> <ToastContainer messages={toast.messages} onClose={toast.removeToast} />
<header className="bg-white shadow-sm border-b border-gray-100 "> <header className="sticky top-0 z-20 border-b border-slate-200/70 bg-white/85 shadow-sm backdrop-blur-md dark:border-slate-700/60 dark:bg-slate-900/70">
<div className="max-w-7xl mx-auto px-4"> <div className="max-w-7xl mx-auto px-4">
<div className="flex items-center justify-between h-16"> <div className="flex items-center justify-between h-16">
<div className="flex items-center"> <div className="flex items-center">
@ -85,7 +84,7 @@ function App() {
</Routes> </Routes>
</main> </main>
<footer className="bg-white border-t border-gray-100 mt-12"> <footer className="mt-12 border-t border-slate-200/70 bg-white/70 backdrop-blur-sm dark:border-slate-700/60 dark:bg-slate-900/45">
<div className="max-w-7xl mx-auto py-6 px-4"> <div className="max-w-7xl mx-auto py-6 px-4">
<p className="text-center text-sm text-gray-500"> <p className="text-center text-sm text-gray-500">
Recipe Manager MVP - Built with React + Vite + TypeScript Recipe Manager MVP - Built with React + Vite + TypeScript

View File

@ -3,20 +3,39 @@
@tailwind utilities; @tailwind utilities;
:root { :root {
--text: #374151; --color-primary: #2563eb;
--text-h: #1e293b; --color-primary-dark: #1d4ed8;
--bg: #fff; --color-primary-light: #dbeafe;
--bg-alt: #f9fafb; --color-accent: #9333ea;
--border: #e5e7eb;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.08);
--accent-border: rgba(170, 59, 255, 0.35);
--card-shadow: 0 2px 8px 0 rgba(28,30,34,0.08);
--sans: system-ui, 'Segoe UI', Roboto, sans-serif; --text: #1f2937;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif; --text-h: #0f172a;
--mono: ui-monospace, Consolas, monospace; --text-dim: #64748b;
--bg: #f4f7fb;
--bg-alt: #edf2f7;
--surface: #ffffff;
--surface-muted: #f8fafc;
--border: #dbe3ef;
--code-bg: #eef2f7;
--radius-sm: 0.5rem;
--radius-md: 0.75rem;
--radius-lg: 1rem;
--shadow-subtle: 0 1px 2px rgba(15, 23, 42, 0.06);
--card-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
--shadow-hover: 0 14px 34px rgba(15, 23, 42, 0.12);
--focus-ring: 0 0 0 3px rgba(37, 99, 235, 0.25);
--surface-gradient:
radial-gradient(1200px 500px at -10% -10%, rgba(147, 51, 234, 0.1), transparent 60%),
radial-gradient(900px 420px at 115% -5%, rgba(37, 99, 235, 0.08), transparent 52%),
linear-gradient(180deg, #f8fbff 0%, #edf2f7 100%);
--sans: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--heading: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-synthesis: none; font-synthesis: none;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
@ -26,16 +45,25 @@
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--text: #d1d5db; --text: #e2e8f0;
--text-h: #f3f4f6; --text-h: #f8fafc;
--bg: #16171d; --text-dim: #94a3b8;
--bg-alt: #1a1b20;
--border: #2e303a; --bg: #0f172a;
--code-bg: #1f2028; --bg-alt: #111b2e;
--accent: #c084fc; --surface: #132136;
--accent-bg: rgba(192, 132, 252, 0.11); --surface-muted: #172842;
--accent-border: rgba(192, 132, 252, 0.33); --border: #22334d;
--card-shadow: 0 3px 14px 0 rgba(32,34,40,0.21); --code-bg: #1a2942;
--shadow-subtle: 0 1px 2px rgba(2, 6, 23, 0.35);
--card-shadow: 0 10px 30px rgba(2, 6, 23, 0.45);
--shadow-hover: 0 14px 34px rgba(2, 6, 23, 0.55);
--surface-gradient:
radial-gradient(900px 400px at 0% -5%, rgba(147, 51, 234, 0.2), transparent 60%),
radial-gradient(800px 420px at 105% -10%, rgba(37, 99, 235, 0.18), transparent 55%),
linear-gradient(180deg, #0f172a 0%, #111b2e 100%);
} }
} }
@ -43,56 +71,125 @@ body {
margin: 0; margin: 0;
font-family: var(--sans); font-family: var(--sans);
color: var(--text); color: var(--text);
background: var(--bg-alt); background: var(--surface-gradient);
background-attachment: fixed;
} }
#root { #root {
min-height: 100vh; min-height: 100vh;
background: var(--bg-alt); background: transparent;
} }
input, button, textarea, select { input,
button,
textarea,
select {
font-family: inherit; font-family: inherit;
} }
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--heading);
color: var(--text-h);
}
a {
color: inherit;
}
button,
.button,
.btn {
border-radius: var(--radius-md);
border: 1px solid transparent;
transition: background-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease,
transform 0.15s ease;
}
button:hover,
.button:hover,
.btn:hover {
box-shadow: var(--shadow-subtle);
}
button:focus-visible,
.button:focus-visible,
.btn:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
}
input,
textarea,
select {
border-radius: var(--radius-md);
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
}
input::placeholder,
textarea::placeholder {
color: color-mix(in srgb, var(--text-dim) 70%, transparent);
}
.card,
.shadow-card {
border-radius: var(--radius-lg);
border: 1px solid var(--border);
background: var(--surface);
box-shadow: var(--card-shadow);
}
.shadow-card { .shadow-card {
box-shadow: var(--card-shadow) !important; box-shadow: var(--card-shadow) !important;
} }
/* Toast animation */ /* Toast animation */
@keyframes slide-in { @keyframes slide-in {
from { transform: translateX(100%); opacity: 0; } from {
to { transform: translateX(0); opacity: 1; } transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
} }
.animate-slide-in { .animate-slide-in {
animation: slide-in 0.3s ease-out; animation: slide-in 0.3s ease-out;
} }
/* Recipe Manager visual polish */
::-webkit-input-placeholder { color: #96a3b7; }
::-moz-placeholder { color: #96a3b7; }
:-ms-input-placeholder { color: #96a3b7; }
::placeholder { color: #96a3b7; }
input:focus, textarea:focus, select:focus { outline: 2px solid #3b82f6; outline-offset: 2px; }
button, .button, .btn {
transition: box-shadow 0.13s, background 0.13s, color 0.13s;
}
.card, .shadow-card { border-radius: 1rem; box-shadow: var(--card-shadow); }
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;
background: #f3f4f6; background: var(--surface-muted);
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #e5e7eb; background: var(--border);
border-radius: 7px; border-radius: 7px;
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.max-w-7xl, .max-w-6xl, .max-w-4xl, .max-w-3xl, .max-w-2xl, .max-w-xl, .max-w-md, .max-w-sm { max-width: 100vw !important; } .max-w-7xl,
.p-8, .p-7, .p-6 { padding: 1rem !important; } .max-w-6xl,
.max-w-4xl,
.max-w-3xl,
.max-w-2xl,
.max-w-xl,
.max-w-md,
.max-w-sm {
max-width: 100vw !important;
}
.p-8,
.p-7,
.p-6 {
padding: 1rem !important;
}
} }

View File

@ -1,54 +1,80 @@
/** theme.ts - Defines visual theme tokens and utility styles across the Recipe Manager frontend */ /**
* Centralized design tokens for the Recipe Manager frontend.
* Keep values semantic so UI primitives can evolve without broad refactors.
*/
export const colors = { export const colors = {
primary: '#2563eb', // Tailwind blue-600 primary: '#2563eb',
primaryDark: '#1d4ed8', primaryDark: '#1d4ed8',
primaryLight: '#eff6ff', primaryLight: '#dbeafe',
accent: '#aa3bff', accent: '#9333ea',
success: '#16a34a', success: '#15803d',
warning: '#eab308', warning: '#ca8a04',
error: '#dc2626', error: '#dc2626',
bg: '#fff', bg: '#f4f7fb',
bgAlt: '#f9fafb', // Tailwind gray-50 bgAlt: '#edf2f7',
surface: '#fcfcff', surface: '#ffffff',
border: '#e5e7eb', // Tailwind gray-200 surfaceMuted: '#f8fafc',
text: '#374151', // Tailwind gray-700 border: '#dbe3ef',
textDim: '#6b7280',
textHeading: '#1e293b',
cardShadow: '0 2px 8px 0 rgba(28,30,34,0.08)',
};
export const radius = { text: '#1f2937',
xs: '4px', textDim: '#64748b',
sm: '6px', textHeading: '#0f172a',
md: '10px',
lg: '16px',
full: '999px',
};
export const spacing = { focusRing: '#2563eb',
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '40px',
};
export const shadows = {
card: '0 2px 8px 0 rgba(28,30,34,0.08)',
hover: '0 4px 20px 0 rgba(28,30,34,0.16)',
}; };
export const typography = { export const typography = {
fontFamily: { fontFamily: {
sans: 'system-ui, Segoe UI, Roboto, sans-serif', sans: "Inter, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
heading: 'system-ui, Segoe UI, Roboto, sans-serif', heading: "Inter, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
mono: 'ui-monospace, Consolas, monospace', mono: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
},
fontSize: {
xs: '0.75rem',
sm: '0.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '1.875rem',
},
lineHeight: {
tight: '1.2',
normal: '1.5',
relaxed: '1.65',
}, },
fontWeight: { fontWeight: {
regular: 400, regular: 400,
medium: 500, medium: 500,
semibold: 600,
bold: 700, bold: 700,
}, },
}; };
export const spacing = {
xxs: '0.25rem',
xs: '0.5rem',
sm: '0.75rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
'2xl': '2.5rem',
};
export const radius = {
xs: '0.375rem',
sm: '0.5rem',
md: '0.75rem',
lg: '1rem',
xl: '1.25rem',
full: '9999px',
};
export const shadows = {
subtle: '0 1px 2px rgba(15, 23, 42, 0.06)',
card: '0 10px 30px rgba(15, 23, 42, 0.08)',
hover: '0 14px 34px rgba(15, 23, 42, 0.12)',
focus: '0 0 0 3px rgba(37, 99, 235, 0.25)',
};