feat: implement responsive column-based styling for all thumbnail cards
All checks were successful
Basil CI/CD Pipeline / Shared Package Tests (pull_request) Successful in 1m10s
Basil CI/CD Pipeline / Code Linting (pull_request) Successful in 1m15s
Basil CI/CD Pipeline / Web Tests (pull_request) Successful in 1m29s
Basil CI/CD Pipeline / API Tests (pull_request) Successful in 1m41s
Basil CI/CD Pipeline / Security Scanning (pull_request) Successful in 1m9s
Basil CI/CD Pipeline / Build All Packages (pull_request) Successful in 1m32s
Basil CI/CD Pipeline / E2E Tests (pull_request) Has been skipped
Basil CI/CD Pipeline / Build & Push Docker Images (pull_request) Has been skipped
Basil CI/CD Pipeline / Trigger Deployment (pull_request) Has been skipped

Implemented consistent responsive styling across all recipe and cookbook thumbnail displays
with column-specific font sizes and description visibility rules.

Changes:
- Added responsive font sizing for 3, 5, 7, and 9 column layouts
- Hide descriptions at 7+ columns to prevent text cutoff
- Show 2-line descriptions for 3 and 5 columns with proper truncation
- Applied consistent card styling (1px border, 8px radius) across all pages
- Updated RecipeList, Cookbooks, and CookbookDetail pages
- Documented all 7 thumbnail display locations in CLAUDE.md

Styling rules:
- Column 3: Larger fonts (0.95rem title, 0.8rem desc, 0.75rem meta)
- Column 5: Medium fonts (0.85rem title, 0.75rem desc, 0.7rem meta)
- Column 7-9: Smallest fonts, descriptions hidden

Pages affected:
- Recipe List (My Recipes)
- Cookbooks page (Recent Recipes section)
- Cookbooks page (Main grid)
- Cookbook Detail (Recipes section)
- Cookbook Detail (Nested cookbooks)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Paul R Kartchner
2026-01-19 17:00:16 -07:00
parent a20dfd848c
commit 0e611c379e
7 changed files with 341 additions and 84 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

@@ -227,6 +227,9 @@ function CookbookDetail() {
gridTemplateColumns: `repeat(${columnCount}, 1fr)`,
};
const recipesGridClassName = `recipes-grid columns-${columnCount}`;
const cookbooksGridClassName = `cookbooks-grid columns-${columnCount}`;
return (
<div className="cookbook-detail-page">
<header className="cookbook-header">
@@ -362,7 +365,7 @@ function CookbookDetail() {
{cookbook.cookbooks && cookbook.cookbooks.length > 0 && (
<section className="included-cookbooks-section">
<h2>Included Cookbooks ({cookbook.cookbooks.length})</h2>
<div className="cookbooks-grid" style={gridStyle}>
<div className={cookbooksGridClassName} style={gridStyle}>
{cookbook.cookbooks.map((childCookbook) => (
<div
key={childCookbook.id}
@@ -420,7 +423,7 @@ function CookbookDetail() {
)}
</div>
) : (
<div className="recipes-grid" style={gridStyle}>
<div className={recipesGridClassName} style={gridStyle}>
{paginatedRecipes.map(recipe => (
<div key={recipe.id} className="recipe-card">
<div onClick={() => navigate(`/recipes/${recipe.id}`)}>

View File

@@ -189,6 +189,9 @@ function Cookbooks() {
gridTemplateColumns: `repeat(${columnCount}, 1fr)`,
};
const recipesGridClassName = `recipes-grid columns-${columnCount}`;
const cookbooksGridClassName = `cookbooks-grid columns-${columnCount}`;
if (loading) {
return (
<div className="cookbooks-page">
@@ -222,6 +225,26 @@ 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>
@@ -235,25 +258,8 @@ function Cookbooks() {
</div>
) : (
<>
{/* Display and Pagination Controls */}
<div className="cookbooks-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>
{/* Pagination Controls */}
<div className="pagination-toolbar">
<div className="pagination-controls">
<div className="control-group">
<label>Per page:</label>
@@ -299,7 +305,7 @@ function Cookbooks() {
)}
</p>
<div className="cookbooks-grid" style={gridStyle}>
<div className={cookbooksGridClassName} style={gridStyle}>
{paginatedCookbooks.map((cookbook) => (
<div
key={cookbook.id}
@@ -339,7 +345,7 @@ function Cookbooks() {
{/* 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
@@ -348,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

@@ -391,10 +391,11 @@
}
.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;
@@ -403,8 +404,8 @@
}
.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 {
@@ -415,10 +416,11 @@
min-height: 0;
}
.recipe-image {
.recipe-card img.recipe-image {
width: 100%;
height: 60%;
object-fit: cover;
display: block;
flex-shrink: 0;
}
@@ -435,17 +437,17 @@
.recipe-info {
padding: 0.5rem;
flex: 1;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
justify-content: space-between;
overflow: hidden;
min-height: 0;
}
.recipe-info h3 {
font-size: 0.75rem;
color: #212121;
margin: 0 0 0.25rem 0;
font-size: 0.75rem;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
@@ -456,14 +458,13 @@
}
.recipe-info .description {
margin: 0;
font-size: 0.65rem;
color: #666;
margin: 0;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
flex-shrink: 1;
}
@@ -472,25 +473,45 @@
display: flex;
gap: 0.4rem;
font-size: 0.6rem;
color: #757575;
color: #888;
flex-shrink: 0;
margin-top: auto;
}
.recipe-tags {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-top: 0.25rem;
display: none;
}
.recipe-tags .tag {
padding: 0.1rem 0.4rem;
background-color: #e8f5e9;
color: #2e7d32;
border-radius: 8px;
font-size: 0.55rem;
font-weight: 500;
/* Column-specific styles for recipes */
.columns-3 .recipe-info h3 {
font-size: 0.95rem;
}
.columns-3 .recipe-info .description {
font-size: 0.8rem;
-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 {
@@ -632,11 +653,16 @@
.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;
@@ -653,11 +679,40 @@
.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,13 +49,12 @@
margin-bottom: 1.5rem;
}
/* Toolbar and Pagination Controls */
.cookbooks-toolbar {
/* Pagination Controls */
.pagination-toolbar {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
align-items: center;
justify-content: space-between;
justify-content: flex-end;
background: white;
padding: 0.75rem 1rem;
border-radius: 8px;
@@ -241,51 +252,70 @@
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 {
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: 60%;
object-fit: cover;
display: block;
flex-shrink: 0;
}
@@ -300,19 +330,19 @@
flex-shrink: 0;
}
.recipe-info {
.recent-recipes-section .recipe-info {
padding: 0.5rem;
flex: 1;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
justify-content: space-between;
overflow: hidden;
min-height: 0;
}
.recipe-info h3 {
font-size: 0.75rem;
color: #212121;
.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;
@@ -322,28 +352,59 @@
flex-shrink: 0;
}
.recipe-info .description {
.recent-recipes-section .recipe-info .description {
margin: 0;
font-size: 0.65rem;
color: #666;
margin: 0;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
flex-shrink: 1;
}
.recipe-meta {
.recent-recipes-section .recipe-meta {
display: flex;
gap: 0.4rem;
font-size: 0.6rem;
color: #757575;
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;
}
.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 {
text-align: center;
@@ -690,7 +751,8 @@
width: 100%;
}
.cookbooks-toolbar {
.page-toolbar,
.pagination-toolbar {
flex-direction: column;
align-items: stretch;
padding: 1rem;

View File

@@ -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;