Compare commits
2 Commits
230100b63f
...
8ed15bbe36
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ed15bbe36 | |||
| 4d7a1a12ae |
47
index.html
47
index.html
@@ -254,9 +254,7 @@
|
||||
}
|
||||
#main-view > div:first-child,
|
||||
#planner-view > div:first-child,
|
||||
#pantry-view > div:first-child,
|
||||
#filter-view > div:first-child,
|
||||
#filter-view > div:last-child {
|
||||
#pantry-view > div:first-child {
|
||||
background: rgb(var(--app-bg-rgb)) !important;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
@@ -345,7 +343,38 @@
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.04), rgba(var(--app-bg-rgb), 0.5) 92%);
|
||||
pointer-events: none;
|
||||
}
|
||||
#recipe-search-shell,
|
||||
#recipe-topbar #recipe-search-shell {
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
margin-inline: 0;
|
||||
isolation: auto;
|
||||
}
|
||||
#recipe-topbar #recipe-search-shell::after {
|
||||
display: none;
|
||||
}
|
||||
#recipe-topbar #recipe-search-shell,
|
||||
#recipe-topbar #recipe-search-shell:focus-within {
|
||||
background: #23221e !important;
|
||||
border: 1px solid #787876 !important;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
#recipe-topbar #recipe-search-input {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
color: #ddd6ca !important;
|
||||
caret-color: #ddd6ca;
|
||||
}
|
||||
#recipe-topbar #recipe-search-input::placeholder {
|
||||
color: #9b978f !important;
|
||||
opacity: 1;
|
||||
}
|
||||
#recipe-topbar #recipe-filter-btn {
|
||||
border-radius: 999px;
|
||||
}
|
||||
#planner-picker-search-shell {
|
||||
min-height: 3rem;
|
||||
width: min(calc(100% - 0.5rem), 22.4rem);
|
||||
@@ -366,7 +395,6 @@
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
}
|
||||
#recipe-search-shell::after,
|
||||
#planner-picker-search-shell::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -381,7 +409,6 @@
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
#recipe-search-shell:focus-within,
|
||||
#planner-picker-search-shell:focus-within {
|
||||
background: #393937 !important;
|
||||
border: 1px solid #4a4b47 !important;
|
||||
@@ -393,7 +420,6 @@
|
||||
inset 0 2px 7px rgba(0, 0, 0, 0.18),
|
||||
inset 0 -1px 2px rgba(255, 255, 255, 0.03) !important;
|
||||
}
|
||||
#recipe-search-input,
|
||||
#planner-picker-search {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
@@ -407,20 +433,17 @@
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
#recipe-search-input::placeholder,
|
||||
#planner-picker-search::placeholder,
|
||||
#pantry-search::placeholder {
|
||||
color: #beb8ae !important;
|
||||
opacity: 1;
|
||||
}
|
||||
#recipe-filter-btn,
|
||||
#planner-picker-filter-btn {
|
||||
border-radius: 999px;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
#recipe-filter-btn:hover,
|
||||
#planner-picker-filter-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.03) !important;
|
||||
}
|
||||
@@ -440,7 +463,7 @@
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
#filter-view {
|
||||
background: rgb(var(--app-bg-rgb)) !important;
|
||||
background: transparent !important;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
#planner-picker-backdrop,
|
||||
@@ -601,7 +624,7 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const APP_ASSET_VERSION = '20260414-118';
|
||||
const APP_ASSET_VERSION = '20260415-163';
|
||||
const APP_VERSION_STORAGE_KEY = 'recipe-app-asset-version';
|
||||
const APP_VERSION_QUERY_KEY = 'appv';
|
||||
|
||||
|
||||
@@ -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('');
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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">
|
||||
<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>
|
||||
</div>
|
||||
<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();
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
<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({
|
||||
titleId: 'cal-period-label',
|
||||
prevId: 'cal-prev',
|
||||
todayId: 'cal-go-today',
|
||||
nextId: 'cal-next',
|
||||
titleClass: 'text-[18px] font-semibold text-[#ddd6ca] leading-none tracking-[-0.03em]',
|
||||
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,
|
||||
|
||||
@@ -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-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 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 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user