feat: implement responsive column-based styling for all thumbnail cards #9

Merged
pkartch merged 11 commits from feature/cookbook-pagination into main 2026-01-20 04:05:03 +00:00
7 changed files with 1013 additions and 96 deletions

View File

@@ -324,3 +324,100 @@ The current version is displayed in:
- API: `GET /api/version` endpoint returns `{ version: '2026.01.002' }` - API: `GET /api/version` endpoint returns `{ version: '2026.01.002' }`
- Web: Footer or about section shows current version - Web: Footer or about section shows current version
- Both packages export `APP_VERSION` constant for internal use - Both packages export `APP_VERSION` constant for internal use
## UI Design System - Thumbnail Cards
### Responsive Column Layout System
All recipe and cookbook thumbnail displays support a responsive column system (3, 5, 7, or 9 columns) with column-specific styling for optimal readability at different densities.
**Column-Responsive Font Sizes:**
- **Column 3** (Largest cards): Title 0.95rem, Description 0.8rem (2 lines), Meta 0.75rem
- **Column 5** (Medium cards): Title 0.85rem, Description 0.75rem (2 lines), Meta 0.7rem
- **Column 7** (Compact): Title 0.75rem, Description hidden, Meta 0.6rem
- **Column 9** (Most compact): Title 0.75rem, Description hidden, Meta 0.6rem
**Implementation Pattern:**
1. Add `gridClassName = \`recipes-grid columns-${columnCount}\`` or `\`cookbooks-grid columns-${columnCount}\``
2. Apply className to grid container: `<div className={gridClassName} style={gridStyle}>`
3. Use column-specific CSS selectors: `.columns-3 .recipe-info h3 { font-size: 0.95rem; }`
### Recipe Thumbnail Display Locations
All locations use square aspect ratio (1:1) cards with 60% image height.
1. **Recipe List Page** (`packages/web/src/pages/RecipeList.tsx`)
- Class: `recipe-grid-enhanced columns-{3|5|7|9}`
- CSS: `packages/web/src/styles/RecipeList.css`
- Features: Main recipe browsing with pagination, search, filtering
- Displays: Image, title, description, time, rating
- Status: ✅ Responsive column styling applied
2. **Cookbooks Page - Recent Recipes** (`packages/web/src/pages/Cookbooks.tsx`)
- Class: `recipes-grid columns-{3|5|7|9}`
- CSS: `packages/web/src/styles/Cookbooks.css`
- Features: Shows 6 most recent recipes below cookbook list
- Displays: Image, title, description, time, rating
- Status: ✅ Responsive column styling applied
3. **Cookbook Detail - Recipes Section** (`packages/web/src/pages/CookbookDetail.tsx`)
- Class: `recipes-grid columns-{3|5|7|9}`
- CSS: `packages/web/src/styles/CookbookDetail.css`
- Features: Paginated recipes within a cookbook, with remove button
- Displays: Image, title, description, time, rating, remove button
- Status: ✅ Responsive column styling applied
4. **Add Meal Modal - Recipe Selection** (`packages/web/src/components/meal-planner/AddMealModal.tsx`)
- Class: `recipe-list` with `recipe-item`
- CSS: `packages/web/src/styles/AddMealModal.css`
- Features: Selectable recipe list for adding to meal plan
- Displays: Small thumbnail, title, description
- Status: ⚠️ Needs responsive column styling review
5. **Meal Card Component** (`packages/web/src/components/meal-planner/MealCard.tsx`)
- Class: `meal-card` with `meal-card-image`
- CSS: `packages/web/src/styles/MealCard.css`
- Features: Recipe thumbnail in meal planner (compact & full views)
- Displays: Recipe image as part of meal display
- Status: ⚠️ Different use case - calendar/list view, not grid-based
### Cookbook Thumbnail Display Locations
All locations use square aspect ratio (1:1) cards with 50% image height.
1. **Cookbooks Page - Main Grid** (`packages/web/src/pages/Cookbooks.tsx`)
- Class: `cookbooks-grid`
- CSS: `packages/web/src/styles/Cookbooks.css`
- Features: Main cookbook browsing with pagination
- Displays: Cover image, name, recipe count, cookbook count
- Status: ✅ Already has compact styling (description/tags hidden)
- Note: Could benefit from column-responsive font sizes
2. **Cookbook Detail - Nested Cookbooks** (`packages/web/src/pages/CookbookDetail.tsx`)
- Class: `cookbooks-grid` with `cookbook-card nested`
- CSS: `packages/web/src/styles/CookbookDetail.css`
- Features: Child cookbooks within parent cookbook
- Displays: Cover image, name, recipe count, cookbook count
- Status: ✅ Already has compact styling (description/tags hidden)
- Note: Could benefit from column-responsive font sizes
### Key CSS Classes
- `recipe-card` - Individual recipe card
- `recipe-grid-enhanced` or `recipes-grid` - Recipe grid container
- `cookbook-card` - Individual cookbook card
- `cookbooks-grid` - Cookbook grid container
- `columns-{3|5|7|9}` - Dynamic column count modifier class
### Styling Consistency Rules
1. **Image Heights**: Recipes 60%, Cookbooks 50%
2. **Aspect Ratio**: All cards are square (1:1)
3. **Border**: 1px solid #e0e0e0 (not box-shadow)
4. **Border Radius**: 8px
5. **Hover Effect**: `translateY(-2px)` with `box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1)`
6. **Description Display**:
- Columns 3 & 5: Show 2 lines
- Columns 7 & 9: Hide completely
7. **Font Scaling**: Larger fonts for fewer columns, smaller for more columns
8. **Text Truncation**: Use `-webkit-line-clamp` with `text-overflow: ellipsis`

View File

@@ -1,9 +1,15 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
import { CookbookWithRecipes, Recipe } from '@basil/shared'; import { CookbookWithRecipes, Recipe } from '@basil/shared';
import { cookbooksApi } from '../services/api'; import { cookbooksApi } from '../services/api';
import '../styles/CookbookDetail.css'; import '../styles/CookbookDetail.css';
const ITEMS_PER_PAGE_OPTIONS = [12, 24, 48, -1]; // -1 = All
// LocalStorage keys
const LS_ITEMS_PER_PAGE = 'basil_cookbook_itemsPerPage';
const LS_COLUMN_COUNT = 'basil_cookbook_columnCount';
// Helper function to extract tag name from string or RecipeTag object // Helper function to extract tag name from string or RecipeTag object
const getTagName = (tag: string | { tag: { name: string } }): string => { const getTagName = (tag: string | { tag: { name: string } }): string => {
return typeof tag === 'string' ? tag : tag.tag.name; return typeof tag === 'string' ? tag : tag.tag.name;
@@ -12,10 +18,33 @@ const getTagName = (tag: string | { tag: { name: string } }): string => {
function CookbookDetail() { function CookbookDetail() {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const [cookbook, setCookbook] = useState<CookbookWithRecipes | null>(null); const [cookbook, setCookbook] = useState<CookbookWithRecipes | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Pagination state
const [currentPage, setCurrentPage] = useState(() => {
const page = searchParams.get('page');
return page ? parseInt(page) : 1;
});
const [itemsPerPage, setItemsPerPage] = useState(() => {
const saved = localStorage.getItem(LS_ITEMS_PER_PAGE);
if (saved) return parseInt(saved);
const param = searchParams.get('limit');
return param ? parseInt(param) : 24;
});
// Display controls state
const [columnCount, setColumnCount] = useState<3 | 5 | 7 | 9>(() => {
const saved = localStorage.getItem(LS_COLUMN_COUNT);
if (saved) {
const val = parseInt(saved);
if (val === 3 || val === 5 || val === 7 || val === 9) return val;
}
return 5;
});
// Filters // Filters
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [selectedTags, setSelectedTags] = useState<string[]>([]); const [selectedTags, setSelectedTags] = useState<string[]>([]);
@@ -27,6 +56,28 @@ function CookbookDetail() {
} }
}, [id]); }, [id]);
// Save preferences to localStorage
useEffect(() => {
localStorage.setItem(LS_ITEMS_PER_PAGE, itemsPerPage.toString());
}, [itemsPerPage]);
useEffect(() => {
localStorage.setItem(LS_COLUMN_COUNT, columnCount.toString());
}, [columnCount]);
// Update URL params
useEffect(() => {
const params = new URLSearchParams();
if (currentPage > 1) params.set('page', currentPage.toString());
if (itemsPerPage !== 24) params.set('limit', itemsPerPage.toString());
setSearchParams(params, { replace: true });
}, [currentPage, itemsPerPage, setSearchParams]);
// Reset page when filters change
useEffect(() => {
setCurrentPage(1);
}, [searchQuery, selectedTags, selectedCuisine]);
const loadCookbook = async (cookbookId: string) => { const loadCookbook = async (cookbookId: string) => {
try { try {
setLoading(true); setLoading(true);
@@ -129,6 +180,24 @@ function CookbookDetail() {
setSelectedCuisine(''); setSelectedCuisine('');
}; };
const handlePageChange = (newPage: number) => {
setCurrentPage(newPage);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handleItemsPerPageChange = (value: number) => {
setItemsPerPage(value);
setCurrentPage(1);
};
// Apply pagination to filtered recipes
const getPaginatedRecipes = (filteredRecipes: Recipe[]): Recipe[] => {
if (itemsPerPage === -1) return filteredRecipes;
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
return filteredRecipes.slice(startIndex, endIndex);
};
if (loading) { if (loading) {
return ( return (
<div className="cookbook-detail-page"> <div className="cookbook-detail-page">
@@ -147,9 +216,19 @@ function CookbookDetail() {
} }
const filteredRecipes = getFilteredRecipes(); const filteredRecipes = getFilteredRecipes();
const paginatedRecipes = getPaginatedRecipes(filteredRecipes);
const allTags = getAllTags(); const allTags = getAllTags();
const allCuisines = getAllCuisines(); const allCuisines = getAllCuisines();
const hasActiveFilters = searchQuery || selectedTags.length > 0 || selectedCuisine; const hasActiveFilters = searchQuery || selectedTags.length > 0 || selectedCuisine;
const totalPages = itemsPerPage === -1 ? 1 : Math.ceil(filteredRecipes.length / itemsPerPage);
// Grid style with CSS variables
const gridStyle: React.CSSProperties = {
gridTemplateColumns: `repeat(${columnCount}, 1fr)`,
};
const recipesGridClassName = `recipes-grid columns-${columnCount}`;
const cookbooksGridClassName = `cookbooks-grid columns-${columnCount}`;
return ( return (
<div className="cookbook-detail-page"> <div className="cookbook-detail-page">
@@ -227,11 +306,66 @@ function CookbookDetail() {
</div> </div>
</div> </div>
{/* Display and Pagination Controls */}
<div className="cookbook-toolbar">
<div className="display-controls">
<div className="control-group">
<label>Columns:</label>
<div className="column-buttons">
{([3, 5, 7, 9] as const).map((count) => (
<button
key={count}
className={columnCount === count ? 'active' : ''}
onClick={() => setColumnCount(count)}
>
{count}
</button>
))}
</div>
</div>
</div>
<div className="pagination-controls">
<div className="control-group">
<label>Per page:</label>
<div className="items-per-page">
{ITEMS_PER_PAGE_OPTIONS.map((count) => (
<button
key={count}
className={itemsPerPage === count ? 'active' : ''}
onClick={() => handleItemsPerPageChange(count)}
>
{count === -1 ? 'All' : count}
</button>
))}
</div>
</div>
<div className="page-navigation">
<button
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage <= 1}
>
Prev
</button>
<span className="page-info">
Page {currentPage} of {totalPages}
</span>
<button
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage >= totalPages}
>
Next
</button>
</div>
</div>
</div>
{/* Included Cookbooks */} {/* Included Cookbooks */}
{cookbook.cookbooks && cookbook.cookbooks.length > 0 && ( {cookbook.cookbooks && cookbook.cookbooks.length > 0 && (
<section className="included-cookbooks-section"> <section className="included-cookbooks-section">
<h2>Included Cookbooks ({cookbook.cookbooks.length})</h2> <h2>Included Cookbooks ({cookbook.cookbooks.length})</h2>
<div className="cookbooks-grid"> <div className={cookbooksGridClassName} style={gridStyle}>
{cookbook.cookbooks.map((childCookbook) => ( {cookbook.cookbooks.map((childCookbook) => (
<div <div
key={childCookbook.id} key={childCookbook.id}
@@ -272,7 +406,12 @@ function CookbookDetail() {
<div className="results-section"> <div className="results-section">
<h2>Recipes</h2> <h2>Recipes</h2>
<p className="results-count"> <p className="results-count">
Showing {filteredRecipes.length} of {cookbook.recipes.length} recipes {itemsPerPage === -1 ? (
`Showing all ${filteredRecipes.length} recipes`
) : (
`Showing ${(currentPage - 1) * itemsPerPage + 1}-${Math.min(currentPage * itemsPerPage, filteredRecipes.length)} of ${filteredRecipes.length} recipes`
)}
{filteredRecipes.length < cookbook.recipes.length && ` (filtered from ${cookbook.recipes.length} total)`}
</p> </p>
{filteredRecipes.length === 0 ? ( {filteredRecipes.length === 0 ? (
@@ -284,8 +423,8 @@ function CookbookDetail() {
)} )}
</div> </div>
) : ( ) : (
<div className="recipes-grid"> <div className={recipesGridClassName} style={gridStyle}>
{filteredRecipes.map(recipe => ( {paginatedRecipes.map(recipe => (
<div key={recipe.id} className="recipe-card"> <div key={recipe.id} className="recipe-card">
<div onClick={() => navigate(`/recipes/${recipe.id}`)}> <div onClick={() => navigate(`/recipes/${recipe.id}`)}>
{recipe.imageUrl ? ( {recipe.imageUrl ? (

View File

@@ -1,11 +1,18 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { Cookbook, Recipe, Tag } from '@basil/shared'; import { Cookbook, Recipe, Tag } from '@basil/shared';
import { cookbooksApi, recipesApi, tagsApi } from '../services/api'; import { cookbooksApi, recipesApi, tagsApi } from '../services/api';
import '../styles/Cookbooks.css'; import '../styles/Cookbooks.css';
const ITEMS_PER_PAGE_OPTIONS = [12, 24, 48, -1]; // -1 = All
// LocalStorage keys
const LS_ITEMS_PER_PAGE = 'basil_cookbooks_itemsPerPage';
const LS_COLUMN_COUNT = 'basil_cookbooks_columnCount';
function Cookbooks() { function Cookbooks() {
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const [cookbooks, setCookbooks] = useState<Cookbook[]>([]); const [cookbooks, setCookbooks] = useState<Cookbook[]>([]);
const [recentRecipes, setRecentRecipes] = useState<Recipe[]>([]); const [recentRecipes, setRecentRecipes] = useState<Recipe[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -22,10 +29,49 @@ function Cookbooks() {
const [availableTags, setAvailableTags] = useState<Tag[]>([]); const [availableTags, setAvailableTags] = useState<Tag[]>([]);
const [autoAddCollapsed, setAutoAddCollapsed] = useState(true); const [autoAddCollapsed, setAutoAddCollapsed] = useState(true);
// Pagination state
const [currentPage, setCurrentPage] = useState(() => {
const page = searchParams.get('page');
return page ? parseInt(page) : 1;
});
const [itemsPerPage, setItemsPerPage] = useState(() => {
const saved = localStorage.getItem(LS_ITEMS_PER_PAGE);
if (saved) return parseInt(saved);
const param = searchParams.get('limit');
return param ? parseInt(param) : 24;
});
// Display controls state
const [columnCount, setColumnCount] = useState<3 | 5 | 7 | 9>(() => {
const saved = localStorage.getItem(LS_COLUMN_COUNT);
if (saved) {
const val = parseInt(saved);
if (val === 3 || val === 5 || val === 7 || val === 9) return val;
}
return 5;
});
useEffect(() => { useEffect(() => {
loadData(); loadData();
}, []); }, []);
// Save preferences to localStorage
useEffect(() => {
localStorage.setItem(LS_ITEMS_PER_PAGE, itemsPerPage.toString());
}, [itemsPerPage]);
useEffect(() => {
localStorage.setItem(LS_COLUMN_COUNT, columnCount.toString());
}, [columnCount]);
// Update URL params
useEffect(() => {
const params = new URLSearchParams();
if (currentPage > 1) params.set('page', currentPage.toString());
if (itemsPerPage !== 24) params.set('limit', itemsPerPage.toString());
setSearchParams(params, { replace: true });
}, [currentPage, itemsPerPage, setSearchParams]);
const loadData = async () => { const loadData = async () => {
try { try {
setLoading(true); setLoading(true);
@@ -117,6 +163,35 @@ function Cookbooks() {
setAutoFilterCookbookTags(autoFilterCookbookTags.filter(t => t !== tag)); setAutoFilterCookbookTags(autoFilterCookbookTags.filter(t => t !== tag));
}; };
const handlePageChange = (newPage: number) => {
setCurrentPage(newPage);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handleItemsPerPageChange = (value: number) => {
setItemsPerPage(value);
setCurrentPage(1);
};
// Apply pagination to cookbooks
const getPaginatedCookbooks = (): Cookbook[] => {
if (itemsPerPage === -1) return cookbooks;
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
return cookbooks.slice(startIndex, endIndex);
};
const paginatedCookbooks = getPaginatedCookbooks();
const totalPages = itemsPerPage === -1 ? 1 : Math.ceil(cookbooks.length / itemsPerPage);
// Grid style with CSS variables
const gridStyle: React.CSSProperties = {
gridTemplateColumns: `repeat(${columnCount}, 1fr)`,
};
const recipesGridClassName = `recipes-grid columns-${columnCount}`;
const cookbooksGridClassName = `cookbooks-grid columns-${columnCount}`;
if (loading) { if (loading) {
return ( return (
<div className="cookbooks-page"> <div className="cookbooks-page">
@@ -150,9 +225,30 @@ function Cookbooks() {
</div> </div>
</header> </header>
{/* Page-level Controls */}
<div className="page-toolbar">
<div className="display-controls">
<div className="control-group">
<label>Columns:</label>
<div className="column-buttons">
{([3, 5, 7, 9] as const).map((count) => (
<button
key={count}
className={columnCount === count ? 'active' : ''}
onClick={() => setColumnCount(count)}
>
{count}
</button>
))}
</div>
</div>
</div>
</div>
{/* Cookbooks Grid */} {/* Cookbooks Grid */}
<section className="cookbooks-section"> <section className="cookbooks-section">
<h2>Cookbooks</h2> <h2>Cookbooks</h2>
{cookbooks.length === 0 ? ( {cookbooks.length === 0 ? (
<div className="empty-state"> <div className="empty-state">
<p>No cookbooks yet. Create your first cookbook to organize your recipes!</p> <p>No cookbooks yet. Create your first cookbook to organize your recipes!</p>
@@ -161,8 +257,56 @@ function Cookbooks() {
</button> </button>
</div> </div>
) : ( ) : (
<div className="cookbooks-grid"> <>
{cookbooks.map((cookbook) => ( {/* Pagination Controls */}
<div className="pagination-toolbar">
<div className="pagination-controls">
<div className="control-group">
<label>Per page:</label>
<div className="items-per-page">
{ITEMS_PER_PAGE_OPTIONS.map((count) => (
<button
key={count}
className={itemsPerPage === count ? 'active' : ''}
onClick={() => handleItemsPerPageChange(count)}
>
{count === -1 ? 'All' : count}
</button>
))}
</div>
</div>
<div className="page-navigation">
<button
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage <= 1}
>
Prev
</button>
<span className="page-info">
Page {currentPage} of {totalPages}
</span>
<button
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage >= totalPages}
>
Next
</button>
</div>
</div>
</div>
{/* Results count */}
<p className="results-count">
{itemsPerPage === -1 ? (
`Showing all ${cookbooks.length} cookbooks`
) : (
`Showing ${(currentPage - 1) * itemsPerPage + 1}-${Math.min(currentPage * itemsPerPage, cookbooks.length)} of ${cookbooks.length} cookbooks`
)}
</p>
<div className={cookbooksGridClassName} style={gridStyle}>
{paginatedCookbooks.map((cookbook) => (
<div <div
key={cookbook.id} key={cookbook.id}
className="cookbook-card" className="cookbook-card"
@@ -195,12 +339,13 @@ function Cookbooks() {
</div> </div>
))} ))}
</div> </div>
</>
)} )}
</section> </section>
{/* Recent Recipes */} {/* Recent Recipes */}
<section className="recent-recipes-section"> <section className="recent-recipes-section">
<div className="section-header"> <div className="section-title-row">
<h2>Recent Recipes</h2> <h2>Recent Recipes</h2>
<button onClick={() => navigate('/recipes')} className="btn-link"> <button onClick={() => navigate('/recipes')} className="btn-link">
View all View all
@@ -209,7 +354,7 @@ function Cookbooks() {
{recentRecipes.length === 0 ? ( {recentRecipes.length === 0 ? (
<p className="empty-state">No recipes yet.</p> <p className="empty-state">No recipes yet.</p>
) : ( ) : (
<div className="recipes-grid"> <div className={recipesGridClassName} style={gridStyle}>
{recentRecipes.map((recipe) => ( {recentRecipes.map((recipe) => (
<div <div
key={recipe.id} key={recipe.id}

View File

@@ -132,6 +132,8 @@ function RecipeList() {
gridTemplateColumns: `repeat(${columnCount}, 1fr)`, gridTemplateColumns: `repeat(${columnCount}, 1fr)`,
}; };
const gridClassName = `recipe-grid-enhanced columns-${columnCount}`;
const handlePageChange = (newPage: number) => { const handlePageChange = (newPage: number) => {
setCurrentPage(newPage); setCurrentPage(newPage);
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
@@ -243,7 +245,7 @@ function RecipeList() {
)} )}
</div> </div>
) : ( ) : (
<div className="recipe-grid-enhanced" style={gridStyle}> <div className={gridClassName} style={gridStyle}>
{recipes.map((recipe) => ( {recipes.map((recipe) => (
<div <div
key={recipe.id} key={recipe.id}

View File

@@ -261,6 +261,118 @@
background-color: #616161; background-color: #616161;
} }
/* Toolbar and Pagination Controls */
.cookbook-toolbar {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
align-items: center;
justify-content: space-between;
background: white;
padding: 0.75rem 1rem;
border-radius: 8px;
margin-bottom: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
border: 1px solid #e0e0e0;
}
.display-controls,
.pagination-controls {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.control-group label {
font-size: 0.8rem;
font-weight: 500;
color: #666;
white-space: nowrap;
}
.column-buttons,
.items-per-page {
display: flex;
gap: 0.25rem;
border: 1px solid #d0d0d0;
border-radius: 6px;
overflow: hidden;
}
.column-buttons button,
.items-per-page button {
min-width: 2rem;
padding: 0.35rem 0.6rem;
border: none;
background: white;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
color: #555;
}
.column-buttons button:not(:last-child),
.items-per-page button:not(:last-child) {
border-right: 1px solid #d0d0d0;
}
.column-buttons button:hover,
.items-per-page button:hover {
background-color: #f5f5f5;
}
.column-buttons button.active,
.items-per-page button.active {
background-color: #2e7d32;
color: white;
}
.page-navigation {
display: flex;
gap: 0.5rem;
align-items: center;
}
.page-navigation button {
padding: 0.35rem 0.75rem;
border: 1px solid #d0d0d0;
background: white;
color: #555;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
}
.page-navigation button:hover:not(:disabled) {
background-color: #f5f5f5;
border-color: #2e7d32;
color: #2e7d32;
}
.page-navigation button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.page-info {
font-size: 0.75rem;
font-weight: 500;
color: #666;
white-space: nowrap;
margin: 0 0.25rem;
}
/* Results Section */ /* Results Section */
.results-section { .results-section {
@@ -275,82 +387,131 @@
.recipes-grid { .recipes-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem; gap: 1.5rem;
} }
.recipe-card { .recipe-card {
background: white; cursor: pointer;
border-radius: 12px; border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden; overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); background: white;
position: relative; position: relative;
transition: transform 0.2s, box-shadow 0.2s; transition: transform 0.2s, box-shadow 0.2s;
display: flex;
flex-direction: column;
aspect-ratio: 1 / 1;
} }
.recipe-card:hover { .recipe-card:hover {
transform: translateY(-4px); transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
} }
.recipe-card > div:first-child { .recipe-card > div:first-child {
cursor: pointer; cursor: pointer;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
} }
.recipe-image { .recipe-card img.recipe-image {
width: 100%; width: 100%;
height: 200px; height: 60%;
object-fit: cover; object-fit: cover;
display: block;
flex-shrink: 0;
} }
.recipe-image-placeholder { .recipe-image-placeholder {
width: 100%; width: 100%;
height: 200px; height: 60%;
background: linear-gradient(135deg, #ffb74d 0%, #ff9800 100%); background: linear-gradient(135deg, #ffb74d 0%, #ff9800 100%);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 4rem; font-size: 3rem;
flex-shrink: 0;
} }
.recipe-info { .recipe-info {
padding: 1.25rem; padding: 0.5rem;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
min-height: 0;
} }
.recipe-info h3 { .recipe-info h3 {
font-size: 1.2rem; margin: 0 0 0.25rem 0;
color: #212121; font-size: 0.75rem;
margin: 0 0 0.5rem 0; line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
flex-shrink: 0;
} }
.recipe-info .description { .recipe-info .description {
font-size: 0.9rem; margin: 0;
font-size: 0.65rem;
color: #666; color: #666;
margin: 0 0 0.75rem 0; overflow: hidden;
line-height: 1.4; text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
flex-shrink: 1;
} }
.recipe-meta { .recipe-meta {
display: flex; display: flex;
gap: 1rem; gap: 0.4rem;
font-size: 0.85rem; font-size: 0.6rem;
color: #757575; color: #888;
margin-bottom: 0.75rem; flex-shrink: 0;
margin-top: auto;
} }
.recipe-tags { .recipe-tags {
display: flex; display: none;
flex-wrap: wrap;
gap: 0.5rem;
} }
.recipe-tags .tag { /* Column-specific styles for recipes */
padding: 0.25rem 0.75rem; .columns-3 .recipe-info h3 {
background-color: #e8f5e9; font-size: 0.95rem;
color: #2e7d32; }
border-radius: 12px;
.columns-3 .recipe-info .description {
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 500; -webkit-line-clamp: 2;
}
.columns-3 .recipe-meta {
font-size: 0.75rem;
}
.columns-5 .recipe-info h3 {
font-size: 0.85rem;
}
.columns-5 .recipe-info .description {
font-size: 0.75rem;
-webkit-line-clamp: 2;
}
.columns-5 .recipe-meta {
font-size: 0.7rem;
}
.columns-7 .recipe-info .description,
.columns-9 .recipe-info .description {
display: none;
} }
.remove-recipe-btn { .remove-recipe-btn {
@@ -427,9 +588,26 @@
width: 100%; width: 100%;
} }
.cookbook-toolbar {
flex-direction: column;
align-items: stretch;
padding: 1rem;
}
.display-controls,
.pagination-controls {
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
.recipes-grid { .recipes-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.included-cookbooks-section .cookbooks-grid {
grid-template-columns: 1fr;
}
} }
/* Included Cookbooks Section */ /* Included Cookbooks Section */
@@ -446,11 +624,19 @@
font-size: 1.5rem; font-size: 1.5rem;
} }
.included-cookbooks-section .cookbooks-grid {
display: grid;
gap: 1.5rem;
}
.cookbook-card.nested { .cookbook-card.nested {
border: 2px solid #e0e0e0; border: 2px solid #e0e0e0;
background: white; background: white;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
aspect-ratio: 1 / 1;
display: flex;
flex-direction: column;
} }
.cookbook-card.nested:hover { .cookbook-card.nested:hover {
@@ -458,3 +644,75 @@
box-shadow: 0 4px 12px rgba(0,0,0,0.15); box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transform: translateY(-2px); transform: translateY(-2px);
} }
.cookbook-card.nested .cookbook-cover,
.cookbook-card.nested .cookbook-cover-placeholder {
height: 50%;
font-size: 2.5rem;
}
.cookbook-card.nested .cookbook-info {
padding: 0.5rem;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}
.cookbook-card.nested .cookbook-info h3 {
font-size: 0.75rem;
color: #212121;
margin: 0 0 0.25rem 0;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
flex-shrink: 0;
}
.cookbook-card.nested .cookbook-info .description {
display: none;
}
.cookbook-card.nested .cookbook-stats {
margin-top: auto;
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.cookbook-card.nested .recipe-count,
.cookbook-card.nested .cookbook-count {
font-size: 0.6rem;
color: #2e7d32;
font-weight: 600;
margin: 0;
line-height: 1.2;
white-space: nowrap;
}
.cookbook-card.nested .cookbook-tags {
display: none;
}
/* Column-specific styles for nested cookbooks */
.cookbooks-grid.columns-3 .cookbook-card.nested .cookbook-info h3 {
font-size: 0.95rem;
}
.cookbooks-grid.columns-3 .cookbook-card.nested .recipe-count,
.cookbooks-grid.columns-3 .cookbook-card.nested .cookbook-count {
font-size: 0.75rem;
}
.cookbooks-grid.columns-5 .cookbook-card.nested .cookbook-info h3 {
font-size: 0.85rem;
}
.cookbooks-grid.columns-5 .cookbook-card.nested .recipe-count,
.cookbooks-grid.columns-5 .cookbook-card.nested .cookbook-count {
font-size: 0.7rem;
}

View File

@@ -26,6 +26,18 @@
gap: 1rem; gap: 1rem;
} }
/* Page-level Controls */
.page-toolbar {
display: flex;
justify-content: flex-start;
background: white;
padding: 0.75rem 1rem;
border-radius: 8px;
margin-bottom: 2rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
border: 1px solid #e0e0e0;
}
/* Cookbooks Section */ /* Cookbooks Section */
.cookbooks-section { .cookbooks-section {
margin-bottom: 3rem; margin-bottom: 3rem;
@@ -37,9 +49,124 @@
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
/* Pagination Controls */
.pagination-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
background: white;
padding: 0.75rem 1rem;
border-radius: 8px;
margin-bottom: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
border: 1px solid #e0e0e0;
}
.display-controls,
.pagination-controls {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.control-group label {
font-size: 0.8rem;
font-weight: 500;
color: #666;
white-space: nowrap;
}
.column-buttons,
.items-per-page {
display: flex;
gap: 0.25rem;
border: 1px solid #d0d0d0;
border-radius: 6px;
overflow: hidden;
}
.column-buttons button,
.items-per-page button {
min-width: 2rem;
padding: 0.35rem 0.6rem;
border: none;
background: white;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
color: #555;
}
.column-buttons button:not(:last-child),
.items-per-page button:not(:last-child) {
border-right: 1px solid #d0d0d0;
}
.column-buttons button:hover,
.items-per-page button:hover {
background-color: #f5f5f5;
}
.column-buttons button.active,
.items-per-page button.active {
background-color: #2e7d32;
color: white;
}
.page-navigation {
display: flex;
gap: 0.5rem;
align-items: center;
}
.page-navigation button {
padding: 0.35rem 0.75rem;
border: 1px solid #d0d0d0;
background: white;
color: #555;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
}
.page-navigation button:hover:not(:disabled) {
background-color: #f5f5f5;
border-color: #2e7d32;
color: #2e7d32;
}
.page-navigation button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.page-info {
font-size: 0.75rem;
font-weight: 500;
color: #666;
white-space: nowrap;
margin: 0 0.25rem;
}
.results-count {
font-size: 0.95rem;
color: #757575;
margin-bottom: 1.5rem;
}
.cookbooks-grid { .cookbooks-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem; gap: 1.5rem;
} }
@@ -50,6 +177,9 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer; cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s; transition: transform 0.2s, box-shadow 0.2s;
display: flex;
flex-direction: column;
aspect-ratio: 1 / 1;
} }
.cookbook-card:hover { .cookbook-card:hover {
@@ -59,42 +189,86 @@
.cookbook-cover { .cookbook-cover {
width: 100%; width: 100%;
height: 200px; height: 50%;
object-fit: cover; object-fit: cover;
flex-shrink: 0;
} }
.cookbook-cover-placeholder { .cookbook-cover-placeholder {
width: 100%; width: 100%;
height: 200px; height: 50%;
background: linear-gradient(135deg, #81c784 0%, #4caf50 100%); background: linear-gradient(135deg, #81c784 0%, #4caf50 100%);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 4rem; font-size: 2.5rem;
flex-shrink: 0;
} }
.cookbook-info { .cookbook-info {
padding: 1.25rem; padding: 0.5rem;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
} }
.cookbook-info h3 { .cookbook-info h3 {
font-size: 1.3rem; font-size: 0.75rem;
color: #212121; color: #212121;
margin: 0 0 0.5rem 0; margin: 0 0 0.25rem 0;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
flex-shrink: 0;
} }
.cookbook-info .description { .cookbook-info .description {
font-size: 0.95rem; display: none;
color: #666;
margin: 0 0 0.75rem 0;
line-height: 1.4;
} }
.cookbook-info .recipe-count { .cookbook-info .cookbook-stats {
font-size: 0.9rem; margin-top: auto;
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.cookbook-info .recipe-count,
.cookbook-info .cookbook-count {
font-size: 0.6rem;
color: #2e7d32; color: #2e7d32;
font-weight: 600; font-weight: 600;
margin: 0; margin: 0;
line-height: 1.2;
white-space: nowrap;
}
.cookbook-info .cookbook-tags {
display: none;
}
/* Column-specific styles for Cookbooks */
.cookbooks-grid.columns-3 .cookbook-info h3 {
font-size: 0.95rem;
}
.cookbooks-grid.columns-3 .recipe-count,
.cookbooks-grid.columns-3 .cookbook-count {
font-size: 0.75rem;
}
.cookbooks-grid.columns-5 .cookbook-info h3 {
font-size: 0.85rem;
}
.cookbooks-grid.columns-5 .recipe-count,
.cookbooks-grid.columns-5 .cookbook-count {
font-size: 0.7rem;
} }
/* Recent Recipes Section */ /* Recent Recipes Section */
@@ -102,77 +276,133 @@
margin-top: 3rem; margin-top: 3rem;
} }
.section-header { .recent-recipes-section h2 {
font-size: 1.8rem;
color: #1b5e20;
margin: 0;
}
.section-title-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.section-header h2 {
font-size: 1.8rem;
color: #1b5e20;
margin: 0;
}
.recipes-grid { .recipes-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem; gap: 1.5rem;
} }
.recipe-card { .recent-recipes-section .recipe-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer; cursor: pointer;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background: white;
transition: transform 0.2s, box-shadow 0.2s; transition: transform 0.2s, box-shadow 0.2s;
display: flex;
flex-direction: column;
aspect-ratio: 1 / 1;
} }
.recipe-card:hover { .recent-recipes-section .recipe-card:hover {
transform: translateY(-4px); transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
} }
.recipe-image { .recent-recipes-section .recipe-card img {
width: 100%; width: 100%;
height: 200px; height: 60%;
object-fit: cover; object-fit: cover;
display: block;
flex-shrink: 0;
} }
.recipe-image-placeholder { .recipe-image-placeholder {
width: 100%; width: 100%;
height: 200px; height: 60%;
background: linear-gradient(135deg, #ffb74d 0%, #ff9800 100%); background: linear-gradient(135deg, #ffb74d 0%, #ff9800 100%);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 4rem; font-size: 3rem;
flex-shrink: 0;
} }
.recipe-info { .recent-recipes-section .recipe-info {
padding: 1.25rem; padding: 0.5rem;
} flex: 1;
.recipe-info h3 {
font-size: 1.2rem;
color: #212121;
margin: 0 0 0.5rem 0;
}
.recipe-info .description {
font-size: 0.9rem;
color: #666;
margin: 0 0 0.75rem 0;
line-height: 1.4;
}
.recipe-meta {
display: flex; display: flex;
gap: 1rem; flex-direction: column;
justify-content: space-between;
overflow: hidden;
min-height: 0;
}
.recent-recipes-section .recipe-info h3 {
margin: 0 0 0.25rem 0;
font-size: 0.75rem;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
flex-shrink: 0;
}
.recent-recipes-section .recipe-info .description {
margin: 0;
font-size: 0.65rem;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
flex-shrink: 1;
}
.recent-recipes-section .recipe-meta {
display: flex;
gap: 0.4rem;
font-size: 0.6rem;
color: #888;
flex-shrink: 0;
margin-top: auto;
}
/* Column-specific styles for Recent Recipes */
.recent-recipes-section .columns-3 .recipe-info h3 {
font-size: 0.95rem;
}
.recent-recipes-section .columns-3 .recipe-info .description {
font-size: 0.8rem;
-webkit-line-clamp: 2;
}
.recent-recipes-section .columns-3 .recipe-meta {
font-size: 0.75rem;
}
.recent-recipes-section .columns-5 .recipe-info h3 {
font-size: 0.85rem; font-size: 0.85rem;
color: #757575; }
.recent-recipes-section .columns-5 .recipe-info .description {
font-size: 0.75rem;
-webkit-line-clamp: 2;
}
.recent-recipes-section .columns-5 .recipe-meta {
font-size: 0.7rem;
}
.recent-recipes-section .columns-7 .recipe-info .description,
.recent-recipes-section .columns-9 .recipe-info .description {
display: none;
} }
/* Empty State */ /* Empty State */
@@ -521,6 +751,20 @@
width: 100%; width: 100%;
} }
.page-toolbar,
.pagination-toolbar {
flex-direction: column;
align-items: stretch;
padding: 1rem;
}
.display-controls,
.pagination-controls {
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
.cookbooks-grid, .cookbooks-grid,
.recipes-grid { .recipes-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;

View File

@@ -266,9 +266,9 @@
overflow: hidden; overflow: hidden;
background: var(--bg-primary, #ffffff); background: var(--bg-primary, #ffffff);
transition: transform 0.2s, box-shadow 0.2s; transition: transform 0.2s, box-shadow 0.2s;
aspect-ratio: 1 / 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
aspect-ratio: 1 / 1;
} }
.recipe-grid-enhanced .recipe-card:hover { .recipe-grid-enhanced .recipe-card:hover {
@@ -278,7 +278,7 @@
.recipe-grid-enhanced .recipe-card img { .recipe-grid-enhanced .recipe-card img {
width: 100%; width: 100%;
height: 65%; height: 60%;
object-fit: cover; object-fit: cover;
display: block; display: block;
flex-shrink: 0; flex-shrink: 0;
@@ -327,6 +327,38 @@
margin-top: auto; margin-top: auto;
} }
/* Column-specific styles for recipe grid */
.recipe-grid-enhanced.columns-3 .recipe-card-content h3 {
font-size: 0.95rem;
}
.recipe-grid-enhanced.columns-3 .recipe-card-content p {
font-size: 0.8rem;
-webkit-line-clamp: 2;
}
.recipe-grid-enhanced.columns-3 .recipe-meta {
font-size: 0.75rem;
}
.recipe-grid-enhanced.columns-5 .recipe-card-content h3 {
font-size: 0.85rem;
}
.recipe-grid-enhanced.columns-5 .recipe-card-content p {
font-size: 0.75rem;
-webkit-line-clamp: 2;
}
.recipe-grid-enhanced.columns-5 .recipe-meta {
font-size: 0.7rem;
}
.recipe-grid-enhanced.columns-7 .recipe-card-content p,
.recipe-grid-enhanced.columns-9 .recipe-card-content p {
display: none;
}
/* Empty state */ /* Empty state */
.empty-state { .empty-state {
text-align: center; text-align: center;