diff --git a/frontend/src/components/RecipeCard.tsx b/frontend/src/components/RecipeCard.tsx index 401e55a..5a2b7d4 100644 --- a/frontend/src/components/RecipeCard.tsx +++ b/frontend/src/components/RecipeCard.tsx @@ -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 ( -
+
{/* Title */} -

- {recipe.title} -

- +

{recipe.title}

{/* Description */} - {recipe.description && ( -

- {recipe.description} -

- )} + {recipe.description &&

{recipe.description}

} {/* Tags */} {tags.length > 0 && ( -
+
{tags.map(tag => ( {tag.name} @@ -67,21 +52,19 @@ export function RecipeCard({ recipe, tags = [] }: RecipeCardProps) { )} {/* Meta information */} -
+
{recipe.servings && (
🍽️ {recipe.servings} servings
)} - {totalTime > 0 && (
⏱️ {formatTime(totalTime)}
)} - {recipe.last_cooked_at && (
👨‍🍳 @@ -90,12 +73,9 @@ export function RecipeCard({ recipe, tags = [] }: RecipeCardProps) { )}
- {/* Footer with ingredient count */} -
-
- {recipe.ingredients.length} ingredients - View Recipe → -
+
+ {recipe.ingredients.length} ingredients + View Recipe →
diff --git a/frontend/src/index.css b/frontend/src/index.css index 2cf6b8c..c456927 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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; } diff --git a/frontend/src/pages/RecipeListPage.tsx b/frontend/src/pages/RecipeListPage.tsx index f95176d..1d71e57 100644 --- a/frontend/src/pages/RecipeListPage.tsx +++ b/frontend/src/pages/RecipeListPage.tsx @@ -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,166 +46,143 @@ export function RecipeListPage() { const hasActiveFilters = searchQuery || selectedTagId !== null; return ( -
- - {/* Header */} -
-
+
+ + +
+
-

My Recipes

-

- Browse and search your recipe collection -

+

My Recipes

+

Browse and search your recipe collection

+ New Recipe
- {/* Search Bar */} -
+ + {/* Search/Tag Filter Row */} +
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 && ( + >✕ )}
- {/* Tag Filter */} + {!tagsLoading && tags.length > 0 && ( -
- -
+
+ Filter by tag: + + {tags.map(tag => ( - {tags.map(tag => ( - - ))} -
+ ))}
)} + + {/* Active Filters */} {hasActiveFilters && ( -
+
Active filters: - {searchQuery && ( - - Search: "{searchQuery}" - - )} + {searchQuery && Search: "{searchQuery}"} {selectedTagId !== null && ( - - Tag: {tags.find(t => t.id === selectedTagId)?.name} - + Tag: {tags.find(t => t.id === selectedTagId)?.name} )} - +
)}
+ {/* Error State */} {error && ( -
-

- Error: {error} -

+
+

Error: {error}

)} + {/* Loading State */} {loading && recipes.length === 0 && ( -
-
-

Loading recipes...

+
+
+

Loading recipes...

)} + {/* Empty State */} {!loading && !error && filteredRecipes.length === 0 && ( -
-
🍳
-

- {searchQuery ? 'No recipes found' : 'No recipes yet'} -

-

- {searchQuery - ? 'Try a different search term' - : 'Get started by adding your first recipe'} -

+
+
🍳
+

{searchQuery ? 'No recipes found' : 'No recipes yet'}

+

{searchQuery + ? 'Try a different search term' + : 'Get started by adding your first recipe.'}

{!searchQuery && ( - - Add Your First Recipe - + Add Your First Recipe )}
)} + {/* Recipe Grid */} {filteredRecipes.length > 0 && ( <> -
+
{filteredRecipes.map((recipe) => ( - + ))}
- {/* Load More Button */} {hasMore && (
)} - {/* Results summary */} -
+
Showing {filteredRecipes.length} recipe{filteredRecipes.length !== 1 ? 's' : ''}
diff --git a/frontend/src/theme.ts b/frontend/src/theme.ts new file mode 100644 index 0000000..02c4514 --- /dev/null +++ b/frontend/src/theme.ts @@ -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, + }, +}; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index dca8ba0..13e3ded 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -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: [], }