+
@@ -40,12 +43,86 @@ export function setupBottomNav({ refreshPantry, refreshShoppingList } = {}) {
if (!main || !planner || !pantry || !shopping || !nav) return;
const TABS = ['recipes', 'planner', 'pantry', 'shopping'];
+ let isRecipeMenuOpen = false;
+ let previousTab = 'planner';
+ let collapseTimer = null;
+
+ const syncRecipeNavMetrics = () => {
+ const rootFontSize = parseFloat(window.getComputedStyle(document.documentElement).fontSize) || 16;
+ const navStyles = window.getComputedStyle(nav);
+ const navPadLeft = parseFloat(navStyles.paddingLeft) || 0;
+ const navPadRight = parseFloat(navStyles.paddingRight) || 0;
+ const navContentWidth = nav.clientWidth - navPadLeft - navPadRight;
+ const isCompact = window.matchMedia('(max-width: 380px)').matches;
+ const dockInset = (isCompact ? 1.6 : 2.4) * rootFontSize;
+ const dockMax = (isCompact ? 22.5 : 24.5) * rootFontSize;
+ const dockWidth = Math.min(navContentWidth - dockInset, dockMax);
+ if (dockWidth <= 0) return;
+
+ const padLeft = (isCompact ? 0.38 : 0.42) * rootFontSize;
+ const padRight = (isCompact ? 0.38 : 0.42) * rootFontSize;
+ const columnGap = (isCompact ? 0.05 : 0.06) * rootFontSize;
+ const slotWidth = Math.max(44, (dockWidth - padLeft - padRight - (columnGap * 3)) / 4);
+ const collapsedSlotWidth = Math.max(42, Math.min(slotWidth, isCompact ? 44 : 46));
+ const collapsedDockWidth = collapsedSlotWidth + padLeft + padRight;
+ const dockLeft = navPadLeft + ((navContentWidth - dockWidth) / 2);
+ const controlSize = (isCompact ? 2.95 : 3.05) * rootFontSize;
+ const expandedDockHeight = (isCompact ? 3.48 : 3.72) * rootFontSize;
+ const controlGap = 0.5 * rootFontSize;
+ const controlsLift = Math.max(0, (expandedDockHeight - controlSize) / 2);
+ const filterLeft = Math.max(
+ dockLeft + collapsedDockWidth + controlGap,
+ dockLeft + dockWidth - padRight - controlSize,
+ );
+ const searchRight = Math.max(16, nav.clientWidth - filterLeft + controlGap);
+
+ nav.style.setProperty('--recipe-dock-width', `${dockWidth}px`);
+ nav.style.setProperty('--recipe-collapsed-dock-width', `${collapsedDockWidth}px`);
+ nav.style.setProperty('--recipe-toggle-size', `${collapsedSlotWidth}px`);
+ document.documentElement.style.setProperty('--catalog-menu-left', `${dockLeft}px`);
+ document.documentElement.style.setProperty('--catalog-menu-width', `${collapsedDockWidth}px`);
+ document.documentElement.style.setProperty('--catalog-filter-left', `${filterLeft}px`);
+ document.documentElement.style.setProperty('--catalog-search-right', `${searchRight}px`);
+ document.documentElement.style.setProperty('--recipe-control-size', `${controlSize}px`);
+ document.documentElement.style.setProperty('--recipe-controls-lift', `${controlsLift}px`);
+ };
+
+ const setRecipeMenuOpen = (open) => {
+ syncRecipeNavMetrics();
+ window.clearTimeout(collapseTimer);
+ isRecipeMenuOpen = open;
+ nav.classList.remove('is-recipes-collapsing');
+ nav.classList.toggle('is-recipes-menu-open', open);
+ document.documentElement.classList.toggle('is-recipes-menu-open', open);
+ document.getElementById('recipe-nav-toggle')?.setAttribute('aria-expanded', open ? 'true' : 'false');
+ };
const apply = (tab) => {
+ const wasRecipeTab = previousTab === 'recipes';
main.classList.toggle('hidden', tab !== 'recipes');
planner.classList.toggle('hidden', tab !== 'planner');
pantry.classList.toggle('hidden', tab !== 'pantry');
shopping.classList.toggle('hidden', tab !== 'shopping');
+ nav.classList.toggle('is-recipes-tab', tab === 'recipes');
+ syncRecipeNavMetrics();
+ setRecipeMenuOpen(false);
+
+ if (tab === 'recipes' && !wasRecipeTab) {
+ nav.classList.add('is-recipes-menu-open', 'is-recipes-collapsing');
+ document.documentElement.classList.add('is-recipes-menu-open');
+ document.getElementById('recipe-nav-toggle')?.setAttribute('aria-expanded', 'false');
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ nav.classList.remove('is-recipes-menu-open');
+ document.documentElement.classList.remove('is-recipes-menu-open');
+ collapseTimer = window.setTimeout(() => {
+ nav.classList.remove('is-recipes-collapsing');
+ }, 500);
+ });
+ });
+ });
+ }
if (tab === 'pantry' && typeof refreshPantry === 'function') refreshPantry();
if (tab === 'shopping' && typeof refreshShoppingList === 'function') refreshShoppingList();
@@ -60,17 +137,45 @@ export function setupBottomNav({ refreshPantry, refreshShoppingList } = {}) {
else btn.removeAttribute('aria-current');
}
});
+
+ window.dispatchEvent(new CustomEvent('app-tab-change', { detail: { tab } }));
+ previousTab = tab;
};
nav.addEventListener('click', (e) => {
+ const toggle = e.target.closest('#recipe-nav-toggle');
+ if (toggle) {
+ e.stopPropagation();
+ setRecipeMenuOpen(!isRecipeMenuOpen);
+ window.closeRecipeSearch?.();
+ window.closeFilters?.();
+ return;
+ }
+
const btn = e.target.closest('.nav-tab[data-tab]');
if (!btn || btn.hasAttribute('disabled')) return;
const tab = btn.getAttribute('data-tab');
if (TABS.includes(tab)) apply(tab);
});
+ document.addEventListener('click', (e) => {
+ if (!isRecipeMenuOpen || !nav.classList.contains('is-recipes-tab')) return;
+ if (e.composedPath().includes(nav)) return;
+ setRecipeMenuOpen(false);
+ });
+
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && isRecipeMenuOpen) setRecipeMenuOpen(false);
+ });
+
+ window.addEventListener('resize', syncRecipeNavMetrics);
+
apply('planner');
+ window.switchAppTab = (tab) => {
+ if (TABS.includes(tab)) apply(tab);
+ };
+
window.refreshStockViews = () => {
if (typeof refreshPantry === 'function') refreshPantry();
};
diff --git a/js/views/Filter.js b/js/views/Filter.js
index a3cd5dd..d86b80d 100644
--- a/js/views/Filter.js
+++ b/js/views/Filter.js
@@ -18,7 +18,7 @@ const PREP_TIME_STEP = 5;
const PREP_TIME_MIN_GAP = PREP_TIME_STEP;
const FILTER_CONTEXTS = {
recipes: {
- anchorShellId: 'recipe-topbar',
+ anchorShellId: 'recipe-bottom-controls',
buttonId: 'recipe-filter-btn',
getState: () => getFilterState(),
applyState: (nextState) => applyFilters(nextState),
@@ -247,13 +247,20 @@ function positionFilterPanel() {
const gap = 8;
const margin = 12;
const width = Math.min(anchorRect.width, viewRect.width - margin * 2);
- const top = Math.max(margin, anchorRect.bottom - viewRect.top + gap);
+ const spaceBelow = viewRect.bottom - anchorRect.bottom - margin;
+ const preferredMaxHeight = Math.min(420, viewRect.height - margin * 2);
+ const spaceAbove = anchorRect.top - viewRect.top - gap - margin;
+ const opensUpward = spaceBelow < 260 && anchorRect.top - viewRect.top > spaceBelow;
+ const maxHeight = opensUpward
+ ? Math.max(220, Math.min(preferredMaxHeight, spaceAbove))
+ : Math.max(220, viewRect.height - Math.max(margin, anchorRect.bottom - viewRect.top + gap) - margin);
+ const top = opensUpward
+ ? Math.max(margin, anchorRect.top - viewRect.top - gap - maxHeight)
+ : Math.max(margin, anchorRect.bottom - viewRect.top + gap);
const left = Math.max(
margin,
Math.min(anchorRect.left - viewRect.left, viewRect.width - width - margin),
);
- const maxHeight = Math.max(220, viewRect.height - top - margin);
-
panel.style.width = `${width}px`;
panel.style.left = `${left}px`;
panel.style.top = `${top}px`;
@@ -373,9 +380,16 @@ function syncPanelCount() {
if (button) {
const highlight = isFilterPanelOpen() || count > 0;
- button.style.setProperty('background', highlight ? 'rgb(var(--sunken-rgb))' : 'rgb(var(--card-rgb))', 'important');
- button.style.setProperty('border-color', highlight ? 'rgb(var(--border-input-rgb))' : 'rgb(var(--border-card-rgb))', 'important');
- button.style.setProperty('color', highlight ? 'rgb(var(--text-emphasis-rgb))' : 'rgb(var(--text-body-rgb))', 'important');
+ const isRecipeGlassButton = buttonId === 'recipe-filter-btn';
+ if (isRecipeGlassButton && !highlight) {
+ button.style.removeProperty('background');
+ button.style.removeProperty('border-color');
+ button.style.removeProperty('color');
+ } else {
+ button.style.setProperty('background', highlight ? 'rgba(var(--overlay-rgb), 0.12)' : 'rgb(var(--card-rgb))', 'important');
+ button.style.setProperty('border-color', highlight ? 'rgb(var(--border-input-rgb))' : 'rgb(var(--border-card-rgb))', 'important');
+ button.style.setProperty('color', highlight ? 'rgb(var(--text-emphasis-rgb))' : 'rgb(var(--text-body-rgb))', 'important');
+ }
}
const badge = button?.querySelector('[id$="-filter-count"]');
diff --git a/js/views/RecipeList.js b/js/views/RecipeList.js
index 176df36..cb35b10 100644
--- a/js/views/RecipeList.js
+++ b/js/views/RecipeList.js
@@ -12,7 +12,6 @@ let filterState = {
maxMinutes: DEFAULT_MAX_MINUTES,
};
-let isSearchExpanded = false;
let recipeListDocListenersBound = false;
function matchesFilters(recipe) {
@@ -51,21 +50,8 @@ function syncRecipeScrollShadow() {
}
function syncRecipeTopbarUI() {
- const defaultRow = document.getElementById('recipe-default-row');
const searchShell = document.getElementById('recipe-search-shell');
-
- const showSearch = isSearchExpanded;
-
- if (defaultRow) {
- defaultRow.style.opacity = showSearch ? '0' : '1';
- defaultRow.style.transform = showSearch ? 'translateY(-2px) scale(0.98)' : 'translateY(0) scale(1)';
- defaultRow.style.pointerEvents = showSearch ? 'none' : 'auto';
- }
-
if (searchShell) {
- searchShell.style.opacity = showSearch ? '1' : '0';
- searchShell.style.transform = showSearch ? 'translateY(0) scale(1)' : 'translateY(-2px) scale(0.98)';
- searchShell.style.pointerEvents = showSearch ? 'auto' : 'none';
searchShell.style.boxShadow = 'var(--shadow-shell)';
}
}
@@ -78,20 +64,10 @@ function closeSearch() {
input.blur();
}
filterState.query = '';
- isSearchExpanded = false;
syncRecipeTopbarUI();
if (hadQuery) renderGrid();
}
-function openSearch() {
- isSearchExpanded = true;
- window.closeFilters?.();
- syncRecipeTopbarUI();
- window.requestAnimationFrame(() => {
- document.getElementById('recipe-search-input')?.focus();
- });
-}
-
function renderGrid() {
const grid = document.getElementById('recipe-grid');
const emptyState = document.getElementById('recipe-empty-state');
@@ -111,44 +87,32 @@ function renderGrid() {
export function getRecipeListHTML() {
return `
-
-
${getRecipeGridSectionHTML({
scrollId: 'recipe-scroll',
gridId: 'recipe-grid',
emptyStateId: 'recipe-empty-state',
- scrollClassName: 'relative flex-1 overflow-y-auto px-4 pt-[5.35rem] pb-24 bg-[rgb(var(--app-bg-rgb))]',
+ scrollClassName: 'relative flex-1 overflow-y-auto px-4 pt-4 pb-24 bg-[rgb(var(--app-bg-rgb))]',
gridClassName: 'grid grid-cols-3 gap-2 bg-[rgb(var(--app-bg-rgb))]',
emptyTitle: 'Brak wyników',
emptyMessage: 'Zmień kryteria wyszukiwania lub filtry',
})}
+
+
+
+
+
+
+
+
+
+
+
`;
}
@@ -178,18 +142,15 @@ export function setupRecipeList() {
renderGrid();
});
document.getElementById('recipe-search-input')?.addEventListener('keydown', (e) => {
- if (e.key === 'Escape') closeSearch();
+ if (e.key !== 'Escape') return;
+ if (e.target.value) closeSearch();
+ else e.target.blur();
});
- document.getElementById('recipe-search-toggle')?.addEventListener('click', () => openSearch());
document.getElementById('recipe-search-close')?.addEventListener('click', () => closeSearch());
-
document.getElementById('recipe-filter-btn')?.addEventListener('click', (e) => {
e.stopPropagation();
- if (isSearchExpanded) {
- isSearchExpanded = false;
- syncRecipeTopbarUI();
- }
+ document.getElementById('recipe-search-input')?.blur();
window.openFilters?.('recipes');
});
@@ -200,24 +161,22 @@ export function setupRecipeList() {
if (recipeId) window.openRecipeDetail?.(recipeId);
});
- const recipeScroll = document.getElementById('recipe-scroll');
- const recipeTopBar = document.getElementById('recipe-top-bar');
- if (recipeScroll && recipeTopBar) {
- const shadow = document.createElement('div');
- shadow.style.cssText = 'position:absolute;left:0;right:0;bottom:-8px;height:8px;background:linear-gradient(to bottom,rgba(var(--overlay-rgb),0.25),transparent);opacity:0;transition:opacity 0.2s;pointer-events:none;';
- recipeTopBar.appendChild(shadow);
- recipeScroll.addEventListener('scroll', () => {
- shadow.style.opacity = recipeScroll.scrollTop > 2 ? '1' : '0';
- });
- }
-
if (!recipeListDocListenersBound) {
recipeListDocListenersBound = true;
document.addEventListener('keydown', (e) => {
- if (e.key !== 'Escape' || !isSearchExpanded) return;
- if (!document.getElementById('main-view')?.classList.contains('hidden')) {
- closeSearch();
+ const isRecipeViewVisible = !document.getElementById('main-view')?.classList.contains('hidden');
+ if (e.key !== 'Escape') return;
+ if (isRecipeViewVisible) {
+ document.getElementById('recipe-search-input')?.blur();
}
});
+
+ window.addEventListener('app-tab-change', () => {
+ document.getElementById('recipe-search-input')?.blur();
+ });
+
+ window.closeRecipeSearch = () => {
+ document.getElementById('recipe-search-input')?.blur();
+ };
}
}