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 = {
selectedBg: 'rgb(var(--card-rgb))',
selectedBorder: 'rgb(var(--border-input-rgb))',
selectedBorder: 'rgba(var(--text-emphasis-rgb),0.34)',
selectedText: '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))',
border: 'rgb(var(--card-raised-rgb))',
border: 'transparent',
text: 'rgb(var(--text-body-soft-rgb))',
dimmedBg: 'transparent',
dimText: 'rgb(var(--text-faint-rgb))',
@@ -58,20 +58,6 @@ export function createSwipePopoverCalendarHTML({
${weekdays.map((d) => `<div>${d}</div>`).join('')}
</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 `
<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>
</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 class="swc-panel" data-panel="prev" style="flex-shrink:0; pointer-events:none; position:relative; overflow:hidden;">
${weekdayHeader}
<div id="${idPrefix}-prev-grid" class="grid grid-cols-7 gap-1.5"></div>
${gripLeft}
${gripRight}
</div>
<div class="swc-panel" data-panel="current" style="flex-shrink:0; position:relative; overflow:hidden;">
${weekdayHeader}
<div id="${idPrefix}-grid" class="grid grid-cols-7 gap-1.5"></div>
${gripLeft}
${gripRight}
</div>
<div class="swc-panel" data-panel="next" style="flex-shrink:0; pointer-events:none; position:relative; overflow:hidden;">
${weekdayHeader}
<div id="${idPrefix}-next-grid" class="grid grid-cols-7 gap-1.5"></div>
${gripLeft}
${gripRight}
</div>
</div>
</div>
@@ -138,50 +118,37 @@ export function initSwipePopoverCalendar({
const SWIPE_THRESHOLD = 40;
const ANIMATION_MS = 260;
let viewportWidth = 0;
let panelWidth = 0;
let panelHandle = 0;
let dragHandleWidth = 0;
let panelInset = 0;
let restOffset = 0;
let animatingNav = false;
let pendingRangeStart = null;
let suppressClickUntil = 0;
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 vw = viewport.clientWidth || viewport.getBoundingClientRect().width;
if (!vw) return;
viewportWidth = vw;
const computedHandle = panelHandlePx == null
const computedInset = panelHandlePx == null
? Math.round(vw * panelHandleRatio)
: panelHandlePx;
panelHandle = Math.max(panelHandleMin, Math.min(panelHandleMax, computedHandle));
dragHandleWidth = panelHandle;
panelInset = Math.max(panelHandleMin, Math.min(panelHandleMax, computedInset));
panelWidth = vw;
restOffset = -panelWidth;
panels.forEach((panel) => {
panel.style.width = `${panelWidth}px`;
panel.style.boxSizing = 'border-box';
panel.style.paddingLeft = `${panelHandle}px`;
panel.style.paddingRight = `${panelHandle}px`;
panel.style.paddingLeft = `${panelInset}px`;
panel.style.paddingRight = `${panelInset}px`;
});
track.style.transition = 'none';
track.style.transform = `translate3d(${restOffset}px, 0, 0)`;
syncGripVisibility(false);
};
const resetTrackPosition = () => {
track.style.transition = 'none';
track.style.transform = `translate3d(${restOffset}px, 0, 0)`;
syncGripVisibility(false);
};
const setDragTranslate = (dx, ms) => {
@@ -189,6 +156,10 @@ export function initSwipePopoverCalendar({
track.style.transform = `translate3d(${restOffset + dx}px, 0, 0)`;
};
const snapBack = () => {
setDragTranslate(0, ANIMATION_MS);
};
const getNavigationTarget = (monthDelta) => {
const anchor = normalizeMonth(getMonthAnchor());
return startOfMonth(new Date(anchor.getFullYear(), anchor.getMonth() + monthDelta, 1));
@@ -232,20 +203,22 @@ export function initSwipePopoverCalendar({
let bg;
let borderColor;
let text;
let borderClass = 'border';
if (isSelected) {
bg = theme.selectedBg;
borderColor = theme.selectedBorder;
text = theme.selectedText;
} else if (dimmed) {
bg = theme.dimmedBg;
let shadow = 'none';
let borderClass = 'border-0';
if (dimmed) {
bg = theme.dimmedBg ?? DEFAULT_THEME.dimmedBg;
borderColor = 'transparent';
text = theme.dimText;
borderClass = 'border-0';
text = theme.dimText || DEFAULT_THEME.dimText;
} else {
bg = theme.bg;
borderColor = theme.border;
text = theme.text;
bg = theme.bg || DEFAULT_THEME.bg;
borderColor = theme.border || DEFAULT_THEME.border;
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 dotColor = isSelected ? theme.selectedDot : theme.dot;
@@ -256,7 +229,7 @@ export function initSwipePopoverCalendar({
return `
<${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"
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="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>` : ''}
@@ -268,7 +241,8 @@ export function initSwipePopoverCalendar({
const render = (previewSelection = null) => {
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);
renderMonthGrid(gridEl, anchor, selectedSet);
renderMonthGrid(prevGridEl, new Date(anchor.getFullYear(), anchor.getMonth() - 1, 1), selectedSet);
@@ -278,8 +252,7 @@ export function initSwipePopoverCalendar({
const commitNavigation = (monthDelta) => {
if (!canNavigate(monthDelta)) {
setDragTranslate(0, ANIMATION_MS);
setTimeout(() => syncGripVisibility(false), ANIMATION_MS + 20);
snapBack();
return;
}
animatingNav = true;
@@ -294,49 +267,28 @@ export function initSwipePopoverCalendar({
};
if (selectionMode === 'range') {
let dragStart = null;
let dragCurrent = null;
let dragging = false;
gridEl.addEventListener('pointerdown', (e) => {
if (animatingNav) return;
gridEl.addEventListener('click', (e) => {
const btn = e.target.closest('.swc-day');
if (!btn) return;
e.stopPropagation();
dragStart = btn.dataset.dk;
dragCurrent = btn.dataset.dk;
dragging = true;
gridEl.setPointerCapture(e.pointerId);
render([dragStart]);
});
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));
const selectedKey = btn.dataset.dk;
if (!pendingRangeStart) {
pendingRangeStart = selectedKey;
if (typeof onSelectionCommit === 'function') onSelectionCommit([selectedKey]);
render([selectedKey]);
return;
}
});
gridEl.addEventListener('pointerup', () => {
if (!dragging) return;
dragging = false;
const range = dayRange(dragStart, dragCurrent);
dragStart = null;
dragCurrent = null;
const range = dayRange(pendingRangeStart, selectedKey);
pendingRangeStart = null;
if (typeof onSelectionCommit === 'function') onSelectionCommit(range);
render();
});
gridEl.addEventListener('pointercancel', () => {
dragging = false;
dragStart = null;
dragCurrent = null;
render();
});
} else {
gridEl.addEventListener('click', (e) => {
const btn = e.target.closest('.swc-day');
if (!btn) return;
e.stopPropagation();
if (typeof onSelectionCommit === 'function') onSelectionCommit(btn.dataset.dk);
render();
});
@@ -347,22 +299,18 @@ export function initSwipePopoverCalendar({
let startY = 0;
let moved = false;
let axis = null;
let hasPointerCapture = false;
viewport.addEventListener('pointerdown', (e) => {
if (animatingNav || ptrId !== null) return;
if (e.pointerType === 'mouse' && e.button !== 0) return;
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;
startX = e.clientX;
startY = e.clientY;
moved = false;
axis = null;
try { viewport.setPointerCapture(e.pointerId); } catch (_) {}
hasPointerCapture = false;
});
viewport.addEventListener('pointermove', (e) => {
@@ -372,29 +320,52 @@ export function initSwipePopoverCalendar({
if (!moved && (Math.abs(dx) > MOVE_THRESHOLD || Math.abs(dy) > MOVE_THRESHOLD)) {
moved = true;
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) => {
if (e && e.pointerId !== ptrId) return;
if (e && hasPointerCapture) {
try { viewport.releasePointerCapture(e.pointerId); } catch (_) {}
}
hasPointerCapture = false;
ptrId = null;
if (!moved || axis !== 'x') return;
const dx = e ? e.clientX - startX : 0;
const monthDelta = dx > 0 ? -1 : 1;
if (Math.abs(dx) >= SWIPE_THRESHOLD && canNavigate(monthDelta)) commitNavigation(monthDelta);
else {
setDragTranslate(0, ANIMATION_MS);
setTimeout(() => syncGripVisibility(false), ANIMATION_MS + 20);
snapBack();
}
moved = false;
axis = null;
};
viewport.addEventListener('pointerup', endGesture);
viewport.addEventListener('pointercancel', endGesture);
viewport.addEventListener('click', (e) => {
if (Date.now() > suppressClickUntil) return;
suppressClickUntil = 0;
e.preventDefault();
e.stopPropagation();
}, true);
window.addEventListener('pointerup', endGesture);
window.addEventListener('pointercancel', endGesture);
window.addEventListener('resize', applyLayout);
return { render, reapplyLayout: applyLayout, resetTrackPosition };
const clearPendingRange = () => {
pendingRangeStart = null;
render();
};
return { render, reapplyLayout: applyLayout, resetTrackPosition, clearPendingRange };
}