diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 7f761e6..dd329f2 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -31,7 +31,8 @@ app/ # Next.js App Router pages components/ Header.tsx / Footer.tsx # Site chrome RecipesClient.tsx # Recipe listing with filter state - RecipeLayout.tsx # Sidebar layout (mobile drawer, desktop persistent) + RecipeLayout.tsx # Recipe listing layout: filter button, Sidebar + results + Sidebar.tsx # Responsive sidebar shell (mobile drawer, desktop sticky column) RecipesSidebar.tsx # Search + category/tag facet lists with result counts FacetGroup.tsx # Collapsible sidebar facet section ActiveFilters.tsx # Removable chips for active filters (above results) diff --git a/.claude/rules/architecture.md b/.claude/rules/architecture.md index 37d339a..6e58536 100644 --- a/.claude/rules/architecture.md +++ b/.claude/rules/architecture.md @@ -3,6 +3,7 @@ ## State and Data Flow - **RecipeLayout** owns sidebar open/close state; passes `handleFilterChange` (memoised with `useCallback`) down to RecipesSidebar +- **Sidebar** is the generic shell: a modal drawer below `lg`, a sticky column at `lg`+. While open it locks page scroll, closes on Escape/overlay/resize to `lg`, and moves focus in (restoring it on close). Its body has `overscroll-contain` so drawer scrolling never chains to the page. `onClose` must be stable (`useCallback`) since its effects depend on it - **RecipesSidebar** owns local UI state (debounced search input, tag filter text, show-more toggles) and reports filter changes via `onFilterChange` - **RecipesClient** owns filter state (synced to the URL), the filtered recipe list, and facet counts (all memoised with `useMemo`). Each facet's counts apply every filter except its own; options with zero results are hidden - **ActiveFilters** renders removable chips for the active filters above the results grid diff --git a/components/RecipeLayout.tsx b/components/RecipeLayout.tsx index b90967a..1101bdd 100644 --- a/components/RecipeLayout.tsx +++ b/components/RecipeLayout.tsx @@ -1,6 +1,7 @@ 'use client'; -import { useState, useCallback, useEffect, ReactNode } from 'react'; +import { useState, useCallback, ReactNode } from 'react'; +import Sidebar from './Sidebar'; import RecipesSidebar from './RecipesSidebar'; import type { FacetCounts, FilterState } from '@/lib/types'; @@ -29,33 +30,17 @@ export default function RecipeLayout({ onFilterChange(newFilters); }, [onFilterChange]); - // Close the mobile drawer on Escape - useEffect(() => { - if (!sidebarOpen) return; - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') setSidebarOpen(false); - }; - document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('keydown', handleKeyDown); - }, [sidebarOpen]); + const closeSidebar = useCallback(() => setSidebarOpen(false), []); const activeFilterCount = (filters.search ? 1 : 0) + (filters.category ? 1 : 0) + filters.selectedTags.length; return (