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:
parent
828efede42
commit
b9eadcdf0e
5 changed files with 1040 additions and 953 deletions
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue