Apply liquid glass to filter panel in recipe list

This commit is contained in:
2026-04-22 18:42:29 +02:00
parent bc505d6b4c
commit 7049cb1d48
3 changed files with 110 additions and 29 deletions

View File

@@ -3,15 +3,15 @@ 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 = 'rgb(var(--sunken-rgb))';
const FILTER_SURFACE_SOFT = 'rgb(var(--app-bg-rgb))';
const FILTER_BORDER = 'rgb(var(--border-input-rgb))';
const FILTER_CHIP_ACTIVE_BG = 'rgb(var(--card-rgb))';
const FILTER_TEXT_SECONDARY = 'rgb(var(--text-body-soft-rgb))';
const FILTER_TEXT_MUTED = 'rgb(var(--text-muted-rgb))';
const FILTER_TEXT_ACTIVE = 'rgb(var(--text-emphasis-rgb))';
const FILTER_TRACK = 'rgb(var(--card-rgb))';
const FILTER_TRACK_FILL = 'rgba(var(--border-input-rgb), 0.58)';
const FILTER_SURFACE = 'var(--filter-liquid-panel-bg)';
const FILTER_SURFACE_SOFT = 'var(--filter-liquid-chip-bg)';
const FILTER_BORDER = 'var(--filter-liquid-border)';
const FILTER_CHIP_ACTIVE_BG = 'var(--filter-liquid-chip-active-bg)';
const FILTER_TEXT_SECONDARY = 'var(--filter-liquid-text-secondary)';
const FILTER_TEXT_MUTED = 'var(--filter-liquid-text-muted)';
const FILTER_TEXT_ACTIVE = 'var(--filter-liquid-text-active)';
const FILTER_TRACK = 'var(--filter-liquid-track-bg)';
const FILTER_TRACK_FILL = 'var(--filter-liquid-accent-bg)';
const PREP_TIME_MIN = 5;
const PREP_TIME_MAX = 120;
const PREP_TIME_STEP = 5;
@@ -57,10 +57,61 @@ function collectAllTags() {
export function getFilterHTML() {
return `
<style id="filter-view-styles">
#filter-view {
--filter-liquid-panel-bg: rgba(var(--app-bg-rgb), 0.78);
--filter-liquid-panel-shine: none;
--filter-liquid-border: rgba(255, 255, 255, 0.32);
--filter-liquid-chip-bg: rgba(var(--surface-rgb), 0.68);
--filter-liquid-chip-active-bg: rgba(var(--surface-rgb), 0.92);
--filter-liquid-track-bg: rgba(var(--overlay-rgb), 0.16);
--filter-liquid-accent-bg: rgba(255, 255, 255, 0.72);
--filter-liquid-text-active: rgb(var(--text-emphasis-rgb));
--filter-liquid-text-secondary: rgb(var(--text-body-soft-rgb));
--filter-liquid-text-muted: rgb(var(--text-muted-rgb));
--filter-liquid-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.6),
inset 0 -1px 0 rgba(var(--overlay-rgb), 0.06),
0 8px 20px rgba(var(--overlay-rgb), 0.16),
0 22px 52px rgba(var(--overlay-rgb), 0.24);
}
.dark #filter-view {
--filter-liquid-panel-bg: rgba(var(--app-bg-rgb), 0.82);
--filter-liquid-border: rgba(255, 255, 255, 0.12);
--filter-liquid-chip-bg: rgba(255, 255, 255, 0.08);
--filter-liquid-chip-active-bg: rgba(255, 255, 255, 0.16);
--filter-liquid-track-bg: rgba(255, 255, 255, 0.1);
--filter-liquid-accent-bg: rgba(255, 255, 255, 0.32);
--filter-liquid-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.24),
inset 0 -1px 0 rgba(0, 0, 0, 0.22),
0 10px 24px rgba(0, 0, 0, 0.36),
0 26px 60px rgba(0, 0, 0, 0.46);
}
#filter-view.filter-open {
pointer-events: auto;
}
#filter-panel {
isolation: isolate;
background: ${FILTER_SURFACE} !important;
background-image: none !important;
border: 1px solid ${FILTER_BORDER} !important;
box-shadow: var(--filter-liquid-shadow) !important;
backdrop-filter: blur(28px) saturate(180%);
-webkit-backdrop-filter: blur(28px) saturate(180%);
}
#filter-panel::before {
display: none;
}
#filter-panel > * {
position: relative;
z-index: 1;
}
#filter-view .prep-time-range-track,
#filter-view .prep-time-range-fill {
border-radius: 9999px;
@@ -71,15 +122,27 @@ export function getFilterHTML() {
width: 1rem;
height: 1rem;
border-radius: 9999px;
border: 1px solid rgba(var(--text-emphasis-rgb),0.16);
background: ${FILTER_TRACK_FILL};
box-shadow: 0 0 0 1px rgba(var(--overlay-rgb),0.12);
border: 1px solid rgba(255,255,255,0.34);
background: rgba(var(--surface-rgb),0.42);
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.42),
0 0 0 1px rgba(var(--overlay-rgb),0.1),
0 4px 10px rgba(var(--overlay-rgb),0.18);
touch-action: none;
outline: none;
}
.dark #filter-view .prep-time-range-handle {
border-color: rgba(255,255,255,0.18);
background: rgba(255,255,255,0.26);
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.24),
0 0 0 1px rgba(0,0,0,0.22),
0 4px 12px rgba(0,0,0,0.34);
}
</style>
<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:var(--shadow-shell); width:min(calc(100% - 1.5rem), 22rem);">
<div id="filter-panel" class="absolute flex flex-col overflow-hidden rounded-[1.35rem]" style="opacity:0; transform:translateY(0.65rem) scale(0.98); transform-origin:bottom center; transition:${FILTER_PANEL_TRANSITION}; 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>
@@ -179,8 +242,8 @@ function getChipStyle(active) {
const color = active ? FILTER_TEXT_ACTIVE : FILTER_TEXT_SECONDARY;
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(var(--overlay-rgb),0.08);'
: '';
? 'box-shadow:0 1px 2px rgba(var(--overlay-rgb),0.08);'
: 'box-shadow:none;';
return `background:${background}; ${borderRule} color:${color}; ${shadow}`;
}
@@ -197,6 +260,12 @@ function getSliderPercent(value) {
return ((value - PREP_TIME_MIN) / (PREP_TIME_MAX - PREP_TIME_MIN)) * 100;
}
function getClosedPanelTransform(panel) {
return panel?.dataset.placement === 'below'
? 'translateY(-0.5rem) scale(0.98)'
: 'translateY(0.65rem) scale(0.98)';
}
function setActiveTimeHandle(activeHandle) {
const minHandle = document.getElementById('prep-time-min-handle');
const maxHandle = document.getElementById('prep-time-max-handle');
@@ -244,26 +313,33 @@ function positionFilterPanel() {
const viewRect = view.getBoundingClientRect();
const anchorRect = (searchShell || button).getBoundingClientRect();
const gap = 8;
const isRecipeContext = activeFilterContext === 'recipes';
const gap = isRecipeContext ? 12 : 8;
const margin = 12;
const width = Math.min(anchorRect.width, viewRect.width - margin * 2);
const maxPanelWidth = isRecipeContext ? 352 : anchorRect.width;
const width = Math.min(maxPanelWidth, viewRect.width - margin * 2);
const spaceBelow = viewRect.bottom - anchorRect.bottom - margin;
const preferredMaxHeight = Math.min(420, viewRect.height - margin * 2);
const spaceAbove = anchorRect.top - viewRect.top - gap - margin;
const opensUpward = spaceBelow < 260 && anchorRect.top - viewRect.top > spaceBelow;
const opensUpward = isRecipeContext || (spaceBelow < 260 && anchorRect.top - viewRect.top > spaceBelow);
const maxHeight = opensUpward
? Math.max(220, Math.min(preferredMaxHeight, spaceAbove))
: Math.max(220, viewRect.height - Math.max(margin, anchorRect.bottom - viewRect.top + gap) - margin);
const top = opensUpward
? Math.max(margin, anchorRect.top - viewRect.top - gap - maxHeight)
: Math.max(margin, anchorRect.bottom - viewRect.top + gap);
const left = Math.max(
margin,
Math.min(anchorRect.left - viewRect.left, viewRect.width - width - margin),
);
const leftBase = isRecipeContext
? (viewRect.width - width) / 2
: anchorRect.left - viewRect.left;
const left = Math.max(margin, Math.min(leftBase, viewRect.width - width - margin));
panel.style.width = `${width}px`;
panel.style.left = `${left}px`;
panel.style.top = `${top}px`;
panel.style.transformOrigin = opensUpward ? 'bottom center' : 'top center';
panel.dataset.placement = opensUpward ? 'above' : 'below';
if (opensUpward) {
panel.style.top = 'auto';
panel.style.bottom = `${Math.max(margin, viewRect.bottom - anchorRect.top + gap)}px`;
} else {
panel.style.top = `${Math.max(margin, anchorRect.bottom - viewRect.top + gap)}px`;
panel.style.bottom = 'auto';
}
panel.style.maxHeight = `${maxHeight}px`;
if (body) body.style.maxHeight = `${maxHeight - 56}px`;
}
@@ -283,6 +359,7 @@ function showFilterPanel() {
view.style.pointerEvents = 'auto';
view.setAttribute('aria-hidden', 'false');
positionFilterPanel();
panel.style.transform = getClosedPanelTransform(panel);
setRecipeAreaBlur(true);
syncPanelCount();
@@ -303,7 +380,7 @@ function hideFilterPanel() {
view.style.pointerEvents = 'none';
view.setAttribute('aria-hidden', 'true');
panel.style.opacity = '0';
panel.style.transform = 'translateY(-0.5rem) scale(0.98)';
panel.style.transform = getClosedPanelTransform(panel);
setRecipeAreaBlur(false);
syncPanelCount();
@@ -381,7 +458,7 @@ function syncPanelCount() {
if (button) {
const highlight = isFilterPanelOpen() || count > 0;
const isRecipeGlassButton = buttonId === 'recipe-filter-btn';
if (isRecipeGlassButton && !highlight) {
if (isRecipeGlassButton) {
button.style.removeProperty('background');
button.style.removeProperty('border-color');
button.style.removeProperty('color');