feat(ui): redesign homepage visuals and hierarchy (T04)

This commit is contained in:
Paul Huliganga 2026-03-26 17:51:52 -04:00
parent 4af20eaf91
commit b5a2588bc4
5 changed files with 121 additions and 42 deletions

View File

@ -62,3 +62,43 @@ Severity legend: **High** = materially harms UX/clarity, **Medium** = noticeable
- **P1**: Introduce consistent, high-quality food imagery strategy (home + detail + cards).
- **P1**: Reduce action clutter in detail and establish one primary action per context.
- **P2**: Replace ad-hoc emoji iconography with a coherent icon system.
---
# T04 Homepage Redesign (After)
Date: 2026-03-26
Task: T04 — Homepage Redesign
Owner: agent-core-ui
## What changed
- Introduced a stronger home hero with clear action hierarchy:
- primary CTA: **Start a Recipe**
- secondary CTA: **Browse Library**
- responsive image panel with safe fallback chain (`hero.png` → `/images/hero-fallback.svg`)
- Added feature-highlight blocks using T03 asset icons:
- `/assets/category/icon-dinner.svg`
- `/assets/category/icon-lunch.svg`
- `/assets/category/icon-breakfast.svg`
- Improved visual hierarchy and spacing for the recipe discovery area:
- renamed section to **Recipe Library**
- improved search input and action sizing for touch targets
- retained filter behavior with clearer card grouping
- Polished footer structure in app shell:
- multi-column footer content
- quick links and stronger brand/utility hierarchy
- Empty state now uses T03 illustration (`/assets/empty-state/no-recipes.svg`) for better visual clarity.
## Token alignment note (T02 dependency)
- T02 token outputs are available (`frontend/src/theme.ts` + CSS custom properties in `frontend/src/index.css`).
- T04 consumed existing token-friendly values (radius + CSS vars via global styles) and avoided introducing breaking token assumptions.
- If T02 token contracts evolve, align CTA/card semantic classes first before changing component behavior.
## After screenshots
Saved under `docs/visual-audit/after/`:
- `home-desktop.png`
- `home-mobile.png`

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

View File

@ -9,7 +9,6 @@ import { ToastContainer } from './components/Toast';
import { useToast } from './hooks/useToast';
import { createContext, useContext } from 'react';
// Create toast context to share toast functionality across the app
interface ToastContextType {
success: (message: string, duration?: number) => string;
error: (message: string, duration?: number) => string;
@ -79,15 +78,15 @@ function App() {
<ToastContainer messages={toast.messages} onClose={toast.removeToast} />
<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="flex items-center justify-between h-16 gap-3">
<div className="mx-auto max-w-7xl px-4">
<div className="flex h-16 items-center justify-between gap-3">
<div className="flex items-center">
<Link
to="/"
className="group inline-flex items-center gap-2 rounded-lg px-2 py-1.5 outline-none transition-colors duration-200 hover:text-blue-700 focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2"
>
<span className="text-xl" aria-hidden="true">🍽</span>
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 tracking-tight">Recipe Manager</h1>
<h1 className="text-xl font-bold tracking-tight text-gray-900 sm:text-2xl">Recipe Manager</h1>
</Link>
</div>
<nav aria-label="Primary" className="flex flex-wrap items-center justify-end gap-2 sm:gap-3">
@ -108,7 +107,7 @@ function App() {
</div>
</header>
<main className="max-w-7xl mx-auto py-8 px-4 min-h-[70vh]">
<main className="mx-auto min-h-[70vh] max-w-7xl px-4 py-8">
<Routes>
<Route path="/" element={<RecipeListPage />} />
<Route path="/recipe/new" element={<RecipeDetailPage />} />
@ -119,11 +118,24 @@ function App() {
</Routes>
</main>
<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">
<p className="text-center text-sm text-gray-500">
Recipe Manager MVP - Built with React + Vite + TypeScript
</p>
<footer className="mt-10 border-t border-slate-200/80 bg-gradient-to-br from-white/90 to-slate-100/90 backdrop-blur-sm dark:border-slate-700/60 dark:from-slate-900/60 dark:to-slate-900/30">
<div className="mx-auto grid max-w-7xl grid-cols-1 gap-6 px-4 py-8 sm:grid-cols-2 lg:grid-cols-3">
<div>
<p className="text-sm font-semibold uppercase tracking-wide text-blue-700">Recipe Manager</p>
<p className="mt-2 text-sm text-slate-600">Save recipes, organize by tags, and keep your kitchen workflow simple.</p>
</div>
<div>
<p className="text-sm font-semibold uppercase tracking-wide text-slate-700">Quick Links</p>
<div className="mt-2 flex flex-wrap gap-2 text-sm text-slate-600">
<Link to="/" className="rounded-full bg-white px-3 py-1 hover:bg-slate-100">Browse</Link>
<Link to="/recipe/new" className="rounded-full bg-white px-3 py-1 hover:bg-slate-100">Add Recipe</Link>
<Link to="/import/url" className="rounded-full bg-white px-3 py-1 hover:bg-slate-100">Import URL</Link>
</div>
</div>
<div className="sm:col-span-2 lg:col-span-1">
<p className="text-sm font-semibold uppercase tracking-wide text-slate-700">Built for everyday cooking</p>
<p className="mt-2 text-sm text-slate-600">React + Vite + TypeScript · Visual redesign in progress</p>
</div>
</div>
</footer>
</div>

View File

@ -14,6 +14,24 @@ const emptyStatus: HarnessStatus = {
uptime: 0,
};
const featureHighlights = [
{
title: 'Organize with tags',
copy: 'Group your meals by cuisine, prep style, or dietary preference so finding dinner is instant.',
icon: '/assets/category/icon-dinner.svg',
},
{
title: 'Capture from the web',
copy: 'Import recipe links and keep your best discoveries in one place instead of scattered bookmarks.',
icon: '/assets/category/icon-lunch.svg',
},
{
title: 'Cook with confidence',
copy: 'Use clear recipe cards and streamlined actions while planning, prepping, and cooking.',
icon: '/assets/category/icon-breakfast.svg',
},
] as const;
export function RecipeListPage() {
const [searchTerm, setSearchTerm] = useState('');
const [searchQuery, setSearchQuery] = useState('');
@ -55,36 +73,36 @@ export function RecipeListPage() {
<MissionControlPanel status={emptyStatus} />
<section
className="relative mt-8 overflow-hidden border border-slate-200/80 bg-white/85 p-0 shadow-card"
style={{ borderRadius: radius.lg }}
className="relative mt-6 overflow-hidden border border-slate-200/80 bg-white/90 p-0 shadow-card"
style={{ borderRadius: radius.xl }}
>
<div className="grid grid-cols-1 md:grid-cols-2">
<div className="flex flex-col justify-center gap-4 px-6 py-8 md:px-8 md:py-10">
<p className="text-sm font-semibold uppercase tracking-wider text-orange-500">Kitchen Companion</p>
<h1 className="text-3xl font-extrabold leading-tight text-slate-900 md:text-4xl">Cook smarter with your favorite recipes in one place</h1>
<div className="grid grid-cols-1 lg:grid-cols-2">
<div className="flex flex-col justify-center gap-4 px-6 py-8 md:px-10 md:py-12">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-orange-500 md:text-sm">Your kitchen companion</p>
<h1 className="text-3xl font-extrabold leading-tight text-slate-900 md:text-5xl">Plan meals faster and keep every favorite recipe organized</h1>
<p className="max-w-lg text-sm text-slate-600 md:text-base">
Build your personal cookbook, tag meals by mood or diet, and find what you need fast.
Build your personal cookbook with better structure, quick tag filters, and easy capture from web links.
</p>
<div className="flex flex-wrap gap-3">
<div className="flex flex-wrap gap-3 pt-1">
<Link
to="/recipe/new"
className="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-5 py-2.5 font-semibold text-white shadow transition-all duration-200 hover:-translate-y-0.5 hover:bg-blue-700 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
className="inline-flex min-h-11 items-center gap-2 rounded-lg bg-blue-600 px-5 py-2.5 font-semibold text-white shadow transition-all duration-200 hover:-translate-y-0.5 hover:bg-blue-700 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
style={{ borderRadius: radius.md }}
>
<span aria-hidden="true"></span>
Add Recipe
<span aria-hidden="true"></span>
Start a Recipe
</Link>
<a
href="#recipes-grid"
className="inline-flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-5 py-2.5 font-semibold text-slate-700 transition-all duration-200 hover:-translate-y-0.5 hover:bg-slate-50 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
className="inline-flex min-h-11 items-center gap-2 rounded-lg border border-slate-200 bg-white px-5 py-2.5 font-semibold text-slate-700 transition-all duration-200 hover:-translate-y-0.5 hover:bg-slate-50 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
style={{ borderRadius: radius.md }}
>
<span aria-hidden="true">📚</span>
Browse Recipes
Browse Library
</a>
</div>
</div>
<div className="relative min-h-[220px] bg-slate-100 md:min-h-full">
<div className="relative min-h-[260px] bg-slate-100 md:min-h-[320px] lg:min-h-full">
<img
src={heroSrc}
alt={visualAssets.hero.alt}
@ -95,28 +113,37 @@ export function RecipeListPage() {
}
}}
/>
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-slate-900/20 via-transparent to-transparent" />
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-slate-900/45 via-slate-900/15 to-transparent" />
<div className="absolute bottom-4 left-4 rounded-full bg-white/90 px-4 py-1.5 text-sm font-semibold text-slate-800 shadow">
{filteredRecipes.length} saved recipes
</div>
</div>
</div>
</section>
<section className="mt-6 grid grid-cols-1 gap-4 md:grid-cols-3">
{featureHighlights.map((feature) => (
<article
key={feature.title}
className="rounded-xl border border-slate-200/80 bg-white/85 p-4 shadow-card"
style={{ borderRadius: radius.lg }}
>
<img src={feature.icon} alt="" aria-hidden="true" className="h-10 w-10" />
<h2 className="mt-3 text-lg font-bold text-slate-900">{feature.title}</h2>
<p className="mt-1 text-sm text-slate-600">{feature.copy}</p>
</article>
))}
</section>
<section
className="mt-8 mb-10 flex flex-col gap-4 rounded-xl border border-slate-200/80 bg-white/90 px-5 py-6 shadow-card md:px-6"
className="mb-10 mt-7 flex flex-col gap-4 rounded-xl border border-slate-200/80 bg-white/90 px-5 py-6 shadow-card md:px-6"
style={{ borderRadius: radius.lg, boxShadow: '0 2px 8px 0 rgba(28,30,34,0.07)' }}
>
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-end">
<div>
<h2 className="mb-0 text-2xl font-extrabold text-gray-900">My Recipes</h2>
<p className="mt-1 text-sm text-gray-500">Browse and search your recipe collection</p>
<h2 className="mb-0 text-2xl font-extrabold text-gray-900">Recipe Library</h2>
<p className="mt-1 text-sm text-gray-500">Search, filter, and jump back into your most-used meals</p>
</div>
<Link
to="/recipe/new"
className="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 font-semibold text-white shadow transition-all duration-200 hover:-translate-y-0.5 hover:bg-blue-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
style={{ borderRadius: radius.md }}
>
<span aria-hidden="true"></span>
New Recipe
</Link>
</div>
<form onSubmit={handleSearch} className="mt-3 flex flex-col items-stretch gap-3 md:mt-0 md:flex-row">
@ -126,8 +153,8 @@ export function RecipeListPage() {
placeholder="Search recipes by title, ingredients, or tags..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full rounded-lg border border-gray-300 px-4 py-2 text-base focus:border-transparent focus:ring-2 focus:ring-blue-500"
style={{ borderRadius: radius.md }}
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-base focus:border-transparent focus:ring-2 focus:ring-blue-500"
style={{ borderRadius: radius.md, minHeight: 44 }}
/>
{!!searchQuery && (
<button
@ -142,7 +169,7 @@ export function RecipeListPage() {
</div>
<button
type="submit"
className="rounded-lg border border-gray-200 bg-gray-100 px-6 py-2 font-semibold text-gray-700 transition-all duration-200 hover:-translate-y-0.5 hover:bg-gray-200 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
className="min-h-11 rounded-lg border border-gray-200 bg-gray-100 px-6 py-2 font-semibold text-gray-700 transition-all duration-200 hover:-translate-y-0.5 hover:bg-gray-200 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
style={{ borderRadius: radius.md }}
>
Search
@ -187,7 +214,7 @@ export function RecipeListPage() {
{selectedTagId !== null && (
<span className="rounded bg-blue-50 px-2 py-1 text-blue-700">Tag: {tags.find((t) => t.id === selectedTagId)?.name}</span>
)}
<button onClick={handleClearFilters} className="font-medium text-blue-600 transition-colors hover:text-blue-700 focus-visible:ring-2 focus-visible:ring-blue-500 rounded-sm">
<button onClick={handleClearFilters} className="rounded-sm font-medium text-blue-600 transition-colors hover:text-blue-700 focus-visible:ring-2 focus-visible:ring-blue-500">
Clear all filters
</button>
</div>
@ -217,13 +244,13 @@ export function RecipeListPage() {
className="mx-auto flex max-w-xl flex-col items-center gap-2 rounded-xl border border-dashed border-orange-200 bg-gradient-to-br from-white to-orange-50 p-14 text-center shadow-card"
style={{ borderRadius: radius.lg }}
>
<div className="mb-2 text-6xl">🧑🍳</div>
<img src="/assets/empty-state/no-recipes.svg" alt="" aria-hidden="true" className="mb-2 h-28 w-28" />
<h3 className="mb-2 text-xl font-bold text-gray-800">{searchQuery ? 'No recipes found' : 'No recipes yet'}</h3>
<p className="mb-4 text-gray-600">{searchQuery ? 'Try another keyword or clear filters.' : 'Start your cookbook with a first delicious recipe.'}</p>
{!searchQuery && (
<Link
to="/recipe/new"
className="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-6 py-3 font-semibold text-white shadow transition-all duration-200 hover:-translate-y-0.5 hover:bg-blue-700 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
className="inline-flex min-h-11 items-center gap-2 rounded-lg bg-blue-600 px-6 py-3 font-semibold text-white shadow transition-all duration-200 hover:-translate-y-0.5 hover:bg-blue-700 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
style={{ borderRadius: radius.md }}
>
<span aria-hidden="true">🍳</span>