Redesign meal planner

This commit is contained in:
2026-04-04 23:26:38 +02:00
parent b44b985e76
commit e83fb49d6b
3 changed files with 127 additions and 243 deletions

View File

@@ -182,24 +182,6 @@
box-shadow: var(--panel-shadow);
}
#planner-summary-card {
position: relative;
overflow: hidden;
background: linear-gradient(145deg, rgba(var(--surface-rgb), 0.98) 0%, rgba(var(--surface-soft-rgb), 0.98) 100%) !important;
border-color: rgba(var(--warm-rgb), 0.26) !important;
box-shadow: var(--panel-shadow) !important;
}
#planner-summary-card::before {
content: '';
position: absolute;
inset: -20% auto auto -8%;
width: 9rem;
height: 9rem;
border-radius: 999px;
background: rgba(var(--warm-rgb), 0.14);
filter: blur(44px);
pointer-events: none;
}
.bg-amber-50 { background-color: rgba(var(--warm-rgb), 0.12) !important; }
.bg-amber-50\/30 { background-color: rgba(var(--warm-rgb), 0.09) !important; }
.bg-amber-50\/50 { background-color: rgba(var(--warm-rgb), 0.14) !important; }
@@ -246,6 +228,15 @@
#pantry-view {
background: rgb(var(--app-bg-rgb)) !important;
}
#planner-view,
#planner-view > div:first-child,
#planner-scroll,
#calendar-swipe-zone,
#calendar-week-wrap,
#calendar-month-wrap,
#planner-meal-slots {
background: #2d2e2b !important;
}
#main-view > div:first-child,
#planner-view > div:first-child,
#pantry-view > div:first-child,
@@ -283,7 +274,6 @@
}
#planner-picker-sheet,
#planner-ing-sheet,
#planner-copy-sheet,
#pv2-edit-sheet,
#mpe-sheet,
#recipe-detail-view > div:last-child {
@@ -303,7 +293,6 @@
}
#planner-picker-backdrop,
#planner-ing-backdrop,
#planner-copy-backdrop,
#pv2-edit-bg,
#mpe-overlay {
background: rgba(7, 6, 5, 0.42) !important;
@@ -313,13 +302,11 @@
/* Planner and common interactive surfaces */
#planner-open-ingredients,
.planner-pick-recipe,
.planner-copy-target,
#mpe-nutrition-section > div,
#mpe-add-area > div,
#pv2-edit-nutrition ul {
box-shadow: var(--panel-shadow) !important;
}
.planner-copy-target,
.planner-pick-recipe,
.pv2-chip,
.pv2-cat-chip,
@@ -329,8 +316,7 @@
#rd-tags span {
transition: transform 160ms ease, background-color 160ms ease, border-color 160ms ease, color 160ms ease, box-shadow 160ms ease;
}
.planner-pick-recipe:hover,
.planner-copy-target:hover { transform: translateY(-1px); }
.planner-pick-recipe:hover { transform: translateY(-1px); }
</style>
</head>
<body class="m-0 min-h-dvh bg-white text-gray-800 antialiased">
@@ -349,6 +335,6 @@
navigator.serviceWorker.register('./sw.js', { scope: './' }).catch(() => {});
}
</script>
<script type="module" src="js/app.js?v=2"></script>
<script type="module" src="js/app.js?v=18"></script>
</body>
</html>

View File

@@ -1,7 +1,7 @@
import { getRecipeListHTML, setupRecipeList } from './views/RecipeList.js?v=2';
import { getFilterHTML, setupFilter } from './views/Filter.js?v=2';
import { getRecipeDetailHTML, setupRecipeDetail } from './views/RecipeDetailV2.js?v=2';
import { getMealPlannerHTML, setupMealPlanner } from './views/MealPlanner.js?v=4';
import { getMealPlannerHTML, setupMealPlanner } from './views/MealPlanner.js?v=20';
import { getPantryHTML, refreshPantry, setupPantry } from './views/Pantry.js?v=2';
import { getMealPlanEditorHTML, setupMealPlanEditor } from './ui/mealPlanEditor.js?v=7';

View File

@@ -1,7 +1,6 @@
import { INGREDIENTS, RECIPES } from '../data/catalog.js?v=2';
import { MEAL_SLOTS } from '../planner/mealSlots.js';
import {
addDays,
addMonths,
addWeeks,
sameMonth,
@@ -61,19 +60,20 @@ function syncTodayButton(mode, weekStart, monthAnchor, selected) {
export function getMealPlannerHTML() {
return `
<div id="planner-view" class="hidden flex flex-col h-full absolute inset-0 overflow-hidden bg-[#2d2e2b] z-10 pb-24">
<div class="shrink-0 bg-white border-b border-gray-200 mt-3">
<div class="shrink-0 bg-[#2d2e2b] border-b border-[#444442] mt-3">
${createCalendarTopbarHTML({
titleId: 'cal-period-label',
prevId: 'cal-prev',
todayId: 'cal-go-today',
nextId: 'cal-next',
titleClass: 'text-[18px] font-semibold text-[#ddd6ca] leading-none tracking-[-0.03em]',
})}
<div id="calendar-swipe-zone" class="overflow-x-hidden" style="touch-action: none">
<div id="calendar-week-wrap" class="px-3 overflow-x-hidden" style="overflow: hidden; max-height: 10rem; opacity: 1; padding-bottom: 0.75rem">
<div id="calendar-swipe-zone" class="overflow-x-hidden bg-[#2d2e2b]" style="touch-action: none">
<div id="calendar-week-wrap" class="px-3 overflow-x-hidden bg-[#2d2e2b]" style="overflow: hidden; max-height: 10rem; opacity: 1; padding-bottom: 0.75rem">
${createCalendarWeekdayHeaderHTML()}
<div id="calendar-week-grid" class="grid grid-cols-7 gap-1.5 max-w-full overflow-x-hidden"></div>
</div>
<div id="calendar-month-wrap" class="px-3" style="overflow: hidden; max-height: 0; opacity: 0; padding-bottom: 0">
<div id="calendar-month-wrap" class="px-3 bg-[#2d2e2b]" style="overflow: hidden; max-height: 0; opacity: 0; padding-bottom: 0">
${createCalendarWeekdayHeaderHTML()}
<div id="calendar-month-grid" class="grid grid-cols-7 gap-1.5"></div>
</div>
@@ -84,97 +84,73 @@ export function getMealPlannerHTML() {
</div>
<div id="planner-scroll" class="flex-1 overflow-y-auto px-4 pt-3 pb-4 bg-[#2d2e2b]">
<div class="flex items-center justify-between mb-2">
<p id="planner-day-heading" class="text-[13px] font-semibold text-gray-900 tabular-nums"></p>
<button type="button" id="planner-copy-day" class="shrink-0 text-[11px] font-semibold text-gray-500 hover:text-gray-900 px-2.5 py-1 rounded-lg hover:bg-gray-100 transition-colors flex items-center gap-1.5">
<i class="fas fa-copy text-[9px]"></i>Kopiuj dzień
</button>
<div id="planner-summary-card" class="mb-3">
<div class="h-full flex flex-col" style="background:#2d2e2b !important; background-image:none !important; box-shadow:none !important;">
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Wartości odżywcze</p>
<div class="flex-1 flex items-center">
<div class="w-full rounded-xl border px-3 py-2.5" style="border-color:#444442 !important;">
<div class="grid grid-cols-4 gap-3 text-left">
<div class="min-w-0">
<span id="planner-nutrition-kcal" class="block text-[15px] font-semibold text-[#ddd6ca] tabular-nums leading-none">—</span>
<span class="text-[9px] text-gray-500">kcal</span>
</div>
<div id="planner-summary-card" class="rounded-xl border border-amber-200/80 bg-[#2d2e2b] p-2.5 shadow-sm mb-3">
<div class="flex items-start justify-between gap-2 mb-2">
<div>
<p class="text-[10px] font-semibold uppercase tracking-wide text-amber-900/70">Dziś — podsumowanie</p>
<p id="planner-summary-kcal" class="text-xl font-bold text-gray-900 tabular-nums leading-tight mt-0.5">0 <span class="text-[13px] font-semibold text-gray-500">kcal</span></p>
<div class="min-w-0">
<span id="planner-nutrition-p" class="block text-[15px] font-semibold text-[#ddd6ca] tabular-nums leading-none">—</span>
<span class="text-[9px] text-gray-500">Białko</span>
</div>
<button type="button" id="planner-toggle-nutrition" class="shrink-0 flex items-center gap-1 text-[11px] font-semibold text-amber-900/80 hover:text-gray-900 py-1 px-2 rounded-lg hover:bg-amber-100/50 transition-colors" aria-expanded="false">
Szczegóły
<i class="fas fa-chevron-down text-[9px] transition-transform" id="planner-nutrition-chevron" aria-hidden="true"></i>
</button>
<div class="min-w-0">
<span id="planner-nutrition-c" class="block text-[15px] font-semibold text-[#ddd6ca] tabular-nums leading-none">—</span>
<span class="text-[9px] text-gray-500">Węgle</span>
</div>
<div id="planner-macro-row" class="flex gap-2 mb-0">
<div class="flex-1 min-w-0 rounded-lg bg-white/80 border border-amber-100 px-2 py-1.5 text-center">
<p class="text-[9px] font-semibold text-gray-500 uppercase">B</p>
<p id="planner-macro-p" class="text-xs font-bold text-gray-900 tabular-nums">0 g</p>
</div>
<div class="flex-1 min-w-0 rounded-lg bg-white/80 border border-amber-100 px-2 py-1.5 text-center">
<p class="text-[9px] font-semibold text-gray-500 uppercase">T</p>
<p id="planner-macro-f" class="text-xs font-bold text-gray-900 tabular-nums">0 g</p>
</div>
<div class="flex-1 min-w-0 rounded-lg bg-white/80 border border-amber-100 px-2 py-1.5 text-center">
<p class="text-[9px] font-semibold text-gray-500 uppercase">W</p>
<p id="planner-macro-c" class="text-xs font-bold text-gray-900 tabular-nums">0 g</p>
<div class="min-w-0">
<span id="planner-nutrition-f" class="block text-[15px] font-semibold text-[#ddd6ca] tabular-nums leading-none">—</span>
<span class="text-[9px] text-gray-500">Tłuszcze</span>
</div>
</div>
<div id="planner-nutrition-details" class="hidden mt-3 pt-3 border-t border-amber-200/60">
<ul class="space-y-0 divide-y divide-amber-100/80 text-sm">
<li class="flex justify-between py-2 font-bold"><span class="text-gray-800">Kalorie</span><span id="planner-detail-kcal" class="text-gray-900 tabular-nums">0 kcal</span></li>
<li class="flex justify-between py-2"><span class="text-gray-700 font-medium">Białko</span><span id="planner-detail-p" class="font-medium text-gray-900 tabular-nums">0 g</span></li>
<li class="flex justify-between py-2"><span class="text-gray-700 font-medium">Tłuszcze</span><span id="planner-detail-f" class="font-medium text-gray-900 tabular-nums">0 g</span></li>
<li class="flex justify-between py-2"><span class="text-gray-700 font-medium">Węglowodany</span><span id="planner-detail-c" class="font-medium text-gray-900 tabular-nums">0 g</span></li>
</ul>
<p id="planner-summary-hint" class="text-[11px] text-gray-500 mt-2">Suma z zaplanowanych posiłków (porcje × wartości z przepisu).</p>
</div>
</div>
<button type="button" id="planner-open-ingredients" class="w-full mb-3 flex items-center justify-center gap-2 py-2.5 rounded-xl border border-dashed border-gray-300 bg-white text-[13px] font-semibold text-gray-800 hover:border-gray-400 hover:bg-gray-50 transition-colors">
<i class="fas fa-shopping-basket text-gray-500 text-xs" aria-hidden="true"></i>
</div>
</div>
<button type="button" id="planner-open-ingredients" class="w-full mb-3 flex items-center justify-center gap-2 py-2.5 rounded-xl border border-dashed border-[#444442] bg-[#2d2e2b] text-[13px] font-semibold text-[#d7d2c8] hover:border-[#6d6c67] hover:bg-[#3a3a37] transition-colors">
<i class="fas fa-shopping-basket text-[#9b978f] text-xs" aria-hidden="true"></i>
Składniki na ten dzień
</button>
<div id="planner-meal-slots" class="space-y-3 pb-2"></div>
<div id="planner-meal-slots" class="space-y-3 pb-2 bg-[#2d2e2b]"></div>
</div>
<div id="planner-picker-backdrop" class="absolute left-0 right-0 top-0 z-[45] bg-black/45 hidden opacity-0 transition-opacity duration-200" style="bottom: ${PLANNER_SHEET_BOTTOM_INSET}" aria-hidden="true"></div>
<div id="planner-picker-sheet" class="absolute left-0 right-0 z-[50] bg-white rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.12)] flex flex-col will-change-transform" style="visibility: hidden; bottom: ${PLANNER_SHEET_BOTTOM_INSET}; height: auto; max-height: ${PLANNER_SHEET_MAX_HEIGHT}; transform: ${PLANNER_SHEET_OFF_TRANSFORM}; transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1)" role="dialog" aria-labelledby="planner-picker-title" aria-modal="true">
<div class="shrink-0 px-4 pt-3 pb-2 border-b border-gray-100 touch-none cursor-grab active:cursor-grabbing select-none" data-planner-sheet-drag-zone aria-label="Przeciągnij w dół, by zamknąć">
<div class="w-10 h-1 bg-gray-200 rounded-full mx-auto mb-2.5" aria-hidden="true"></div>
<h2 id="planner-picker-title" class="text-[15px] font-bold text-gray-900 leading-tight pr-2">Wybierz przepis</h2>
<p id="planner-picker-sub" class="text-[11px] text-gray-500 mt-1"></p>
<div id="planner-picker-sheet" class="absolute left-0 right-0 z-[50] rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.12)] flex flex-col will-change-transform" style="visibility: hidden; bottom: ${PLANNER_SHEET_BOTTOM_INSET}; height: auto; max-height: ${PLANNER_SHEET_MAX_HEIGHT}; transform: ${PLANNER_SHEET_OFF_TRANSFORM}; transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1); background:#2d2e2b !important; background-image:none !important;" role="dialog" aria-labelledby="planner-picker-title" aria-modal="true">
<div class="shrink-0 px-4 pt-3 pb-2 border-b border-[#444442] touch-none cursor-grab active:cursor-grabbing select-none" data-planner-sheet-drag-zone aria-label="Przeciągnij w dół, by zamknąć">
<div class="w-10 h-1 bg-[#6d6c67]/75 rounded-full mx-auto mb-2.5" aria-hidden="true"></div>
<h2 id="planner-picker-title" class="text-[15px] font-bold text-[#ddd6ca] leading-tight pr-2">Wybierz przepis</h2>
<p id="planner-picker-sub" class="text-[11px] text-[#9b978f] mt-1"></p>
</div>
<div class="shrink-0 px-4 pt-2 pb-2">
<input type="text" id="planner-picker-search" class="w-full rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 text-sm outline-none focus:border-gray-400 placeholder:text-gray-400" placeholder="Szukaj przepisu…" />
<input type="text" id="planner-picker-search" class="w-full rounded-xl border border-[#444442] bg-[#2f2f2d] px-3 py-2 text-sm text-[#ddd6ca] outline-none focus:border-[#6d6c67] placeholder:text-[#7d7a74]" placeholder="Szukaj przepisu…" />
</div>
<div id="planner-picker-list" class="min-h-0 flex-1 overflow-y-auto no-scrollbar px-4 py-2.5 pb-8 space-y-2"></div>
</div>
<div id="planner-ing-backdrop" class="absolute left-0 right-0 top-0 z-[45] bg-black/45 hidden opacity-0 transition-opacity duration-200" style="bottom: ${PLANNER_SHEET_BOTTOM_INSET}" aria-hidden="true"></div>
<div id="planner-ing-sheet" class="absolute left-0 right-0 z-[50] bg-white rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.12)] flex flex-col will-change-transform" style="visibility: hidden; bottom: ${PLANNER_SHEET_BOTTOM_INSET}; height: auto; max-height: ${PLANNER_SHEET_MAX_HEIGHT}; transform: ${PLANNER_SHEET_OFF_TRANSFORM}; transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1)" role="dialog" aria-labelledby="planner-ing-title" aria-modal="true">
<div class="shrink-0 px-4 pt-3 pb-2 border-b border-gray-100 touch-none cursor-grab active:cursor-grabbing select-none" data-planner-sheet-drag-zone aria-label="Przeciągnij w dół, by zamknąć">
<div class="w-10 h-1 bg-gray-200 rounded-full mx-auto mb-2.5" aria-hidden="true"></div>
<h2 id="planner-ing-title" class="text-[15px] font-bold text-gray-900 leading-tight pr-2">Składniki i spiżarnia</h2>
<p id="planner-ing-sub" class="text-[11px] text-gray-500 mt-1">Porównanie potrzeb z zapasami.</p>
<div id="planner-ing-sheet" class="absolute left-0 right-0 z-[50] rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.12)] flex flex-col will-change-transform" style="visibility: hidden; bottom: ${PLANNER_SHEET_BOTTOM_INSET}; height: auto; max-height: ${PLANNER_SHEET_MAX_HEIGHT}; transform: ${PLANNER_SHEET_OFF_TRANSFORM}; transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1); background:#2d2e2b !important; background-image:none !important;" role="dialog" aria-labelledby="planner-ing-title" aria-modal="true">
<div class="shrink-0 px-4 pt-3 pb-2 border-b border-[#444442] touch-none cursor-grab active:cursor-grabbing select-none" data-planner-sheet-drag-zone aria-label="Przeciągnij w dół, by zamknąć">
<div class="w-10 h-1 bg-[#6d6c67]/75 rounded-full mx-auto mb-2.5" aria-hidden="true"></div>
<h2 id="planner-ing-title" class="text-[15px] font-bold text-[#ddd6ca] leading-tight pr-2">Składniki i spiżarnia</h2>
<p id="planner-ing-sub" class="text-[11px] text-[#9b978f] mt-1">Porównanie potrzeb z zapasami.</p>
</div>
<div id="planner-ing-body" class="min-h-0 flex-1 overflow-y-auto no-scrollbar px-4 py-2 pb-2"></div>
<div id="planner-ing-footer" class="shrink-0 p-4 pt-2 pb-5 border-t border-gray-100 bg-white space-y-2">
<div id="planner-ing-footer" class="shrink-0 p-4 pt-2 pb-5 border-t border-[#444442] bg-[#2d2e2b] space-y-2">
<button type="button" id="planner-ing-add-all" class="w-full bg-gray-900 hover:bg-black text-white py-3 rounded-xl font-semibold shadow-sm transition-colors text-[13px] flex items-center justify-center gap-2">
<i class="fas fa-cart-plus text-xs" aria-hidden="true"></i>
Dodaj braki na dziś do listy
</button>
<button type="button" id="planner-ing-add-btn" class="hidden w-full border border-gray-200 bg-white text-gray-800 hover:bg-gray-50 py-2.5 rounded-xl font-semibold text-[13px] flex items-center justify-center gap-2 transition-colors">
<i class="fas fa-calendar-week text-gray-500 text-[11px]" aria-hidden="true"></i>
<button type="button" id="planner-ing-add-btn" class="hidden w-full border border-[#444442] bg-[#2d2e2b] text-[#d7d2c8] hover:bg-[#3a3a37] py-2.5 rounded-xl font-semibold text-[13px] flex items-center justify-center gap-2 transition-colors">
<i class="fas fa-calendar-week text-[#9b978f] text-[11px]" aria-hidden="true"></i>
Dodaj braki na cały tydzień
</button>
</div>
</div>
<div id="planner-copy-backdrop" class="absolute left-0 right-0 top-0 z-[45] bg-black/45 hidden opacity-0 transition-opacity duration-200" style="bottom: ${PLANNER_SHEET_BOTTOM_INSET}" aria-hidden="true"></div>
<div id="planner-copy-sheet" class="absolute left-0 right-0 z-[50] bg-white rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.12)] flex flex-col will-change-transform" style="visibility: hidden; bottom: ${PLANNER_SHEET_BOTTOM_INSET}; height: auto; max-height: ${PLANNER_SHEET_MAX_HEIGHT}; transform: ${PLANNER_SHEET_OFF_TRANSFORM}; transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1)" role="dialog" aria-modal="true">
<div class="shrink-0 px-4 pt-3 pb-2 border-b border-gray-100 touch-none cursor-grab active:cursor-grabbing select-none" data-planner-sheet-drag-zone>
<div class="w-10 h-1 bg-gray-200 rounded-full mx-auto mb-2.5"></div>
<h2 class="text-[15px] font-bold text-gray-900 leading-tight">Kopiuj plan dnia</h2>
<p id="planner-copy-sub" class="text-[11px] text-gray-500 mt-1">Wybierz dzień docelowy.</p>
</div>
<div id="planner-copy-list" class="min-h-0 flex-1 overflow-y-auto no-scrollbar px-4 py-2.5 pb-8 space-y-2"></div>
</div>
<div id="planner-toast" class="pointer-events-none absolute left-4 right-4 bottom-28 z-[55] opacity-0 translate-y-2 transition-all duration-300" role="status">
<div class="rounded-xl bg-gray-900 text-white text-sm font-medium px-4 py-3 shadow-lg text-center" id="planner-toast-text"></div>
</div>
@@ -346,30 +322,26 @@ function bindPlannerSheetDragClose(sheet, closeFn) {
function renderDayContent(state) {
const sel = state.selected;
const heading = document.getElementById('planner-day-heading');
if (heading) {
const wd = WEEKDAYS_LONG[sel.getDay()];
heading.textContent = `${wd}, ${sel.getDate()} ${CALENDAR_MONTHS_SHORT[sel.getMonth()]}`;
}
const dayPlan = getDayPlan(state.plans, sel);
const totals = sumDayNutrition(dayPlan);
const kcalEl = document.getElementById('planner-summary-kcal');
if (kcalEl) {
kcalEl.innerHTML = totals.mealCount === 0
? `— <span class="text-sm font-semibold text-gray-500">kcal</span>`
: `${totals.kcal} <span class="text-sm font-semibold text-gray-500">kcal</span>`;
}
const fmt = (n) => `${n} g`;
document.getElementById('planner-macro-p').textContent = totals.mealCount ? fmt(totals.protein) : '—';
document.getElementById('planner-macro-f').textContent = totals.mealCount ? fmt(totals.fat) : '—';
document.getElementById('planner-macro-c').textContent = totals.mealCount ? fmt(totals.carbs) : '—';
const setText = (id, value) => {
const el = document.getElementById(id);
if (el) el.textContent = value;
};
const setGrams = (id, value) => {
const el = document.getElementById(id);
if (!el) return;
el.innerHTML = value === null
? '—'
: `${value}<span class="ml-0.5 text-[12px] font-medium text-[#9b978f]">g</span>`;
};
document.getElementById('planner-detail-kcal').textContent = `${totals.kcal} kcal`;
document.getElementById('planner-detail-p').textContent = fmt(totals.protein);
document.getElementById('planner-detail-f').textContent = fmt(totals.fat);
document.getElementById('planner-detail-c').textContent = fmt(totals.carbs);
const hasMeals = totals.mealCount > 0;
setText('planner-nutrition-kcal', hasMeals ? String(totals.kcal) : '—');
setGrams('planner-nutrition-p', hasMeals ? totals.protein : null);
setGrams('planner-nutrition-f', hasMeals ? totals.fat : null);
setGrams('planner-nutrition-c', hasMeals ? totals.carbs : null);
const ingBtn = document.getElementById('planner-open-ingredients');
if (ingBtn) {
@@ -407,7 +379,7 @@ function renderDayContent(state) {
if (entry?.recipeId && RECIPES[entry.recipeId]) slotKcal += computeEntryNutrition(entry).kcal;
});
const kcalBadge = slotKcal > 0
? `<span class="text-[10px] font-semibold text-amber-600 tabular-nums shrink-0 ml-auto">${slotKcal} kcal</span>`
? `<span class="text-[10px] font-semibold text-[#f2efe8] tabular-nums shrink-0 ml-auto">${slotKcal} kcal</span>`
: '';
const countLabel = entries.length > 1
? `<span class="text-[10px] font-semibold text-gray-400 tabular-nums shrink-0">${entries.length} dania</span>`
@@ -424,30 +396,30 @@ function renderDayContent(state) {
(entry.addedIngredients?.length > 0) ||
(entry.substitutions && Object.keys(entry.substitutions).length > 0);
const customDot = hasCustom ? '<span class="w-1.5 h-1.5 rounded-full bg-amber-400 inline-block shrink-0 ml-1"></span>' : '';
const servLabel = servings > 1 ? `<span class="mx-1.5 text-gray-300">·</span>×${servings}` : '';
const servLabel = servings > 1 ? `<span class="mx-1.5 text-[#6d6c67]">·</span>×${servings}` : '';
return `
<div class="rounded-lg border border-gray-200 bg-white p-2 shadow-sm" data-slot-id="${slot.id}" data-entry-id="${eid}">
<div class="rounded-lg bg-[#2d2e2b] p-2" data-slot-id="${slot.id}" data-entry-id="${eid}">
<div class="flex items-start justify-between gap-2">
<div class="flex items-center gap-2 min-w-0 cursor-pointer planner-open-recipe" data-recipe-id="${escapeHtml(recipe.id)}">
<div class="w-8 h-8 rounded-lg bg-[#d4d4d4] overflow-hidden shrink-0">
<div class="w-8 h-8 rounded-lg bg-[#3a3a37] overflow-hidden shrink-0">
${recipe.image
? `<img src="${escapeHtml(recipe.image)}" alt="" class="w-full h-full object-cover">`
: `<span class="w-full h-full flex items-center justify-center text-white text-[8px] font-medium">${escapeHtml(recipe.thumbLabel)}</span>`}
</div>
<div class="min-w-0">
<div class="flex items-center"><p class="text-[13px] font-bold text-gray-900 truncate underline decoration-1 underline-offset-2">${escapeHtml(recipe.title)}</p>${customDot}</div>
<p class="text-[11px] text-gray-500 mt-0.5 tabular-nums">
<i class="fas fa-clock text-gray-400 mr-0.5" aria-hidden="true"></i>${recipe.minutes} min
<span class="mx-1.5 text-gray-300">·</span>
<i class="fas fa-fire text-gray-400 mr-0.5" aria-hidden="true"></i>${entryN.kcal} kcal${servLabel}
<div class="flex items-center"><p class="text-[13px] font-bold text-[#ddd6ca] truncate underline decoration-1 underline-offset-2">${escapeHtml(recipe.title)}</p>${customDot}</div>
<p class="text-[11px] text-[#9b978f] mt-0.5 tabular-nums">
<i class="fas fa-clock text-[#7d7a74] mr-0.5" aria-hidden="true"></i>${recipe.minutes} min
<span class="mx-1.5 text-[#6d6c67]">·</span>
<i class="fas fa-fire text-[#7d7a74] mr-0.5" aria-hidden="true"></i>${entryN.kcal} kcal${servLabel}
</p>
</div>
</div>
<div class="flex items-center gap-1 shrink-0">
<button type="button" class="planner-edit-meal w-6 h-6 rounded-full border border-gray-200 text-gray-400 hover:text-gray-900 hover:border-gray-400 hover:bg-gray-50 flex items-center justify-center transition-colors" data-slot-id="${slot.id}" data-entry-id="${eid}" aria-label="Edytuj ten przepis">
<button type="button" class="planner-edit-meal w-6 h-6 rounded-full border border-[#444442] text-[#9b978f] hover:text-[#ddd6ca] hover:border-[#6d6c67] hover:bg-[#3a3a37] flex items-center justify-center transition-colors" data-slot-id="${slot.id}" data-entry-id="${eid}" aria-label="Edytuj ten przepis">
<i class="fas fa-pencil text-[9px]" aria-hidden="true"></i>
</button>
<button type="button" class="planner-clear-meal w-6 h-6 rounded-full border border-gray-200 text-gray-400 hover:text-red-600 hover:border-red-200 hover:bg-red-50 transition-colors flex items-center justify-center" data-slot-id="${slot.id}" data-entry-id="${eid}" aria-label="Usuń ten przepis">
<button type="button" class="planner-clear-meal w-6 h-6 rounded-full border border-[#444442] text-[#9b978f] hover:text-red-400 hover:border-red-300/60 hover:bg-[#3a2326] transition-colors flex items-center justify-center" data-slot-id="${slot.id}" data-entry-id="${eid}" aria-label="Usuń ten przepis">
<i class="fas fa-times text-[9px]" aria-hidden="true"></i>
</button>
</div>
@@ -457,27 +429,25 @@ function renderDayContent(state) {
if (isSkipped) {
return `
<div class="rounded-xl border border-gray-200 bg-white shadow-sm overflow-hidden opacity-60" data-slot-id="${slot.id}">
<div class="flex items-center gap-2 px-3 py-2 border-b border-gray-100 bg-gray-50/90">
<span class="w-7 h-7 rounded-lg bg-gray-100 flex items-center justify-center text-gray-400 shrink-0">
<i class="fas ${slot.icon} text-[13px]" aria-hidden="true"></i>
</span>
<span class="text-[13px] font-semibold text-gray-400 truncate min-w-0 flex-1">${slot.label}</span>
<div class="rounded-xl bg-[#393937] overflow-hidden opacity-60" style="background:#393937 !important;" data-slot-id="${slot.id}">
<div class="flex items-center gap-2 px-3 py-2 border-b border-[#444442] bg-[#393937]" style="background:#393937 !important;">
<i class="fas ${slot.icon} w-7 text-center text-[13px] text-[#7d7a74] shrink-0" aria-hidden="true"></i>
<span class="text-[13px] font-semibold text-[#9b978f] truncate min-w-0 flex-1">${slot.label}</span>
</div>
<div class="p-2.5 flex items-center justify-between">
<span class="text-xs text-gray-400 italic"><i class="fas fa-forward text-[9px] mr-1.5"></i>Pominięto</span>
<button type="button" class="planner-unskip text-[11px] font-semibold text-gray-500 hover:text-gray-900 px-2 py-1 rounded-lg hover:bg-gray-100 transition-colors" data-slot-id="${slot.id}">Cofnij</button>
<span class="text-xs text-[#9b978f] italic"><i class="fas fa-forward text-[9px] mr-1.5"></i>Pominięto</span>
<button type="button" class="planner-unskip text-[11px] font-semibold text-[#9b978f] hover:text-[#ddd6ca] px-2 py-1 rounded-lg hover:bg-[#3a3a37] transition-colors" data-slot-id="${slot.id}">Cofnij</button>
</div>
</div>`;
}
const addLabel = entries.length === 0 ? 'Dodaj przepis' : 'Dodaj kolejny';
const addClasses = entries.length === 0
? 'planner-add-meal flex-1 py-2 rounded-lg border border-dashed border-gray-200 text-[13px] font-semibold text-gray-700 hover:bg-gray-50 hover:border-gray-300 transition-colors'
: 'planner-add-meal w-full py-1.5 rounded-lg border border-dashed border-gray-200 text-xs font-semibold text-gray-600 hover:bg-gray-50 hover:border-gray-300 transition-colors';
? 'planner-add-meal flex-1 py-2 rounded-lg border border-dashed border-[#444442] text-[13px] font-semibold text-[#d7d2c8] hover:bg-[#3a3a37] hover:border-[#6d6c67] transition-colors'
: 'planner-add-meal w-full py-1.5 rounded-lg border border-dashed border-[#444442] text-xs font-semibold text-[#d7d2c8] hover:bg-[#3a3a37] hover:border-[#6d6c67] transition-colors';
const skipBtn = entries.length === 0
? `<button type="button" class="planner-skip-meal shrink-0 py-2 px-3 rounded-lg text-[11px] font-semibold text-gray-400 hover:text-gray-600 hover:bg-gray-100 transition-colors" data-slot-id="${slot.id}"><i class="fas fa-forward text-[9px] mr-1"></i>Pomijam</button>`
? `<button type="button" class="planner-skip-meal shrink-0 py-2 px-3 rounded-lg text-[11px] font-semibold text-[#9b978f] hover:text-[#ddd6ca] hover:bg-[#3a3a37] transition-colors" data-slot-id="${slot.id}"><i class="fas fa-forward text-[9px] mr-1"></i>Pomijam</button>`
: '';
const bottomRow = entries.length === 0
@@ -485,12 +455,10 @@ function renderDayContent(state) {
: `<button type="button" class="${addClasses}" data-slot-id="${slot.id}"><i class="fas fa-plus text-[10px] mr-1 opacity-70" aria-hidden="true"></i>${addLabel}</button>`;
return `
<div class="rounded-xl border border-gray-200 bg-white shadow-sm overflow-hidden" data-slot-id="${slot.id}">
<div class="flex items-center gap-2 px-3 py-2 border-b border-gray-100 bg-gray-50/90">
<span class="w-7 h-7 rounded-lg bg-gray-100 flex items-center justify-center text-gray-500 shrink-0">
<i class="fas ${slot.icon} text-[13px]" aria-hidden="true"></i>
</span>
<span class="text-[13px] font-semibold text-gray-900 truncate min-w-0 flex-1">${slot.label}</span>
<div class="rounded-xl bg-[#393937] overflow-hidden" style="background:#393937 !important;" data-slot-id="${slot.id}">
<div class="flex items-center gap-2 px-3 py-2 border-b border-[#444442] bg-[#393937]" style="background:#393937 !important;">
<i class="fas ${slot.icon} w-7 text-center text-[13px] text-[#9b978f] shrink-0" aria-hidden="true"></i>
<span class="text-[13px] font-semibold text-[#ddd6ca] truncate min-w-0 flex-1">${slot.label}</span>
${countLabel}
${kcalBadge}
</div>
@@ -533,18 +501,18 @@ function getRecentRecipeIds(plans, limit = 5) {
function recipeCardHtml(r) {
return `
<button type="button" class="planner-pick-recipe w-full flex gap-2.5 p-2.5 rounded-xl border border-gray-200 bg-gray-50/80 hover:border-gray-900 hover:bg-white text-left transition-all" data-recipe-id="${r.id}">
<div class="w-11 h-11 rounded-lg bg-[#d4d4d4] overflow-hidden shrink-0">
<button type="button" class="planner-pick-recipe w-full flex gap-2.5 p-2.5 rounded-xl border border-[#444442] bg-[#2d2e2b] hover:border-[#6d6c67] hover:bg-[#3a3a37] text-left transition-all" data-recipe-id="${r.id}">
<div class="w-11 h-11 rounded-lg bg-[#3a3a37] overflow-hidden shrink-0">
${r.image
? `<img src="${escapeHtml(r.image)}" alt="" class="w-full h-full object-cover">`
: `<span class="w-full h-full flex items-center justify-center text-white text-[9px] font-medium">${escapeHtml(r.thumbLabel)}</span>`}
</div>
<div class="min-w-0 flex-1 py-0.5">
<p class="text-[13px] font-bold text-gray-900 line-clamp-2">${escapeHtml(r.title)}</p>
<p class="text-[11px] text-gray-500 mt-1 tabular-nums">
<i class="fas fa-fire text-gray-400 mr-0.5" aria-hidden="true"></i>${r.nutritionPerServing.kcal} kcal
<span class="mx-1 text-gray-300">·</span>
<i class="fas fa-clock text-gray-400 mr-0.5" aria-hidden="true"></i>${r.minutes} min
<p class="text-[13px] font-bold text-[#ddd6ca] line-clamp-2">${escapeHtml(r.title)}</p>
<p class="text-[11px] text-[#9b978f] mt-1 tabular-nums">
<i class="fas fa-fire text-[#7d7a74] mr-0.5" aria-hidden="true"></i>${r.nutritionPerServing.kcal} kcal
<span class="mx-1 text-[#6d6c67]">·</span>
<i class="fas fa-clock text-[#7d7a74] mr-0.5" aria-hidden="true"></i>${r.minutes} min
</p>
</div>
</button>`;
@@ -573,11 +541,11 @@ function renderPickerList(slotId, plans, query = '') {
: allRecipes;
if (filtered.length === 0 && q) {
list.innerHTML = '<p class="text-sm text-gray-500 text-center py-6">Brak wyników.</p>';
list.innerHTML = '<p class="text-sm text-[#9b978f] text-center py-6">Brak wyników.</p>';
return;
}
if (filtered.length === 0) {
list.innerHTML = '<p class="text-sm text-gray-500 text-center py-6">Brak dopasowanych przepisów.</p>';
list.innerHTML = '<p class="text-sm text-[#9b978f] text-center py-6">Brak dopasowanych przepisów.</p>';
return;
}
@@ -589,7 +557,7 @@ function renderPickerList(slotId, plans, query = '') {
if (recentInSlot.length > 0) {
html += `<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider px-0.5 pt-1 pb-1"><i class="fas fa-history text-[9px] mr-1"></i>Ostatnio używane</p>`;
html += recentInSlot.map(recipeCardHtml).join('');
html += `<div class="border-t border-gray-100 my-2"></div>`;
html += `<div class="border-t border-[#444442] my-2"></div>`;
html += `<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider px-0.5 pt-1 pb-1">Wszystkie</p>`;
}
}
@@ -659,7 +627,7 @@ function renderIngredientsSheet(state) {
if (subEl) subEl.textContent = 'Porównanie potrzeb z zapasami w spiżarni.';
if (!today || today.items.length === 0) {
body.innerHTML = '<p class="text-sm text-gray-500 text-center py-8">Najpierw zaplanuj posiłki.</p>';
body.innerHTML = '<p class="text-sm text-[#9b978f] text-center py-8">Najpierw zaplanuj posiłki.</p>';
updateIngButtons(state);
return;
}
@@ -669,22 +637,22 @@ function renderIngredientsSheet(state) {
let html = '';
if (shortItems.length === 0) {
html += `<div class="rounded-xl bg-emerald-50 border border-emerald-200/80 p-3 mb-4 flex items-center gap-2.5">
<div class="w-8 h-8 rounded-full bg-emerald-100 flex items-center justify-center shrink-0">
html += `<div class="rounded-xl bg-[#2d2e2b] border border-emerald-400/40 p-3 mb-4 flex items-center gap-2.5">
<div class="w-8 h-8 rounded-full bg-[#24352a] flex items-center justify-center shrink-0">
<i class="fas fa-check text-emerald-600 text-sm"></i>
</div>
<div>
<p class="text-[13px] font-semibold text-emerald-800">Wszystko masz w spiżarni</p>
<p class="text-[13px] font-semibold text-emerald-300">Wszystko masz w spiżarni</p>
<p class="text-[11px] text-emerald-600/80">${today.items.length} ${plIngredientWord(today.items.length)} — zapasy wystarczą</p>
</div>
</div>`;
} else {
html += `<div class="rounded-xl bg-red-50 border border-red-200/80 p-3 mb-4 flex items-center gap-2.5">
<div class="w-8 h-8 rounded-full bg-red-100 flex items-center justify-center shrink-0">
html += `<div class="rounded-xl bg-[#2d2e2b] border border-red-300/40 p-3 mb-4 flex items-center gap-2.5">
<div class="w-8 h-8 rounded-full bg-[#3a2326] flex items-center justify-center shrink-0">
<i class="fas fa-exclamation text-red-500 text-sm"></i>
</div>
<div>
<p class="text-[13px] font-semibold text-red-800">${shortItems.length} ${plIngredientWord(shortItems.length)} do kupienia</p>
<p class="text-[13px] font-semibold text-red-300">${shortItems.length} ${plIngredientWord(shortItems.length)} do kupienia</p>
<p class="text-[11px] text-red-600/80">Brakuje składników na zaplanowane posiłki</p>
</div>
</div>`;
@@ -695,15 +663,15 @@ function renderIngredientsSheet(state) {
<p class="text-[10px] font-bold text-red-400 uppercase tracking-wider mb-2 px-0.5">
<i class="fas fa-cart-shopping text-[9px] mr-1"></i>Do kupienia
</p>
<ul class="border border-red-100/80 rounded-xl overflow-hidden bg-white divide-y divide-red-50">
<ul class="border border-red-300/30 rounded-xl overflow-hidden bg-[#2d2e2b] divide-y divide-[#3a2326]">
${shortItems.map((ing) => `
<li class="flex items-start gap-3 py-3 px-3">
<div class="w-2 h-2 rounded-full bg-red-400 mt-1.5 shrink-0"></div>
<div class="flex-1 min-w-0">
<p class="text-[13px] font-semibold text-gray-900">${escapeHtml(ing.name)}</p>
<p class="text-[11px] text-gray-500 mt-0.5">
potrzeba <span class="font-medium text-gray-700">${formatAmount(ing.amount)} ${escapeHtml(ing.pantryUnit)}</span>
<span class="mx-1 text-gray-300">&middot;</span>
<p class="text-[13px] font-semibold text-[#ddd6ca]">${escapeHtml(ing.name)}</p>
<p class="text-[11px] text-[#9b978f] mt-0.5">
potrzeba <span class="font-medium text-[#d7d2c8]">${formatAmount(ing.amount)} ${escapeHtml(ing.pantryUnit)}</span>
<span class="mx-1 text-[#6d6c67]">&middot;</span>
w spiżarni <span class="font-medium ${ing.pantryQty > 0 ? 'text-amber-600' : 'text-gray-400'}">${ing.pantryQty > 0 ? formatAmount(ing.pantryQty) + ' ' + escapeHtml(ing.pantryUnit) : 'brak'}</span>
</p>
</div>
@@ -721,15 +689,15 @@ function renderIngredientsSheet(state) {
<p class="text-[10px] font-bold text-emerald-500 uppercase tracking-wider mb-2 px-0.5">
<i class="fas fa-check text-[9px] mr-1"></i>W spiżarni
</p>
<ul class="border border-gray-100 rounded-xl overflow-hidden bg-white divide-y divide-gray-50">
<ul class="border border-[#444442] rounded-xl overflow-hidden bg-[#2d2e2b] divide-y divide-[#353632]">
${okItems.map((ing) => `
<li class="flex items-start gap-3 py-2.5 px-3">
<div class="w-2 h-2 rounded-full bg-emerald-400 mt-1.5 shrink-0"></div>
<div class="flex-1 min-w-0">
<p class="text-[13px] font-medium text-gray-700">${escapeHtml(ing.name)}</p>
<p class="text-[11px] text-gray-400 mt-0.5">
potrzeba <span class="font-medium">${formatAmount(ing.amount)} ${escapeHtml(ing.pantryUnit)}</span>
<span class="mx-1 text-gray-300">&middot;</span>
<p class="text-[13px] font-medium text-[#d7d2c8]">${escapeHtml(ing.name)}</p>
<p class="text-[11px] text-[#9b978f] mt-0.5">
potrzeba <span class="font-medium text-[#d7d2c8]">${formatAmount(ing.amount)} ${escapeHtml(ing.pantryUnit)}</span>
<span class="mx-1 text-[#6d6c67]">&middot;</span>
masz <span class="font-medium text-emerald-600">${formatAmount(ing.pantryQty)} ${escapeHtml(ing.pantryUnit)}</span>
</p>
</div>
@@ -748,7 +716,7 @@ function renderIngredientsSheet(state) {
const wd = WEEKDAYS_LONG[day.date.getDay()];
const label = `${wd}, ${day.date.getDate()} ${CALENDAR_MONTHS_SHORT[day.date.getMonth()]}`;
const shorts = day.items.filter((it) => !it.enough);
return `<div class="rounded-xl border border-amber-200/80 bg-amber-50/50 p-3">
return `<div class="rounded-xl border border-amber-200/80 bg-[#2d2e2b] p-3">
<p class="text-[12px] font-semibold text-amber-900">
<i class="fas fa-calendar-day text-[10px] mr-1.5 text-amber-500"></i>${escapeHtml(label)}
</p>
@@ -797,7 +765,6 @@ export function setupMealPlanner() {
monthAnchor: startOfDay(new Date()),
selected: startOfDay(new Date()),
plans,
nutritionExpanded: false,
pickerSlot: null,
};
@@ -808,8 +775,6 @@ export function setupMealPlanner() {
const pickerSheet = document.getElementById('planner-picker-sheet');
const ingBackdrop = document.getElementById('planner-ing-backdrop');
const ingSheet = document.getElementById('planner-ing-sheet');
const copyBackdrop = document.getElementById('planner-copy-backdrop');
const copySheet = document.getElementById('planner-copy-sheet');
const rerender = () => {
syncModeToggle(state.mode);
@@ -883,16 +848,6 @@ export function setupMealPlanner() {
rerender();
});
document.getElementById('planner-toggle-nutrition')?.addEventListener('click', () => {
state.nutritionExpanded = !state.nutritionExpanded;
const details = document.getElementById('planner-nutrition-details');
const chev = document.getElementById('planner-nutrition-chevron');
const btn = document.getElementById('planner-toggle-nutrition');
if (details) details.classList.toggle('hidden', !state.nutritionExpanded);
if (chev) chev.classList.toggle('rotate-180', state.nutritionExpanded);
if (btn) btn.setAttribute('aria-expanded', state.nutritionExpanded ? 'true' : 'false');
});
document.getElementById('planner-meal-slots')?.addEventListener('click', (e) => {
const skipBtn = e.target.closest('.planner-skip-meal');
if (skipBtn) {
@@ -1014,63 +969,6 @@ export function setupMealPlanner() {
closeSheet(ingBackdrop, ingSheet);
});
const closeCopy = () => closeSheet(copyBackdrop, copySheet);
bindPlannerSheetDragClose(copySheet, closeCopy);
copyBackdrop?.addEventListener('click', closeCopy);
document.getElementById('planner-copy-day')?.addEventListener('click', () => {
const srcKey = dateKey(state.selected);
const srcPlan = state.plans[srcKey];
if (!srcPlan || Object.keys(srcPlan).length === 0) {
showPlannerToast('Ten dzień jest pusty — nie ma co kopiować.');
return;
}
const copyList = document.getElementById('planner-copy-list');
if (!copyList) return;
const days = [];
for (let i = -3; i <= 10; i++) {
if (i === 0) continue;
const d = addDays(state.selected, i);
days.push(d);
}
copyList.innerHTML = days.map((d) => {
const wd = WEEKDAYS_LONG[d.getDay()];
const label = `${wd}, ${d.getDate()} ${CALENDAR_MONTHS_SHORT[d.getMonth()]}`;
const hasMeals = dayHasAnyMeal(state.plans, d);
const badge = hasMeals ? '<span class="text-[10px] text-amber-600 font-semibold">ma posiłki</span>' : '';
return `<button type="button" class="planner-copy-target w-full flex items-center justify-between gap-2 p-3 rounded-xl border border-gray-200 bg-gray-50/80 hover:border-gray-900 hover:bg-white transition-all text-left" data-target-ts="${d.getTime()}">
<span class="text-[13px] font-semibold text-gray-900">${escapeHtml(label)}</span>
${badge}
</button>`;
}).join('');
openSheet(copyBackdrop, copySheet);
});
document.getElementById('planner-copy-list')?.addEventListener('click', (e) => {
const btn = e.target.closest('.planner-copy-target');
if (!btn) return;
const targetDate = new Date(Number(btn.getAttribute('data-target-ts')));
const srcKey = dateKey(state.selected);
const tgtKey = dateKey(targetDate);
const srcPlan = state.plans[srcKey];
if (!srcPlan) return;
const copy = {};
for (const [slotId, entries] of Object.entries(srcPlan)) {
if (slotId === '_skipped') {
copy._skipped = { ...entries };
continue;
}
if (Array.isArray(entries)) {
copy[slotId] = entries.map((e) => ({ ...e, id: newPlanEntryId() }));
}
}
state.plans[tgtKey] = copy;
closeCopy();
persist();
showPlannerToast('Plan skopiowany!');
});
ingSheet?.addEventListener('click', (e) => {
const row = e.target.closest('.planner-ing-row');
if (!row || !ingSheet.contains(row)) return;