Unify calendar code
Some checks failed
Build and Deploy / build-and-push (push) Failing after 1m16s

This commit is contained in:
2026-04-20 23:44:18 +02:00
parent c43b3766cd
commit 08a275093c
7 changed files with 783 additions and 486 deletions

View File

@@ -16,6 +16,7 @@ import { aggregateSelectedDaysIngredientNeed } from '../services/planIngredients
import { loadPlans, dateKey } from '../services/planStore.js?v=2';
import { addDays, startOfDay, startOfMonth } from '../services/dateUtils.js';
import { createSwipePopoverCalendarHTML, initSwipePopoverCalendar } from '../ui/swipePopoverCalendar.js';
import { createCalendarPopoverController, createCalendarPopoverHTML } from '../ui/calendarPopover.js';
import { showAppToast } from '../ui/toast.js';
/* ── helpers ── */
@@ -64,6 +65,7 @@ let expandedAmount = 0;
let calendarOpen = false;
let calendarMonth = startOfMonth(new Date());
let shoppingCalendar = null;
let shoppingCalendarPopover = null;
/* ── day helpers ── */
@@ -133,14 +135,13 @@ export function getShoppingListHTML() {
</button>
<!-- popup calendar (absolute, overlays content below) -->
<div id="sl-calendar-popup" style="position:absolute; top:calc(100% + 0.5rem); left:0; right:0; z-index:50; pointer-events:none; opacity:0; transform:translateY(-6px) scale(0.98); transition: opacity 0.2s ease, transform 0.2s ease;">
<div class="rounded-[1.35rem] py-3" style="background:rgb(var(--sunken-rgb)); border:1px solid rgb(var(--border-input-rgb)); box-shadow:var(--shadow-shell);">
${createSwipePopoverCalendarHTML({
${createCalendarPopoverHTML({
id: 'sl-calendar-popup',
calendarHTML: createSwipePopoverCalendarHTML({
idPrefix: 'sl-cal',
weekdays: WEEKDAY_SHORT,
}),
})}
</div>
</div>
<!-- popup bought (absolute, overlays content below) -->
<div id="sl-bought-popup" style="position:absolute; top:calc(100% + 0.5rem); left:0; right:0; z-index:50; pointer-events:none; opacity:0; transform:translateY(-6px) scale(0.98); transition: opacity 0.2s ease, transform 0.2s ease;">
@@ -201,6 +202,14 @@ function initShoppingCalendar() {
dot: 'rgb(var(--text-faint-rgb))',
},
});
shoppingCalendarPopover = createCalendarPopoverController({
popupId: 'sl-calendar-popup',
viewportId: 'sl-cal-viewport',
triggerId: 'sl-range-pill',
chevronId: 'sl-range-chevron',
getCalendar: () => shoppingCalendar,
hideViewportDuringLayout: true,
});
}
function updatePillLabel() {
@@ -212,64 +221,12 @@ function openCalendar() {
if (boughtPopupOpen) closeBoughtPopup();
calendarOpen = true;
calendarMonth = startOfMonth(new Date());
const popup = document.getElementById('sl-calendar-popup');
const viewport = document.getElementById('sl-cal-viewport');
const chevron = document.getElementById('sl-range-chevron');
const pill = document.getElementById('sl-range-pill');
if (popup) {
popup.style.pointerEvents = 'auto';
popup.style.opacity = '1';
popup.style.transform = 'translateY(0) scale(1)';
}
if (chevron) chevron.style.transform = 'rotate(180deg)';
if (pill) {
pill.style.background = 'rgb(var(--sunken-rgb))';
pill.style.borderColor = 'rgb(var(--border-input-rgb))';
}
if (viewport) {
viewport.style.opacity = '0';
viewport.style.visibility = 'hidden';
viewport.style.transition = 'opacity 120ms ease';
}
shoppingCalendar?.render();
// Compute geometry while hidden; reveal only after stable layout.
const ensureStableCalendarLayout = (attempt = 0) => {
const vw = viewport ? (viewport.clientWidth || viewport.getBoundingClientRect().width) : 0;
if (vw < 8 && attempt < 8) {
requestAnimationFrame(() => ensureStableCalendarLayout(attempt + 1));
return;
}
shoppingCalendar?.reapplyLayout();
shoppingCalendar?.resetTrackPosition();
requestAnimationFrame(() => {
shoppingCalendar?.reapplyLayout();
shoppingCalendar?.resetTrackPosition();
if (viewport) {
viewport.style.visibility = 'visible';
viewport.style.opacity = '1';
}
});
};
requestAnimationFrame(() => ensureStableCalendarLayout());
shoppingCalendarPopover?.open();
}
function closeCalendar() {
calendarOpen = false;
const popup = document.getElementById('sl-calendar-popup');
const chevron = document.getElementById('sl-range-chevron');
const pill = document.getElementById('sl-range-pill');
if (popup) {
popup.style.pointerEvents = 'none';
popup.style.opacity = '0';
popup.style.transform = 'translateY(-6px) scale(0.98)';
}
if (chevron) chevron.style.transform = '';
if (pill) {
pill.style.background = 'rgb(var(--card-rgb))';
pill.style.borderColor = 'rgb(var(--border-card-rgb))';
}
shoppingCalendar?.clearPendingRange?.();
shoppingCalendar?.resetTrackPosition();
shoppingCalendarPopover?.close({ clearPendingRange: true });
}
function toggleCalendar() {
@@ -327,11 +284,11 @@ function activeItemHtml(item) {
const stepAmt = isExpanded ? expandedAmount : Math.max(step, Math.round(item.shortfall / step) * step);
return `
<div class="sl-swipe-wrap relative rounded-xl mb-1.5 overflow-hidden" style="background:rgb(var(--success-rgb)); box-shadow:var(--shadow-card);">
<div class="sl-swipe-bg-buy absolute inset-0 flex items-center pr-5 justify-end">
<div class="sl-swipe-wrap relative rounded-xl mb-1.5 overflow-hidden" style="box-shadow:var(--shadow-card);">
<div class="sl-swipe-bg-buy absolute inset-0 flex items-center pr-5 justify-end" style="background:rgb(var(--success-rgb)); opacity:0;">
<i class="fas fa-check text-white text-lg"></i>
</div>
<div class="sl-swipe-inner rounded-xl" style="background:rgb(var(--card-rgb)); position:relative;"
<div class="sl-swipe-inner" style="background:rgb(var(--card-rgb)); position:relative;"
data-id="${esc(item.ingredientId)}" data-unit="${esc(item.unit)}" data-shortfall="${item.shortfall}">
<div class="sl-item-main flex items-center gap-3 py-1.5 px-3 cursor-pointer select-none">
${mediaHtml}
@@ -371,11 +328,11 @@ function boughtItemHtml(entry) {
: `<div class="w-8 h-8 rounded-lg flex items-center justify-center shrink-0" style="background:rgb(var(--card-soft-rgb));"><i class="fas ${icon} text-xs" style="color:rgb(var(--text-faint-rgb));"></i></div>`;
return `
<div class="sl-swipe-wrap relative rounded-xl mb-1.5 overflow-hidden" style="background:rgb(var(--danger-rgb)); box-shadow:var(--shadow-card);">
<div class="sl-swipe-bg-undo absolute inset-0 flex items-center pl-5 justify-start">
<div class="sl-swipe-wrap relative rounded-xl mb-1.5 overflow-hidden" style="box-shadow:var(--shadow-card);">
<div class="sl-swipe-bg-undo absolute inset-0 flex items-center pl-5 justify-start" style="background:rgb(var(--danger-rgb)); opacity:0;">
<i class="fas fa-rotate-left text-white text-lg"></i>
</div>
<div class="sl-swipe-inner flex items-center gap-3 py-1.5 px-3 rounded-xl" style="background:rgb(var(--card-rgb)); position:relative;" data-entry-id="${esc(entry.id)}">
<div class="sl-swipe-inner flex items-center gap-3 py-1.5 px-3" style="background:rgb(var(--card-rgb)); position:relative;" data-entry-id="${esc(entry.id)}">
${mediaHtml}
<div class="flex-1 min-w-0">
<span class="block text-[13px] font-medium leading-tight truncate" style="color:rgb(var(--text-body-rgb));">${esc(entry.name)}</span>
@@ -390,6 +347,10 @@ function boughtItemHtml(entry) {
function attachSwipe(container, opts) {
const inner = container.querySelector('.sl-swipe-inner');
if (!inner) return;
const bgBuy = container.querySelector('.sl-swipe-bg-buy');
const bgUndo = container.querySelector('.sl-swipe-bg-undo');
const showBg = (el) => { if (el) el.style.opacity = '1'; };
const hideBgs = () => { if (bgBuy) bgBuy.style.opacity = '0'; if (bgUndo) bgUndo.style.opacity = '0'; };
let startX = 0, startY = 0, dx = 0, tracking = false, decided = false, goingH = false;
container.addEventListener('pointerdown', (e) => {
@@ -408,10 +369,10 @@ function attachSwipe(container, opts) {
decided = true;
goingH = Math.abs(ddx) > Math.abs(ddy);
}
if (!goingH) { tracking = false; inner.style.transform = ''; return; }
if (!goingH) { tracking = false; inner.style.transform = ''; hideBgs(); return; }
dx = ddx;
if (dx > 0 && opts.onRight) inner.style.transform = `translateX(${Math.min(dx, 90)}px)`;
else if (dx < 0 && opts.onLeft) inner.style.transform = `translateX(${Math.max(dx, -90)}px)`;
if (dx > 0 && opts.onRight) { inner.style.transform = `translateX(${Math.min(dx, 90)}px)`; showBg(bgBuy); }
else if (dx < 0 && opts.onLeft) { inner.style.transform = `translateX(${Math.max(dx, -90)}px)`; showBg(bgUndo); }
});
const finish = () => {
@@ -427,6 +388,7 @@ function attachSwipe(container, opts) {
setTimeout(opts.onLeft, 180);
} else {
inner.style.transform = '';
hideBgs();
}
dx = 0;
};
@@ -436,6 +398,7 @@ function attachSwipe(container, opts) {
tracking = false;
inner.style.transition = 'transform 0.2s ease';
inner.style.transform = '';
hideBgs();
dx = 0;
});
}