feat: surface smart dinner fit panel
This commit is contained in:
@@ -70,6 +70,7 @@ Meal planning should be a native Oikos kitchen workflow, not a chatbot shortcut.
|
|||||||
- [x] Added deterministic meal-fit service for the next planning slice: day context, energy, guests, cleanup, interruption tolerance, concrete inventory/leftover use, preferences/allergies, recency, and grocery delta all affect ranking before AI gets involved.
|
- [x] Added deterministic meal-fit service for the next planning slice: day context, energy, guests, cleanup, interruption tolerance, concrete inventory/leftover use, preferences/allergies, recency, and grocery delta all affect ranking before AI gets involved.
|
||||||
- [x] Added `/api/v1/meal-planning/suggestions` as a thin native API boundary returning ranked suggestions plus optional generated grocery output.
|
- [x] Added `/api/v1/meal-planning/suggestions` as a thin native API boundary returning ranked suggestions plus optional generated grocery output.
|
||||||
- [x] Added `test:meal-fit` acceptance coverage proving busy/low-energy days, guest days, allergy blocks, dislike/repetition, concrete inventory matching, and grocery dedupe change outputs materially.
|
- [x] Added `test:meal-fit` acceptance coverage proving busy/low-energy days, guest days, allergy blocks, dislike/repetition, concrete inventory matching, and grocery dedupe change outputs materially.
|
||||||
|
- [x] Added a first native Meals-page “Smart dinner fit” panel that lets the family change day pressure, energy, dinner window, guests, and solo-parent context, then see grounded ranked suggestions from saved recipes.
|
||||||
|
|
||||||
## Remaining Combined Plan
|
## Remaining Combined Plan
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ let state = {
|
|||||||
categories: [], // Einkaufskategorien für Zutaten
|
categories: [], // Einkaufskategorien für Zutaten
|
||||||
modal: null,
|
modal: null,
|
||||||
visibleMealTypes: ['breakfast', 'lunch', 'dinner', 'snack'],
|
visibleMealTypes: ['breakfast', 'lunch', 'dinner', 'snack'],
|
||||||
|
planner: {
|
||||||
|
suggestions: [],
|
||||||
|
selectedDate: null,
|
||||||
|
context: { busyness: 'normal', energy: 'normal', dinnerWindowMinutes: 45, guests: false, soloParent: false },
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Container-Referenz für Hilfsfunktionen (wird in render() gesetzt)
|
// Container-Referenz für Hilfsfunktionen (wird in render() gesetzt)
|
||||||
@@ -222,6 +228,47 @@ export async function render(container, { user }) {
|
|||||||
<i data-lucide="chevron-right" aria-hidden="true"></i>
|
<i data-lucide="chevron-right" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="meal-fit-panel" id="meal-fit-panel">
|
||||||
|
<div class="meal-fit-panel__header">
|
||||||
|
<div>
|
||||||
|
<p class="meal-fit-panel__eyebrow">Family logistics</p>
|
||||||
|
<h2 class="meal-fit-panel__title">Smart dinner fit</h2>
|
||||||
|
<p class="meal-fit-panel__subtitle">Ranks saved recipes against the actual day: pressure, energy, guests, leftovers and grocery burden.</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn--primary" id="meal-fit-run" type="button">Suggest dinners</button>
|
||||||
|
</div>
|
||||||
|
<div class="meal-fit-panel__controls">
|
||||||
|
<label class="meal-fit-control">Day
|
||||||
|
<select class="form-input" id="meal-fit-date"></select>
|
||||||
|
</label>
|
||||||
|
<label class="meal-fit-control">Pressure
|
||||||
|
<select class="form-input" id="meal-fit-busyness">
|
||||||
|
<option value="normal">Normal</option>
|
||||||
|
<option value="high">Busy / late activity</option>
|
||||||
|
<option value="low">Calm</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="meal-fit-control">Energy
|
||||||
|
<select class="form-input" id="meal-fit-energy">
|
||||||
|
<option value="normal">Normal</option>
|
||||||
|
<option value="low">Low</option>
|
||||||
|
<option value="high">High</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="meal-fit-control">Dinner window
|
||||||
|
<select class="form-input" id="meal-fit-window">
|
||||||
|
<option value="25">25 min</option>
|
||||||
|
<option value="45" selected>45 min</option>
|
||||||
|
<option value="90">90 min</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="meal-fit-check"><input type="checkbox" id="meal-fit-guests"> Guests</label>
|
||||||
|
<label class="meal-fit-check"><input type="checkbox" id="meal-fit-solo"> Solo parent</label>
|
||||||
|
</div>
|
||||||
|
<div class="meal-fit-results" id="meal-fit-results" aria-live="polite">
|
||||||
|
<p class="meal-fit-empty">Choose a day context and run suggestions. If the ranking does not change when the day changes, we have failed.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="week-grid" id="week-grid">
|
<div class="week-grid" id="week-grid">
|
||||||
<div style="margin:auto;padding:2rem;text-align:center;color:var(--color-text-disabled)">${t('meals.loadingIndicator')}</div>
|
<div style="margin:auto;padding:2rem;text-align:center;color:var(--color-text-disabled)">${t('meals.loadingIndicator')}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,7 +288,9 @@ export async function render(container, { user }) {
|
|||||||
|
|
||||||
await Promise.all([loadWeek(monday), loadLists(), loadPreferences(), loadCategories(), loadRecipes(), loadFamilyMembers(), loadRecipeSignals()]);
|
await Promise.all([loadWeek(monday), loadLists(), loadPreferences(), loadCategories(), loadRecipes(), loadFamilyMembers(), loadRecipeSignals()]);
|
||||||
renderWeekGrid();
|
renderWeekGrid();
|
||||||
|
renderMealFitPanel();
|
||||||
wireNav();
|
wireNav();
|
||||||
|
wireMealFitPanel();
|
||||||
|
|
||||||
const selectedRecipeId = Number(params.get('recipe'));
|
const selectedRecipeId = Number(params.get('recipe'));
|
||||||
if (selectedRecipeId) {
|
if (selectedRecipeId) {
|
||||||
@@ -362,6 +411,151 @@ function renderSlot(date, type, mealsForDay) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// Smart dinner fit panel
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
function recipeToPlannerMeal(recipe) {
|
||||||
|
const tags = parseRecipeTags(recipe.tags_json);
|
||||||
|
const style = recipe.style || 'family';
|
||||||
|
const isQuick = style === 'quick' || tags.includes('quick') || tags.includes('hurtig');
|
||||||
|
const isLeftovers = style === 'leftovers' || recipe.meal_category === 'leftovers';
|
||||||
|
const isCozy = style === 'cozy' || tags.includes('cozy') || tags.includes('hygge');
|
||||||
|
const guestFit = Boolean(recipe.guest_fit || recipe.batch_friendly || tags.includes('guest') || tags.includes('guests') || tags.includes('batch'));
|
||||||
|
return {
|
||||||
|
id: recipe.id,
|
||||||
|
name: recipe.title,
|
||||||
|
ingredients: recipe.ingredients || [],
|
||||||
|
activeMinutes: Number(recipe.active_minutes || recipe.activeMinutes || (isQuick ? 15 : isCozy ? 45 : 30)),
|
||||||
|
totalMinutes: Number(recipe.total_minutes || recipe.totalMinutes || (isQuick ? 20 : isCozy ? 75 : 45)),
|
||||||
|
effort: isQuick || isLeftovers ? 'easy' : isCozy ? 'project' : 'normal',
|
||||||
|
cleanup: tags.includes('one-pot') || tags.includes('traybake') || isLeftovers ? 'low' : isCozy ? 'high' : 'medium',
|
||||||
|
interruptionTolerance: isQuick || isLeftovers || tags.includes('slow') || tags.includes('traybake') ? 'high' : 'medium',
|
||||||
|
kidFit: recipe.style === 'kids' || tags.includes('kids') || tags.includes('børnevenlig') ? 'safe' : 'mixed',
|
||||||
|
guestFit,
|
||||||
|
batchFriendly: guestFit || tags.includes('batch'),
|
||||||
|
meal_category: recipe.meal_category,
|
||||||
|
protein: recipe.protein,
|
||||||
|
style,
|
||||||
|
tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRecipeTags(value) {
|
||||||
|
if (Array.isArray(value)) return value.map((tag) => String(tag).toLowerCase());
|
||||||
|
if (!value) return [];
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
return Array.isArray(parsed) ? parsed.map((tag) => String(tag).toLowerCase()) : [];
|
||||||
|
} catch {
|
||||||
|
return String(value).split(',').map((tag) => tag.trim().toLowerCase()).filter(Boolean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function plannerInventory() {
|
||||||
|
return state.meals
|
||||||
|
.filter((meal) => meal.meal_type === 'dinner')
|
||||||
|
.slice(-8)
|
||||||
|
.map((meal) => ({ name: meal.title, portions: 1, sourceMealId: meal.id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function plannerPreferences() {
|
||||||
|
return state.recipeSignals
|
||||||
|
.map((signal) => {
|
||||||
|
const recipe = state.recipes.find((candidate) => Number(candidate.id) === Number(signal.recipe_id));
|
||||||
|
if (!recipe || !['like', 'favorite', 'dislike'].includes(signal.preference)) return null;
|
||||||
|
return { type: signal.preference === 'favorite' ? 'like' : signal.preference, target: recipe.title, strength: signal.preference === 'favorite' ? 'high' : 'medium' };
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedPlannerContext() {
|
||||||
|
const busyness = _container.querySelector('#meal-fit-busyness')?.value || 'normal';
|
||||||
|
const energy = _container.querySelector('#meal-fit-energy')?.value || 'normal';
|
||||||
|
const dinnerWindowMinutes = Number(_container.querySelector('#meal-fit-window')?.value || 45);
|
||||||
|
const guests = Boolean(_container.querySelector('#meal-fit-guests')?.checked);
|
||||||
|
const soloParent = Boolean(_container.querySelector('#meal-fit-solo')?.checked);
|
||||||
|
const labels = [
|
||||||
|
busyness === 'high' ? 'late_activity' : null,
|
||||||
|
energy === 'low' ? 'low_energy' : null,
|
||||||
|
soloParent ? 'solo_parent' : null,
|
||||||
|
].filter(Boolean);
|
||||||
|
return { busyness, energy, dinnerWindowMinutes, guests, soloParent, labels };
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMealFitPanel() {
|
||||||
|
if (!_container) return;
|
||||||
|
const dateSelect = _container.querySelector('#meal-fit-date');
|
||||||
|
const results = _container.querySelector('#meal-fit-results');
|
||||||
|
if (!dateSelect || !results) return;
|
||||||
|
const days = Array.from({ length: 7 }, (_, i) => addDays(state.currentWeek, i));
|
||||||
|
if (!state.planner.selectedDate || !days.includes(state.planner.selectedDate)) state.planner.selectedDate = days[0];
|
||||||
|
dateSelect.innerHTML = days.map((date, idx) => `<option value="${date}" ${date === state.planner.selectedDate ? 'selected' : ''}>${esc(DAY_NAMES()[idx])} · ${esc(formatDayDate(date))}</option>`).join('');
|
||||||
|
|
||||||
|
if (state.planner.loading) {
|
||||||
|
results.innerHTML = '<p class="meal-fit-empty">Scoring dinners against family reality…</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state.planner.suggestions.length) {
|
||||||
|
results.innerHTML = '<p class="meal-fit-empty">Choose a day context and run suggestions. If the ranking does not change when the day changes, we have failed.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
results.innerHTML = state.planner.suggestions.slice(0, 3).map(renderMealFitSuggestion).join('');
|
||||||
|
if (window.lucide) lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMealFitSuggestion(suggestion, idx) {
|
||||||
|
const reasons = (suggestion.reasons || []).slice(0, 3).map((reason) => `<li>${esc(reason)}</li>`).join('');
|
||||||
|
const warnings = (suggestion.warnings || []).slice(0, 2).map((warning) => `<li>${esc(warning)}</li>`).join('');
|
||||||
|
const grocery = suggestion.groceryDelta ? `${suggestion.groceryDelta.newItems} new grocery item${suggestion.groceryDelta.newItems === 1 ? '' : 's'}` : '';
|
||||||
|
return `
|
||||||
|
<article class="meal-fit-card">
|
||||||
|
<div class="meal-fit-card__rank">#${idx + 1}</div>
|
||||||
|
<div class="meal-fit-card__body">
|
||||||
|
<div class="meal-fit-card__topline">
|
||||||
|
<h3>${esc(suggestion.mealName)}</h3>
|
||||||
|
<span class="meal-fit-card__score">Fit ${esc(String(suggestion.score))}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meal-fit-card__chips">
|
||||||
|
${(suggestion.fitLabels || []).slice(0, 4).map((label) => `<span>${esc(label.replaceAll('-', ' '))}</span>`).join('')}
|
||||||
|
${grocery ? `<span>${esc(grocery)}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
${reasons ? `<ul class="meal-fit-card__reasons">${reasons}</ul>` : ''}
|
||||||
|
${warnings ? `<ul class="meal-fit-card__warnings">${warnings}</ul>` : ''}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireMealFitPanel() {
|
||||||
|
const runBtn = _container.querySelector('#meal-fit-run');
|
||||||
|
if (!runBtn) return;
|
||||||
|
_container.querySelector('#meal-fit-date')?.addEventListener('change', (event) => { state.planner.selectedDate = event.target.value; });
|
||||||
|
runBtn.addEventListener('click', async () => {
|
||||||
|
state.planner.selectedDate = _container.querySelector('#meal-fit-date')?.value || state.planner.selectedDate;
|
||||||
|
state.planner.context = selectedPlannerContext();
|
||||||
|
state.planner.loading = true;
|
||||||
|
renderMealFitPanel();
|
||||||
|
try {
|
||||||
|
const res = await api.post('/meal-planning/suggestions', {
|
||||||
|
meals: state.recipes.map(recipeToPlannerMeal),
|
||||||
|
dayContext: state.planner.context,
|
||||||
|
preferences: plannerPreferences(),
|
||||||
|
inventory: plannerInventory(),
|
||||||
|
recentMeals: state.meals.filter((meal) => meal.date < state.planner.selectedDate).slice(-6),
|
||||||
|
pantryStaples: ['salt', 'pepper', 'oil', 'olive oil', 'water'],
|
||||||
|
});
|
||||||
|
state.planner.suggestions = res.data.suggestions || [];
|
||||||
|
} catch (err) {
|
||||||
|
state.planner.suggestions = [];
|
||||||
|
window.oikos?.showToast(err.data?.error ?? 'Meal suggestions failed', 'error');
|
||||||
|
} finally {
|
||||||
|
state.planner.loading = false;
|
||||||
|
renderMealFitPanel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Event-Delegation
|
// Event-Delegation
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -370,11 +564,13 @@ function wireNav() {
|
|||||||
_container.querySelector('#week-prev')?.addEventListener('click', async () => {
|
_container.querySelector('#week-prev')?.addEventListener('click', async () => {
|
||||||
await loadWeek(addDays(state.currentWeek, -7));
|
await loadWeek(addDays(state.currentWeek, -7));
|
||||||
renderWeekGrid();
|
renderWeekGrid();
|
||||||
|
renderMealFitPanel();
|
||||||
});
|
});
|
||||||
|
|
||||||
_container.querySelector('#week-next')?.addEventListener('click', async () => {
|
_container.querySelector('#week-next')?.addEventListener('click', async () => {
|
||||||
await loadWeek(addDays(state.currentWeek, 7));
|
await loadWeek(addDays(state.currentWeek, 7));
|
||||||
renderWeekGrid();
|
renderWeekGrid();
|
||||||
|
renderMealFitPanel();
|
||||||
});
|
});
|
||||||
|
|
||||||
_container.querySelector('#week-today')?.addEventListener('click', async () => {
|
_container.querySelector('#week-today')?.addEventListener('click', async () => {
|
||||||
@@ -382,6 +578,7 @@ function wireNav() {
|
|||||||
if (monday === state.currentWeek) return;
|
if (monday === state.currentWeek) return;
|
||||||
await loadWeek(monday);
|
await loadWeek(monday);
|
||||||
renderWeekGrid();
|
renderWeekGrid();
|
||||||
|
renderMealFitPanel();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,167 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------------------------
|
||||||
|
* Smart dinner fit panel
|
||||||
|
* -------------------------------------------------------- */
|
||||||
|
.meal-fit-panel {
|
||||||
|
margin: var(--space-3) var(--space-4);
|
||||||
|
padding: var(--space-4);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--module-accent) 28%, var(--color-border));
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, color-mix(in srgb, var(--module-accent) 16%, transparent), transparent 40%),
|
||||||
|
var(--color-surface);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-panel__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-panel__eyebrow {
|
||||||
|
margin: 0 0 var(--space-1);
|
||||||
|
color: var(--module-accent);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-panel__title {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-panel__subtitle {
|
||||||
|
margin: var(--space-1) 0 0;
|
||||||
|
max-width: 68ch;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-panel__controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
|
align-items: end;
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-top: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-control,
|
||||||
|
.meal-fit-check {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-control {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-check {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
min-height: var(--target-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-results {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-top: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-empty {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-3);
|
||||||
|
border: 1px solid var(--color-border-subtle);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: color-mix(in srgb, var(--color-surface) 84%, var(--module-accent) 6%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-card__rank {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background: var(--module-accent);
|
||||||
|
color: var(--color-text-inverse, #fff);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-card__topline {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-card__topline h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: var(--text-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-card__score {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-card__chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-1);
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-card__chips span {
|
||||||
|
padding: 2px var(--space-2);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background: var(--color-accent-light);
|
||||||
|
color: var(--color-accent);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-card__reasons,
|
||||||
|
.meal-fit-card__warnings {
|
||||||
|
margin: var(--space-2) 0 0;
|
||||||
|
padding-left: var(--space-4);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-card__reasons { color: var(--color-text-secondary); }
|
||||||
|
.meal-fit-card__warnings { color: var(--color-warning, #b45309); }
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.meal-fit-panel__header {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meal-fit-panel__header .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------------
|
/* --------------------------------------------------------
|
||||||
* Wochengitter (scroll horizontal auf Mobil)
|
* Wochengitter (scroll horizontal auf Mobil)
|
||||||
* -------------------------------------------------------- */
|
* -------------------------------------------------------- */
|
||||||
|
|||||||
Reference in New Issue
Block a user