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;
|
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
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue