Merge pull request 'feat: implement responsive column-based styling for all thumbnail cards' (#9) from feature/cookbook-pagination into main
All checks were successful
Basil CI/CD Pipeline / Shared Package Tests (push) Successful in 1m29s
Basil CI/CD Pipeline / Code Linting (push) Successful in 1m41s
Basil CI/CD Pipeline / Web Tests (push) Successful in 1m59s
Basil CI/CD Pipeline / Security Scanning (push) Successful in 1m44s
Basil CI/CD Pipeline / API Tests (push) Successful in 2m4s
Basil CI/CD Pipeline / Build All Packages (push) Successful in 1m34s
Basil CI/CD Pipeline / E2E Tests (push) Has been skipped
Basil CI/CD Pipeline / Build & Push Docker Images (push) Successful in 5m9s
Basil CI/CD Pipeline / Trigger Deployment (push) Successful in 12s

Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
2026-01-20 04:05:02 +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' }`
- Web: Footer or about section shows current version
- 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 { useParams, useNavigate } from 'react-router-dom';
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
import { CookbookWithRecipes, Recipe } from '@basil/shared';
import { cookbooksApi } from '../services/api';
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
const getTagName = (tag: string | { tag: { name: string } }): string => {
return typeof tag === 'string' ? tag : tag.tag.name;
@@ -12,10 +18,33 @@ const getTagName = (tag: string | { tag: { name: string } }): string => {
function CookbookDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const [cookbook, setCookbook] = useState<CookbookWithRecipes | null>(null);
const [loading, setLoading] = useState(true);
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
const [searchQuery, setSearchQuery] = useState('');
const [selectedTags, setSelectedTags] = useState<string[]>([]);
@@ -27,6 +56,28 @@ function CookbookDetail() {
}
}, [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) => {
try {
setLoading(true);
@@ -129,6 +180,24 @@ function CookbookDetail() {
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) {
return (
<div className="cookbook-detail-page">
@@ -147,9 +216,19 @@ function CookbookDetail() {
}
const filteredRecipes = getFilteredRecipes();
const paginatedRecipes = getPaginatedRecipes(filteredRecipes);
const allTags = getAllTags();
const allCuisines = getAllCuisines();
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 (
<div className="cookbook-detail-page">
@@ -227,11 +306,66 @@ function CookbookDetail() {
</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 */}
{cookbook.cookbooks && cookbook.cookbooks.length > 0 && (
<section className="included-cookbooks-section">
<h2>Included Cookbooks ({cookbook.cookbooks.length})</h2>
<div className="cookbooks-grid">
<div className={cookbooksGridClassName} style={gridStyle}>
{cookbook.cookbooks.map((childCookbook) => (
<div
key={childCookbook.id}
@@ -272,7 +406,12 @@ function CookbookDetail() {
<div className="results-section">
<h2>Recipes</h2>
<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>
{filteredRecipes.length === 0 ? (
@@ -284,8 +423,8 @@ function CookbookDetail() {
)}
</div>
) : (
<div className="recipes-grid">
{filteredRecipes.map(recipe => (
<div className={recipesGridClassName} style={gridStyle}>
{paginatedRecipes.map(recipe => (
<div key={recipe.id} className="recipe-card">
<div onClick={() => navigate(`/recipes/${recipe.id}`)}>
{recipe.imageUrl ? (

View File

@@ -1,11 +1,18 @@
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 { cookbooksApi, recipesApi, tagsApi } from '../services/api';
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() {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const [cookbooks, setCookbooks] = useState<Cookbook[]>([]);
const [recentRecipes, setRecentRecipes] = useState<Recipe[]>([]);
const [loading, setLoading] = useState(true);
@@ -22,10 +29,49 @@ function Cookbooks() {
const [availableTags, setAvailableTags] = useState<Tag[]>([]);
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(() => {
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 () => {
try {
setLoading(true);
@@ -117,6 +163,35 @@ function Cookbooks() {
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) {
return (
<div className="cookbooks-page">
@@ -150,9 +225,30 @@ function Cookbooks() {
</div>
</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 */}
<section className="cookbooks-section">
<h2>Cookbooks</h2>
{cookbooks.length === 0 ? (
<div className="empty-state">
<p>No cookbooks yet. Create your first cookbook to organize your recipes!</p>
@@ -161,8 +257,56 @@ function Cookbooks() {
</button>
</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
key={cookbook.id}
className="cookbook-card"
@@ -195,12 +339,13 @@ function Cookbooks() {
</div>
))}
</div>
</>
)}
</section>
{/* Recent Recipes */}
<section className="recent-recipes-section">
<div className="section-header">
<div className="section-title-row">
<h2>Recent Recipes</h2>
<button onClick={() => navigate('/recipes')} className="btn-link">
View all
@@ -209,7 +354,7 @@ function Cookbooks() {
{recentRecipes.length === 0 ? (
<p className="empty-state">No recipes yet.</p>
) : (
<div className="recipes-grid">
<div className={recipesGridClassName} style={gridStyle}>
{recentRecipes.map((recipe) => (
<div
key={recipe.id}

View File

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

View File

@@ -261,6 +261,118 @@
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 {
@@ -275,82 +387,131 @@
.recipes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.recipe-card {
background: white;
border-radius: 12px;
cursor: pointer;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background: white;
position: relative;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
flex-direction: column;
aspect-ratio: 1 / 1;
}
.recipe-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.recipe-card > div:first-child {
cursor: pointer;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
.recipe-image {
.recipe-card img.recipe-image {
width: 100%;
height: 200px;
height: 60%;
object-fit: cover;
display: block;
flex-shrink: 0;
}
.recipe-image-placeholder {
width: 100%;
height: 200px;
height: 60%;
background: linear-gradient(135deg, #ffb74d 0%, #ff9800 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 4rem;
font-size: 3rem;
flex-shrink: 0;
}
.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 {
font-size: 1.2rem;
color: #212121;
margin: 0 0 0.5rem 0;
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;
}
.recipe-info .description {
font-size: 0.9rem;
margin: 0;
font-size: 0.65rem;
color: #666;
margin: 0 0 0.75rem 0;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
flex-shrink: 1;
}
.recipe-meta {
display: flex;
gap: 1rem;
font-size: 0.85rem;
color: #757575;
margin-bottom: 0.75rem;
gap: 0.4rem;
font-size: 0.6rem;
color: #888;
flex-shrink: 0;
margin-top: auto;
}
.recipe-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
display: none;
}
.recipe-tags .tag {
padding: 0.25rem 0.75rem;
background-color: #e8f5e9;
color: #2e7d32;
border-radius: 12px;
/* Column-specific styles for recipes */
.columns-3 .recipe-info h3 {
font-size: 0.95rem;
}
.columns-3 .recipe-info .description {
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 {
@@ -427,9 +588,26 @@
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 {
grid-template-columns: 1fr;
}
.included-cookbooks-section .cookbooks-grid {
grid-template-columns: 1fr;
}
}
/* Included Cookbooks Section */
@@ -446,11 +624,19 @@
font-size: 1.5rem;
}
.included-cookbooks-section .cookbooks-grid {
display: grid;
gap: 1.5rem;
}
.cookbook-card.nested {
border: 2px solid #e0e0e0;
background: white;
cursor: pointer;
transition: all 0.2s ease;
aspect-ratio: 1 / 1;
display: flex;
flex-direction: column;
}
.cookbook-card.nested:hover {
@@ -458,3 +644,75 @@
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
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;
}
/* 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 {
margin-bottom: 3rem;
@@ -37,9 +49,124 @@
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 {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
@@ -50,6 +177,9 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
flex-direction: column;
aspect-ratio: 1 / 1;
}
.cookbook-card:hover {
@@ -59,42 +189,86 @@
.cookbook-cover {
width: 100%;
height: 200px;
height: 50%;
object-fit: cover;
flex-shrink: 0;
}
.cookbook-cover-placeholder {
width: 100%;
height: 200px;
height: 50%;
background: linear-gradient(135deg, #81c784 0%, #4caf50 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 4rem;
font-size: 2.5rem;
flex-shrink: 0;
}
.cookbook-info {
padding: 1.25rem;
padding: 0.5rem;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}
.cookbook-info h3 {
font-size: 1.3rem;
font-size: 0.75rem;
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 {
font-size: 0.95rem;
color: #666;
margin: 0 0 0.75rem 0;
line-height: 1.4;
display: none;
}
.cookbook-info .recipe-count {
font-size: 0.9rem;
.cookbook-info .cookbook-stats {
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;
font-weight: 600;
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 */
@@ -102,77 +276,133 @@
margin-top: 3rem;
}
.section-header {
.recent-recipes-section h2 {
font-size: 1.8rem;
color: #1b5e20;
margin: 0;
}
.section-title-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.section-header h2 {
font-size: 1.8rem;
color: #1b5e20;
margin: 0;
}
.recipes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.recipe-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.recent-recipes-section .recipe-card {
cursor: pointer;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background: white;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
flex-direction: column;
aspect-ratio: 1 / 1;
}
.recipe-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
.recent-recipes-section .recipe-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.recipe-image {
.recent-recipes-section .recipe-card img {
width: 100%;
height: 200px;
height: 60%;
object-fit: cover;
display: block;
flex-shrink: 0;
}
.recipe-image-placeholder {
width: 100%;
height: 200px;
height: 60%;
background: linear-gradient(135deg, #ffb74d 0%, #ff9800 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 4rem;
font-size: 3rem;
flex-shrink: 0;
}
.recipe-info {
padding: 1.25rem;
}
.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 {
.recent-recipes-section .recipe-info {
padding: 0.5rem;
flex: 1;
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;
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 */
@@ -521,6 +751,20 @@
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,
.recipes-grid {
grid-template-columns: 1fr;

View File

@@ -266,9 +266,9 @@
overflow: hidden;
background: var(--bg-primary, #ffffff);
transition: transform 0.2s, box-shadow 0.2s;
aspect-ratio: 1 / 1;
display: flex;
flex-direction: column;
aspect-ratio: 1 / 1;
}
.recipe-grid-enhanced .recipe-card:hover {
@@ -278,7 +278,7 @@
.recipe-grid-enhanced .recipe-card img {
width: 100%;
height: 65%;
height: 60%;
object-fit: cover;
display: block;
flex-shrink: 0;
@@ -327,6 +327,38 @@
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 {
text-align: center;