From b9eadcdf0e64f16946d5f2c4f2def38e89c57696 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Wed, 1 Apr 2026 22:29:55 -0700 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20warm=20organic=20design=20ove?= =?UTF-8?q?rhaul=20=E2=80=94=20Fraunces/DM=20fonts,=20saffron=20accent,=20?= =?UTF-8?q?compact=20inventory=20shelf=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- frontend/src/components/EditItemModal.vue | 126 +- frontend/src/components/InventoryList.vue | 1546 ++++++++++----------- frontend/src/components/ReceiptsView.vue | 166 +-- frontend/src/components/RecipesView.vue | 153 +- frontend/src/components/SettingsView.vue | 2 +- 5 files changed, 1040 insertions(+), 953 deletions(-) diff --git a/frontend/src/components/EditItemModal.vue b/frontend/src/components/EditItemModal.vue index 3ad5333..ab02389 100644 --- a/frontend/src/components/EditItemModal.vue +++ b/frontend/src/components/EditItemModal.vue @@ -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; - } } diff --git a/frontend/src/components/InventoryList.vue b/frontend/src/components/InventoryList.vue index 7b8476f..abb3381 100644 --- a/frontend/src/components/InventoryList.vue +++ b/frontend/src/components/InventoryList.vue @@ -1,311 +1,353 @@ @@ -362,6 +397,9 @@ const { items, stats, loading, locationFilter, statusFilter } = storeToRefs(stor const filteredItems = computed(() => store.filteredItems) const editingItem = ref(null) +// Scan mode toggle +const scanMode = ref<'gun' | 'camera' | 'manual'>('gun') + // Options for button groups const locations = [ { value: 'fridge', label: 'Fridge', icon: '๐ŸงŠ' }, @@ -542,7 +580,7 @@ async function handleScannerGunInput() { const item = result.results[0] scannerResults.value.push({ type: 'success', - message: `โœ“ Added: ${item.product_name || 'item'}${''} to ${scannerLocation.value}`, + message: `Added: ${item.product_name || 'item'} to ${scannerLocation.value}`, }) await refreshItems() } else { @@ -588,7 +626,7 @@ async function handleBarcodeImageSelect(e: Event) { const item = result.results[0] barcodeResults.value.push({ type: 'success', - message: `โœ“ Found: ${item.product_name || 'item'}${''}`, + message: `Found: ${item.product_name || 'item'}`, }) await refreshItems() } else { @@ -668,40 +706,31 @@ function exportExcel() { window.open(`${apiUrl}/export/inventory/excel`, '_blank') } -function formatDate(dateStr: string): string { +// Short date for compact row display +function formatDateShort(dateStr: string): string { const date = new Date(dateStr) - return date.toLocaleDateString() -} - -function getDaysUntilExpiry(expiryStr: string): string { const today = new Date() today.setHours(0, 0, 0, 0) - const expiry = new Date(expiryStr) + const expiry = new Date(dateStr) expiry.setHours(0, 0, 0, 0) + const diffDays = Math.ceil((expiry.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)) - const diffTime = expiry.getTime() - today.getTime() - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) - - if (diffDays < 0) { - return `${Math.abs(diffDays)} days ago` - } else if (diffDays === 0) { - return 'today' - } else if (diffDays === 1) { - return 'tomorrow' - } else { - return `in ${diffDays} days` - } + if (diffDays < 0) return `${Math.abs(diffDays)}d ago` + if (diffDays === 0) return 'today' + if (diffDays === 1) return 'tmrw' + if (diffDays <= 14) return `${diffDays}d` + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) } -function getExpiryClass(expiryStr: string): string { +function getExpiryBadgeClass(expiryStr: string): string { const today = new Date() const expiry = new Date(expiryStr) const diffDays = Math.ceil((expiry.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)) - if (diffDays < 0) return 'expired' - if (diffDays <= 3) return 'expiring-soon' - if (diffDays <= 7) return 'expiring-warning' - return '' + if (diffDays < 0) return 'expiry-expired' + if (diffDays <= 3) return 'expiry-urgent' + if (diffDays <= 7) return 'expiry-soon' + return 'expiry-ok' } function getItemClass(item: InventoryItem): string { @@ -720,562 +749,509 @@ function getItemClass(item: InventoryItem): string { diff --git a/frontend/src/components/ReceiptsView.vue b/frontend/src/components/ReceiptsView.vue index 9aeb495..f84a411 100644 --- a/frontend/src/components/ReceiptsView.vue +++ b/frontend/src/components/ReceiptsView.vue @@ -2,7 +2,7 @@
-

๐Ÿ“ธ Upload Receipt

+

Upload Receipt

-
+
-

Processing receipt...

+ Processing receiptโ€ฆ
@@ -39,8 +39,8 @@
-

๐Ÿ“‹ Recent Receipts

-
+

Recent Receipts

+

No receipts yet. Upload one above!

@@ -89,9 +89,9 @@
-
- - +
+ +
@@ -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; - } } diff --git a/frontend/src/components/RecipesView.vue b/frontend/src/components/RecipesView.vue index 0430084..68914b4 100644 --- a/frontend/src/components/RecipesView.vue +++ b/frontend/src/components/RecipesView.vue @@ -2,7 +2,7 @@
-

Find Recipes

+

Find Recipes

@@ -98,6 +98,62 @@ />
+ +
+ + Nutrition Filters (per recipe, optional) + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+

+ Recipes without nutrition data always appear. Filters apply to food.com and estimated values. +

+
+ @@ -165,13 +221,44 @@
{{ recipe.match_count }} matched Level {{ recipe.level }} - Wildcard ๐ŸŽฒ + Wildcard

{{ recipe.notes }}

+ +
+ + ๐Ÿ”ฅ {{ Math.round(recipe.nutrition.calories) }} kcal + + + ๐Ÿงˆ {{ recipe.nutrition.fat_g.toFixed(1) }}g fat + + + ๐Ÿ’ช {{ recipe.nutrition.protein_g.toFixed(1) }}g protein + + + ๐ŸŒพ {{ recipe.nutrition.carbs_g.toFixed(1) }}g carbs + + + ๐ŸŒฟ {{ recipe.nutrition.fiber_g.toFixed(1) }}g fiber + + + ๐Ÿฌ {{ recipe.nutrition.sugar_g.toFixed(1) }}g sugar + + + ๐Ÿง‚ {{ Math.round(recipe.nutrition.sodium_mg) }}mg sodium + + + ๐Ÿฝ๏ธ {{ recipe.nutrition.servings }} serving{{ recipe.nutrition.servings !== 1 ? 's' : '' }} + + + ~ estimated + +
+

You'd need:

@@ -255,7 +342,11 @@ v-if="!recipesStore.result && !recipesStore.loading && pantryItems.length > 0" class="card text-center text-muted" > -

๐Ÿณ

+ + + + +

Tap "Suggest Recipes" to find recipes using your pantry items.

@@ -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; + } } diff --git a/frontend/src/components/SettingsView.vue b/frontend/src/components/SettingsView.vue index 058cb83..4e71ba5 100644 --- a/frontend/src/components/SettingsView.vue +++ b/frontend/src/components/SettingsView.vue @@ -1,7 +1,7 @@