diff --git a/js/ui/swipePopoverCalendar.js b/js/ui/swipePopoverCalendar.js
index 74d1f6f..05cb83e 100644
--- a/js/ui/swipePopoverCalendar.js
+++ b/js/ui/swipePopoverCalendar.js
@@ -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) => `
${d}
`).join('')}
`;
- const gripLeft = `
-
-
-
-
-
- `;
- const gripRight = `
-
-
-
-
-
- `;
return `
@@ -79,25 +65,19 @@ export function createSwipePopoverCalendarHTML({
-
+
${weekdayHeader}
- ${gripLeft}
- ${gripRight}
${weekdayHeader}
- ${gripLeft}
- ${gripRight}
${weekdayHeader}
- ${gripLeft}
- ${gripRight}
@@ -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;">
${day.getDate()}
${showDot ? `` : ''}
@@ -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 };
}
diff --git a/js/views/Pantry.js b/js/views/Pantry.js
index 56872eb..8400689 100644
--- a/js/views/Pantry.js
+++ b/js/views/Pantry.js
@@ -86,10 +86,10 @@ const PANTRY_CALENDAR_THEME = {
dimmedBg: 'transparent',
dimmedBorder: 'transparent',
dot: 'rgb(var(--text-faint-rgb))',
- 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)',
};
/* ── state ── */
diff --git a/js/views/ShoppingList.js b/js/views/ShoppingList.js
index cc41e46..b6ebce7 100644
--- a/js/views/ShoppingList.js
+++ b/js/views/ShoppingList.js
@@ -188,12 +188,12 @@ function initShoppingCalendar() {
showDot: dateKey(day) === todayKey() && !isSelected,
}),
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: CALENDAR_DIM_TEXT,
@@ -268,6 +268,7 @@ function closeCalendar() {
pill.style.background = 'rgb(var(--card-rgb))';
pill.style.borderColor = 'rgb(var(--border-card-rgb))';
}
+ shoppingCalendar?.clearPendingRange?.();
shoppingCalendar?.resetTrackPosition();
}