Reorganizacja górnych paneli i ujednolicenie stylu filtrów

- Kalendarz: data między strzałkami zamiast napisu "Dziś", nawigacja po prawej, mniejszy komponent dopasowany do wysokości dnia
- MealPlanner/Pantry/RecipeList: spójne nagłówki z tytułem po lewej i kontrolkami po prawej
- RecipeList: nowy top bar z przyciskami filtrów i wyszukiwania wzorowany na spiżarni
- Filter popup: ujednolicony styl z popoverem spiżarni (ciemniejsze tło, jaśniejsze obramowanie, spójne chipy)
- Usunięcie przyciemnienia otoczenia przy otwieraniu filtrów
- Badge z liczbą aktywnych filtrów na przycisku, zachowujący stan po zamknięciu popupu
- Usunięcie ikon kalendarza z pigułek w spiżarni

Made-with: Cursor
This commit is contained in:
2026-04-16 00:17:41 +02:00
parent d3a68a80eb
commit 4d7a1a12ae
7 changed files with 553 additions and 170 deletions

View File

@@ -10,10 +10,9 @@ import {
bindCalendarDayClicks,
createCalendarTopbarHTML,
createCalendarWeekdayHeaderHTML,
formatCalendarMonthYear,
renderCalendarGrid,
syncCalendarTodayButton,
} from '../ui/mealCalendar.js';
} from '../ui/mealCalendar.js?v=11';
import { createIngredientCardController, getIngredientCardHTML } from '../ui/ingredientCard.js?v=20260410-107';
/* ── helpers ── */
@@ -35,6 +34,30 @@ function formatQty(n) {
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1).replace(/\.0$/, '');
}
function toggleStringFilter(list, value) {
return list.includes(value)
? list.filter((item) => item !== value)
: [...list, value];
}
function hasActivePantryFilters() {
return pantryFilters.categories.length > 0 || pantryFilters.sections.length > 0;
}
function getActivePantryFilterCount() {
return pantryFilters.categories.length + pantryFilters.sections.length;
}
function filterChipStyle(active) {
const background = active ? '#393937' : '#2d2e2b';
const color = active ? '#f2efe8' : '#d7d2c8';
const borderRule = active ? 'border:1px solid #787876;' : 'border:none;';
const shadow = active
? 'box-shadow:inset 0 1px 0 rgba(255,255,255,0.04), 0 0 0 1px rgba(0,0,0,0.08);'
: '';
return `background:${background}; ${borderRule} color:${color}; ${shadow}`;
}
const CATEGORY_ICONS = {
pieczywo: 'fa-bread-slice',
nabial: 'fa-cheese',
@@ -45,6 +68,12 @@ const CATEGORY_ICONS = {
przyprawy: 'fa-leaf',
inne: 'fa-jar',
};
const CATEGORY_ORDER = ['pieczywo', 'nabial', 'mieso_ryby', 'warzywa', 'owoce', 'suche', 'przyprawy', 'inne'];
const PANTRY_SECTION_FILTERS = [
{ id: 'shortfalls', label: 'Potrzebne' },
{ id: 'sufficient', label: 'W spiżarni' },
{ id: 'notPlanned', label: 'Poza planem' },
];
const DAY_NAMES_SHORT = ['nd.', 'pon.', 'wt.', 'śr.', 'czw.', 'pt.', 'sob.'];
const MONTHS_SHORT = ['sty', 'lut', 'mar', 'kwi', 'maj', 'cze', 'lip', 'sie', 'wrz', 'paź', 'lis', 'gru'];
@@ -52,6 +81,21 @@ const MONTHS_SHORT = ['sty', 'lut', 'mar', 'kwi', 'maj', 'cze', 'lip', 'sie', 'w
const SEARCH_SHELL_SHADOW = '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)';
const DEFAULT_HORIZON_DAYS = 7;
const PANTRY_CALENDAR_DAY_ATTR = 'data-pantry-calendar-day';
const SHORTFALL_ACCENT = '#CB4A48';
const PANTRY_CALENDAR_THEME = {
bg: '#272622',
border: '#34312c',
text: '#d7d2c8',
dimText: '#5a5752',
dimOpacity: 0.38,
dimmedBg: 'transparent',
dimmedBorder: 'transparent',
dot: '#7d7a74',
selectedBg: '#393937',
selectedBorder: '#787876',
selectedText: '#f2efe8',
selectedDot: '#f2efe8',
};
/* ── state ── */
@@ -59,8 +103,13 @@ let ingredientCard = null;
let horizonEndDate = addDays(startOfDay(new Date()), DEFAULT_HORIZON_DAYS - 1);
let isSearchExpanded = false;
let isCalendarOpen = false;
let isFilterOpen = false;
let calendarMonthAnchor = startOfMonth(horizonEndDate);
let pantryGlobalListenersBound = false;
let pantryFilters = {
categories: [],
sections: [],
};
/* ── date formatting ── */
@@ -127,32 +176,29 @@ export function getPantryHTML() {
<!-- ── floating top bar ── -->
<div class="pointer-events-none absolute inset-x-0 top-0 z-[12] px-4 pt-4" style="background:transparent !important; border:none !important;">
<div class="pointer-events-auto relative z-[1] mx-auto" style="width:min(calc(100% - 0.5rem), 22.4rem);">
<div id="pantry-topbar" class="relative h-11">
<div id="pantry-compact-controls" class="flex items-center gap-2 transition-all duration-200" style="opacity:1; transform:translateY(0) scale(1);">
<div id="pantry-horizon-wrap" class="relative flex-1 min-w-0">
<button type="button" id="pantry-horizon-toggle" class="w-full h-11 rounded-full flex items-center gap-2 px-3 transition-all" style="background:#393937 !important; border:1px solid #41423f !important; box-shadow:${SEARCH_SHELL_SHADOW} !important;">
<div class="w-8 h-8 rounded-full shrink-0 flex items-center justify-center" style="background:#2f2f2d;">
<i class="far fa-calendar text-[12px]" style="color:#9b978f;"></i>
</div>
<span id="pantry-horizon-label" class="flex-1 min-w-0 text-left text-[13px] font-normal truncate" style="color:#ddd6ca;"></span>
<i id="pantry-horizon-chevron" class="fas fa-chevron-down text-[10px] shrink-0 transition-transform duration-200" style="color:#9b978f;"></i>
<div id="pantry-topbar" class="relative min-h-12">
<div id="pantry-default-row" class="flex min-h-12 items-center gap-2 transition-all duration-200" style="opacity:1; transform:translateY(0) scale(1);">
<h1 class="flex-1 min-w-0 truncate" style="margin:0;padding:0;color:#f2efe8;font-family:var(--app-font);font-size:18px;font-weight:700;line-height:1.2;letter-spacing:-0.02em;">Zapasy</h1>
<button type="button" id="pantry-horizon-compact" class="min-w-0 max-w-[12rem] h-10 rounded-full flex items-center gap-1.5 px-2.5 transition-all shrink" style="background:#393937 !important; border:1px solid #41423f !important; box-shadow:${SEARCH_SHELL_SHADOW} !important;">
<span id="pantry-horizon-compact-label" class="min-w-0 flex-1 text-left text-[13px] font-normal truncate" style="color:#ddd6ca;"></span>
<i class="fas fa-chevron-down text-[10px] shrink-0" style="color:#9b978f;"></i>
</button>
<div id="pantry-filter-wrap" class="relative shrink-0">
<button type="button" id="pantry-filter-toggle" class="relative w-11 h-11 rounded-full shrink-0 flex items-center justify-center transition-all duration-200" style="background:#393937; border:1px solid #41423f; box-shadow:${SEARCH_SHELL_SHADOW}; color:#ddd6ca;">
<i class="fas fa-sliders-h text-[12px]"></i>
<span id="pantry-filter-count" class="hidden absolute -top-1 -right-1 min-w-[1.1rem] h-[1.1rem] px-1 rounded-full text-[9px] font-bold leading-none items-center justify-center" style="background:#23221e; border:1px solid #787876; color:#f2efe8;"></span>
</button>
<div id="pantry-calendar-popover" class="absolute left-0 right-0 top-full mt-2 rounded-[1.35rem] px-3 py-3 transition-all duration-200 pointer-events-none" style="background:#393937 !important; border:1px solid #41423f !important; box-shadow:${SEARCH_SHELL_SHADOW} !important; opacity:0; transform:translateY(-6px) scale(0.98);">
${createCalendarTopbarHTML({
titleId: 'pantry-cal-title',
prevId: 'pantry-cal-prev',
todayId: 'pantry-cal-today',
nextId: 'pantry-cal-next',
wrapperClass: 'pb-3 flex items-center gap-3',
titleClass: 'text-[13px] font-semibold text-[#ddd6ca] leading-none tracking-[-0.02em]',
})}
${createCalendarWeekdayHeaderHTML()}
<div id="pantry-calendar-grid" class="grid grid-cols-7 gap-1.5"></div>
<div class="mt-3 pt-3 border-t" style="border-color:#444442;">
<p id="pantry-cal-selection" class="min-w-0 text-[11px] leading-snug" style="color:#9b978f;"></p>
<div id="pantry-filter-popover" class="absolute right-0 top-full mt-2 w-[19rem] max-w-[calc(100vw-2rem)] rounded-[1.35rem] px-3 py-3 transition-all duration-200 pointer-events-none" style="background:#23221e !important; border:1px solid #787876 !important; box-shadow:${SEARCH_SHELL_SHADOW} !important; opacity:0; transform:translateY(-6px) scale(0.98);">
<div class="flex items-center justify-between gap-3 px-0.5 pb-3">
<p class="text-[11px] font-semibold leading-none" style="color:#f2efe8;">Filtry</p>
<button type="button" id="pantry-filter-clear" class="h-8 px-2 rounded-full text-[11px] font-semibold transition-colors" style="background:transparent; border:none; color:#b5afa5;">
Wyczyść
</button>
</div>
<div id="pantry-filter-panel-body" class="space-y-4"></div>
</div>
</div>
@@ -161,7 +207,37 @@ export function getPantryHTML() {
</button>
</div>
<div id="pantry-search-shell" class="absolute inset-0 flex items-center gap-2 rounded-full px-3 transition-all duration-200 pointer-events-none" style="background:#393937 !important; border:1px solid #41423f !important; box-shadow:${SEARCH_SHELL_SHADOW} !important; opacity:0; transform:translateY(-2px) scale(0.98);">
<div id="pantry-horizon-expanded" class="absolute inset-0 transition-all duration-200 pointer-events-none" style="opacity:0; transform:translateY(-2px) scale(0.98);">
<div id="pantry-horizon-wrap" class="relative">
<button type="button" id="pantry-horizon-toggle" class="w-full h-10 rounded-full flex items-center gap-1.5 px-2.5 transition-all" style="background:#23221e !important; border:1px solid #787876 !important; box-shadow:${SEARCH_SHELL_SHADOW} !important;">
<span id="pantry-horizon-label" class="flex-1 min-w-0 text-left text-[13px] font-normal truncate" style="color:#ddd6ca;"></span>
<i id="pantry-horizon-chevron" class="fas fa-chevron-down text-[10px] shrink-0 transition-transform duration-200" style="color:#9b978f;"></i>
</button>
<div id="pantry-calendar-popover" class="absolute left-0 right-0 top-full mt-2 rounded-[1.35rem] px-3 py-3 transition-all duration-200 pointer-events-none" style="background:#23221e !important; border:1px solid #787876 !important; box-shadow:${SEARCH_SHELL_SHADOW} !important; opacity:0; transform:translateY(-6px) scale(0.98);">
${createCalendarTopbarHTML({
prevId: 'pantry-cal-prev',
todayId: 'pantry-cal-today',
nextId: 'pantry-cal-next',
wrapperClass: 'pb-3 flex items-center justify-end gap-3',
controlsStyle: `background:${PANTRY_CALENDAR_THEME.bg};border-color:${PANTRY_CALENDAR_THEME.border};`,
navButtonClass: 'shrink-0 w-7 h-full flex items-center justify-center rounded-full border-0 bg-transparent text-[#d7d2c8] transition-colors',
todayButtonActiveClass: 'h-full shrink-0 inline-flex min-w-[5.75rem] max-w-[9rem] items-center justify-center rounded-full bg-transparent px-1.5 text-[10px] font-semibold leading-none tabular-nums text-[#d7d2c8] active:bg-transparent whitespace-nowrap',
todayButtonDimClass: 'h-full shrink-0 inline-flex items-center justify-center rounded-full px-2 text-[10px] font-semibold leading-none text-[#7d7a74] cursor-default',
})}
${createCalendarWeekdayHeaderHTML(undefined, {
wrapperClass: 'grid grid-cols-7 gap-1.5 text-center text-[8px] font-medium text-[#9b978f] uppercase tracking-wide mb-1 leading-none',
})}
<div id="pantry-calendar-grid" class="grid grid-cols-7 gap-1.5"></div>
<div class="mt-3 pt-3 border-t" style="border-color:#444442;">
<p id="pantry-cal-selection" class="min-w-0 text-[11px] leading-snug" style="color:#9b978f;"></p>
</div>
</div>
</div>
</div>
<div id="pantry-search-shell" class="absolute inset-0 flex items-center gap-2 rounded-full px-3 transition-all duration-200 pointer-events-none" style="background:#23221e !important; border:1px solid #787876 !important; box-shadow:${SEARCH_SHELL_SHADOW} !important; opacity:0; transform:translateY(-2px) scale(0.98);">
<i class="fas fa-search text-[13px] shrink-0" style="color:#9b978f;"></i>
<input type="search" id="pantry-search" autocomplete="off" placeholder="Szukaj w spiżarni…"
class="flex-1 min-w-0 h-full bg-transparent outline-none text-[15px] leading-none py-0" style="background:transparent !important; border:none !important; box-shadow:none !important; color:#ddd6ca; margin:0;">
@@ -174,7 +250,7 @@ export function getPantryHTML() {
</div>
<!-- ── scrollable content ── -->
<div id="pantry-scroll" class="flex-1 overflow-y-auto no-scrollbar px-4 pt-[5.25rem] pb-24" style="background:#2d2e2b !important;">
<div id="pantry-scroll" class="flex-1 overflow-y-auto no-scrollbar px-4 pt-[5.35rem] pb-24" style="background:#2d2e2b !important;">
<div id="pantry-board"></div>
</div>
@@ -190,21 +266,41 @@ export function getPantryHTML() {
function syncHorizonUI() {
ensureValidHorizonDate();
const compactControls = document.getElementById('pantry-compact-controls');
const defaultRow = document.getElementById('pantry-default-row');
const horizonExpanded = document.getElementById('pantry-horizon-expanded');
const searchShell = document.getElementById('pantry-search-shell');
const popover = document.getElementById('pantry-calendar-popover');
const filterPopover = document.getElementById('pantry-filter-popover');
const filterToggle = document.getElementById('pantry-filter-toggle');
const filterCount = document.getElementById('pantry-filter-count');
const compactLabel = document.getElementById('pantry-horizon-compact-label');
const horizonLabel = document.getElementById('pantry-horizon-label');
const chevron = document.getElementById('pantry-horizon-chevron');
const titleEl = document.getElementById('pantry-cal-title');
const selectionEl = document.getElementById('pantry-cal-selection');
const prevBtn = document.getElementById('pantry-cal-prev');
const todayBtn = document.getElementById('pantry-cal-today');
if (compactLabel) compactLabel.textContent = formatHorizonLabel(horizonEndDate);
if (horizonLabel) horizonLabel.textContent = formatHorizonLabel(horizonEndDate);
if (titleEl) titleEl.textContent = formatCalendarMonthYear(calendarMonthAnchor);
if (selectionEl) selectionEl.textContent = formatRangeSummary(horizonEndDate);
const showCalendar = isCalendarOpen && !isSearchExpanded;
const showDefault = !isSearchExpanded && !isCalendarOpen;
const showFilter = isFilterOpen && showDefault;
const activeFilterCount = getActivePantryFilterCount();
if (defaultRow) {
defaultRow.style.opacity = showDefault ? '1' : '0';
defaultRow.style.transform = showDefault ? 'translateY(0) scale(1)' : 'translateY(-2px) scale(0.98)';
defaultRow.style.pointerEvents = showDefault ? 'auto' : 'none';
}
if (horizonExpanded) {
horizonExpanded.style.opacity = showCalendar ? '1' : '0';
horizonExpanded.style.transform = showCalendar ? 'translateY(0) scale(1)' : 'translateY(-2px) scale(0.98)';
horizonExpanded.style.pointerEvents = showCalendar ? 'auto' : 'none';
}
if (popover) {
popover.style.opacity = showCalendar ? '1' : '0';
popover.style.transform = showCalendar ? 'translateY(0) scale(1)' : 'translateY(-6px) scale(0.98)';
@@ -214,19 +310,38 @@ function syncHorizonUI() {
chevron.style.transform = showCalendar ? 'rotate(180deg)' : 'rotate(0deg)';
}
if (filterPopover) {
filterPopover.style.opacity = showFilter ? '1' : '0';
filterPopover.style.transform = showFilter ? 'translateY(0) scale(1)' : 'translateY(-6px) scale(0.98)';
filterPopover.style.pointerEvents = showFilter ? 'auto' : 'none';
}
if (filterToggle) {
const isActive = showFilter || hasActivePantryFilters();
filterToggle.style.setProperty('background', isActive ? '#23221e' : '#393937', 'important');
filterToggle.style.setProperty('border-color', isActive ? '#787876' : '#41423f', 'important');
filterToggle.style.setProperty('color', isActive ? '#f2efe8' : '#ddd6ca', 'important');
}
if (filterCount) {
filterCount.textContent = String(activeFilterCount);
filterCount.classList.toggle('hidden', activeFilterCount === 0);
filterCount.classList.toggle('flex', activeFilterCount > 0);
}
if (prevBtn) {
const isCurrentMonth = sameMonth(calendarMonthAnchor, getToday());
prevBtn.disabled = isCurrentMonth;
prevBtn.style.opacity = isCurrentMonth ? '0.45' : '1';
prevBtn.style.cursor = isCurrentMonth ? 'default' : 'pointer';
}
syncCalendarTodayButton(todayBtn, sameMonth(calendarMonthAnchor, getToday()));
if (compactControls) {
compactControls.style.opacity = isSearchExpanded ? '0' : '1';
compactControls.style.transform = isSearchExpanded ? 'translateY(-2px) scale(0.98)' : 'translateY(0) scale(1)';
compactControls.style.pointerEvents = isSearchExpanded ? 'none' : 'auto';
}
syncCalendarTodayButton(
todayBtn,
sameMonth(calendarMonthAnchor, getToday()),
horizonEndDate,
{
ariaLabelGo: 'Pokaż bieżący miesiąc w kalendarzu',
ariaLabelCurrent: 'Wyświetlany jest bieżący miesiąc',
},
);
if (searchShell) {
searchShell.style.opacity = isSearchExpanded ? '1' : '0';
@@ -235,6 +350,7 @@ function syncHorizonUI() {
}
renderCalendarPopover();
renderFilterPopover();
}
function renderCalendarPopover() {
@@ -259,9 +375,54 @@ function renderCalendarPopover() {
};
},
dayAttr: PANTRY_CALENDAR_DAY_ATTR,
theme: PANTRY_CALENDAR_THEME,
});
}
function filterChipHtml(kind, value, label, active) {
return `<button
type="button"
class="px-3 py-2 rounded-full text-[11px] font-semibold transition-colors"
style="${filterChipStyle(active)}"
data-pantry-filter-kind="${esc(kind)}"
data-pantry-filter-value="${esc(value)}"
>${esc(label)}</button>`;
}
function renderFilterPopover() {
const body = document.getElementById('pantry-filter-panel-body');
if (!body) return;
const categoryChips = CATEGORY_ORDER
.map((category) => filterChipHtml(
'category',
category,
CATEGORY_LABELS[category] || category,
pantryFilters.categories.includes(category),
))
.join('');
const sectionChips = PANTRY_SECTION_FILTERS
.map((item) => filterChipHtml(
'section',
item.id,
item.label,
pantryFilters.sections.includes(item.id),
))
.join('');
body.innerHTML = `
<section>
<p class="text-[10px] font-bold uppercase tracking-wider mb-3 px-0.5" style="color:#b5afa5;">Kategorie</p>
<div class="flex flex-wrap gap-2">${categoryChips}</div>
</section>
<section>
<p class="text-[10px] font-bold uppercase tracking-wider mb-3 px-0.5" style="color:#b5afa5;">Sekcje</p>
<div class="flex flex-wrap gap-2">${sectionChips}</div>
</section>
`;
}
function closeSearch() {
const input = document.getElementById('pantry-search');
const hadQuery = Boolean(input?.value);
@@ -277,6 +438,7 @@ function closeSearch() {
function openSearch() {
isSearchExpanded = true;
isCalendarOpen = false;
isFilterOpen = false;
syncHorizonUI();
window.requestAnimationFrame(() => {
document.getElementById('pantry-search')?.focus();
@@ -292,10 +454,25 @@ function closeCalendar() {
function openCalendar() {
ensureValidHorizonDate();
calendarMonthAnchor = startOfMonth(horizonEndDate);
isSearchExpanded = false;
isFilterOpen = false;
isCalendarOpen = true;
syncHorizonUI();
}
function closeFilter() {
if (!isFilterOpen) return;
isFilterOpen = false;
syncHorizonUI();
}
function toggleFilterPanel() {
isCalendarOpen = false;
isSearchExpanded = false;
isFilterOpen = !isFilterOpen;
syncHorizonUI();
}
function selectHorizonDate(date) {
const next = startOfDay(date);
if (next.getTime() < getToday().getTime()) return;
@@ -322,6 +499,8 @@ function classifyIngredients(searchQuery) {
if (!q) return true;
return name.toLowerCase().includes(q) || (CATEGORY_LABELS[category] || '').toLowerCase().includes(q);
};
const matchesCategory = (category) => pantryFilters.categories.length === 0 || pantryFilters.categories.includes(category);
const matchesSection = (section) => pantryFilters.sections.length === 0 || pantryFilters.sections.includes(section);
const neededIds = new Set();
const shortfalls = [];
@@ -329,7 +508,7 @@ function classifyIngredients(searchQuery) {
for (const need of needs) {
neededIds.add(need.ingredientId);
if (!matchesSearch(need.name, need.category)) continue;
if (!matchesSearch(need.name, need.category) || !matchesCategory(need.category)) continue;
const have = getPantryTotal(need.ingredientId, pantry);
const def = INGREDIENTS[need.ingredientId];
@@ -347,12 +526,14 @@ function classifyIngredients(searchQuery) {
};
if (have < need.amount) {
if (!matchesSection('shortfalls')) continue;
shortfalls.push({
...item,
shortfall: Math.round((need.amount - have) * 10) / 10,
fillPct: need.amount > 0 ? Math.min(100, Math.round((have / need.amount) * 100)) : 0,
});
} else {
if (!matchesSection('sufficient')) continue;
sufficient.push({
...item,
fillPct: 100,
@@ -363,7 +544,9 @@ function classifyIngredients(searchQuery) {
// "Poza planem": all ingredients NOT in any plan need
const notPlanned = Object.keys(INGREDIENTS)
.filter((id) => !neededIds.has(id))
.filter(() => matchesSection('notPlanned'))
.filter((id) => matchesSearch(INGREDIENTS[id].name, INGREDIENTS[id].category))
.filter((id) => matchesCategory(INGREDIENTS[id].category))
.map((id) => {
const def = INGREDIENTS[id];
return {
@@ -385,13 +568,13 @@ function shortfallTileHtml(item) {
return `
<button type="button" class="pv2-tile w-full text-left rounded-2xl flex items-center gap-3 px-3 py-3 transition-all active:scale-[0.99] mb-2" style="background:#393937; border:none; box-shadow:0 2px 8px rgba(0,0,0,0.28);" data-id="${esc(item.ingredientId)}">
<div class="w-10 h-10 rounded-xl flex items-center justify-center shrink-0" style="background:#2f2f2d;">
<i class="fas ${item.icon} text-[17px]" style="color:#914945;"></i>
<i class="fas ${item.icon} text-[17px]" style="color:${SHORTFALL_ACCENT};"></i>
</div>
<div class="flex-1 min-w-0">
<p class="text-[13px] font-normal leading-tight truncate mb-1.5" style="color:#ddd6ca;">${esc(item.name)}</p>
<div class="flex items-center gap-2">
<div class="flex-1 h-2 rounded-full overflow-hidden" style="background:#2a2a28;">
<div class="h-full rounded-full" style="width:${item.fillPct}%; background:#914945;"></div>
<div class="h-full rounded-full" style="width:${item.fillPct}%; background:${SHORTFALL_ACCENT};"></div>
</div>
<span class="text-[11px] font-semibold tabular-nums shrink-0" style="color:#ddd6ca;">${esc(formatQty(item.pantryQty))} <span class="font-medium text-[10px]" style="color:#9b978f;">/ ${esc(formatQty(item.needed))} ${esc(unitLabel(item.unit))}</span></span>
</div>
@@ -432,9 +615,6 @@ function notPlannedChipHtml(item) {
function sectionHeaderHtml(icon, iconBg, iconColor, title, titleColor, count) {
return `
<div class="flex items-center gap-2 mb-2.5 px-0.5">
<div class="w-6 h-6 rounded-lg flex items-center justify-center" style="background:${iconBg};">
<i class="fas ${icon} text-[10px]" style="color:${iconColor};"></i>
</div>
<span class="text-[10px] font-bold uppercase tracking-wider" style="color:${titleColor};">${esc(title)}</span>
<span class="text-[10px]" style="color:#6d6c67;">${count}</span>
</div>`;
@@ -445,11 +625,12 @@ function renderBoard() {
if (!root) return;
const q = document.getElementById('pantry-search')?.value || '';
const hasFilters = hasActivePantryFilters();
const { shortfalls, sufficient, notPlanned } = classifyIngredients(q);
const totalVisible = shortfalls.length + sufficient.length + notPlanned.length;
if (totalVisible === 0 && q) {
root.innerHTML = `<p class="text-sm text-center py-10" style="color:#9b978f;">Brak wyników — zmień wyszukiwanie.</p>`;
if (totalVisible === 0 && (q || hasFilters)) {
root.innerHTML = `<p class="text-sm text-center py-10" style="color:#9b978f;">Brak wyników — zmień filtry lub wyszukiwanie.</p>`;
return;
}
@@ -458,7 +639,7 @@ function renderBoard() {
// Section 1: Potrzebne (shortfalls)
if (shortfalls.length > 0) {
html += `<section class="mb-5">`;
html += sectionHeaderHtml('fa-exclamation', '#914945', '#f2efe8', 'Potrzebne', '#7B756D', shortfalls.length);
html += sectionHeaderHtml('fa-exclamation', '#2f2f2d', SHORTFALL_ACCENT, 'Potrzebne', '#7B756D', shortfalls.length);
html += shortfalls.map(shortfallTileHtml).join('');
html += `</section>`;
}
@@ -481,7 +662,7 @@ function renderBoard() {
}
// Empty state: no plans at all
if (shortfalls.length === 0 && sufficient.length === 0 && !q) {
if (shortfalls.length === 0 && sufficient.length === 0 && !q && !hasFilters) {
html = `
<div class="flex flex-col items-center justify-center py-10 text-center mb-6">
<div class="w-12 h-12 rounded-2xl flex items-center justify-center mb-3" style="background:#393937;">
@@ -535,8 +716,45 @@ export function setupPantry() {
});
document.getElementById('pantry-search-toggle')?.addEventListener('click', () => openSearch());
document.getElementById('pantry-search-close')?.addEventListener('click', () => closeSearch());
document.getElementById('pantry-filter-toggle')?.addEventListener('click', () => toggleFilterPanel());
document.getElementById('pantry-filter-clear')?.addEventListener('click', () => {
pantryFilters = { categories: [], sections: [] };
syncHorizonUI();
renderBoard();
});
document.getElementById('pantry-filter-panel-body')?.addEventListener('click', (event) => {
const target = event.target;
if (!(target instanceof Element)) return;
const chip = target.closest('[data-pantry-filter-kind]');
if (!(chip instanceof Element)) return;
const kind = chip.getAttribute('data-pantry-filter-kind');
const value = chip.getAttribute('data-pantry-filter-value');
if (!kind || !value) return;
if (kind === 'category') {
pantryFilters = {
...pantryFilters,
categories: toggleStringFilter(pantryFilters.categories, value),
};
}
if (kind === 'section') {
pantryFilters = {
...pantryFilters,
sections: toggleStringFilter(pantryFilters.sections, value),
};
}
syncHorizonUI();
renderBoard();
});
// Horizon pill + calendar
document.getElementById('pantry-horizon-compact')?.addEventListener('click', (event) => {
event.stopPropagation();
openCalendar();
});
document.getElementById('pantry-horizon-toggle')?.addEventListener('click', () => {
if (isCalendarOpen) {
closeCalendar();
@@ -567,9 +785,12 @@ export function setupPantry() {
document.addEventListener('click', (event) => {
const target = event.target;
if (!(target instanceof Element)) return;
if (isCalendarOpen && !target.closest('#pantry-horizon-wrap')) {
if (isCalendarOpen && !target.closest('#pantry-horizon-wrap, #pantry-horizon-compact')) {
closeCalendar();
}
if (isFilterOpen && !target.closest('#pantry-filter-wrap')) {
closeFilter();
}
});
}