UI polish: Introduce visual theme tokens, refreshed RecipeList page & cards, improved empty/loading states
This commit is contained in:
parent
2ffb1da919
commit
b7e7e9955e
|
|
@ -1,18 +1,12 @@
|
|||
/**
|
||||
* RecipeCard - Displays a single recipe in the list view
|
||||
*/
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { Recipe, Tag } from '../types/recipe';
|
||||
import { colors, radius, shadows } from '../theme';
|
||||
|
||||
interface RecipeCardProps {
|
||||
recipe: Recipe;
|
||||
tags?: Tag[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time in minutes to readable string
|
||||
*/
|
||||
function formatTime(minutes?: number): string {
|
||||
if (!minutes) return '';
|
||||
if (minutes < 60) return `${minutes}m`;
|
||||
|
|
@ -21,9 +15,6 @@ function formatTime(minutes?: number): string {
|
|||
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date timestamp to readable string
|
||||
*/
|
||||
function formatDate(timestamp?: number): string {
|
||||
if (!timestamp) return '';
|
||||
const date = new Date(timestamp * 1000);
|
||||
|
|
@ -36,29 +27,23 @@ export function RecipeCard({ recipe, tags = [] }: RecipeCardProps) {
|
|||
return (
|
||||
<Link
|
||||
to={`/recipe/${recipe.id}`}
|
||||
className="block bg-white rounded-lg shadow hover:shadow-md transition-shadow border border-gray-200"
|
||||
className="block bg-white border border-gray-200 rounded-xl shadow-card hover:shadow-lg hover:border-blue-300 transition-shadow group outline-none focus-visible:ring-2 focus-visible:ring-blue-600 min-h-[200px]"
|
||||
style={{ boxShadow: shadows.card, borderRadius: radius.lg }}
|
||||
>
|
||||
<div className="p-5">
|
||||
<div className="p-5 flex flex-col h-full">
|
||||
{/* Title */}
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2 line-clamp-2">
|
||||
{recipe.title}
|
||||
</h3>
|
||||
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-1 line-clamp-2 group-hover:text-blue-700 transition-colors">{recipe.title}</h3>
|
||||
{/* Description */}
|
||||
{recipe.description && (
|
||||
<p className="text-sm text-gray-600 mb-3 line-clamp-2">
|
||||
{recipe.description}
|
||||
</p>
|
||||
)}
|
||||
{recipe.description && <p className="text-xs text-gray-600 mb-2 line-clamp-2">{recipe.description}</p>}
|
||||
|
||||
{/* Tags */}
|
||||
{tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mb-3">
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{tags.map(tag => (
|
||||
<span
|
||||
key={tag.id}
|
||||
className="px-2 py-0.5 rounded-full text-xs font-medium text-white"
|
||||
style={{ backgroundColor: tag.color || '#3B82F6' }}
|
||||
className="px-2 py-0.5 rounded-full text-xs font-semibold text-white shadow"
|
||||
style={{ backgroundColor: tag.color || colors.primary, borderRadius: radius.full }}
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
|
|
@ -67,21 +52,19 @@ export function RecipeCard({ recipe, tags = [] }: RecipeCardProps) {
|
|||
)}
|
||||
|
||||
{/* Meta information */}
|
||||
<div className="flex flex-wrap gap-3 text-xs text-gray-500">
|
||||
<div className="flex flex-wrap gap-3 text-xs text-gray-500 mb-2">
|
||||
{recipe.servings && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>🍽️</span>
|
||||
<span>{recipe.servings} servings</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{totalTime > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>⏱️</span>
|
||||
<span>{formatTime(totalTime)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{recipe.last_cooked_at && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>👨🍳</span>
|
||||
|
|
@ -90,12 +73,9 @@ export function RecipeCard({ recipe, tags = [] }: RecipeCardProps) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer with ingredient count */}
|
||||
<div className="mt-4 pt-3 border-t border-gray-100">
|
||||
<div className="flex justify-between items-center text-xs text-gray-500">
|
||||
<div className="mt-auto pt-3 border-t border-gray-100 flex justify-between items-center text-xs text-gray-500">
|
||||
<span>{recipe.ingredients.length} ingredients</span>
|
||||
<span className="text-blue-600 font-medium">View Recipe →</span>
|
||||
</div>
|
||||
<span className="text-blue-600 font-medium group-hover:underline">View Recipe →</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -3,17 +3,16 @@
|
|||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--text: #6b6375;
|
||||
--text-h: #08060d;
|
||||
--text: #374151;
|
||||
--text-h: #1e293b;
|
||||
--bg: #fff;
|
||||
--border: #e5e4e7;
|
||||
--bg-alt: #f9fafb;
|
||||
--border: #e5e7eb;
|
||||
--code-bg: #f4f3ec;
|
||||
--accent: #aa3bff;
|
||||
--accent-bg: rgba(170, 59, 255, 0.1);
|
||||
--accent-border: rgba(170, 59, 255, 0.5);
|
||||
--social-bg: rgba(244, 243, 236, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
||||
--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;
|
||||
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
|
|
@ -27,17 +26,16 @@
|
|||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text: #9ca3af;
|
||||
--text: #d1d5db;
|
||||
--text-h: #f3f4f6;
|
||||
--bg: #16171d;
|
||||
--bg-alt: #1a1b20;
|
||||
--border: #2e303a;
|
||||
--code-bg: #1f2028;
|
||||
--accent: #c084fc;
|
||||
--accent-bg: rgba(192, 132, 252, 0.15);
|
||||
--accent-border: rgba(192, 132, 252, 0.5);
|
||||
--social-bg: rgba(47, 48, 58, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
||||
--accent-bg: rgba(192, 132, 252, 0.11);
|
||||
--accent-border: rgba(192, 132, 252, 0.33);
|
||||
--card-shadow: 0 3px 14px 0 rgba(32,34,40,0.21);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -45,25 +43,27 @@ body {
|
|||
margin: 0;
|
||||
font-family: var(--sans);
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
background: var(--bg-alt);
|
||||
}
|
||||
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-alt);
|
||||
}
|
||||
|
||||
input, button, textarea, select {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.shadow-card {
|
||||
box-shadow: var(--card-shadow) !important;
|
||||
}
|
||||
|
||||
/* Toast animation */
|
||||
@keyframes slide-in {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
.animate-slide-in {
|
||||
animation: slide-in 0.3s ease-out;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
/**
|
||||
* RecipeListPage - Displays a list of all recipes with search and filtering
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useRecipes } from '../hooks/useRecipes';
|
||||
import { useTags } from '../hooks/useTags';
|
||||
import { RecipeCard } from '../components/RecipeCard';
|
||||
import { MissionControlPanel } from '../components/MissionControlPanel';
|
||||
import type { HarnessStatus } from '../types/recipe';
|
||||
import { radius } from '../theme';
|
||||
|
||||
const emptyStatus: HarnessStatus = {
|
||||
running: false,
|
||||
version: '-',
|
||||
uptime: 0,
|
||||
};
|
||||
|
||||
export function RecipeListPage() {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
|
@ -42,66 +46,62 @@ export function RecipeListPage() {
|
|||
const hasActiveFilters = searchQuery || selectedTagId !== null;
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto pb-8">
|
||||
<MissionControlPanel status={emptyStatus} />
|
||||
|
||||
<div className="bg-white border rounded-xl shadow-card px-6 py-7 mt-8 mb-10 flex flex-col gap-4" style={{borderRadius: radius.lg, boxShadow: '0 2px 8px 0 rgba(28,30,34,0.07)'}}>
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||
<div>
|
||||
<MissionControlPanel />
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">My Recipes</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Browse and search your recipe collection
|
||||
</p>
|
||||
<h2 className="text-2xl font-extrabold text-gray-900 mb-0">My Recipes</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">Browse and search your recipe collection</p>
|
||||
</div>
|
||||
<Link
|
||||
to="/recipe/new"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition-colors"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-semibold transition-colors shadow focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||
style={{borderRadius: radius.md}}
|
||||
>
|
||||
+ New Recipe
|
||||
</Link>
|
||||
</div>
|
||||
{/* Search Bar */}
|
||||
<form onSubmit={handleSearch} className="flex gap-2">
|
||||
|
||||
{/* Search/Tag Filter Row */}
|
||||
<form onSubmit={handleSearch} className="flex flex-col md:flex-row items-stretch gap-3 mt-3 md:mt-0">
|
||||
<div className="flex-1 relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search recipes by title, ingredients, or tags..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-base"
|
||||
style={{borderRadius: radius.md}}
|
||||
/>
|
||||
{searchQuery && (
|
||||
{!!searchQuery && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearSearch}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
aria-label="Clear search"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
>✕</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-6 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg font-medium transition-colors"
|
||||
className="px-6 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg font-semibold transition-colors border border-gray-200"
|
||||
style={{borderRadius: radius.md}}
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
{/* Tag Filter */}
|
||||
|
||||
{!tagsLoading && tags.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Filter by tag:
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-wrap gap-2 items-center mt-0 md:mt-2">
|
||||
<span className="text-sm text-gray-700 font-medium mr-1">Filter by tag:</span>
|
||||
<button
|
||||
onClick={() => setSelectedTagId(null)}
|
||||
className={
|
||||
selectedTagId === null
|
||||
? 'bg-blue-600 text-white px-3 py-1.5 rounded-full text-sm font-medium transition-colors'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 px-3 py-1.5 rounded-full text-sm font-medium transition-colors'
|
||||
}
|
||||
className={selectedTagId === null
|
||||
? 'bg-blue-600 text-white px-3 py-1.5 rounded-full text-sm font-semibold shadow transition-colors outline-none'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 px-3 py-1.5 rounded-full text-sm font-medium transition-colors outline-none'}
|
||||
style={{borderRadius: radius.full}}
|
||||
>
|
||||
All Recipes
|
||||
</button>
|
||||
|
|
@ -109,99 +109,80 @@ export function RecipeListPage() {
|
|||
<button
|
||||
key={tag.id}
|
||||
onClick={() => setSelectedTagId(tag.id)}
|
||||
className={
|
||||
selectedTagId === tag.id
|
||||
? 'text-white bg-blue-600 px-3 py-1.5 rounded-full text-sm font-medium transition-colors'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 px-3 py-1.5 rounded-full text-sm font-medium transition-colors'
|
||||
}
|
||||
className={selectedTagId === tag.id
|
||||
? 'text-white bg-blue-600 px-3 py-1.5 rounded-full text-sm font-semibold shadow outline-none'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 px-3 py-1.5 rounded-full text-sm font-medium transition-colors outline-none'}
|
||||
style={{backgroundColor: selectedTagId === tag.id ? tag.color : '', borderRadius: radius.full}}
|
||||
>
|
||||
{tag.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Active Filters */}
|
||||
{hasActiveFilters && (
|
||||
<div className="mt-3 flex items-center gap-3 text-sm">
|
||||
<div className="mt-2 flex items-center gap-3 text-sm">
|
||||
<span className="text-gray-600">Active filters:</span>
|
||||
{searchQuery && (
|
||||
<span className="px-2 py-1 bg-blue-50 text-blue-700 rounded">
|
||||
Search: "{searchQuery}"
|
||||
</span>
|
||||
)}
|
||||
{searchQuery && <span className="px-2 py-1 bg-blue-50 text-blue-700 rounded">Search: "{searchQuery}"</span>}
|
||||
{selectedTagId !== null && (
|
||||
<span className="px-2 py-1 bg-blue-50 text-blue-700 rounded">
|
||||
Tag: {tags.find(t => t.id === selectedTagId)?.name}
|
||||
</span>
|
||||
<span className="px-2 py-1 bg-blue-50 text-blue-700 rounded">Tag: {tags.find(t => t.id === selectedTagId)?.name}</span>
|
||||
)}
|
||||
<button
|
||||
onClick={handleClearFilters}
|
||||
className="text-blue-600 hover:text-blue-700 font-medium"
|
||||
>
|
||||
Clear all filters
|
||||
</button>
|
||||
<button onClick={handleClearFilters} className="text-blue-600 hover:text-blue-700 font-medium">Clear all filters</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Error State */}
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
||||
<p className="text-red-800">
|
||||
<strong>Error:</strong> {error}
|
||||
</p>
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 my-6 text-center">
|
||||
<p className="text-red-800"><strong>Error:</strong> {error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading State */}
|
||||
{loading && recipes.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<p className="mt-4 text-gray-600">Loading recipes...</p>
|
||||
<div className="text-center py-16">
|
||||
<div className="inline-block animate-spin rounded-full h-10 w-10 border-b-2 border-blue-600"></div>
|
||||
<p className="mt-4 text-gray-700 text-lg font-medium">Loading recipes...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{!loading && !error && filteredRecipes.length === 0 && (
|
||||
<div className="bg-white rounded-lg shadow p-12 text-center">
|
||||
<div className="text-6xl mb-4">🍳</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
{searchQuery ? 'No recipes found' : 'No recipes yet'}
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-6">
|
||||
{searchQuery
|
||||
<div className="bg-gradient-to-br from-white to-blue-50 rounded-xl shadow-card p-14 text-center flex flex-col items-center gap-2 border border-dashed border-blue-200 mx-auto max-w-xl" style={{borderRadius: radius.lg}}>
|
||||
<div className="text-6xl mb-2">🍳</div>
|
||||
<h3 className="text-xl font-bold text-gray-800 mb-2">{searchQuery ? 'No recipes found' : 'No recipes yet'}</h3>
|
||||
<p className="text-gray-600 mb-4">{searchQuery
|
||||
? 'Try a different search term'
|
||||
: 'Get started by adding your first recipe'}
|
||||
</p>
|
||||
: 'Get started by adding your first recipe.'}</p>
|
||||
{!searchQuery && (
|
||||
<Link
|
||||
to="/recipe/new"
|
||||
className="inline-block bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Add Your First Recipe
|
||||
</Link>
|
||||
<Link to="/recipe/new" className="inline-block bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-semibold shadow transition-colors" style={{borderRadius: radius.md}}>Add Your First Recipe</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recipe Grid */}
|
||||
{filteredRecipes.length > 0 && (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-8">
|
||||
{filteredRecipes.map((recipe) => (
|
||||
<RecipeCard key={recipe.id} recipe={recipe} />
|
||||
<RecipeCard key={recipe.id} recipe={recipe} tags={recipe.tags} />
|
||||
))}
|
||||
</div>
|
||||
{/* Load More Button */}
|
||||
{hasMore && (
|
||||
<div className="mt-8 text-center">
|
||||
<button
|
||||
onClick={loadMore}
|
||||
disabled={loading}
|
||||
className="px-6 py-3 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="px-6 py-3 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed border"
|
||||
style={{borderRadius: radius.md}}
|
||||
>
|
||||
{loading ? 'Loading...' : 'Load More'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Results summary */}
|
||||
<div className="mt-6 text-center text-sm text-gray-500">
|
||||
<div className="mt-7 text-center text-sm text-gray-500">
|
||||
Showing {filteredRecipes.length} recipe{filteredRecipes.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
/** theme.ts - Defines visual theme tokens and utility styles across the Recipe Manager frontend */
|
||||
|
||||
export const colors = {
|
||||
primary: '#2563eb', // Tailwind blue-600
|
||||
primaryDark: '#1d4ed8',
|
||||
primaryLight: '#eff6ff',
|
||||
accent: '#aa3bff',
|
||||
success: '#16a34a',
|
||||
warning: '#eab308',
|
||||
error: '#dc2626',
|
||||
|
||||
bg: '#fff',
|
||||
bgAlt: '#f9fafb', // Tailwind gray-50
|
||||
surface: '#fcfcff',
|
||||
border: '#e5e7eb', // Tailwind gray-200
|
||||
text: '#374151', // Tailwind gray-700
|
||||
textDim: '#6b7280',
|
||||
textHeading: '#1e293b',
|
||||
cardShadow: '0 2px 8px 0 rgba(28,30,34,0.08)',
|
||||
};
|
||||
|
||||
export const radius = {
|
||||
xs: '4px',
|
||||
sm: '6px',
|
||||
md: '10px',
|
||||
lg: '16px',
|
||||
full: '999px',
|
||||
};
|
||||
|
||||
export const spacing = {
|
||||
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 = {
|
||||
fontFamily: {
|
||||
sans: 'system-ui, Segoe UI, Roboto, sans-serif',
|
||||
heading: 'system-ui, Segoe UI, Roboto, sans-serif',
|
||||
mono: 'ui-monospace, Consolas, monospace',
|
||||
},
|
||||
fontWeight: {
|
||||
regular: 400,
|
||||
medium: 500,
|
||||
bold: 700,
|
||||
},
|
||||
};
|
||||
|
|
@ -5,7 +5,31 @@ export default {
|
|||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
borderRadius: {
|
||||
xs: '4px',
|
||||
sm: '6px',
|
||||
md: '10px',
|
||||
lg: '16px',
|
||||
full: '999px',
|
||||
},
|
||||
boxShadow: {
|
||||
card: '0 2px 8px 0 rgba(28,30,34,0.08)',
|
||||
hover: '0 4px 20px 0 rgba(28,30,34,0.16)',
|
||||
},
|
||||
colors: {
|
||||
primary: '#2563eb',
|
||||
accent: '#aa3bff',
|
||||
success: '#16a34a',
|
||||
warning: '#eab308',
|
||||
error: '#dc2626',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['system-ui', 'Segoe UI', 'Roboto', 'sans-serif'],
|
||||
heading: ['system-ui', 'Segoe UI', 'Roboto', 'sans-serif'],
|
||||
mono: ['ui-monospace', 'Consolas', 'monospace'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue