Merge branch 'claude/stoic-swartz'
Some checks failed
Build and Deploy / build-and-push (push) Failing after 1m18s

This commit is contained in:
2026-04-16 00:19:04 +02:00
7 changed files with 553 additions and 170 deletions

View File

@@ -21,18 +21,39 @@ export const CALENDAR_WEEKDAYS_SHORT = ['pn', 'wt', 'śr', 'cz', 'pt', 'so', 'nd
export const CALENDAR_DAY_ATTR = 'data-calendar-day';
export const CALENDAR_HANDLE_CLASS = 'block h-1 w-10 rounded-full bg-[#6d6c67]/75';
function getCalendarDayHTML(day, meta, dayState, dayAttr) {
const { mode, selectedDate, inCurrentMonth } = meta;
function getCalendarDayHTML(day, meta, dayState, dayAttr, theme = {}) {
const { mode, selectedDate } = meta;
const isSelected = selectedDate && sameDay(day, selectedDate);
const showIndicator = !!dayState.showIndicator;
const isDisabled = !!dayState.disabled;
const isDimmed = !!dayState.dimmed && !isSelected;
const bg = isSelected ? '#23221e' : '#2f2f2d';
const border = isSelected ? '#787876' : '#444442';
const text = isSelected ? '#f2efe8' : (isDimmed ? '#7d7a74' : '#d7d2c8');
const dot = isSelected ? '#f2efe8' : '#a59f92';
const opacity = isDimmed ? '0.72' : '1';
const outerClass = `${mode === 'month' ? 'mx-auto ' : ''}flex h-[2.05rem] w-full min-w-0 max-w-full items-center justify-center rounded-full border text-xs font-medium transition-colors leading-tight overflow-hidden`;
const defaultBg = '#2f2f2d';
const defaultBorder = '#444442';
const defaultText = '#d7d2c8';
let bg;
let borderColor;
let text;
let borderClass = 'border';
if (isSelected) {
bg = theme.selectedBg || '#23221e';
borderColor = theme.selectedBorder || '#787876';
text = theme.selectedText || '#f2efe8';
} else if (isDimmed) {
bg = theme.dimmedBg ?? theme.bg ?? defaultBg;
text = theme.dimText || '#7d7a74';
borderClass = 'border-0';
} else {
bg = theme.bg || defaultBg;
borderColor = theme.border || defaultBorder;
text = theme.text || defaultText;
}
const dot = isSelected ? (theme.selectedDot || '#f2efe8') : (theme.dot || '#a59f92');
const opacity = isDimmed ? String(theme.dimOpacity ?? 0.72) : '1';
const borderStyle = isDimmed ? 'border:none;' : `border-color:${borderColor};`;
const outerClass = `${mode === 'month' ? 'mx-auto ' : ''}flex h-[2.05rem] w-full min-w-0 max-w-full items-center justify-center rounded-full ${borderClass} text-xs font-medium transition-colors leading-tight overflow-hidden`;
const innerClass = mode === 'month'
? 'relative flex h-full w-full flex-col items-center justify-center'
: 'relative flex h-full w-full items-center justify-center';
@@ -43,7 +64,7 @@ function getCalendarDayHTML(day, meta, dayState, dayAttr) {
return `
<${tagName}${buttonAttrs}
class="${outerClass}"
style="background:${bg};border-color:${border};color:${text};opacity:${opacity};">
style="background:${bg};${borderStyle}color:${text};opacity:${opacity};">
<span class="${innerClass}">
<span class="text-[13px] font-semibold leading-none ${showIndicator ? '-translate-y-[0.18rem]' : ''}">${day.getDate()}</span>
${showIndicator
@@ -82,37 +103,39 @@ function getDayState(day, meta, resolveDayState) {
};
}
export function createCalendarWeekdayHeaderHTML(labels = CALENDAR_WEEKDAYS_SHORT) {
export function createCalendarWeekdayHeaderHTML(labels = CALENDAR_WEEKDAYS_SHORT, {
wrapperClass = 'grid grid-cols-7 gap-1.5 text-center text-[8px] font-medium text-gray-400 uppercase tracking-wide mb-1 leading-none',
} = {}) {
return `
<div class="grid grid-cols-7 gap-1.5 text-center text-[8px] font-medium text-gray-400 uppercase tracking-wide mb-1 leading-none">
<div class="${wrapperClass}">
${labels.map((label) => `<div>${label}</div>`).join('')}
</div>
`;
}
export function createCalendarTopbarHTML({
titleId,
prevId,
todayId,
nextId,
wrapperClass = 'px-4 pt-4 pb-3 flex items-center gap-3',
titleClass = 'text-[18px] font-semibold text-gray-900 leading-none tracking-[-0.03em]',
wrapperClass = 'px-4 pt-4 pb-3 flex items-center justify-end',
controlsStyle = 'background:#2f2f2d;border-color:#444442;',
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',
}) {
return `
<div class="${wrapperClass}">
<div class="min-w-0 flex-1">
<p id="${titleId}" class="${titleClass}"></p>
</div>
<div class="shrink-0 flex h-[2.3rem] items-center gap-0.5 rounded-full border px-1" style="background:#2f2f2d;border-color:#444442;">
<button type="button" id="${prevId}" class="shrink-0 w-8 h-full flex items-center justify-center rounded-full border-0 bg-transparent text-[#d7d2c8] transition-colors" aria-label="Poprzedni okres">
<i class="fas fa-chevron-left text-[11px]" aria-hidden="true"></i>
<div class="flex h-[2.05rem] min-w-0 max-w-[min(100%,20rem)] items-center gap-px rounded-full border px-0.5" style="${controlsStyle}">
<button type="button" id="${prevId}" class="${navButtonClass}" aria-label="Poprzedni okres">
<i class="fas fa-chevron-left text-[10px]" aria-hidden="true"></i>
</button>
<button type="button" id="${todayId}" title="Dziś" aria-label="Przejdź do dzisiejszego dnia"
class="h-full shrink-0 inline-flex items-center justify-center rounded-full px-2.5 text-[11px] font-semibold leading-none text-[#d7d2c8] transition-colors hover:bg-[#3a3a37]">
Dziś
<button type="button" id="${todayId}"
class="${todayButtonActiveClass}"
data-cal-active-class="${todayButtonActiveClass}"
data-cal-dim-class="${todayButtonDimClass}">
</button>
<button type="button" id="${nextId}" class="shrink-0 w-8 h-full flex items-center justify-center rounded-full border-0 bg-transparent text-[#d7d2c8] transition-colors" aria-label="Następny okres">
<i class="fas fa-chevron-right text-[11px]" aria-hidden="true"></i>
<button type="button" id="${nextId}" class="${navButtonClass}" aria-label="Następny okres">
<i class="fas fa-chevron-right text-[10px]" aria-hidden="true"></i>
</button>
</div>
</div>
@@ -134,13 +157,25 @@ export function isCalendarOnToday(mode, weekStart, monthAnchor, selectedDate) {
return startOfMonth(monthAnchor).getTime() === startOfMonth(today).getTime();
}
export function syncCalendarTodayButton(buttonEl, isOnToday) {
/**
* Środkowy przycisk pokazuje wybraną datę; działa jak „Dziś” (skok do bieżącego okresu).
* Styl pozostaje jak aktywny — bez wyciszania przy isOnToday.
*/
export function syncCalendarTodayButton(buttonEl, isOnToday, selectedDate, options = {}) {
if (!buttonEl) return;
const base = 'h-full shrink-0 inline-flex items-center justify-center rounded-full px-2.5 text-[11px] font-semibold leading-none transition-colors';
const active = `${base} text-[#d7d2c8] hover:bg-[#3a3a37]`;
const dim = `${base} text-[#7d7a74] cursor-default`;
buttonEl.className = isOnToday ? dim : active;
buttonEl.disabled = isOnToday;
const {
ariaLabelGo = 'Przejdź do dzisiejszego dnia',
ariaLabelCurrent = 'Widok jest ustawiony na bieżący okres',
} = options;
const active = buttonEl.dataset.calActiveClass
|| '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';
if (selectedDate != null) {
buttonEl.textContent = formatCalendarSelectedDate(selectedDate);
}
buttonEl.className = active;
buttonEl.removeAttribute('disabled');
buttonEl.setAttribute('aria-disabled', isOnToday ? 'true' : 'false');
buttonEl.setAttribute('aria-label', isOnToday ? ariaLabelCurrent : ariaLabelGo);
}
export function renderCalendarGrid({
@@ -150,6 +185,7 @@ export function renderCalendarGrid({
selectedDate,
resolveDayState,
dayAttr = CALENDAR_DAY_ATTR,
theme,
}) {
if (!gridEl) return;
@@ -163,7 +199,7 @@ export function renderCalendarGrid({
selectedDate,
inCurrentMonth: true,
};
cells.push(getCalendarDayHTML(day, meta, getDayState(day, meta, resolveDayState), dayAttr));
cells.push(getCalendarDayHTML(day, meta, getDayState(day, meta, resolveDayState), dayAttr, theme));
}
gridEl.innerHTML = cells.join('');
return;
@@ -176,7 +212,7 @@ export function renderCalendarGrid({
selectedDate,
inCurrentMonth: day.getMonth() === month,
};
return getCalendarDayHTML(day, meta, getDayState(day, meta, resolveDayState), dayAttr);
return getCalendarDayHTML(day, meta, getDayState(day, meta, resolveDayState), dayAttr, theme);
}).join('');
}

View File

@@ -20,12 +20,10 @@ import {
bindCalendarDayClicks,
createCalendarTopbarHTML,
createCalendarWeekdayHeaderHTML,
formatCalendarMonthYear,
formatCalendarSelectedDate,
isCalendarOnToday,
renderCalendarGrid,
syncCalendarTodayButton,
} from './mealCalendar.js?v=1';
} from './mealCalendar.js?v=11';
import { createIngredientCardController, getIngredientCardHTML } from './ingredientCard.js?v=20260410-107';
function esc(s) {
@@ -56,11 +54,10 @@ export function getMealPlanEditorHTML() {
<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;">
<div id="mpe-cal-section" class="hidden">
${createCalendarTopbarHTML({
titleId: 'mpe-cal-title',
prevId: 'mpe-cal-prev',
todayId: 'mpe-cal-today',
nextId: 'mpe-cal-next',
wrapperClass: 'mb-2 flex items-center gap-3',
wrapperClass: 'mb-2 flex items-center justify-end gap-3',
})}
${createCalendarWeekdayHeaderHTML()}
<div id="mpe-cal-grid" class="grid grid-cols-7 gap-1.5"></div>
@@ -174,21 +171,20 @@ export function setupMealPlanEditor() {
wrap.classList.remove('hidden');
sec.classList.remove('hidden');
const grid = document.getElementById('mpe-cal-grid');
const title = document.getElementById('mpe-cal-title');
const todayBtn = document.getElementById('mpe-cal-today');
const icon = document.getElementById('mpe-cal-toggle-icon');
if (!grid || !title) return;
if (!grid) return;
const today = startOfDay(new Date());
const plans = loadPlans();
const mode = S.calExpanded ? 'month' : 'week';
title.textContent = S.calExpanded ? formatCalendarMonthYear(S.calDate) : formatCalendarSelectedDate(S.date);
if (icon) {
icon.className = S.calExpanded ? 'fas fa-chevron-up text-[10px]' : 'fas fa-chevron-down text-[10px]';
}
syncCalendarTodayButton(
todayBtn,
isCalendarOnToday(mode, startOfWeekMonday(S.calDate), S.calDate, S.date),
S.date,
);
renderCalendarGrid({

View File

@@ -3,26 +3,23 @@ import { MEAL_SLOTS } from '../planner/mealSlots.js';
import { applyFilters, getFilterState } from './RecipeList.js';
const FILTER_PANEL_TRANSITION = 'opacity 180ms ease, transform 180ms ease';
const FILTER_SURFACE = '#2d2e2b';
const FILTER_SURFACE_SOFT = '#2f2f2d';
const FILTER_BORDER = '#444442';
const FILTER_BORDER_ACTIVE = '#787876';
const FILTER_CHIP_ACTIVE = '#23221e';
const FILTER_TEXT_PRIMARY = '#ddd6ca';
const FILTER_SURFACE = '#23221e';
const FILTER_SURFACE_SOFT = '#2d2e2b';
const FILTER_BORDER = '#787876';
const FILTER_CHIP_ACTIVE_BG = '#393937';
const FILTER_TEXT_SECONDARY = '#d7d2c8';
const FILTER_TEXT_MUTED = '#9b978f';
const FILTER_TEXT_DIM = '#7d7a74';
const FILTER_TEXT_MUTED = '#b5afa5';
const FILTER_TEXT_ACTIVE = '#f2efe8';
const FILTER_TRACK = '#393937';
const FILTER_TRACK_FILL = '#56534f';
const FILTER_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 PREP_TIME_MIN = 5;
const PREP_TIME_MAX = 120;
const PREP_TIME_STEP = 5;
const PREP_TIME_MIN_GAP = PREP_TIME_STEP;
const FILTER_RECIPE_BLUR = 'blur(3px) saturate(0.94)';
const FILTER_CONTEXTS = {
recipes: {
anchorShellId: 'recipe-search-shell',
anchorShellId: 'recipe-topbar',
buttonId: 'recipe-filter-btn',
getState: () => getFilterState(),
applyState: (nextState) => applyFilters(nextState),
@@ -82,35 +79,31 @@ export function getFilterHTML() {
outline: none;
}
</style>
<div id="filter-view" class="absolute inset-0 z-[70] hidden opacity-0 transition-opacity duration-150" style="pointer-events:none; background:rgba(0,0,0,0.5) !important; background-image:none !important;" aria-hidden="true">
<div id="filter-panel" class="absolute flex flex-col overflow-hidden rounded-[1.5rem] border" style="background:${FILTER_SURFACE} !important; background-image:none !important; border-color:${FILTER_BORDER} !important; opacity:0; transform:translateY(-0.5rem) scale(0.98); transform-origin:top center; transition:${FILTER_PANEL_TRANSITION}; box-shadow:0 18px 40px rgba(0,0,0,0.34), 0 4px 12px rgba(0,0,0,0.18); width:min(calc(100% - 1.5rem), 22rem);">
<div class="pointer-events-none absolute inset-x-0 top-0 h-px" style="background:rgba(242,239,232,0.12);" aria-hidden="true"></div>
<div class="shrink-0 px-3.5 pt-3 pb-2 flex justify-end" style="background:${FILTER_SURFACE} !important; background-image:none !important;">
<div class="min-w-0 flex items-center justify-end gap-2">
<button id="filter-clear-btn" type="button" class="shrink-0 h-8 px-3 rounded-full border text-[11px] font-semibold transition-colors" style="background:${FILTER_SURFACE_SOFT} !important; border-color:${FILTER_BORDER} !important; color:${FILTER_TEXT_SECONDARY} !important;">Wyczyść</button>
</div>
<div id="filter-view" class="absolute inset-0 z-[70] hidden opacity-0 transition-opacity duration-150" style="pointer-events:none; background:transparent !important; background-image:none !important;" aria-hidden="true">
<div id="filter-panel" class="absolute flex flex-col overflow-hidden rounded-[1.35rem]" style="background:${FILTER_SURFACE} !important; background-image:none !important; border:1px solid ${FILTER_BORDER} !important; opacity:0; transform:translateY(-0.5rem) scale(0.98); transform-origin:top center; transition:${FILTER_PANEL_TRANSITION}; box-shadow:${FILTER_SHADOW}; width:min(calc(100% - 1.5rem), 22rem);">
<div class="shrink-0 px-3 pt-3 pb-2 flex items-center justify-between gap-3">
<p class="text-[11px] font-semibold leading-none" style="color:${FILTER_TEXT_ACTIVE};">Filtry</p>
<button id="filter-clear-btn" type="button" class="h-8 px-2 rounded-full text-[11px] font-semibold transition-colors" style="background:transparent; border:none; color:${FILTER_TEXT_MUTED};">Wyczyść</button>
</div>
<div id="filter-panel-body" class="min-h-0 flex-1 overflow-y-auto no-scrollbar px-4 pb-4 space-y-2.5" style="background:${FILTER_SURFACE} !important; background-image:none !important;">
<section id="filter-slot-section" class="p-3.5" style="background:${FILTER_SURFACE} !important; background-image:none !important;">
<p class="text-[10px] font-bold uppercase tracking-wider mb-3" style="color:${FILTER_TEXT_MUTED};">Pora posiłku</p>
<div id="filter-slot-chips" class="flex flex-wrap gap-2"></div>
<div id="filter-panel-body" class="min-h-0 flex-1 overflow-y-auto no-scrollbar px-3 pb-3 space-y-4">
<section id="filter-slot-section">
<p class="text-[10px] font-bold uppercase tracking-wider mb-3 px-0.5" style="color:${FILTER_TEXT_MUTED};">Pora posiłku</p>
<div id="filter-slot-chips" class="flex flex-wrap gap-2 px-1.5"></div>
</section>
<section class="p-3.5" style="background:${FILTER_SURFACE} !important; background-image:none !important;">
<p class="text-[10px] font-bold uppercase tracking-wider mb-3" style="color:${FILTER_TEXT_MUTED};">Dieta i tagi</p>
<div id="filter-tag-chips" class="flex flex-wrap gap-2"></div>
<section>
<p class="text-[10px] font-bold uppercase tracking-wider mb-3 px-0.5" style="color:${FILTER_TEXT_MUTED};">Dieta i tagi</p>
<div id="filter-tag-chips" class="flex flex-wrap gap-2 px-1.5"></div>
</section>
<section class="p-3.5" style="background:${FILTER_SURFACE} !important; background-image:none !important;">
<div class="flex items-center justify-between gap-3 mb-3">
<div class="min-w-0">
<p class="text-[10px] font-bold uppercase tracking-wider" style="color:${FILTER_TEXT_MUTED};">Czas przygotowania</p>
</div>
<section>
<div class="flex items-center justify-between gap-3 mb-3 px-0.5">
<p class="text-[10px] font-bold uppercase tracking-wider" style="color:${FILTER_TEXT_MUTED};">Czas przygotowania</p>
<span id="time-display-range" class="shrink-0 text-[11px] font-semibold tabular-nums text-right" style="color:${FILTER_TEXT_ACTIVE};">5 min - 120 min</span>
</div>
<div class="px-1">
<div class="relative h-9">
<div class="mx-1.5">
<div class="relative h-9 mx-2">
<div class="prep-time-range-track absolute inset-x-0 top-1/2 -translate-y-1/2" style="background:${FILTER_TRACK};" aria-hidden="true"></div>
<div id="prep-time-range-fill" class="prep-time-range-fill absolute top-1/2 -translate-y-1/2" style="background:${FILTER_TRACK_FILL}; left:0%; width:100%;" aria-hidden="true"></div>
<button
@@ -183,10 +176,13 @@ function formatTimeRangeSummary(minMinutes, maxMinutes) {
}
function getChipStyle(active) {
const background = active ? FILTER_CHIP_ACTIVE : FILTER_SURFACE_SOFT;
const border = active ? FILTER_BORDER_ACTIVE : FILTER_BORDER;
const background = active ? FILTER_CHIP_ACTIVE_BG : FILTER_SURFACE_SOFT;
const color = active ? FILTER_TEXT_ACTIVE : FILTER_TEXT_SECONDARY;
return `background:${background} !important; background-image:none !important; box-shadow:none !important; border-color:${border} !important; color:${color} !important;`;
const borderRule = active ? `border:1px solid ${FILTER_BORDER};` : '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}`;
}
function clampTimeValue(value) {
@@ -283,6 +279,8 @@ function showFilterPanel() {
positionFilterPanel();
setRecipeAreaBlur(true);
syncPanelCount();
requestAnimationFrame(() => {
view.classList.add('opacity-100');
panel.style.opacity = '1';
@@ -301,6 +299,7 @@ function hideFilterPanel() {
panel.style.opacity = '0';
panel.style.transform = 'translateY(-0.5rem) scale(0.98)';
setRecipeAreaBlur(false);
syncPanelCount();
closeTimer = setTimeout(() => {
view.classList.add('hidden');
@@ -317,7 +316,7 @@ function renderSlotChips() {
wrap.innerHTML = MEAL_SLOTS.map((slot) => {
const active = localSlots.includes(slot.id);
return `<button type="button" data-filter-slot="${escapeHtml(slot.id)}" class="px-3 py-1.5 rounded-full border text-[12px] font-semibold transition-colors" style="${getChipStyle(active)}">${escapeHtml(slot.label)}</button>`;
return `<button type="button" data-filter-slot="${escapeHtml(slot.id)}" class="px-3 py-2 rounded-full text-[11px] font-semibold transition-colors" style="${getChipStyle(active)}">${escapeHtml(slot.label)}</button>`;
}).join('');
wrap.querySelectorAll('[data-filter-slot]').forEach((btn) => {
@@ -339,7 +338,7 @@ function renderTagChips() {
const allTags = collectAllTags();
wrap.innerHTML = allTags.map((tag) => {
const active = localTags.includes(tag.toLowerCase());
return `<button type="button" data-filter-tag="${escapeHtml(tag)}" class="px-3 py-1.5 rounded-full border text-[12px] font-semibold transition-colors" style="${getChipStyle(active)}">${escapeHtml(tag)}</button>`;
return `<button type="button" data-filter-tag="${escapeHtml(tag)}" class="px-3 py-2 rounded-full text-[11px] font-semibold transition-colors" style="${getChipStyle(active)}">${escapeHtml(tag)}</button>`;
}).join('');
wrap.querySelectorAll('[data-filter-tag]').forEach((btn) => {
@@ -360,6 +359,33 @@ function syncFilterSections() {
slotSection.classList.toggle('hidden', !getActiveFilterConfig().showSlots);
}
function getActiveFilterCount() {
const config = getActiveFilterConfig();
let count = localTags.length;
if (config.showSlots) count += localSlots.length;
if (localMinMinutes > PREP_TIME_MIN || localMaxMinutes < PREP_TIME_MAX) count += 1;
return count;
}
function syncPanelCount() {
const count = getActiveFilterCount();
const { buttonId } = getActiveFilterConfig();
const button = buttonId ? document.getElementById(buttonId) : null;
if (button) {
const highlight = isFilterPanelOpen() || count > 0;
button.style.setProperty('background', highlight ? '#23221e' : '#393937', 'important');
button.style.setProperty('border-color', highlight ? '#787876' : '#41423f', 'important');
button.style.setProperty('color', highlight ? '#f2efe8' : '#ddd6ca', 'important');
}
const badge = button?.querySelector('[id$="-filter-count"]');
if (!badge) return;
badge.textContent = String(count);
badge.classList.toggle('hidden', count === 0);
badge.classList.toggle('flex', count > 0);
}
function syncLiveFilters() {
const config = getActiveFilterConfig();
config.applyState?.({
@@ -368,6 +394,7 @@ function syncLiveFilters() {
minMinutes: localMinMinutes,
maxMinutes: localMaxMinutes,
});
syncPanelCount();
}
export function setupFilter() {
@@ -522,6 +549,7 @@ export function setupFilter() {
renderSlotChips();
renderTagChips();
syncFilterSections();
syncPanelCount();
showFilterPanel();
};

View File

@@ -30,13 +30,11 @@ import {
bindCalendarDayClicks,
createCalendarTopbarHTML,
createCalendarWeekdayHeaderHTML,
formatCalendarMonthYear,
formatCalendarSelectedDate,
isCalendarOnToday,
renderCollapsibleCalendar,
syncCalendarTodayButton,
syncCollapsibleCalendarMode,
} from '../ui/mealCalendar.js?v=1';
} from '../ui/mealCalendar.js?v=11';
import {
filterRecipesByQuery,
renderRecipeGrid,
@@ -66,6 +64,7 @@ function syncTodayButton(mode, weekStart, monthAnchor, selected) {
syncCalendarTodayButton(
document.getElementById('cal-go-today'),
isCalendarOnToday(mode, weekStart, monthAnchor, selected),
selected,
);
}
@@ -73,13 +72,15 @@ export function getMealPlannerHTML() {
return `
<div id="planner-view" class="hidden flex flex-col h-full absolute inset-0 overflow-hidden bg-[#2d2e2b] z-10">
<div id="planner-cal-bar" class="shrink-0 bg-[#2d2e2b] border-b border-[#444442] mt-3 relative z-10">
${createCalendarTopbarHTML({
titleId: 'cal-period-label',
<div class="min-h-12 px-4 pt-4 pb-3 flex items-center justify-between gap-3 min-w-0">
<h1 class="min-w-0 flex-1 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;">Plan posiłków</h1>
${createCalendarTopbarHTML({
prevId: 'cal-prev',
todayId: 'cal-go-today',
nextId: 'cal-next',
titleClass: 'text-[18px] font-semibold text-[#ddd6ca] leading-none tracking-[-0.03em]',
wrapperClass: 'flex shrink-0 items-center justify-end',
})}
</div>
<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()}
@@ -185,17 +186,6 @@ export function getMealPlannerHTML() {
`;
}
function updatePeriodLabel(mode, weekStart, monthAnchor, selected) {
const el = document.getElementById('cal-period-label');
if (!el) return;
if (mode === 'week') {
el.textContent = formatCalendarSelectedDate(selected);
} else {
el.textContent = formatCalendarMonthYear(monthAnchor);
}
}
function syncModeToggle(mode) {
syncCollapsibleCalendarMode({
mode,
@@ -1140,7 +1130,6 @@ export function setupMealPlanner() {
const rerender = () => {
syncModeToggle(state.mode);
updatePeriodLabel(state.mode, state.weekStart, state.monthAnchor, state.selected);
syncTodayButton(state.mode, state.weekStart, state.monthAnchor, state.selected);
renderCollapsibleCalendar({
weekGridEl: weekGrid,

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();
}
});
}

View File

@@ -1,13 +1,12 @@
import { RECIPES } from '../data/catalog.js?v=8';
import { getRecipeGridSectionHTML, renderRecipeGrid } from '../ui/recipeGrid.js';
import {
getRecipeSearchFieldHTML,
syncRecipeSearchShellShadow,
} from '../ui/recipeSearchField.js';
const DEFAULT_MIN_MINUTES = 5;
const DEFAULT_MAX_MINUTES = 120;
/** Jak w spiżarni — cień „pigówek” i powłoki wyszukiwania */
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)';
let filterState = {
query: '',
slots: [],
@@ -16,6 +15,9 @@ let filterState = {
maxMinutes: DEFAULT_MAX_MINUTES,
};
let isSearchExpanded = false;
let recipeListDocListenersBound = false;
function matchesFilters(recipe) {
const { query, slots, tags, minMinutes, maxMinutes } = filterState;
@@ -46,7 +48,51 @@ function getFilteredRecipes() {
function syncRecipeScrollShadow() {
const searchShell = document.getElementById('recipe-search-shell');
syncRecipeSearchShellShadow(searchShell);
if (searchShell) {
searchShell.style.boxShadow = SEARCH_SHELL_SHADOW;
}
}
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 = SEARCH_SHELL_SHADOW;
}
}
function closeSearch() {
const input = document.getElementById('recipe-search-input');
const hadQuery = Boolean(input?.value);
if (input) {
input.value = '';
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() {
@@ -61,6 +107,7 @@ function renderGrid() {
showSlotLabels: false,
cardClassName: 'recipe-list-card',
});
syncRecipeTopbarUI();
requestAnimationFrame(syncRecipeScrollShadow);
}
@@ -68,15 +115,33 @@ 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;">
<div id="recipe-top-bar" 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">
${getRecipeSearchFieldHTML({
shellId: 'recipe-search-shell',
inputId: 'recipe-search-input',
placeholder: 'Szukaj przepisów...',
filterButtonId: 'recipe-filter-btn',
filterButtonAction: 'openFilters()',
filterButtonLabel: 'Otwórz filtry',
})}
<div class="pointer-events-auto relative z-[1] mx-auto" style="width:min(calc(100% - 0.5rem), 22.4rem);">
<div id="recipe-topbar" class="relative min-h-12">
<div id="recipe-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="min-w-0 flex-1 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;">Katalog przepisów</h1>
<div id="recipe-filter-wrap" class="relative shrink-0">
<button type="button" id="recipe-filter-btn" 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;" aria-label="Otwórz filtry">
<i class="fas fa-sliders-h text-[12px]" aria-hidden="true"></i>
<span id="recipe-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>
<button type="button" id="recipe-search-toggle" class="w-11 h-11 rounded-full shrink-0 flex items-center justify-center transition-all duration-200" style="background:#393937 !important; border:1px solid #41423f !important; box-shadow:${SEARCH_SHELL_SHADOW} !important; color:#ddd6ca;" aria-label="Szukaj">
<i class="fas fa-search text-[13px]" aria-hidden="true"></i>
</button>
</div>
<div id="recipe-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="recipe-search-input" autocomplete="off" placeholder="Szukaj przepisów…"
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;">
<button type="button" id="recipe-search-close" class="w-8 h-8 rounded-full shrink-0 flex items-center justify-center transition-colors" style="background:#2f2f2d; border:none; color:#9b978f;">
<i class="fas fa-xmark text-[13px]"></i>
</button>
</div>
</div>
</div>
</div>
@@ -84,7 +149,7 @@ export function getRecipeListHTML() {
scrollId: 'recipe-scroll',
gridId: 'recipe-grid',
emptyStateId: 'recipe-empty-state',
scrollClassName: 'relative flex-1 overflow-y-auto px-4 pt-24 pb-24 bg-[#2d2e2b]',
scrollClassName: 'relative flex-1 overflow-y-auto px-4 pt-[5.35rem] pb-24 bg-[#2d2e2b]',
gridClassName: 'grid grid-cols-3 gap-2 bg-[#2d2e2b]',
emptyTitle: 'Brak wyników',
emptyMessage: 'Zmień kryteria wyszukiwania lub filtry',
@@ -117,6 +182,21 @@ export function setupRecipeList() {
filterState.query = e.target.value.trim();
renderGrid();
});
document.getElementById('recipe-search-input')?.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeSearch();
});
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();
}
window.openFilters?.('recipes');
});
document.getElementById('recipe-grid')?.addEventListener('click', (e) => {
const card = e.target.closest('.recipe-browser-card');
@@ -127,4 +207,14 @@ export function setupRecipeList() {
document.getElementById('recipe-scroll')?.addEventListener('scroll', syncRecipeScrollShadow);
syncRecipeScrollShadow();
if (!recipeListDocListenersBound) {
recipeListDocListenersBound = true;
document.addEventListener('keydown', (e) => {
if (e.key !== 'Escape' || !isSearchExpanded) return;
if (!document.getElementById('main-view')?.classList.contains('hidden')) {
closeSearch();
}
});
}
}