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 828efede42
commit b9eadcdf0e
5 changed files with 1040 additions and 953 deletions

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -2,7 +2,7 @@
<div class="recipes-view">
<!-- Controls Panel -->
<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 -->
<div class="form-group">
@ -98,6 +98,62 @@
/>
</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 -->
<button
class="btn btn-primary btn-lg w-full"
@ -107,7 +163,7 @@
<span v-if="recipesStore.loading">
<span class="spinner spinner-sm inline-spinner"></span> Finding recipes
</span>
<span v-else>🍳 Suggest Recipes</span>
<span v-else>Suggest Recipes</span>
</button>
<!-- Empty pantry nudge -->
@ -165,13 +221,44 @@
<div class="flex flex-wrap gap-xs">
<span class="status-badge status-success">{{ recipe.match_count }} matched</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>
<!-- Notes -->
<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 -->
<div v-if="recipe.missing_ingredients.length > 0" class="mb-sm">
<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"
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>
</div>
</div>
@ -361,6 +452,14 @@ function onMaxMissingInput(e: Event) {
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
async function handleSuggest() {
await recipesStore.suggest(pantryItems.value)
@ -509,6 +608,48 @@ details[open] .collapsible-summary::before {
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 */
@media (max-width: 480px) {
.flex-between {
@ -520,5 +661,9 @@ details[open] .collapsible-summary::before {
.recipe-title {
margin-right: 0;
}
.nutrition-filters-grid {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -1,7 +1,7 @@
<template>
<div class="settings-view">
<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 -->
<section>