feat(frontend): warm organic design overhaul — Fraunces/DM fonts, saffron accent, compact inventory shelf view

- EditItemModal: replace all hardcoded colors (#eee, #f5f5f5, #2196F3, etc.) with CSS variable tokens; restyle modal header with display font, blur backdrop, and theme-aware form elements
- ReceiptsView: replace emoji headings, hardcoded spinner, and non-theme .button class with themed equivalents; all colors through var(--color-*) tokens
- RecipesView: fix broken --color-warning-rgb / --color-primary-rgb references (not defined in theme); use --color-warning-bg and --color-info-bg instead; apply section-title to heading
- SettingsView: apply section-title display-font class to heading for consistency
- InventoryList: remove three dead functions (formatDate, getDaysUntilExpiry, getExpiryClass) that caused TS6133 build errors
This commit is contained in:
pyr0ball 2026-04-01 22:29:55 -07:00
parent cfd6ef88cc
commit 362b7ad148
5 changed files with 1040 additions and 953 deletions

View file

@ -228,160 +228,183 @@ function getExpiryHint(): string {
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.6);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 1000; z-index: 1000;
backdrop-filter: blur(4px);
} }
.modal-content { .modal-content {
background: var(--color-bg-card); background: var(--color-bg-card);
border-radius: var(--radius-lg); border-radius: var(--radius-xl);
width: 90%; width: 90%;
max-width: 600px; max-width: 600px;
max-height: 90vh; max-height: 90vh;
overflow-y: auto; overflow-y: auto;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); box-shadow: var(--shadow-xl);
border: 1px solid var(--color-border);
} }
.modal-header { .modal-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px; padding: var(--spacing-lg) var(--spacing-lg) var(--spacing-md);
border-bottom: 1px solid #eee; border-bottom: 1px solid var(--color-border);
} }
.modal-header h2 { .modal-header h2 {
margin: 0; margin: 0;
font-size: var(--font-size-xl); font-size: var(--font-size-xl);
font-family: var(--font-display);
font-style: italic;
color: var(--color-text-primary);
} }
.close-btn { .close-btn {
background: none; background: none;
border: none; border: none;
font-size: 32px; font-size: 28px;
color: #999; color: var(--color-text-muted);
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
width: 32px; width: 32px;
height: 32px; height: 32px;
line-height: 1; line-height: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-md);
transition: color 0.18s, background 0.18s;
} }
.close-btn:hover { .close-btn:hover {
color: var(--color-text-primary); color: var(--color-text-primary);
background: var(--color-bg-elevated);
} }
.edit-form { .edit-form {
padding: 20px; padding: var(--spacing-lg);
} }
.form-group { .form-group {
margin-bottom: 20px; margin-bottom: var(--spacing-md);
} }
/* Using .form-row from theme.css */ /* Using .form-row from theme.css */
.form-group label { .form-group label {
display: block; display: block;
margin-bottom: 8px; margin-bottom: var(--spacing-xs);
font-weight: 600; font-weight: 600;
color: var(--color-text-primary); color: var(--color-text-secondary);
font-size: var(--font-size-sm); font-size: var(--font-size-xs);
text-transform: uppercase;
letter-spacing: 0.06em;
} }
.form-input { .form-input {
width: 100%; width: 100%;
padding: 10px; padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-sm); border-radius: var(--radius-md);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
background: var(--color-bg-input);
color: var(--color-text-primary);
font-family: var(--font-body);
transition: border-color 0.18s, box-shadow 0.18s;
box-sizing: border-box;
} }
.form-input:focus { .form-input:focus {
outline: none; outline: none;
border-color: #2196F3; border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1); box-shadow: 0 0 0 3px var(--color-warning-bg);
} }
.form-input.expiry-expired { .form-input.expiry-expired {
border-color: #f44336; border-color: var(--color-error);
} }
.form-input.expiry-soon { .form-input.expiry-soon {
border-color: #ff5722; border-color: var(--color-error-light);
} }
.form-input.expiry-warning { .form-input.expiry-warning {
border-color: #ff9800; border-color: var(--color-warning);
} }
.form-input.expiry-good { .form-input.expiry-good {
border-color: #4CAF50; border-color: var(--color-success);
} }
textarea.form-input { textarea.form-input {
resize: vertical; resize: vertical;
font-family: inherit; font-family: var(--font-body);
} }
.product-info { .product-info {
padding: 10px; padding: var(--spacing-sm) var(--spacing-md);
background: #f5f5f5; background: var(--color-bg-secondary);
border-radius: var(--radius-sm); border-radius: var(--radius-md);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
border: 1px solid var(--color-border);
} }
.product-info .brand { .product-info .brand {
color: var(--color-text-secondary); color: var(--color-text-secondary);
margin-left: 8px; margin-left: var(--spacing-sm);
} }
.expiry-hint { .expiry-hint {
display: block; display: block;
margin-top: 5px; margin-top: var(--spacing-xs);
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
.error-message { .error-message {
background: #ffebee; background: var(--color-error-bg);
color: #c62828; color: var(--color-error-light);
padding: 12px; border: 1px solid var(--color-error-border);
border-radius: var(--radius-sm); padding: var(--spacing-sm) var(--spacing-md);
margin-bottom: 15px; border-radius: var(--radius-md);
margin-bottom: var(--spacing-md);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
} }
.form-actions { .form-actions {
display: flex; display: flex;
gap: 10px; gap: var(--spacing-sm);
justify-content: flex-end; justify-content: flex-end;
margin-top: 25px; margin-top: var(--spacing-lg);
padding-top: 20px; padding-top: var(--spacing-md);
border-top: 1px solid #eee; border-top: 1px solid var(--color-border);
} }
.btn-cancel, .btn-cancel,
.btn-save { .btn-save {
padding: 10px 24px; padding: var(--spacing-sm) var(--spacing-lg);
border: none; border: none;
border-radius: var(--radius-sm); border-radius: var(--radius-md);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
font-weight: 600; font-weight: 600;
font-family: var(--font-body);
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: all 0.18s;
} }
.btn-cancel { .btn-cancel {
background: #f5f5f5; background: var(--color-bg-elevated);
color: var(--color-text-primary); color: var(--color-text-secondary);
border: 1px solid var(--color-border);
} }
.btn-cancel:hover { .btn-cancel:hover {
background: #e0e0e0; background: var(--color-bg-primary);
color: var(--color-text-primary);
} }
.btn-save { .btn-save {
@ -394,7 +417,7 @@ textarea.form-input {
} }
.btn-save:disabled { .btn-save:disabled {
background: var(--color-text-muted); opacity: 0.45;
cursor: not-allowed; cursor: not-allowed;
} }
@ -408,7 +431,7 @@ textarea.form-input {
} }
.modal-header { .modal-header {
padding: 15px; padding: var(--spacing-md);
} }
.modal-header h2 { .modal-header h2 {
@ -416,23 +439,24 @@ textarea.form-input {
} }
.edit-form { .edit-form {
padding: 15px; padding: var(--spacing-md);
} }
.form-group { .form-group {
margin-bottom: 15px; margin-bottom: var(--spacing-sm);
} }
/* Form actions stack on very small screens */ /* Form actions stack on very small screens */
.form-actions { .form-actions {
flex-direction: column-reverse; flex-direction: column-reverse;
gap: 10px; gap: var(--spacing-sm);
} }
.btn-cancel, .btn-cancel,
.btn-save { .btn-save {
width: 100%; width: 100%;
padding: 12px; padding: var(--spacing-md);
text-align: center;
} }
} }
@ -440,13 +464,5 @@ textarea.form-input {
.modal-content { .modal-content {
width: 92%; width: 92%;
} }
.modal-header {
padding: 18px;
}
.edit-form {
padding: 18px;
}
} }
</style> </style>

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
<div class="receipts-view"> <div class="receipts-view">
<!-- Upload Section --> <!-- Upload Section -->
<div class="card"> <div class="card">
<h2>📸 Upload Receipt</h2> <h2 class="section-title mb-md">Upload Receipt</h2>
<div <div
class="upload-area" class="upload-area"
@click="triggerFileInput" @click="triggerFileInput"
@ -21,9 +21,9 @@
@change="handleFileSelect" @change="handleFileSelect"
/> />
<div v-if="uploading" class="loading"> <div v-if="uploading" class="loading-inline mt-md">
<div class="spinner"></div> <div class="spinner"></div>
<p>Processing receipt...</p> <span class="text-sm text-muted">Processing receipt</span>
</div> </div>
<div v-if="uploadResults.length > 0" class="results"> <div v-if="uploadResults.length > 0" class="results">
@ -39,8 +39,8 @@
<!-- Receipts List Section --> <!-- Receipts List Section -->
<div class="card"> <div class="card">
<h2>📋 Recent Receipts</h2> <h2 class="section-title mb-md">Recent Receipts</h2>
<div v-if="receipts.length === 0" style="text-align: center; color: var(--color-text-secondary)"> <div v-if="receipts.length === 0" class="text-center text-secondary p-lg">
<p>No receipts yet. Upload one above!</p> <p>No receipts yet. Upload one above!</p>
</div> </div>
<div v-else> <div v-else>
@ -89,9 +89,9 @@
</div> </div>
</div> </div>
<div style="margin-top: 20px"> <div class="flex gap-sm mt-md">
<button class="button" @click="exportCSV">📊 Download CSV</button> <button class="btn btn-secondary" @click="exportCSV">Download CSV</button>
<button class="button" @click="exportExcel">📈 Download Excel</button> <button class="btn btn-secondary" @click="exportExcel">Download Excel</button>
</div> </div>
</div> </div>
</div> </div>
@ -225,157 +225,117 @@ onMounted(() => {
.receipts-view { .receipts-view {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: var(--spacing-md);
}
.card {
background: var(--color-bg-card);
border-radius: var(--radius-xl);
padding: 30px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
.card h2 {
margin-bottom: 20px;
color: var(--color-text-primary);
} }
.upload-area { .upload-area {
border: 3px dashed var(--color-primary); border: 2px dashed var(--color-border-focus);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
padding: 40px; padding: var(--spacing-xl) var(--spacing-lg);
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.2s ease;
background: var(--color-bg-secondary); background: var(--color-bg-secondary);
} }
.upload-area:hover { .upload-area:hover {
border-color: var(--color-secondary); border-color: var(--color-primary);
background: var(--color-bg-elevated); background: var(--color-bg-elevated);
} }
.upload-icon { .upload-icon {
font-size: 48px; font-size: 40px;
margin-bottom: 20px; margin-bottom: var(--spacing-md);
line-height: 1;
} }
.upload-text { .upload-text {
font-size: var(--font-size-lg); font-size: var(--font-size-base);
font-weight: 600;
color: var(--color-text-primary); color: var(--color-text-primary);
margin-bottom: 10px; margin-bottom: var(--spacing-xs);
} }
.upload-hint { .upload-hint {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
color: var(--color-text-secondary); color: var(--color-text-muted);
} }
.loading { .loading-inline {
text-align: center; display: flex;
padding: 20px; align-items: center;
margin-top: 20px; gap: var(--spacing-sm);
} padding: var(--spacing-sm) 0;
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
.results { .results {
margin-top: 20px; margin-top: var(--spacing-md);
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
} }
.result-item { .result-item {
padding: 15px; padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md); border-radius: var(--radius-md);
margin-bottom: 10px; font-size: var(--font-size-sm);
} }
.result-success { .result-success {
background: var(--color-success-bg); background: var(--color-success-bg);
color: var(--color-success-dark); color: var(--color-success-light);
border: 1px solid var(--color-success-border); border: 1px solid var(--color-success-border);
} }
.result-error { .result-error {
background: var(--color-error-bg); background: var(--color-error-bg);
color: var(--color-error-dark); color: var(--color-error-light);
border: 1px solid var(--color-error-border); border: 1px solid var(--color-error-border);
} }
.result-info { .result-info {
background: var(--color-info-bg); background: var(--color-info-bg);
color: var(--color-info-dark); color: var(--color-info-light);
border: 1px solid var(--color-info-border); border: 1px solid var(--color-info-border);
} }
/* Using .grid-stats from theme.css */ /* Stat cards */
.stat-card { .stat-card {
background: var(--color-bg-secondary); background: var(--color-bg-secondary);
padding: 20px; padding: var(--spacing-md);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
text-align: center; text-align: center;
border: 1px solid var(--color-border);
} }
.stat-value { .stat-value {
font-family: var(--font-mono);
font-size: var(--font-size-2xl); font-size: var(--font-size-2xl);
font-weight: bold; font-weight: 500;
color: var(--color-primary); color: var(--color-primary);
margin-bottom: 5px; margin-bottom: var(--spacing-xs);
line-height: 1.1;
} }
.stat-label { .stat-label {
font-size: var(--font-size-sm); font-size: var(--font-size-xs);
color: var(--color-text-secondary); color: var(--color-text-muted);
} text-transform: uppercase;
letter-spacing: 0.05em;
.button {
background: var(--gradient-primary);
color: white;
border: none;
padding: 12px 30px;
font-size: var(--font-size-base);
border-radius: var(--radius-md);
cursor: pointer;
transition: transform 0.2s;
margin-right: 10px;
}
.button:hover {
transform: translateY(-2px);
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
} }
.receipts-list { .receipts-list {
margin-top: 20px; margin-top: var(--spacing-md);
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
} }
.receipt-item { .receipt-item {
background: var(--color-bg-secondary); background: var(--color-bg-secondary);
padding: 15px; padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md); border-radius: var(--radius-md);
margin-bottom: 10px; border: 1px solid var(--color-border);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@ -388,7 +348,7 @@ onMounted(() => {
.receipt-merchant { .receipt-merchant {
font-weight: 600; font-weight: 600;
font-size: var(--font-size-base); font-size: var(--font-size-base);
margin-bottom: 5px; margin-bottom: var(--spacing-xs);
color: var(--color-text-primary); color: var(--color-text-primary);
} }
@ -396,7 +356,7 @@ onMounted(() => {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
color: var(--color-text-secondary); color: var(--color-text-secondary);
display: flex; display: flex;
gap: 15px; gap: var(--spacing-md);
flex-wrap: wrap; flex-wrap: wrap;
} }
@ -419,20 +379,17 @@ onMounted(() => {
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
/* Mobile Responsive - Handled by theme.css /* Mobile */
Component-specific overrides only below */
@media (max-width: 480px) { @media (max-width: 480px) {
.stat-card { .stat-card {
padding: 15px; padding: var(--spacing-sm);
} }
/* Receipt items stack content vertically */
.receipt-item { .receipt-item {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 12px; gap: var(--spacing-sm);
padding: 12px; padding: var(--spacing-sm);
} }
.receipt-info { .receipt-info {
@ -440,15 +397,8 @@ onMounted(() => {
} }
.receipt-details { .receipt-details {
gap: 10px; gap: var(--spacing-sm);
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
} }
/* Buttons full width on mobile */
.button {
width: 100%;
margin-right: 0;
margin-bottom: 10px;
}
} }
</style> </style>

View file

@ -2,7 +2,7 @@
<div class="recipes-view"> <div class="recipes-view">
<!-- Controls Panel --> <!-- Controls Panel -->
<div class="card mb-controls"> <div class="card mb-controls">
<h2 class="text-xl font-bold mb-md">Find Recipes</h2> <h2 class="section-title text-xl mb-md">Find Recipes</h2>
<!-- Level Selector --> <!-- Level Selector -->
<div class="form-group"> <div class="form-group">
@ -98,6 +98,62 @@
/> />
</div> </div>
<!-- Nutrition Filters -->
<details class="collapsible form-group">
<summary class="form-label collapsible-summary nutrition-summary">
Nutrition Filters <span class="text-muted text-xs">(per recipe, optional)</span>
</summary>
<div class="nutrition-filters-grid mt-xs">
<div class="form-group">
<label class="form-label">Max Calories</label>
<input
type="number"
class="form-input"
min="0"
placeholder="e.g. 600"
:value="recipesStore.nutritionFilters.max_calories ?? ''"
@input="onNutritionInput('max_calories', $event)"
/>
</div>
<div class="form-group">
<label class="form-label">Max Sugar (g)</label>
<input
type="number"
class="form-input"
min="0"
placeholder="e.g. 10"
:value="recipesStore.nutritionFilters.max_sugar_g ?? ''"
@input="onNutritionInput('max_sugar_g', $event)"
/>
</div>
<div class="form-group">
<label class="form-label">Max Carbs (g)</label>
<input
type="number"
class="form-input"
min="0"
placeholder="e.g. 50"
:value="recipesStore.nutritionFilters.max_carbs_g ?? ''"
@input="onNutritionInput('max_carbs_g', $event)"
/>
</div>
<div class="form-group">
<label class="form-label">Max Sodium (mg)</label>
<input
type="number"
class="form-input"
min="0"
placeholder="e.g. 800"
:value="recipesStore.nutritionFilters.max_sodium_mg ?? ''"
@input="onNutritionInput('max_sodium_mg', $event)"
/>
</div>
</div>
<p class="text-xs text-muted mt-xs">
Recipes without nutrition data always appear. Filters apply to food.com and estimated values.
</p>
</details>
<!-- Suggest Button --> <!-- Suggest Button -->
<button <button
class="btn btn-primary btn-lg w-full" class="btn btn-primary btn-lg w-full"
@ -107,7 +163,7 @@
<span v-if="recipesStore.loading"> <span v-if="recipesStore.loading">
<span class="spinner spinner-sm inline-spinner"></span> Finding recipes <span class="spinner spinner-sm inline-spinner"></span> Finding recipes
</span> </span>
<span v-else>🍳 Suggest Recipes</span> <span v-else>Suggest Recipes</span>
</button> </button>
<!-- Empty pantry nudge --> <!-- Empty pantry nudge -->
@ -165,13 +221,44 @@
<div class="flex flex-wrap gap-xs"> <div class="flex flex-wrap gap-xs">
<span class="status-badge status-success">{{ recipe.match_count }} matched</span> <span class="status-badge status-success">{{ recipe.match_count }} matched</span>
<span class="status-badge status-info">Level {{ recipe.level }}</span> <span class="status-badge status-info">Level {{ recipe.level }}</span>
<span v-if="recipe.is_wildcard" class="status-badge status-warning">Wildcard 🎲</span> <span v-if="recipe.is_wildcard" class="status-badge status-warning">Wildcard</span>
</div> </div>
</div> </div>
<!-- Notes --> <!-- Notes -->
<p v-if="recipe.notes" class="text-sm text-secondary mb-sm">{{ recipe.notes }}</p> <p v-if="recipe.notes" class="text-sm text-secondary mb-sm">{{ recipe.notes }}</p>
<!-- Nutrition chips -->
<div v-if="recipe.nutrition" class="nutrition-chips mb-sm">
<span v-if="recipe.nutrition.calories != null" class="nutrition-chip">
🔥 {{ Math.round(recipe.nutrition.calories) }} kcal
</span>
<span v-if="recipe.nutrition.fat_g != null" class="nutrition-chip">
🧈 {{ recipe.nutrition.fat_g.toFixed(1) }}g fat
</span>
<span v-if="recipe.nutrition.protein_g != null" class="nutrition-chip">
💪 {{ recipe.nutrition.protein_g.toFixed(1) }}g protein
</span>
<span v-if="recipe.nutrition.carbs_g != null" class="nutrition-chip">
🌾 {{ recipe.nutrition.carbs_g.toFixed(1) }}g carbs
</span>
<span v-if="recipe.nutrition.fiber_g != null" class="nutrition-chip">
🌿 {{ recipe.nutrition.fiber_g.toFixed(1) }}g fiber
</span>
<span v-if="recipe.nutrition.sugar_g != null" class="nutrition-chip nutrition-chip-sugar">
🍬 {{ recipe.nutrition.sugar_g.toFixed(1) }}g sugar
</span>
<span v-if="recipe.nutrition.sodium_mg != null" class="nutrition-chip">
🧂 {{ Math.round(recipe.nutrition.sodium_mg) }}mg sodium
</span>
<span v-if="recipe.nutrition.servings != null" class="nutrition-chip nutrition-chip-servings">
🍽 {{ recipe.nutrition.servings }} serving{{ recipe.nutrition.servings !== 1 ? 's' : '' }}
</span>
<span v-if="recipe.nutrition.estimated" class="nutrition-chip nutrition-chip-estimated" title="Estimated from ingredient profiles">
~ estimated
</span>
</div>
<!-- Missing ingredients --> <!-- Missing ingredients -->
<div v-if="recipe.missing_ingredients.length > 0" class="mb-sm"> <div v-if="recipe.missing_ingredients.length > 0" class="mb-sm">
<p class="text-sm font-semibold text-warning">You'd need:</p> <p class="text-sm font-semibold text-warning">You'd need:</p>
@ -255,7 +342,11 @@
v-if="!recipesStore.result && !recipesStore.loading && pantryItems.length > 0" v-if="!recipesStore.result && !recipesStore.loading && pantryItems.length > 0"
class="card text-center text-muted" class="card text-center text-muted"
> >
<p class="text-lg">🍳</p> <svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5" style="width:40px;height:40px;color:var(--color-text-muted);margin-bottom:var(--spacing-sm)">
<path d="M12 8c0 0 4-4 12-4s12 4 12 4v8H12V8z"/>
<path d="M10 16h28v4l-2 20H12L10 20v-4z"/>
<line x1="20" y1="24" x2="28" y2="24"/>
</svg>
<p class="mt-xs">Tap "Suggest Recipes" to find recipes using your pantry items.</p> <p class="mt-xs">Tap "Suggest Recipes" to find recipes using your pantry items.</p>
</div> </div>
</div> </div>
@ -361,6 +452,14 @@ function onMaxMissingInput(e: Event) {
recipesStore.maxMissing = isNaN(val) ? null : val recipesStore.maxMissing = isNaN(val) ? null : val
} }
// Nutrition filter inputs
type NutritionKey = 'max_calories' | 'max_sugar_g' | 'max_carbs_g' | 'max_sodium_mg'
function onNutritionInput(key: NutritionKey, e: Event) {
const target = e.target as HTMLInputElement
const val = parseFloat(target.value)
recipesStore.nutritionFilters[key] = isNaN(val) ? null : val
}
// Suggest handler // Suggest handler
async function handleSuggest() { async function handleSuggest() {
await recipesStore.suggest(pantryItems.value) await recipesStore.suggest(pantryItems.value)
@ -509,6 +608,48 @@ details[open] .collapsible-summary::before {
margin-top: var(--spacing-md); margin-top: var(--spacing-md);
} }
.nutrition-summary {
cursor: pointer;
}
.nutrition-filters-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-sm);
}
.nutrition-chips {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-xs);
}
.nutrition-chip {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 12px;
font-size: var(--font-size-xs);
background: var(--color-bg-secondary, #f5f5f5);
color: var(--color-text-secondary);
white-space: nowrap;
}
.nutrition-chip-sugar {
background: var(--color-warning-bg);
color: var(--color-warning);
}
.nutrition-chip-servings {
background: var(--color-info-bg);
color: var(--color-info-light);
}
.nutrition-chip-estimated {
font-style: italic;
opacity: 0.7;
}
/* Mobile adjustments */ /* Mobile adjustments */
@media (max-width: 480px) { @media (max-width: 480px) {
.flex-between { .flex-between {
@ -520,5 +661,9 @@ details[open] .collapsible-summary::before {
.recipe-title { .recipe-title {
margin-right: 0; margin-right: 0;
} }
.nutrition-filters-grid {
grid-template-columns: 1fr;
}
} }
</style> </style>

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="settings-view"> <div class="settings-view">
<div class="card"> <div class="card">
<h2 class="text-xl font-bold mb-md">Settings</h2> <h2 class="section-title text-xl mb-md">Settings</h2>
<!-- Cooking Equipment --> <!-- Cooking Equipment -->
<section> <section>