Fix calendar styling

This commit is contained in:
2026-04-20 23:04:28 +02:00
parent 63937ed7d1
commit c43b3766cd
3 changed files with 82 additions and 110 deletions

View File

@@ -7,12 +7,12 @@ const DEFAULT_MONTHS_LONG = [
]; ];
const DEFAULT_THEME = { const DEFAULT_THEME = {
selectedBg: 'rgb(var(--card-rgb))', selectedBorder: 'rgba(var(--text-emphasis-rgb),0.34)',
selectedBorder: 'rgb(var(--border-input-rgb))',
selectedText: 'rgb(var(--text-emphasis-rgb))', selectedText: 'rgb(var(--text-emphasis-rgb))',
selectedDot: 'rgb(var(--text-emphasis-rgb))', selectedDot: 'rgb(var(--text-emphasis-rgb))',
selectedShadow: '0 0 0 1px rgba(var(--text-emphasis-rgb),0.10)',
bg: 'rgb(var(--app-bg-rgb))', bg: 'rgb(var(--app-bg-rgb))',
border: 'rgb(var(--card-raised-rgb))', border: 'transparent',
text: 'rgb(var(--text-body-soft-rgb))', text: 'rgb(var(--text-body-soft-rgb))',
dimmedBg: 'transparent', dimmedBg: 'transparent',
dimText: 'rgb(var(--text-faint-rgb))', dimText: 'rgb(var(--text-faint-rgb))',
@@ -58,20 +58,6 @@ export function createSwipePopoverCalendarHTML({
${weekdays.map((d) => `<div>${d}</div>`).join('')} ${weekdays.map((d) => `<div>${d}</div>`).join('')}
</div> </div>
`; `;
const gripLeft = `
<div data-swc-grip aria-hidden="true" style="position:absolute; left:0; top:50%; transform:translateY(-50%); width:0.76rem; height:1rem; pointer-events:none; opacity:0.66;">
<span style="position:absolute; left:0; top:50%; transform:translateY(-50%); width:1.5px; height:1.22rem; border-radius:999px; background:rgba(var(--text-faint-rgb),0.84);"></span>
<span style="position:absolute; left:0.16rem; top:50%; transform:translateY(-50%); width:1.5px; height:1.0rem; border-radius:999px; background:rgba(var(--text-faint-rgb),0.74);"></span>
<span style="position:absolute; left:0.34rem; top:50%; transform:translateY(-50%); width:1.5px; height:0.56rem; border-radius:999px; background:rgba(var(--text-faint-rgb),0.62);"></span>
</div>
`;
const gripRight = `
<div data-swc-grip aria-hidden="true" style="position:absolute; right:0; top:50%; transform:translateY(-50%); width:0.76rem; height:1rem; pointer-events:none; opacity:0.66;">
<span style="position:absolute; right:0; top:50%; transform:translateY(-50%); width:1.5px; height:1.22rem; border-radius:999px; background:rgba(var(--text-faint-rgb),0.84);"></span>
<span style="position:absolute; right:0.16rem; top:50%; transform:translateY(-50%); width:1.5px; height:1.0rem; border-radius:999px; background:rgba(var(--text-faint-rgb),0.74);"></span>
<span style="position:absolute; right:0.34rem; top:50%; transform:translateY(-50%); width:1.5px; height:0.56rem; border-radius:999px; background:rgba(var(--text-faint-rgb),0.62);"></span>
</div>
`;
return ` return `
<div class="pb-3 px-3 flex items-center justify-end gap-3"> <div class="pb-3 px-3 flex items-center justify-end gap-3">
@@ -79,25 +65,19 @@ export function createSwipePopoverCalendarHTML({
<span id="${idPrefix}-month-label" class="h-full shrink-0 inline-flex min-w-[5.75rem] max-w-[9rem] items-center justify-center px-3 ${monthLabelTextClass}" style="color:rgb(var(--text-body-soft-rgb));"></span> <span id="${idPrefix}-month-label" class="h-full shrink-0 inline-flex min-w-[5.75rem] max-w-[9rem] items-center justify-center px-3 ${monthLabelTextClass}" style="color:rgb(var(--text-body-soft-rgb));"></span>
</div> </div>
</div> </div>
<div id="${idPrefix}-viewport" style="overflow:hidden; position:relative; touch-action:pan-y;"> <div id="${idPrefix}-viewport" style="overflow:hidden; position:relative; touch-action:pan-y; -webkit-user-select:none; user-select:none; cursor:grab;">
<div id="${idPrefix}-track" style="display:flex; align-items:flex-start; position:relative; will-change:transform;"> <div id="${idPrefix}-track" style="display:flex; align-items:flex-start; position:relative; will-change:transform;">
<div class="swc-panel" data-panel="prev" style="flex-shrink:0; pointer-events:none; position:relative; overflow:hidden;"> <div class="swc-panel" data-panel="prev" style="flex-shrink:0; pointer-events:none; position:relative; overflow:hidden;">
${weekdayHeader} ${weekdayHeader}
<div id="${idPrefix}-prev-grid" class="grid grid-cols-7 gap-1.5"></div> <div id="${idPrefix}-prev-grid" class="grid grid-cols-7 gap-1.5"></div>
${gripLeft}
${gripRight}
</div> </div>
<div class="swc-panel" data-panel="current" style="flex-shrink:0; position:relative; overflow:hidden;"> <div class="swc-panel" data-panel="current" style="flex-shrink:0; position:relative; overflow:hidden;">
${weekdayHeader} ${weekdayHeader}
<div id="${idPrefix}-grid" class="grid grid-cols-7 gap-1.5"></div> <div id="${idPrefix}-grid" class="grid grid-cols-7 gap-1.5"></div>
${gripLeft}
${gripRight}
</div> </div>
<div class="swc-panel" data-panel="next" style="flex-shrink:0; pointer-events:none; position:relative; overflow:hidden;"> <div class="swc-panel" data-panel="next" style="flex-shrink:0; pointer-events:none; position:relative; overflow:hidden;">
${weekdayHeader} ${weekdayHeader}
<div id="${idPrefix}-next-grid" class="grid grid-cols-7 gap-1.5"></div> <div id="${idPrefix}-next-grid" class="grid grid-cols-7 gap-1.5"></div>
${gripLeft}
${gripRight}
</div> </div>
</div> </div>
</div> </div>
@@ -138,50 +118,37 @@ export function initSwipePopoverCalendar({
const SWIPE_THRESHOLD = 40; const SWIPE_THRESHOLD = 40;
const ANIMATION_MS = 260; const ANIMATION_MS = 260;
let viewportWidth = 0;
let panelWidth = 0; let panelWidth = 0;
let panelHandle = 0; let panelInset = 0;
let dragHandleWidth = 0;
let restOffset = 0; let restOffset = 0;
let animatingNav = false; let animatingNav = false;
let pendingRangeStart = null;
let suppressClickUntil = 0;
const panels = Array.from(track.querySelectorAll('.swc-panel')); const panels = Array.from(track.querySelectorAll('.swc-panel'));
const syncGripVisibility = (showAdjacent = false) => {
panels.forEach((panel) => {
const isCurrent = panel.dataset.panel === 'current';
panel.querySelectorAll('[data-swc-grip]').forEach((grip) => {
grip.style.opacity = (isCurrent || showAdjacent) ? '0.66' : '0';
});
});
};
const applyLayout = () => { const applyLayout = () => {
const vw = viewport.clientWidth || viewport.getBoundingClientRect().width; const vw = viewport.clientWidth || viewport.getBoundingClientRect().width;
if (!vw) return; if (!vw) return;
viewportWidth = vw; const computedInset = panelHandlePx == null
const computedHandle = panelHandlePx == null
? Math.round(vw * panelHandleRatio) ? Math.round(vw * panelHandleRatio)
: panelHandlePx; : panelHandlePx;
panelHandle = Math.max(panelHandleMin, Math.min(panelHandleMax, computedHandle)); panelInset = Math.max(panelHandleMin, Math.min(panelHandleMax, computedInset));
dragHandleWidth = panelHandle;
panelWidth = vw; panelWidth = vw;
restOffset = -panelWidth; restOffset = -panelWidth;
panels.forEach((panel) => { panels.forEach((panel) => {
panel.style.width = `${panelWidth}px`; panel.style.width = `${panelWidth}px`;
panel.style.boxSizing = 'border-box'; panel.style.boxSizing = 'border-box';
panel.style.paddingLeft = `${panelHandle}px`; panel.style.paddingLeft = `${panelInset}px`;
panel.style.paddingRight = `${panelHandle}px`; panel.style.paddingRight = `${panelInset}px`;
}); });
track.style.transition = 'none'; track.style.transition = 'none';
track.style.transform = `translate3d(${restOffset}px, 0, 0)`; track.style.transform = `translate3d(${restOffset}px, 0, 0)`;
syncGripVisibility(false);
}; };
const resetTrackPosition = () => { const resetTrackPosition = () => {
track.style.transition = 'none'; track.style.transition = 'none';
track.style.transform = `translate3d(${restOffset}px, 0, 0)`; track.style.transform = `translate3d(${restOffset}px, 0, 0)`;
syncGripVisibility(false);
}; };
const setDragTranslate = (dx, ms) => { const setDragTranslate = (dx, ms) => {
@@ -189,6 +156,10 @@ export function initSwipePopoverCalendar({
track.style.transform = `translate3d(${restOffset + dx}px, 0, 0)`; track.style.transform = `translate3d(${restOffset + dx}px, 0, 0)`;
}; };
const snapBack = () => {
setDragTranslate(0, ANIMATION_MS);
};
const getNavigationTarget = (monthDelta) => { const getNavigationTarget = (monthDelta) => {
const anchor = normalizeMonth(getMonthAnchor()); const anchor = normalizeMonth(getMonthAnchor());
return startOfMonth(new Date(anchor.getFullYear(), anchor.getMonth() + monthDelta, 1)); return startOfMonth(new Date(anchor.getFullYear(), anchor.getMonth() + monthDelta, 1));
@@ -232,20 +203,22 @@ export function initSwipePopoverCalendar({
let bg; let bg;
let borderColor; let borderColor;
let text; let text;
let borderClass = 'border'; let shadow = 'none';
if (isSelected) { let borderClass = 'border-0';
bg = theme.selectedBg; if (dimmed) {
borderColor = theme.selectedBorder; bg = theme.dimmedBg ?? DEFAULT_THEME.dimmedBg;
text = theme.selectedText;
} else if (dimmed) {
bg = theme.dimmedBg;
borderColor = 'transparent'; borderColor = 'transparent';
text = theme.dimText; text = theme.dimText || DEFAULT_THEME.dimText;
borderClass = 'border-0';
} else { } else {
bg = theme.bg; bg = theme.bg || DEFAULT_THEME.bg;
borderColor = theme.border; borderColor = theme.border || DEFAULT_THEME.border;
text = theme.text; text = theme.text || DEFAULT_THEME.text;
}
if (isSelected) {
borderColor = theme.selectedBorder || DEFAULT_THEME.selectedBorder;
text = theme.selectedText || DEFAULT_THEME.selectedText;
shadow = theme.selectedShadow || DEFAULT_THEME.selectedShadow;
borderClass = 'border';
} }
const opacity = dimmed && !isSelected ? String(theme.dimOpacity ?? 0.58) : '1'; const opacity = dimmed && !isSelected ? String(theme.dimOpacity ?? 0.58) : '1';
const dotColor = isSelected ? theme.selectedDot : theme.dot; const dotColor = isSelected ? theme.selectedDot : theme.dot;
@@ -256,7 +229,7 @@ export function initSwipePopoverCalendar({
return ` return `
<${tag} ${attrs} <${tag} ${attrs}
class="mx-auto flex h-[2.05rem] w-full min-w-0 max-w-full items-center justify-center rounded-full ${borderClass}${dayClass} text-xs font-medium leading-tight overflow-hidden" class="mx-auto flex h-[2.05rem] w-full min-w-0 max-w-full items-center justify-center rounded-full ${borderClass}${dayClass} text-xs font-medium leading-tight overflow-hidden"
style="background:${bg}; border-color:${borderColor}; color:${text}; opacity:${opacity}; touch-action:none;"> style="background:${bg}; border-color:${borderColor}; color:${text}; opacity:${opacity}; box-shadow:${shadow}; touch-action:pan-y;">
<span class="relative flex h-full w-full flex-col items-center justify-center"> <span class="relative flex h-full w-full flex-col items-center justify-center">
<span class="text-[13px] font-semibold leading-none ${showDot ? '-translate-y-[0.18rem]' : ''}">${day.getDate()}</span> <span class="text-[13px] font-semibold leading-none ${showDot ? '-translate-y-[0.18rem]' : ''}">${day.getDate()}</span>
${showDot ? `<span class="absolute left-1/2 w-1 h-1 -translate-x-1/2 rounded-full opacity-75" style="bottom:0.24rem; background:${dotColor};" aria-hidden="true"></span>` : ''} ${showDot ? `<span class="absolute left-1/2 w-1 h-1 -translate-x-1/2 rounded-full opacity-75" style="bottom:0.24rem; background:${dotColor};" aria-hidden="true"></span>` : ''}
@@ -268,7 +241,8 @@ export function initSwipePopoverCalendar({
const render = (previewSelection = null) => { const render = (previewSelection = null) => {
const anchor = normalizeMonth(getMonthAnchor()); const anchor = normalizeMonth(getMonthAnchor());
const selectedSet = getSelectedSet(previewSelection); const rangePreview = selectionMode === 'range' && pendingRangeStart ? [pendingRangeStart] : null;
const selectedSet = getSelectedSet(previewSelection ?? rangePreview);
if (monthLabelEl) monthLabelEl.textContent = monthLabel(anchor, monthsLong); if (monthLabelEl) monthLabelEl.textContent = monthLabel(anchor, monthsLong);
renderMonthGrid(gridEl, anchor, selectedSet); renderMonthGrid(gridEl, anchor, selectedSet);
renderMonthGrid(prevGridEl, new Date(anchor.getFullYear(), anchor.getMonth() - 1, 1), selectedSet); renderMonthGrid(prevGridEl, new Date(anchor.getFullYear(), anchor.getMonth() - 1, 1), selectedSet);
@@ -278,8 +252,7 @@ export function initSwipePopoverCalendar({
const commitNavigation = (monthDelta) => { const commitNavigation = (monthDelta) => {
if (!canNavigate(monthDelta)) { if (!canNavigate(monthDelta)) {
setDragTranslate(0, ANIMATION_MS); snapBack();
setTimeout(() => syncGripVisibility(false), ANIMATION_MS + 20);
return; return;
} }
animatingNav = true; animatingNav = true;
@@ -294,49 +267,28 @@ export function initSwipePopoverCalendar({
}; };
if (selectionMode === 'range') { if (selectionMode === 'range') {
let dragStart = null; gridEl.addEventListener('click', (e) => {
let dragCurrent = null;
let dragging = false;
gridEl.addEventListener('pointerdown', (e) => {
if (animatingNav) return;
const btn = e.target.closest('.swc-day'); const btn = e.target.closest('.swc-day');
if (!btn) return; if (!btn) return;
e.stopPropagation(); e.stopPropagation();
dragStart = btn.dataset.dk; const selectedKey = btn.dataset.dk;
dragCurrent = btn.dataset.dk; if (!pendingRangeStart) {
dragging = true; pendingRangeStart = selectedKey;
gridEl.setPointerCapture(e.pointerId); if (typeof onSelectionCommit === 'function') onSelectionCommit([selectedKey]);
render([dragStart]); render([selectedKey]);
}); return;
gridEl.addEventListener('pointermove', (e) => {
if (!dragging || animatingNav) return;
e.preventDefault();
const el = document.elementFromPoint(e.clientX, e.clientY);
const btn = el?.closest('.swc-day');
if (btn?.dataset.dk && btn.dataset.dk !== dragCurrent) {
dragCurrent = btn.dataset.dk;
render(dayRange(dragStart, dragCurrent));
} }
});
gridEl.addEventListener('pointerup', () => { const range = dayRange(pendingRangeStart, selectedKey);
if (!dragging) return; pendingRangeStart = null;
dragging = false;
const range = dayRange(dragStart, dragCurrent);
dragStart = null;
dragCurrent = null;
if (typeof onSelectionCommit === 'function') onSelectionCommit(range); if (typeof onSelectionCommit === 'function') onSelectionCommit(range);
render(); render();
}); });
gridEl.addEventListener('pointercancel', () => {
dragging = false;
dragStart = null;
dragCurrent = null;
render();
});
} else { } else {
gridEl.addEventListener('click', (e) => { gridEl.addEventListener('click', (e) => {
const btn = e.target.closest('.swc-day'); const btn = e.target.closest('.swc-day');
if (!btn) return; if (!btn) return;
e.stopPropagation();
if (typeof onSelectionCommit === 'function') onSelectionCommit(btn.dataset.dk); if (typeof onSelectionCommit === 'function') onSelectionCommit(btn.dataset.dk);
render(); render();
}); });
@@ -347,22 +299,18 @@ export function initSwipePopoverCalendar({
let startY = 0; let startY = 0;
let moved = false; let moved = false;
let axis = null; let axis = null;
let hasPointerCapture = false;
viewport.addEventListener('pointerdown', (e) => { viewport.addEventListener('pointerdown', (e) => {
if (animatingNav || ptrId !== null) return; if (animatingNav || ptrId !== null) return;
if (e.pointerType === 'mouse' && e.button !== 0) return; if (e.pointerType === 'mouse' && e.button !== 0) return;
if (!panelWidth) applyLayout(); if (!panelWidth) applyLayout();
const rect = viewport.getBoundingClientRect();
const localX = e.clientX - rect.left;
const inLeft = localX <= dragHandleWidth;
const inRight = localX >= (viewportWidth - dragHandleWidth);
if (!inLeft && !inRight) return;
ptrId = e.pointerId; ptrId = e.pointerId;
startX = e.clientX; startX = e.clientX;
startY = e.clientY; startY = e.clientY;
moved = false; moved = false;
axis = null; axis = null;
try { viewport.setPointerCapture(e.pointerId); } catch (_) {} hasPointerCapture = false;
}); });
viewport.addEventListener('pointermove', (e) => { viewport.addEventListener('pointermove', (e) => {
@@ -372,29 +320,52 @@ export function initSwipePopoverCalendar({
if (!moved && (Math.abs(dx) > MOVE_THRESHOLD || Math.abs(dy) > MOVE_THRESHOLD)) { if (!moved && (Math.abs(dx) > MOVE_THRESHOLD || Math.abs(dy) > MOVE_THRESHOLD)) {
moved = true; moved = true;
axis = Math.abs(dx) >= Math.abs(dy) ? 'x' : 'y'; axis = Math.abs(dx) >= Math.abs(dy) ? 'x' : 'y';
if (axis === 'x') syncGripVisibility(true);
} }
if (axis === 'x') setDragTranslate(getAllowedDragDx(dx), 0); if (axis === 'x') {
e.preventDefault();
suppressClickUntil = Date.now() + 450;
if (!hasPointerCapture) {
try {
viewport.setPointerCapture(e.pointerId);
hasPointerCapture = true;
} catch (_) {}
}
setDragTranslate(getAllowedDragDx(dx), 0);
}
}); });
const endGesture = (e) => { const endGesture = (e) => {
if (e && e.pointerId !== ptrId) return; if (e && e.pointerId !== ptrId) return;
if (e && hasPointerCapture) {
try { viewport.releasePointerCapture(e.pointerId); } catch (_) {}
}
hasPointerCapture = false;
ptrId = null; ptrId = null;
if (!moved || axis !== 'x') return; if (!moved || axis !== 'x') return;
const dx = e ? e.clientX - startX : 0; const dx = e ? e.clientX - startX : 0;
const monthDelta = dx > 0 ? -1 : 1; const monthDelta = dx > 0 ? -1 : 1;
if (Math.abs(dx) >= SWIPE_THRESHOLD && canNavigate(monthDelta)) commitNavigation(monthDelta); if (Math.abs(dx) >= SWIPE_THRESHOLD && canNavigate(monthDelta)) commitNavigation(monthDelta);
else { else {
setDragTranslate(0, ANIMATION_MS); snapBack();
setTimeout(() => syncGripVisibility(false), ANIMATION_MS + 20);
} }
moved = false; moved = false;
axis = null; axis = null;
}; };
viewport.addEventListener('pointerup', endGesture); viewport.addEventListener('click', (e) => {
viewport.addEventListener('pointercancel', endGesture); if (Date.now() > suppressClickUntil) return;
suppressClickUntil = 0;
e.preventDefault();
e.stopPropagation();
}, true);
window.addEventListener('pointerup', endGesture);
window.addEventListener('pointercancel', endGesture);
window.addEventListener('resize', applyLayout); window.addEventListener('resize', applyLayout);
return { render, reapplyLayout: applyLayout, resetTrackPosition }; const clearPendingRange = () => {
pendingRangeStart = null;
render();
};
return { render, reapplyLayout: applyLayout, resetTrackPosition, clearPendingRange };
} }

View File

@@ -86,10 +86,10 @@ const PANTRY_CALENDAR_THEME = {
dimmedBg: 'transparent', dimmedBg: 'transparent',
dimmedBorder: 'transparent', dimmedBorder: 'transparent',
dot: 'rgb(var(--text-faint-rgb))', dot: 'rgb(var(--text-faint-rgb))',
selectedBg: 'rgb(var(--card-rgb))', selectedBorder: 'rgba(var(--text-emphasis-rgb),0.34)',
selectedBorder: 'rgb(var(--border-input-rgb))',
selectedText: 'rgb(var(--text-emphasis-rgb))', selectedText: 'rgb(var(--text-emphasis-rgb))',
selectedDot: 'rgb(var(--text-emphasis-rgb))', selectedDot: 'rgb(var(--text-emphasis-rgb))',
selectedShadow: '0 0 0 1px rgba(var(--text-emphasis-rgb),0.10)',
}; };
/* ── state ── */ /* ── state ── */

View File

@@ -188,12 +188,12 @@ function initShoppingCalendar() {
showDot: dateKey(day) === todayKey() && !isSelected, showDot: dateKey(day) === todayKey() && !isSelected,
}), }),
theme: { theme: {
selectedBg: 'rgb(var(--card-rgb))', selectedBorder: 'rgba(var(--text-emphasis-rgb),0.34)',
selectedBorder: 'rgb(var(--border-input-rgb))',
selectedText: 'rgb(var(--text-emphasis-rgb))', selectedText: 'rgb(var(--text-emphasis-rgb))',
selectedDot: 'rgb(var(--text-emphasis-rgb))', selectedDot: 'rgb(var(--text-emphasis-rgb))',
selectedShadow: '0 0 0 1px rgba(var(--text-emphasis-rgb),0.10)',
bg: 'rgb(var(--app-bg-rgb))', bg: 'rgb(var(--app-bg-rgb))',
border: 'rgb(var(--card-raised-rgb))', border: 'transparent',
text: 'rgb(var(--text-body-soft-rgb))', text: 'rgb(var(--text-body-soft-rgb))',
dimmedBg: 'transparent', dimmedBg: 'transparent',
dimText: CALENDAR_DIM_TEXT, dimText: CALENDAR_DIM_TEXT,
@@ -268,6 +268,7 @@ function closeCalendar() {
pill.style.background = 'rgb(var(--card-rgb))'; pill.style.background = 'rgb(var(--card-rgb))';
pill.style.borderColor = 'rgb(var(--border-card-rgb))'; pill.style.borderColor = 'rgb(var(--border-card-rgb))';
} }
shoppingCalendar?.clearPendingRange?.();
shoppingCalendar?.resetTrackPosition(); shoppingCalendar?.resetTrackPosition();
} }