Składniki
@@ -155,6 +168,18 @@ export function setupMealPlanEditor() {
/* ── Calendar ──────────────────────────────────── */
+ function resolveCalendarDayState(day, meta, plans = loadPlans(), today = startOfDay(new Date())) {
+ const isSelected = S.date && sameDay(day, S.date);
+ const isPast = day.getTime() < today.getTime();
+ return {
+ disabled: isPast && !isSelected,
+ dimmed: (isPast || (meta.mode === 'month' && !meta.inCurrentMonth)) && !isSelected,
+ showIndicator: meta.mode === 'month'
+ ? meta.inCurrentMonth && dayHasAnyMeal(plans, day)
+ : dayHasAnyMeal(plans, day),
+ };
+ }
+
function renderCal() {
const wrap = document.getElementById('mpe-cal-wrap');
const sec = document.getElementById('mpe-cal-section');
@@ -169,45 +194,46 @@ export function setupMealPlanEditor() {
}
wrap.classList.remove('hidden');
sec.classList.remove('hidden');
- const grid = document.getElementById('mpe-cal-grid');
+ const weekGrid = document.getElementById('mpe-cal-week-grid');
+ const monthGrid = document.getElementById('mpe-cal-month-grid');
const todayBtn = document.getElementById('mpe-cal-today');
const icon = document.getElementById('mpe-cal-toggle-icon');
- if (!grid) return;
+ if (!weekGrid || !monthGrid) return;
const today = startOfDay(new Date());
const plans = loadPlans();
const mode = S.calExpanded ? 'month' : 'week';
- if (icon) {
- icon.className = S.calExpanded ? 'fas fa-chevron-up text-[10px]' : 'fas fa-chevron-down text-[10px]';
- }
+ syncCollapsibleCalendarMode({
+ mode,
+ weekWrapEl: document.getElementById('mpe-cal-week-wrap'),
+ monthWrapEl: document.getElementById('mpe-cal-month-wrap'),
+ activePaddingBottom: '0.25rem',
+ });
+ syncCollapsibleCalendarToggleIcon(icon, mode);
syncCalendarTodayButton(
todayBtn,
isCalendarOnToday(mode, startOfWeekMonday(S.calDate), S.calDate, S.date),
S.date,
+ {
+ labelText: formatCalendarPeriodLabel(mode, S.calDate, S.calDate),
+ },
);
- renderCalendarGrid({
- gridEl: grid,
- mode,
- anchorDate: S.calDate,
+ renderCollapsibleCalendar({
+ weekGridEl: weekGrid,
+ monthGridEl: monthGrid,
+ weekAnchorDate: S.calDate,
+ monthAnchorDate: S.calDate,
selectedDate: S.date,
- resolveDayState: (day, meta) => {
- const isSelected = S.date && sameDay(day, S.date);
- const isPast = day.getTime() < today.getTime();
- return {
- disabled: isPast && !isSelected,
- dimmed: (isPast || (meta.mode === 'month' && !meta.inCurrentMonth)) && !isSelected,
- showIndicator: meta.mode === 'month'
- ? meta.inCurrentMonth && dayHasAnyMeal(plans, day)
- : dayHasAnyMeal(plans, day),
- };
- },
+ resolveDayState: (day, meta) => resolveCalendarDayState(day, meta, plans, today),
});
syncScrollShadows();
}
function renderSlots() {
const el = document.getElementById('mpe-slot-chips');
+ const sec = document.getElementById('mpe-slot-section');
+ if (sec) sec.classList.toggle('hidden', !S.showCal);
if (!el || !S.showCal) return;
const r = RECIPES[S.recipeId];
if (!r) return;
@@ -611,50 +637,39 @@ export function setupMealPlanEditor() {
});
}
document.getElementById('mpe-confirm-btn')?.addEventListener('click', handleConfirm);
- bindCalendarDayClicks(document.getElementById('mpe-cal-grid'), (date) => {
+ const selectCalendarDate = (date) => {
S.date = date;
S.calDate = new Date(date);
renderCal();
- });
+ };
- bindCalendarHorizontalSwipe(document.getElementById('mpe-cal-grid'), {
- onPrev: () => {
- if (!S.showCal) return;
- S.calDate = S.calExpanded ? addMonths(S.calDate, -1) : addDays(S.calDate, -7);
- renderCal();
+ bindCalendarDayClicks(document.getElementById('mpe-cal-week-grid'), selectCalendarDate);
+ bindCalendarDayClicks(document.getElementById('mpe-cal-month-grid'), selectCalendarDate);
+
+ bindCollapsibleCalendarSwipeGesture({
+ zoneEl: document.getElementById('mpe-cal-swipe-zone'),
+ weekWrapEl: document.getElementById('mpe-cal-week-wrap'),
+ monthWrapEl: document.getElementById('mpe-cal-month-wrap'),
+ getMode: () => (S.calExpanded ? 'month' : 'week'),
+ setMode: (mode) => {
+ S.calExpanded = mode === 'month';
},
- onNext: () => {
- if (!S.showCal) return;
- S.calDate = S.calExpanded ? addMonths(S.calDate, 1) : addDays(S.calDate, 7);
- renderCal();
+ getWeekAnchor: () => S.calDate,
+ setWeekAnchor: (date) => {
+ S.calDate = startOfDay(date);
},
- renderGhost: (ghostGrid, direction) => {
- if (!S.showCal) return false;
- const sign = direction === 'prev' ? -1 : 1;
- const mode = S.calExpanded ? 'month' : 'week';
- const anchor = S.calExpanded
- ? addMonths(S.calDate, sign)
- : addDays(S.calDate, 7 * sign);
- const today = startOfDay(new Date());
- const plans = loadPlans();
- renderCalendarGrid({
- gridEl: ghostGrid,
- mode,
- anchorDate: anchor,
- selectedDate: S.date,
- resolveDayState: (day, meta) => {
- const isSelected = S.date && sameDay(day, S.date);
- const isPast = day.getTime() < today.getTime();
- return {
- disabled: isPast && !isSelected,
- dimmed: (isPast || (meta.mode === 'month' && !meta.inCurrentMonth)) && !isSelected,
- showIndicator: meta.mode === 'month'
- ? meta.inCurrentMonth && dayHasAnyMeal(plans, day)
- : dayHasAnyMeal(plans, day),
- };
- },
- });
+ getMonthAnchor: () => S.calDate,
+ setMonthAnchor: (date) => {
+ S.calDate = startOfDay(date);
},
+ getSelectedDate: () => S.date,
+ setSelectedDate: (date) => {
+ S.date = startOfDay(date);
+ },
+ rerender: renderCal,
+ resolveDayState: (day, meta) => resolveCalendarDayState(day, meta),
+ selectOnNavigateOutside: false,
+ enableVerticalModeSwipe: false,
});
document.getElementById('mpe-cal-today')?.addEventListener('click', () => {
const today = startOfDay(new Date());
@@ -824,7 +839,10 @@ export function setupMealPlanEditor() {
if (e.target.closest('#mpe-add-btn')) {
S.addOpen = true; S.addQuery = '';
renderAddArea();
- document.getElementById('mpe-add-search')?.focus();
+ requestAnimationFrame(() => {
+ document.getElementById('mpe-add-area')?.scrollIntoView({ behavior: 'smooth', block: 'end' });
+ document.getElementById('mpe-add-search')?.focus({ preventScroll: true });
+ });
return;
}
diff --git a/js/ui/swipePopoverCalendar.js b/js/ui/swipePopoverCalendar.js
index 05cb83e..3e23cfb 100644
--- a/js/ui/swipePopoverCalendar.js
+++ b/js/ui/swipePopoverCalendar.js
@@ -1,4 +1,5 @@
-import { addDays, startOfMonth, startOfWeekMonday } from '../services/dateUtils.js';
+import { addDays, startOfMonth } from '../services/dateUtils.js';
+import { renderCalendarGrid } from './mealCalendar.js?v=15';
const DEFAULT_WEEKDAYS = ['pn', 'wt', 'śr', 'cz', 'pt', 'sb', 'nd'];
const DEFAULT_MONTHS_LONG = [
@@ -8,13 +9,16 @@ const DEFAULT_MONTHS_LONG = [
const DEFAULT_THEME = {
selectedBorder: 'rgba(var(--text-emphasis-rgb),0.34)',
+ selectedBorderClass: 'border',
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: 'transparent',
+ borderClass: 'border-0',
text: 'rgb(var(--text-body-soft-rgb))',
dimmedBg: 'transparent',
+ dimmedBorderClass: 'border-0',
dimText: 'rgb(var(--text-faint-rgb))',
dimOpacity: 0.58,
dot: 'rgb(var(--text-faint-rgb))',
@@ -186,57 +190,37 @@ export function initSwipePopoverCalendar({
const renderMonthGrid = (targetGrid, monthAnchor, selectedSet) => {
if (!targetGrid) return;
- const first = startOfMonth(monthAnchor);
- const gridStart = startOfWeekMonday(first);
- const cells = Array.from({ length: 42 }, (_, i) => addDays(gridStart, i));
- targetGrid.innerHTML = cells.map((day) => {
- const dk = dateKeyLocal(day);
- const inCurrentMonth = day.getMonth() === first.getMonth();
- const isSelected = selectedSet.has(dk);
- const resolved = (typeof resolveDayState === 'function'
- ? resolveDayState(day, { inCurrentMonth, isSelected })
- : {}) || {};
- const disabled = !!resolved.disabled;
- const dimmed = !!resolved.dimmed;
- const showDot = !!resolved.showDot;
+ const calendarTheme = {
+ ...DEFAULT_THEME,
+ ...theme,
+ borderClass: theme.borderClass || DEFAULT_THEME.borderClass,
+ dimmedBorderClass: theme.dimmedBorderClass || DEFAULT_THEME.dimmedBorderClass,
+ selectedBorderClass: theme.selectedBorderClass || DEFAULT_THEME.selectedBorderClass,
+ };
- let bg;
- let borderColor;
- let text;
- let shadow = 'none';
- let borderClass = 'border-0';
- if (dimmed) {
- bg = theme.dimmedBg ?? DEFAULT_THEME.dimmedBg;
- borderColor = 'transparent';
- text = theme.dimText || DEFAULT_THEME.dimText;
- } else {
- 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;
- const tag = disabled ? 'div' : 'button';
- const attrs = disabled ? '' : `type="button" data-dk="${dk}"`;
- const dayClass = disabled ? '' : ' swc-day';
-
- 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}; box-shadow:${shadow}; touch-action:pan-y;">
-
- ${day.getDate()}
- ${showDot ? `` : ''}
-
- ${tag}>
- `;
- }).join('');
+ renderCalendarGrid({
+ gridEl: targetGrid,
+ mode: 'month',
+ anchorDate: monthAnchor,
+ fixedWeekCount: 6,
+ selectedDate: null,
+ isSelectedDate: (day) => selectedSet.has(dateKeyLocal(day)),
+ dayAttr: 'data-dk',
+ getDayAttrValue: dateKeyLocal,
+ dayClassName: 'swc-day',
+ dayStyle: 'touch-action:pan-y;',
+ theme: calendarTheme,
+ resolveDayState: (day, { inCurrentMonth, isSelected }) => {
+ const resolved = (typeof resolveDayState === 'function'
+ ? resolveDayState(day, { inCurrentMonth, isSelected })
+ : {}) || {};
+ return {
+ disabled: !!resolved.disabled,
+ dimmed: !!resolved.dimmed,
+ showIndicator: !!resolved.showDot,
+ };
+ },
+ });
};
const render = (previewSelection = null) => {
diff --git a/js/views/MealPlanner.js b/js/views/MealPlanner.js
index bb4c9bd..19b4694 100644
--- a/js/views/MealPlanner.js
+++ b/js/views/MealPlanner.js
@@ -1,14 +1,10 @@
import { INGREDIENTS, RECIPES } from '../data/catalog.js?v=9';
import { MEAL_SLOTS } from '../planner/mealSlots.js';
import {
- addMonths,
- addWeeks,
sameDay,
- sameMonth,
startOfDay,
startOfMonth,
startOfWeekMonday,
- weekContains,
} from '../services/dateUtils.js';
import {
computeEntryNutrition,
@@ -26,17 +22,18 @@ import {
savePlans,
} from '../services/planStore.js?v=2';
import {
- CALENDAR_HANDLE_CLASS,
CALENDAR_MONTHS_SHORT,
+ bindCollapsibleCalendarSwipeGesture,
bindCalendarDayClicks,
+ createCollapsibleCalendarHTML,
createCalendarTopbarHTML,
- createCalendarWeekdayHeaderHTML,
+ formatCalendarPeriodLabel,
isCalendarOnToday,
- renderCalendarGrid,
renderCollapsibleCalendar,
syncCalendarTodayButton,
syncCollapsibleCalendarMode,
-} from '../ui/mealCalendar.js?v=14';
+ syncCollapsibleCalendarToggleIcon,
+} from '../ui/mealCalendar.js?v=15';
import {
filterRecipesByQuery,
renderRecipeGrid,
@@ -67,6 +64,9 @@ function syncTodayButton(mode, weekStart, monthAnchor, selected) {
document.getElementById('cal-go-today'),
isCalendarOnToday(mode, weekStart, monthAnchor, selected),
selected,
+ {
+ labelText: formatCalendarPeriodLabel(mode, weekStart, monthAnchor),
+ },
);
}
@@ -81,19 +81,7 @@ export function getMealPlannerHTML() {
wrapperClass: 'flex shrink-0 items-center justify-end',
})}
+ ${createCollapsibleCalendarHTML({ idPrefix: 'calendar' })}