Reorganizacja górnych paneli i ujednolicenie stylu filtrów

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

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

View File

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