Compare commits

..

7 Commits

Author SHA1 Message Date
422b29e3f0 Update style
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m16s
2026-04-06 22:32:46 +02:00
c341e2e813 Add shadows to recipe detail tabs 2026-04-06 20:32:54 +02:00
a29b01e413 Add shadow to recipe cards 2026-04-06 18:53:40 +02:00
8669256ca2 Restyle menu 2026-04-06 18:51:03 +02:00
3c271e1632 Extract menu to a separate component 2026-04-06 18:41:23 +02:00
3258965bcc Restyle menu 2026-04-06 18:36:10 +02:00
7cf7aef6e4 Filter popup improvement 2026-04-06 18:12:48 +02:00
8 changed files with 269 additions and 193 deletions

View File

@@ -7,8 +7,11 @@
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Recipe">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Recipe App - Modular</title>
<link rel="manifest" href="./manifest.webmanifest?v=20260406-42">
<link rel="manifest" href="./manifest.webmanifest?v=20260406-63">
<link rel="icon" type="image/png" sizes="192x192" href="./icons/icon-192.png">
<link rel="apple-touch-icon" href="./icons/apple-touch-icon.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
@@ -384,40 +387,30 @@
}
#app-bottom-nav .bottom-dock {
position: relative;
z-index: 0;
isolation: isolate;
width: min(calc(100% - 2rem), 22.4rem);
min-height: 3.7rem;
box-sizing: border-box;
width: min(calc(100% - 3.6rem), 20.9rem);
height: 3.34rem;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
align-items: center;
gap: 0.1rem;
padding: 0.35rem 0.45rem;
border-radius: 999px;
align-items: stretch;
gap: 0.06rem;
padding: 0.22rem;
border-radius: 1.68rem;
background: #393937;
border: 1px solid #41423f;
box-shadow:
inset 0 1px 8px rgba(0, 0, 0, 0.15),
0 5px 10px rgba(0, 0, 0, 0.16),
0 14px 22px rgba(0, 0, 0, 0.24),
0 22px 34px rgba(0, 0, 0, 0.18),
inset 0 1px 0 rgba(255, 255, 255, 0.04);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
0 22px 34px rgba(0, 0, 0, 0.18);
pointer-events: auto;
}
#app-bottom-nav .bottom-dock::after {
content: '';
position: absolute;
left: 11%;
right: 11%;
bottom: -0.72rem;
height: 1.05rem;
border-radius: 999px;
background: rgba(0, 0, 0, 0.36);
filter: blur(12px);
opacity: 0.9;
z-index: -1;
pointer-events: none;
#app-bottom-nav .nav-slot {
min-width: 0;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
#app-bottom-nav .nav-tab,
#app-bottom-nav .nav-action {
@@ -426,15 +419,15 @@
border: 0;
background: transparent !important;
box-shadow: none !important;
width: 2.55rem;
height: 2.55rem;
width: 2.18rem;
height: 2.18rem;
margin: 0;
padding: 0;
border-radius: 999px;
border-radius: 1.35rem;
display: inline-flex;
align-items: center;
justify-content: center;
justify-self: center;
flex: 0 0 auto;
color: #ece8e0;
cursor: pointer;
transition:
@@ -455,13 +448,12 @@
background: rgba(255, 255, 255, 0.04) !important;
}
#app-bottom-nav .nav-tab.is-active {
width: 2.95rem;
height: 2.95rem;
width: 100%;
height: 100%;
color: #fff;
background: #2d2e2b !important;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.04),
0 10px 20px rgba(0, 0, 0, 0.24) !important;
border-radius: 1.46rem;
box-shadow: none !important;
}
#app-bottom-nav .nav-tab:active,
#app-bottom-nav .nav-action:active {
@@ -478,18 +470,22 @@
padding-inline: 0.7rem;
}
#app-bottom-nav .bottom-dock {
width: min(calc(100% - 1.4rem), 21.6rem);
min-height: 3.45rem;
padding-inline: 0.35rem;
width: min(calc(100% - 2.4rem), 19.7rem);
height: 3.12rem;
gap: 0.05rem;
padding: 0.2rem;
border-radius: 1.56rem;
}
#app-bottom-nav .nav-tab,
#app-bottom-nav .nav-action {
width: 2.35rem;
height: 2.35rem;
width: 2.02rem;
height: 2.02rem;
border-radius: 1.28rem;
}
#app-bottom-nav .nav-tab.is-active {
width: 2.75rem;
height: 2.75rem;
height: 100%;
width: 100%;
border-radius: 1.36rem;
}
}
@@ -528,7 +524,7 @@
</div>
<script>
const APP_ASSET_VERSION = '20260406-42';
const APP_ASSET_VERSION = '20260406-63';
const APP_VERSION_STORAGE_KEY = 'recipe-app-asset-version';
const APP_VERSION_QUERY_KEY = 'appv';
@@ -562,7 +558,7 @@
})();
</script>
<script type="module">
const appVersion = window.__APP_ASSET_VERSION__ || '20260406-42';
const appVersion = window.__APP_ASSET_VERSION__ || '20260406-63';
const recoveryKey = `recipe-app-recovery-${appVersion}`;
function renderBootstrapError(message) {

View File

@@ -15,6 +15,8 @@ let refreshPantry;
let setupPantry;
let getMealPlanEditorHTML;
let setupMealPlanEditor;
let getBottomNavHTML;
let setupBottomNav;
const moduleLoadPromise = Promise.all([
import(`./views/RecipeList.js?v=${APP_ASSET_VERSION}`),
@@ -23,6 +25,7 @@ const moduleLoadPromise = Promise.all([
import(`./views/MealPlanner.js?v=${APP_ASSET_VERSION}`),
import(`./views/Pantry.js?v=${APP_ASSET_VERSION}`),
import(`./ui/mealPlanEditor.js?v=${APP_ASSET_VERSION}`),
import(`./ui/bottomNav.js?v=${APP_ASSET_VERSION}`),
]).then(([
recipeListModule,
filterModule,
@@ -30,6 +33,7 @@ const moduleLoadPromise = Promise.all([
mealPlannerModule,
pantryModule,
mealPlanEditorModule,
bottomNavModule,
]) => {
({ getRecipeListHTML, setupRecipeList } = recipeListModule);
({ getFilterHTML, setupFilter } = filterModule);
@@ -37,6 +41,7 @@ const moduleLoadPromise = Promise.all([
({ getMealPlannerHTML, setupMealPlanner } = mealPlannerModule);
({ getPantryHTML, refreshPantry, setupPantry } = pantryModule);
({ getMealPlanEditorHTML, setupMealPlanEditor } = mealPlanEditorModule);
({ getBottomNavHTML, setupBottomNav } = bottomNavModule);
});
function getAppToastHTML() {
@@ -47,92 +52,6 @@ function getAppToastHTML() {
`;
}
function getBottomNavHTML() {
const isDark = document.documentElement.classList.contains('dark');
return `
<nav id="app-bottom-nav" aria-label="Główna nawigacja">
<div class="bottom-dock">
<button type="button" data-tab="recipes" id="nav-recipes" class="nav-tab is-active" aria-label="Przepisy" aria-current="page">
<i class="fas fa-book" aria-hidden="true"></i>
</button>
<button type="button" data-tab="planner" id="nav-planner" class="nav-tab" aria-label="Planer">
<i class="far fa-calendar-alt" aria-hidden="true"></i>
</button>
<button type="button" data-tab="pantry" id="nav-pantry" class="nav-tab" aria-label="Spiżarnia">
<i class="fas fa-warehouse" aria-hidden="true"></i>
</button>
<button type="button" id="nav-theme-toggle" class="nav-action" aria-label="${isDark ? 'Włącz jasny motyw' : 'Włącz ciemny motyw'}" title="${isDark ? 'Jasny motyw' : 'Ciemny motyw'}">
<i class="${isDark ? 'fas fa-sun' : 'fas fa-moon'}" aria-hidden="true"></i>
</button>
</div>
</nav>
`;
}
function syncThemeToggleButton(btn, isDark) {
if (!btn) return;
const icon = btn.querySelector('i');
if (icon) icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
btn.setAttribute('aria-label', isDark ? 'Włącz jasny motyw' : 'Włącz ciemny motyw');
btn.title = isDark ? 'Jasny motyw' : 'Ciemny motyw';
}
function setupThemeToggle() {
const btn = document.getElementById('nav-theme-toggle');
if (!btn) return;
btn.addEventListener('click', () => {
const html = document.documentElement;
const isDark = html.classList.toggle('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
syncThemeToggleButton(btn, isDark);
const meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute('content', isDark ? '#161513' : '#f3efe9');
});
}
function setupTabs() {
const main = document.getElementById('main-view');
const planner = document.getElementById('planner-view');
const pantry = document.getElementById('pantry-view');
const nav = document.getElementById('app-bottom-nav');
if (!main || !planner || !pantry || !nav) return;
const apply = (tab) => {
main.classList.toggle('hidden', tab !== 'recipes');
planner.classList.toggle('hidden', tab !== 'planner');
pantry.classList.toggle('hidden', tab !== 'pantry');
if (tab === 'pantry') refreshPantry();
nav.querySelectorAll('.nav-tab[data-tab]').forEach((btn) => {
const id = btn.getAttribute('data-tab');
if (btn.hasAttribute('disabled')) return;
if (id === 'recipes' || id === 'planner' || id === 'pantry') {
const isActive = id === tab;
btn.classList.toggle('is-active', isActive);
if (isActive) btn.setAttribute('aria-current', 'page');
else btn.removeAttribute('aria-current');
}
});
};
nav.addEventListener('click', (e) => {
const btn = e.target.closest('.nav-tab[data-tab]');
if (!btn || btn.hasAttribute('disabled')) return;
const tab = btn.getAttribute('data-tab');
if (tab === 'recipes' || tab === 'planner' || tab === 'pantry') apply(tab);
});
apply('recipes');
window.refreshStockViews = () => {
refreshPantry();
};
}
let initAppPromise = null;
function renderAppBootError(message) {
@@ -170,8 +89,7 @@ async function initApp() {
${getAppToastHTML()}
`;
setupTabs();
setupThemeToggle();
setupBottomNav({ refreshPantry });
setupRecipeList();
setupMealPlanner();
setupPantry();

94
js/ui/bottomNav.js Normal file
View File

@@ -0,0 +1,94 @@
function syncThemeToggleButton(btn, isDark) {
if (!btn) return;
const icon = btn.querySelector('i');
if (icon) icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
btn.setAttribute('aria-label', isDark ? 'Włącz jasny motyw' : 'Włącz ciemny motyw');
btn.title = isDark ? 'Jasny motyw' : 'Ciemny motyw';
}
function setupThemeToggle() {
const btn = document.getElementById('nav-theme-toggle');
if (!btn) return;
btn.addEventListener('click', () => {
const html = document.documentElement;
const isDark = html.classList.toggle('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
syncThemeToggleButton(btn, isDark);
const meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute('content', isDark ? '#161513' : '#f3efe9');
});
}
export function getBottomNavHTML() {
const isDark = document.documentElement.classList.contains('dark');
return `
<nav id="app-bottom-nav" aria-label="Główna nawigacja">
<div class="bottom-dock">
<div class="nav-slot">
<button type="button" data-tab="planner" id="nav-planner" class="nav-tab is-active" aria-label="Planer" aria-current="page">
<i class="far fa-calendar-alt" aria-hidden="true"></i>
</button>
</div>
<div class="nav-slot">
<button type="button" data-tab="recipes" id="nav-recipes" class="nav-tab" aria-label="Przepisy">
<i class="fas fa-search" aria-hidden="true"></i>
</button>
</div>
<div class="nav-slot">
<button type="button" data-tab="pantry" id="nav-pantry" class="nav-tab" aria-label="Spiżarnia">
<i class="fas fa-warehouse" aria-hidden="true"></i>
</button>
</div>
<div class="nav-slot">
<button type="button" id="nav-theme-toggle" class="nav-action" aria-label="${isDark ? 'Włącz jasny motyw' : 'Włącz ciemny motyw'}" title="${isDark ? 'Jasny motyw' : 'Ciemny motyw'}">
<i class="${isDark ? 'fas fa-sun' : 'fas fa-moon'}" aria-hidden="true"></i>
</button>
</div>
</div>
</nav>
`;
}
export function setupBottomNav({ refreshPantry } = {}) {
const main = document.getElementById('main-view');
const planner = document.getElementById('planner-view');
const pantry = document.getElementById('pantry-view');
const nav = document.getElementById('app-bottom-nav');
if (!main || !planner || !pantry || !nav) return;
const apply = (tab) => {
main.classList.toggle('hidden', tab !== 'recipes');
planner.classList.toggle('hidden', tab !== 'planner');
pantry.classList.toggle('hidden', tab !== 'pantry');
if (tab === 'pantry' && typeof refreshPantry === 'function') refreshPantry();
nav.querySelectorAll('.nav-tab[data-tab]').forEach((btn) => {
const id = btn.getAttribute('data-tab');
if (btn.hasAttribute('disabled')) return;
if (id === 'recipes' || id === 'planner' || id === 'pantry') {
const isActive = id === tab;
btn.classList.toggle('is-active', isActive);
if (isActive) btn.setAttribute('aria-current', 'page');
else btn.removeAttribute('aria-current');
}
});
};
nav.addEventListener('click', (e) => {
const btn = e.target.closest('.nav-tab[data-tab]');
if (!btn || btn.hasAttribute('disabled')) return;
const tab = btn.getAttribute('data-tab');
if (tab === 'recipes' || tab === 'planner' || tab === 'pantry') apply(tab);
});
setupThemeToggle();
apply('planner');
window.refreshStockViews = () => {
if (typeof refreshPantry === 'function') refreshPantry();
};
}

View File

@@ -46,9 +46,6 @@ export function getMealPlanEditorHTML() {
<h2 id="mpe-title" class="text-[15px] font-bold text-gray-900 leading-tight"></h2>
<p id="mpe-subtitle" class="text-[11px] text-gray-500 mt-0.5 truncate"></p>
</div>
<button id="mpe-close-btn" type="button" class="shrink-0 w-8 h-8 rounded-full border border-gray-200 flex items-center justify-center text-gray-400 hover:text-gray-900 hover:bg-gray-50 transition-colors ml-3">
<i class="fas fa-times text-xs"></i>
</button>
</div>
</div>
<div id="mpe-cal-wrap" class="hidden relative z-[1] shrink-0 px-5 pt-3 pb-3 bg-[#2d2e2b]" style="background:#2d2e2b !important; background-image:none !important;">
@@ -74,17 +71,16 @@ export function getMealPlanEditorHTML() {
</div>
<div id="mpe-top-shadow" class="pointer-events-none absolute inset-x-0 -bottom-3 h-3 opacity-0 transition-opacity duration-200" style="background:linear-gradient(to bottom, rgba(0,0,0,0.12), rgba(0,0,0,0.03), rgba(0,0,0,0));"></div>
</div>
<div id="mpe-ing-scroll" class="flex-1 min-h-0 overflow-y-auto no-scrollbar px-5 pb-3 bg-[#2d2e2b]" style="background:#2d2e2b !important; background-image:none !important;">
<div id="mpe-ing-scroll" class="flex-1 min-h-0 overflow-y-auto no-scrollbar px-5 pb-16 bg-[#2d2e2b]" style="background:#2d2e2b !important; background-image:none !important;">
<div id="mpe-ing-section" class="mb-4">
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Składniki</p>
<div id="mpe-ing-list" class="space-y-1.5"></div>
<div id="mpe-add-area" class="mt-2"></div>
</div>
</div>
<div id="mpe-footer-wrap" class="relative z-[1] shrink-0 px-5 pt-2 pb-3 flex justify-center bg-[#2d2e2b]" style="background:#2d2e2b !important; background-image:none !important; padding-bottom:calc(1.1rem + env(safe-area-inset-bottom));">
<div id="mpe-footer-shadow" class="pointer-events-none absolute inset-x-0 -top-3 h-3 opacity-0 transition-opacity duration-200" style="background:linear-gradient(to top, rgba(0,0,0,0.12), rgba(0,0,0,0.03), rgba(0,0,0,0));"></div>
<button id="mpe-confirm-btn" type="button" class="border text-white px-4 py-3 rounded-xl font-semibold text-[13px] transition-colors inline-flex items-center justify-center gap-2" style="background:#2d2e2b !important; background-image:none !important; box-shadow:none !important; border-color:#444442 !important;">
<i class="fas fa-check text-xs"></i> <span id="mpe-confirm-label">Dodaj do planu</span>
<div id="mpe-footer-wrap" class="absolute bottom-0 left-0 right-0 z-[2] px-5 pb-3 flex justify-center" style="pointer-events:none; padding-bottom:calc(1.1rem + env(safe-area-inset-bottom));">
<button id="mpe-confirm-btn" type="button" class="border text-white px-6 py-3 rounded-full font-semibold text-[13px] transition-colors inline-flex items-center justify-center gap-2" style="pointer-events:auto; background:#2d2e2b !important; background-image:none !important; border-color:#444442 !important; box-shadow:0 4px 16px rgba(0,0,0,0.4), 0 1px 4px rgba(0,0,0,0.25);">
<span id="mpe-confirm-label">Dodaj do planu</span>
</button>
</div>
</div>
@@ -247,8 +243,8 @@ export function setupMealPlanEditor() {
el.innerHTML = `
<div class="h-full rounded-xl pb-2 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">Porcje</p>
<div class="flex items-center pt-2">
<div class="flex h-[2rem] w-full items-center gap-0.5 rounded-full border px-0.5" style="background:#2f2f2d;border-color:#444442;">
<div class="flex items-start">
<div class="flex h-[2rem] w-full items-center gap-0.5 rounded-full border px-0.5" style="background:#2f2f2d;border-color:#444442;box-shadow:0 2px 8px rgba(0,0,0,0.25);">
<button type="button" id="mpe-serv-minus" class="shrink-0 w-7 h-full flex items-center justify-center rounded-full border-0 bg-transparent text-[#d7d2c8] transition-colors" aria-label="Zmniejsz liczbę porcji">
<i class="fas fa-minus text-[10px]"></i>
</button>
@@ -287,7 +283,7 @@ export function setupMealPlanEditor() {
const disp = base * S.servings;
const modified = id in S.overrides;
const rowStyle = 'background:#393937 !important; background-image:none !important; box-shadow:none !important; border:none !important;';
const rowStyle = 'background:#393937 !important; background-image:none !important; box-shadow:0 2px 8px rgba(0,0,0,0.25) !important; border:none !important;';
const shuffleBtn = hasAlts
? `<button type="button" class="mpe-shuffle shrink-0 w-5 h-5 flex items-center justify-center transition-colors text-gray-400 hover:text-gray-300" style="background:transparent !important; box-shadow:none !important;" data-orig-id="${esc(id)}" aria-label="Wybierz zamiennik składnika"><i class="fas fa-shuffle text-[10px]"></i></button>`
@@ -332,7 +328,7 @@ export function setupMealPlanEditor() {
const def = INGREDIENTS[a.ingredientId];
const name = def?.name || a.ingredientId;
const disp = a.amount * S.servings;
html += `<div class="mpe-ing-row rounded-xl p-2.5" style="background:#393937 !important; background-image:none !important; box-shadow:none !important; border:none !important;" data-ing-id="${esc(a.ingredientId)}" data-type="added">`;
html += `<div class="mpe-ing-row rounded-xl p-2.5" style="background:#393937 !important; background-image:none !important; box-shadow:0 2px 8px rgba(0,0,0,0.25) !important; border:none !important;" data-ing-id="${esc(a.ingredientId)}" data-type="added">`;
html += `<div class="flex items-center gap-2">`;
html += `<div class="flex-1 min-w-0 flex items-center gap-1.5"><span class="text-[12px] font-semibold text-gray-900 truncate">${esc(name)}</span><span class="shrink-0 inline-flex items-center justify-center text-[#8f8b84]" title="Dodany składnik" aria-label="Dodany składnik"><i class="fas fa-plus text-[8px]"></i></span></div>`;
html += `<button type="button" class="mpe-edit-amt shrink-0 flex items-center gap-1 px-2 py-1 rounded-lg hover:bg-gray-100 transition-colors" data-ing-id="${esc(a.ingredientId)}" data-type="added">`;
@@ -374,7 +370,7 @@ export function setupMealPlanEditor() {
<input type="text" id="mpe-add-search" class="flex-1 rounded-lg px-3 py-1.5 text-[12px] outline-none placeholder:text-[#8f8b84]" style="background:#2f2f2d !important; border:1px solid #444442; color:#ddd6ca;" placeholder="Szukaj składnika…" value="${esc(S.addQuery)}">
<button type="button" id="mpe-add-cancel" class="text-[11px] font-semibold px-2 py-1 transition-colors" style="color:#9b978f;">Anuluj</button>
</div>
<div class="max-h-40 overflow-y-auto space-y-0.5 no-scrollbar" id="mpe-add-results">
<div class="max-h-40 overflow-y-auto space-y-1 no-scrollbar" id="mpe-add-results">
${avail.length === 0
? '<p class="rounded-lg px-2.5 py-3 text-[11px] text-center" style="background:#2f2f2d !important; color:#9b978f;">Brak wyników</p>'
: avail.slice(0, 20).map((i) => `<button type="button" class="mpe-add-pick w-full text-left px-2.5 py-2 rounded-lg transition-colors text-[12px] font-medium" style="background:#2f2f2d !important; color:#ddd6ca;" data-ing-id="${esc(i.id)}">${esc(i.name)} <span class="text-[10px]" style="color:#9b978f;">${esc(i.category)}</span></button>`).join('')}
@@ -413,7 +409,7 @@ export function setupMealPlanEditor() {
<div class="h-full pb-2 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="rounded-xl border px-3 py-2" style="border-color:#444442 !important;">
<div class="rounded-xl border px-3 py-2" style="background:#2f2f2d !important; border-color:#444442 !important; box-shadow:0 2px 8px rgba(0,0,0,0.25);">
<div class="grid grid-flow-col auto-cols-max gap-3 text-left">
<div><span class="block text-[15px] font-semibold text-[#ddd6ca] tabular-nums leading-none">${n.kcal}</span><span class="text-[9px] text-gray-500">kcal</span></div>
<div><span class="block text-[15px] font-semibold text-[#ddd6ca] tabular-nums leading-none">${n.protein}<span class="ml-0.5 text-[12px] font-medium text-[#9b978f]">g</span></span><span class="text-[9px] text-gray-500">Białko</span></div>
@@ -542,14 +538,39 @@ export function setupMealPlanEditor() {
savePlans(plans);
closeEditor();
showAppToast(S.mode === 'edit' ? 'Zapisano zmiany!' : 'Dodano do planera!');
window.refreshPlanner?.();
}
/* ── Event bindings ───────────────────────────── */
document.getElementById('mpe-close-btn')?.addEventListener('click', closeEditor);
overlay?.addEventListener('click', (e) => { if (e.target === overlay) closeEditor(); });
/* ── Swipe-to-dismiss ────────────────────────── */
const header = sheet?.children[0];
if (header && sheet) {
let startY = 0, currentY = 0, dragging = false;
header.addEventListener('touchstart', (e) => {
startY = e.touches[0].clientY;
currentY = 0;
dragging = true;
sheet.style.transition = 'none';
}, { passive: true });
header.addEventListener('touchmove', (e) => {
if (!dragging) return;
currentY = Math.max(0, e.touches[0].clientY - startY);
sheet.style.transform = `translateY(${currentY}px)`;
}, { passive: true });
header.addEventListener('touchend', () => {
if (!dragging) return;
dragging = false;
sheet.style.transition = 'transform 300ms cubic-bezier(0.32,0.72,0,1)';
if (currentY > 120) {
closeEditor();
} else {
sheet.style.transform = 'translateY(0)';
}
});
}
document.getElementById('mpe-confirm-btn')?.addEventListener('click', handleConfirm);
bindCalendarDayClicks(document.getElementById('mpe-cal-grid'), (date) => {
S.date = date;

View File

@@ -491,7 +491,6 @@ export function setupFilter() {
renderSlotChips();
renderTagChips();
syncLiveFilters();
showFilterPanel();
};

View File

@@ -60,7 +60,7 @@ 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-[#2d2e2b] border-b border-[#444442] mt-3">
<div id="planner-cal-bar" class="shrink-0 bg-[#2d2e2b] border-b border-[#444442] mt-3 relative z-10">
${createCalendarTopbarHTML({
titleId: 'cal-period-label',
prevId: 'cal-prev',
@@ -88,7 +88,7 @@ export function getMealPlannerHTML() {
<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="w-full rounded-xl border px-3 py-2.5" style="background:#2f2f2d !important; border-color:#444442 !important; box-shadow:0 2px 8px rgba(0,0,0,0.25);">
<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>
@@ -801,6 +801,18 @@ export function setupMealPlanner() {
rerender();
};
/* ── calendar scroll shadow ─────────────────── */
const plannerScroll = document.getElementById('planner-scroll');
const calBar = document.getElementById('planner-cal-bar');
if (plannerScroll && calBar) {
const shadow = document.createElement('div');
shadow.style.cssText = 'position:absolute;left:0;right:0;bottom:-8px;height:8px;background:linear-gradient(to bottom,rgba(0,0,0,0.25),transparent);opacity:0;transition:opacity 0.2s;pointer-events:none;';
calBar.appendChild(shadow);
plannerScroll.addEventListener('scroll', () => {
shadow.style.opacity = plannerScroll.scrollTop > 2 ? '1' : '0';
});
}
bindCalendarDayClicks(weekGrid, (date) => {
state.selected = date;
rerender();

View File

@@ -43,6 +43,21 @@ function setTabButtonState(btn, active) {
export function getRecipeDetailHTML() {
return `
<style>
#rd-tab-bar::after {
content: '';
position: absolute;
left: 0; right: 0; bottom: -8px;
height: 8px;
background: linear-gradient(to bottom, rgba(0,0,0,0.25), transparent);
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
#rd-tab-bar.rd-scrolled::after {
opacity: 1;
}
</style>
<div id="recipe-detail-view" class="absolute inset-0 bg-[#2d2e2b] z-30 transition-all duration-300 ease-in-out translate-x-full opacity-0 pointer-events-none flex flex-col overflow-hidden" style="background:#2d2e2b !important; background-image:none !important;">
<div class="absolute top-0 w-full p-3.5 flex justify-between z-40 mt-3">
<button onclick="closeRecipeDetail()" class="w-9 h-9 rounded-full border flex items-center justify-center transition-opacity opacity-95 hover:opacity-100" style="background:rgba(47,47,45,0.92) !important; backdrop-filter:none !important; border-color:#444442 !important; color:#ddd6ca !important;">
@@ -59,7 +74,7 @@ export function getRecipeDetailHTML() {
<span id="rd-hero-label" class="absolute inset-0 z-10 flex items-center justify-center font-medium text-[15px]" style="color:#ddd6ca;"></span>
</div>
<div class="bg-[#2d2e2b] rounded-t-3xl -mt-6 relative z-30 pt-6 flex flex-col flex-1 overflow-hidden" style="background:#2d2e2b !important; background-image:none !important;">
<div class="bg-[#2d2e2b] rounded-t-3xl -mt-6 relative z-30 pt-6 flex flex-col flex-1 overflow-hidden" style="background:#2d2e2b !important; background-image:none !important; box-shadow:0 -8px 20px rgba(0,0,0,0.35) !important;">
<div class="mb-3 px-5 shrink-0">
<div class="flex justify-between items-start mb-2.5">
<h1 id="rd-title" class="text-xl font-bold leading-tight" style="color:#ddd6ca;"></h1>
@@ -70,14 +85,10 @@ export function getRecipeDetailHTML() {
<i class="fas fa-clock text-[10px]" style="color:#9b978f;"></i>
<span id="rd-time"></span>
</div>
<div class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full border" style="background:#2f2f2d; border-color:#444442; color:#d7d2c8;">
<i class="fas fa-fire text-[10px]" style="color:#9b978f;"></i>
<span id="rd-kcal" class="tabular-nums"></span>
</div>
</div>
</div>
<div class="flex border-b mb-2 px-5 shrink-0" style="border-color:#444442;">
<div id="rd-tab-bar" class="flex border-b mb-2 px-5 shrink-0 relative z-10" style="border-color:#444442;">
<button class="flex-1 pb-2.5 text-[13px] border-b-2 rd-tab-btn" data-rd-tab="ingredients" style="color:#ddd6ca; border-bottom-color:#787876; font-weight:600;">Składniki</button>
<button class="flex-1 pb-2.5 text-[13px] border-b-2 rd-tab-btn" data-rd-tab="steps" style="color:#9b978f; border-bottom-color:transparent; font-weight:500;">Kroki</button>
</div>
@@ -127,7 +138,6 @@ function populateDetail(recipeId) {
}
document.getElementById('rd-title').textContent = recipe.title;
document.getElementById('rd-time').textContent = `${recipe.minutes} min`;
updateKcalDisplay();
const tagsHtml = [];
for (const slotId of recipe.allowedSlots) {
@@ -155,15 +165,6 @@ function populateDetail(recipeId) {
/* ── helpers ───────────────────────────────────────────── */
function updateKcalDisplay() {
const el = document.getElementById('rd-kcal');
if (!el) return;
const recipe = RECIPES[currentRecipeId];
if (!recipe) return;
const kcal = Math.round(recipe.nutritionPerServing.kcal * currentServings);
el.textContent = `${kcal} kcal`;
}
function nutritionForAmount(ingredientId, amount, unit) {
const def = INGREDIENTS[ingredientId];
if (!def?.nutritionPer100g) return null;
@@ -216,7 +217,7 @@ function renderNutritionSummary(recipe) {
<div class="h-full pb-2 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="rounded-xl border px-3 py-2" style="border-color:#444442 !important;">
<div class="rounded-xl border px-3 py-2" style="background:#2f2f2d !important; border-color:#444442 !important; box-shadow:0 2px 8px rgba(0,0,0,0.25);">
<div class="grid grid-flow-col auto-cols-max gap-3 text-left">
<div><span class="block text-[15px] font-semibold text-[#ddd6ca] tabular-nums leading-none">${total.kcal}</span><span class="text-[9px] text-gray-500">kcal</span></div>
<div><span class="block text-[15px] font-semibold text-[#ddd6ca] tabular-nums leading-none">${total.protein}<span class="ml-0.5 text-[12px] font-medium text-[#9b978f]">g</span></span><span class="text-[9px] text-gray-500">Białko</span></div>
@@ -230,8 +231,8 @@ function renderNutritionSummary(recipe) {
<div class="shrink-0 w-[5.25rem]">
<div class="h-full rounded-xl pb-2 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">Porcje</p>
<div class="flex items-center pt-2">
<div class="flex h-[2rem] w-full items-center gap-0.5 rounded-full border px-0.5" style="background:#2f2f2d;border-color:#444442;">
<div class="flex items-start">
<div class="flex h-[2rem] w-full items-center gap-0.5 rounded-full border px-0.5" style="background:#2f2f2d;border-color:#444442;box-shadow:0 2px 8px rgba(0,0,0,0.25);">
<button type="button" id="rd-serv-minus" class="shrink-0 w-7 h-full flex items-center justify-center rounded-full border-0 bg-transparent text-[#d7d2c8] transition-colors" aria-label="Zmniejsz liczbę porcji">
<i class="fas fa-minus text-[10px]"></i>
</button>
@@ -259,7 +260,7 @@ function renderIngredients(recipe) {
const effectiveName = effectiveDef?.name || effectiveId;
const scaledAmount = ing.amount * currentServings;
const isExpanded = expandedAlternatives.has(origId);
const rowStyle = 'background:#393937 !important; background-image:none !important; box-shadow:none !important; border:none !important;';
const rowStyle = 'background:#393937 !important; background-image:none !important; box-shadow:0 2px 8px rgba(0,0,0,0.25) !important; border:none !important;';
const toggleBtn = hasAlts
? `<button type="button" class="rd-alt-toggle shrink-0 w-5 h-5 flex items-center justify-center transition-colors text-gray-400 hover:text-gray-300" style="background:transparent !important; box-shadow:none !important;" data-original-id="${escapeHtml(origId)}" aria-label="Wybierz zamiennik składnika"><i class="fas fa-shuffle text-[10px]"></i></button>`
@@ -315,14 +316,12 @@ function renderIngredients(recipe) {
if (currentServings <= 1) return;
currentServings--;
renderIngredients(recipe);
updateKcalDisplay();
});
container.querySelector('#rd-serv-plus')?.addEventListener('click', () => {
if (currentServings >= 12) return;
currentServings++;
renderIngredients(recipe);
updateKcalDisplay();
});
container.querySelectorAll('.rd-alt-toggle').forEach((btn) => {
@@ -369,8 +368,8 @@ function renderSteps(recipe) {
container.innerHTML = `
<div class="space-y-2 pb-5">
${steps.map((step, i) => `
<div class="rounded-xl p-3 flex gap-3" style="${forceBg(RD_THEME.surface)} border:none !important;">
<div class="w-6 h-6 rounded-full flex items-center justify-center text-[11px] font-bold shrink-0" style="${forceBgBorder(RD_THEME.surfaceActive, RD_THEME.borderSoft)} color:${RD_THEME.textSecondary} !important;">${i + 1}</div>
<div class="rounded-xl p-3 flex gap-3" style="background:transparent !important; background-image:none !important; box-shadow:none !important; border:none !important;">
<div class="w-6 h-6 rounded-full flex items-center justify-center text-[11px] font-bold shrink-0" style="background:transparent !important; border:none !important; box-shadow:none !important; color:${RD_THEME.textSecondary} !important;">${i + 1}</div>
<div class="pt-0.5"><p class="text-[13px] leading-relaxed" style="color:${RD_THEME.textSecondary};">${escapeHtml(step)}</p></div>
</div>`).join('')}
</div>`;
@@ -400,6 +399,16 @@ export function setupRecipeDetail() {
});
});
/* ── tab-bar scroll shadow ─────────────────── */
const scrollContainer = document.querySelector('#recipe-detail-view .overflow-y-auto');
const tabBar = document.getElementById('rd-tab-bar');
if (scrollContainer && tabBar) {
scrollContainer.addEventListener('scroll', () => {
tabBar.classList.toggle('rd-scrolled', scrollContainer.scrollTop > 2);
});
}
/* ── planner — delegate to MealPlanEditor ─────── */
document.getElementById('rd-add-to-planner-btn')?.addEventListener('click', () => {

View File

@@ -60,7 +60,7 @@ function getFilteredRecipes() {
function renderRecipeCard(recipe) {
const labels = slotLabelsFor(recipe);
return `
<div onclick="openRecipeDetail('${escapeHtml(recipe.id)}')" class="rounded-xl overflow-hidden flex flex-col bg-[#393937] cursor-pointer transition-shadow" style="background:#393937 !important; border:none !important; box-shadow:none !important;">
<div data-recipe-id="${escapeHtml(recipe.id)}" onclick="openRecipeDetail('${escapeHtml(recipe.id)}')" class="recipe-card rounded-xl overflow-hidden flex flex-col bg-[#393937] cursor-pointer transition-shadow" style="background:#393937 !important; border:none !important; box-shadow:0 2px 8px rgba(0,0,0,0.28) !important;">
<div class="h-32 bg-[#d4d4d4] relative overflow-hidden">
${recipe.image
? `<img src="${escapeHtml(recipe.image)}" alt="${escapeHtml(recipe.title)}" class="w-full h-full object-cover">`
@@ -81,6 +81,17 @@ function renderRecipeCard(recipe) {
</div>`;
}
function getEmptyStateHTML() {
return `
<div id="recipe-empty-state" class="hidden flex flex-col items-center justify-center py-16 text-center">
<div class="w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mb-4">
<i class="fas fa-search text-2xl text-gray-300"></i>
</div>
<p class="text-sm font-semibold text-gray-700">Brak wyników</p>
<p class="text-xs text-gray-500 mt-1 max-w-[220px] leading-relaxed">Zmień kryteria wyszukiwania lub filtry</p>
</div>`;
}
function syncRecipeScrollShadow() {
const scroll = document.getElementById('recipe-scroll');
const searchShell = document.getElementById('recipe-search-shell');
@@ -94,28 +105,43 @@ function syncRecipeScrollShadow() {
searchShell.style.boxShadow = SEARCH_SHELL_BASE_SHADOW;
}
function renderGrid() {
function renderAllRecipeCards() {
const grid = document.getElementById('recipe-grid');
if (!grid) return;
const recipes = getFilteredRecipes();
if (recipes.length === 0) {
grid.innerHTML = `
<div class="col-span-2 flex flex-col items-center justify-center py-16 text-center">
<div class="w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mb-4">
<i class="fas fa-search text-2xl text-gray-300"></i>
</div>
<p class="text-sm font-semibold text-gray-700">Brak wyników</p>
<p class="text-xs text-gray-500 mt-1 max-w-[220px] leading-relaxed">Zmień kryteria wyszukiwania lub filtry</p>
</div>`;
requestAnimationFrame(syncRecipeScrollShadow);
return;
grid.innerHTML = Object.values(RECIPES).map(renderRecipeCard).join('');
}
grid.innerHTML = recipes.map(renderRecipeCard).join('');
function syncVisibleRecipeCards() {
const grid = document.getElementById('recipe-grid');
const emptyState = document.getElementById('recipe-empty-state');
if (!grid || !emptyState) return;
let visibleCount = 0;
grid.querySelectorAll('[data-recipe-id]').forEach((card) => {
const recipeId = card.getAttribute('data-recipe-id');
const recipe = recipeId ? RECIPES[recipeId] : null;
const isVisible = Boolean(recipe && matchesFilters(recipe));
card.classList.toggle('hidden', !isVisible);
if (isVisible) visibleCount += 1;
});
grid.classList.toggle('hidden', visibleCount === 0);
emptyState.classList.toggle('hidden', visibleCount !== 0);
requestAnimationFrame(syncRecipeScrollShadow);
}
function renderGrid({ rebuild = false } = {}) {
const grid = document.getElementById('recipe-grid');
if (!grid) return;
if (rebuild || !grid.querySelector('[data-recipe-id]')) {
renderAllRecipeCards();
}
syncVisibleRecipeCards();
}
export function getRecipeListHTML() {
return `
<div id="main-view" class="flex flex-col h-full absolute inset-0 bg-[#2d2e2b] z-10" style="background:#2d2e2b !important;">
@@ -130,6 +156,7 @@ export function getRecipeListHTML() {
<div id="recipe-scroll" class="relative flex-1 overflow-y-auto px-4 pt-20 pb-24 bg-[#2d2e2b]" style="background:#2d2e2b !important;">
<div id="recipe-grid" class="grid grid-cols-2 gap-3 bg-[#2d2e2b]" style="background:#2d2e2b !important;"></div>
${getEmptyStateHTML()}
</div>
</div>
`;
@@ -149,11 +176,11 @@ export function getFilteredCount() {
}
export function refreshRecipeList() {
renderGrid();
renderGrid({ rebuild: true });
}
export function setupRecipeList() {
renderGrid();
renderGrid({ rebuild: true });
document.getElementById('recipe-search-input')?.addEventListener('input', (e) => {
filterState.query = e.target.value.trim();