// Calendar.jsx — cycle calendar with phases, ovulation, predictions and memorable days
(function () {
const { useState, useMemo, useRef, useEffect, useCallback } = React;
const { t, cycleMarksForRange, cycleMarksFromPeriods, currentCycleInfo, calculateCycle, formatDayMonth, PHASE_INFO, localISO, pluralDayRu } = window.AliaData;
const PHASE_COLOR = { period:'#E8637A', fertile:'#16A8A8', ovulation:'#16A8A8', follicular:'#7FA8E0', luteal:'#C98BD4', delay:'#E8923D' };
function phaseLabel(lang) {
const ru = lang === 'ru';
return {
period: ru ? 'Менструация' : 'Period',
follicular: ru ? 'Фолликулярная фаза' : 'Follicular phase',
fertile: ru ? 'Фертильное окно' : 'Fertile window',
ovulation: ru ? 'Овуляция' : 'Ovulation',
luteal: ru ? 'Лютеиновая фаза' : 'Luteal phase',
delay: ru ? 'Задержка' : 'Delay',
};
}
const C = {
period: '#E8637A',
periodTxt: '#FFFFFF',
fertile: '#16A8A8',
ovulation: '#16A8A8',
delay: '#E8923D',
};
const iso = d => localISO(d);
const startOfMonth = (y, m) => new Date(y, m, 1);
const daysInMonth = (y, m) => new Date(y, m + 1, 0).getDate();
// Monday-first index for a Date
const mondayIndex = d => (d.getDay() + 6) % 7;
function todayISO() {
const d = new Date(); d.setHours(0, 0, 0, 0); return iso(d);
}
// ── Symptom / mood / flow / event option tables ────────────────
const FLOWS = [['light','💧'], ['medium','💧💧'], ['heavy','💧💧💧']];
const MOODS = [['happy','😊'], ['calm','😌'], ['sad','😢'], ['anxious','😰'], ['irritable','😣']];
const SYMS = [['cramps','🤕'], ['headache','🤯'], ['tender','🫶'], ['bloating','🎈'], ['fatigue','😴'], ['acne','🩹']];
const EVENTS = [['intimacy','❤️'], ['test','🧪'], ['doctor','🩺'], ['pill','💊'], ['anniversary','⭐'], ['note','📝']];
function capFirst(k) { return k.charAt(0).toUpperCase() + k.slice(1); }
function flowLabel(lang, k) { return t('flow' + capFirst(k), lang); }
function moodLabel(lang, k) { return t('mood' + capFirst(k), lang); }
function symLabel(lang, k) { return t('sym' + capFirst(k), lang); }
function evtLabel(lang, k) { return t('evt' + capFirst(k), lang); }
// payload helper — does this day have any user mark?
function hasMark(p) {
if (!p) return false;
return !!(p.flow || p.mood || (p.symptoms && p.symptoms.length) || (p.events && p.events.length));
}
/* ── Phase info bottom sheet ───────────────────────────────────── */
function PhaseInfoSheet({ lang, phase, onClose }) {
const info = (PHASE_INFO[lang] || PHASE_INFO.ru)[phase];
if (!info) return null;
const color = PHASE_COLOR[phase] || 'var(--primary)';
const s = {
overlay: { position:'fixed', inset:0, background:'rgba(0,0,0,0.4)', zIndex:400, display:'flex', alignItems:'flex-end', backdropFilter:'blur(4px)' },
sheet: { width:'100%', background:'var(--bg-card)', borderRadius:'24px 24px 0 0', padding:'18px 24px',
paddingBottom:'calc(26px + env(safe-area-inset-bottom))', display:'flex', flexDirection:'column', gap:14,
boxShadow:'0 -4px 24px rgba(0,0,0,0.15)', maxWidth:520, margin:'0 auto', maxHeight:'82vh', overflowY:'auto' },
handle: { width:40, height:4, borderRadius:2, background:'var(--border)', margin:'0 auto' },
head: { display:'flex', alignItems:'center', gap:12 },
badge: { width:48, height:48, borderRadius:14, display:'flex', alignItems:'center', justifyContent:'center',
fontSize:24, background: color + '22', flexShrink:0 },
title: { fontSize:19, fontWeight:800, color: color },
text: { fontSize:15, lineHeight:1.55, color:'var(--text)', fontWeight:500 },
close: { padding:'14px 0', borderRadius:14, border:'none', background:'var(--primary-light)', color:'var(--primary)',
fontSize:15, fontWeight:800, cursor:'pointer', fontFamily:'Nunito,sans-serif', marginTop:2 },
};
return (
e.stopPropagation()}>
{info.emoji}
{info.title}
{info.text}
);
}
/* ── Bottom sheet for a single day ─────────────────────────────── */
function DaySheet({ lang, dateISO, mark, payload, onSave, onClose, onPhaseInfo }) {
const ru = lang === 'ru';
const [flow, setFlow] = useState(payload.flow || '');
const [mood, setMood] = useState(payload.mood || '');
const [syms, setSyms] = useState(payload.symptoms || []);
const [events, setEvents] = useState(payload.events || []);
const toggle = (arr, set, v) => set(arr.includes(v) ? arr.filter(x => x !== v) : [...arr, v]);
const d = new Date(dateISO + 'T00:00:00');
const dateLabel = d.getDate() + ' ' + t('calMonths', lang)[d.getMonth()].toLowerCase();
const s = {
overlay: { position:'fixed', inset:0, background:'rgba(0,0,0,0.4)', zIndex:300, display:'flex', alignItems:'flex-end', backdropFilter:'blur(4px)' },
sheet: { width:'100%', background:'var(--bg-card)', borderRadius:'24px 24px 0 0', padding:'18px 22px',
paddingBottom:'calc(24px + env(safe-area-inset-bottom))', display:'flex', flexDirection:'column', gap:14,
boxShadow:'0 -4px 24px rgba(0,0,0,0.15)', maxWidth:520, margin:'0 auto', maxHeight:'82vh', overflowY:'auto' },
handle: { width:40, height:4, borderRadius:2, background:'var(--border)', margin:'0 auto' },
title: { fontSize:18, fontWeight:800, color:'var(--text)', textTransform:'capitalize' },
cdsub: { fontSize:13, color:'var(--text-2)', fontWeight:600 },
lbl: { fontSize:11, fontWeight:700, color:'var(--text-2)', letterSpacing:'0.5px', textTransform:'uppercase', marginBottom:9, display:'block' },
chips: { display:'flex', flexWrap:'wrap', gap:8 },
chip: (a) => ({ display:'flex', alignItems:'center', gap:6, padding:'9px 13px', borderRadius:13, border:'2px solid',
borderColor: a ? 'var(--primary)' : 'var(--border)', background: a ? 'var(--primary-light)' : 'var(--bg)',
color: a ? 'var(--primary)' : 'var(--text)', fontSize:13, fontWeight:700, cursor:'pointer', fontFamily:'Nunito,sans-serif' }),
save: { padding:'15px 0', borderRadius:15, border:'none', background:'var(--primary)', color:'#fff', fontSize:16, fontWeight:800, cursor:'pointer', fontFamily:'Nunito,sans-serif', marginTop:4 },
};
return (
e.stopPropagation()}>
{dateLabel}
{mark && mark.phase && (
)}
{t('dayFlow', lang)}
{FLOWS.map(([k, e]) => (
))}
{t('dayMood', lang)}
{MOODS.map(([k, e]) => (
))}
{t('daySymptoms', lang)}
{SYMS.map(([k, e]) => (
))}
{t('dayEvents', lang)}
{EVENTS.map(([k, e]) => (
))}
);
}
/* ── A single month grid ───────────────────────────────────────── */
function MonthGrid({ lang, year, month, marks, logs, today, onDayTap }) {
const total = daysInMonth(year, month);
const lead = mondayIndex(startOfMonth(year, month));
const cells = [];
for (let i = 0; i < lead; i++) cells.push(null);
for (let day = 1; day <= total; day++) cells.push(day);
const st = {
wrap: { padding:'4px 0 18px' },
title: { fontSize:17, fontWeight:800, color:'var(--text)', textAlign:'center', margin:'10px 0 12px' },
grid: { display:'grid', gridTemplateColumns:'repeat(7, 1fr)', rowGap:10 },
cellWrap: { minHeight:46, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'flex-start', position:'relative' },
cd: { fontSize:9, fontWeight:700, color:'var(--text-2)', height:11, lineHeight:'11px', marginBottom:1 },
};
function renderDay(day) {
const d = new Date(year, month, day);
const key = iso(d);
const m = marks[key];
const p = logs[key];
const isToday = key === today;
const marked = hasMark(p);
// base cell styles — большая цифра = ДАТА, маленькая сверху = день цикла
let circle = {
width:34, height:34, borderRadius:'50%', display:'flex', alignItems:'center', justifyContent:'center',
fontSize:14, fontWeight:700, color:'var(--text)', cursor:'pointer', position:'relative',
border:'2px solid transparent', boxSizing:'border-box', lineHeight:1,
};
if (m && m.phase === 'delay') {
circle.background = C.delay; circle.color = '#fff'; circle.fontWeight = 800;
} else if (m && m.phase === 'period') {
if (m.predicted) { circle.border = '2px dashed ' + C.period; circle.color = C.period; }
else { circle.background = C.period; circle.color = C.periodTxt; }
} else if (m && m.isOvulation) {
circle.border = '2px dashed ' + C.ovulation; circle.color = C.ovulation; circle.fontWeight = 800;
} else if (m && m.phase === 'fertile') {
circle.color = C.fertile; circle.fontWeight = 800;
}
if (isToday) {
circle.boxShadow = '0 0 0 2px var(--bg), 0 0 0 4px var(--text)';
}
return (
onDayTap(key, m, p || {})}>
{m && m.cycleDay ? m.cycleDay : ''}
{day}
{marked &&
}
);
}
return (
{t('calMonths', lang)[month] + ', ' + year}
{cells.map((c, idx) => c === null
?
: renderDay(c))}
);
}
/* ── Year overview (12 mini months) ────────────────────────────── */
function YearGrid({ lang, year, marks, today, onMonthTap }) {
const st = {
grid: { display:'grid', gridTemplateColumns:'repeat(3, 1fr)', gap:14, padding:'8px 0 18px' },
mini: { cursor:'pointer', background:'var(--bg-card)', borderRadius:14, padding:'10px 8px', boxShadow:'0 2px 10px var(--shadow)' },
mt: { fontSize:12, fontWeight:800, color:'var(--text)', textAlign:'center', marginBottom:6 },
mg: { display:'grid', gridTemplateColumns:'repeat(7, 1fr)', rowGap:2 },
dot: (bg, brd) => ({ width:11, height:11, margin:'0 auto', borderRadius:'50%', fontSize:7, lineHeight:'11px',
textAlign:'center', background:bg || 'transparent', border: brd || 'none', color:'#fff', boxSizing:'border-box' }),
};
function miniMonth(month) {
const total = daysInMonth(year, month);
const lead = mondayIndex(startOfMonth(year, month));
const cells = [];
for (let i = 0; i < lead; i++) cells.push(null);
for (let day = 1; day <= total; day++) cells.push(day);
return (
onMonthTap(month)}>
{t('calMonths', lang)[month]}
{cells.map((c, idx) => {
if (c === null) return
;
const key = iso(new Date(year, month, c));
const m = marks[key];
let bg = 'transparent', brd = 'none', color = 'var(--text-2)';
if (m && m.phase === 'delay') { bg = C.delay; color = '#fff'; }
else if (m && m.phase === 'period') { if (m.predicted) { brd = '1px dashed ' + C.period; color = C.period; } else { bg = C.period; color = '#fff'; } }
else if (m && (m.isOvulation || m.phase === 'fertile')) { brd = '1px solid ' + C.fertile; color = C.fertile; }
return
{c}
;
})}
);
}
return {[0,1,2,3,4,5,6,7,8,9,10,11].map(miniMonth)}
;
}
/* ── Calendar screen ───────────────────────────────────────────── */
function CalendarScreen({ lang, cycle, periods, logs, onSaveLog, onEditPeriod, onClose }) {
const ru = lang === 'ru';
const today = todayISO();
const td = new Date(today + 'T00:00:00');
const [view, setView] = useState('month'); // 'month' | 'year'
const [year, setYear] = useState(td.getFullYear());
const [daySheet, setDaySheet] = useState(null); // { dateISO, mark, payload }
const [phaseSheet, setPhaseSheet] = useState(null); // phase key
const info = useMemo(() => currentCycleInfo(periods, cycle), [periods, cycle]);
const labels = phaseLabel(lang);
const scrollRef = useRef(null);
const todayRef = useRef(null);
// Range of months to render: 6 back .. 12 forward
const months = useMemo(() => {
const list = [];
const base = new Date(td.getFullYear(), td.getMonth(), 1);
for (let i = -6; i <= 12; i++) {
const d = new Date(base.getFullYear(), base.getMonth() + i, 1);
list.push({ y: d.getFullYear(), m: d.getMonth() });
}
return list;
}, [today]); // eslint-disable-line
const marks = useMemo(() => {
const first = months[0];
const last = months[months.length - 1];
const fromISO = iso(new Date(first.y, first.m, 1));
const toISO = iso(new Date(last.y, last.m, daysInMonth(last.y, last.m)));
return cycleMarksFromPeriods(periods, cycle, fromISO, toISO);
}, [months, cycle, periods]);
// year-view marks (full selected year)
const yearMarks = useMemo(() => {
return cycleMarksFromPeriods(periods, cycle, year + '-01-01', year + '-12-31');
}, [year, cycle, periods]);
const scrollToToday = useCallback(() => {
if (todayRef.current && scrollRef.current) {
todayRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, []);
useEffect(() => {
// jump to current month on first open (no animation)
if (view === 'month' && todayRef.current) {
todayRef.current.scrollIntoView({ block: 'center' });
}
}, [view]); // eslint-disable-line
const handleDayTap = useCallback((dateISO, m, payload) => {
setDaySheet({ dateISO, mark: m, payload });
}, []);
const handleSave = useCallback((dateISO, payload) => {
onSaveLog(dateISO, payload);
setDaySheet(null);
}, [onSaveLog]);
const st = {
root: { position:'fixed', inset:0, zIndex:250, background:'var(--bg)', display:'flex', flexDirection:'column', fontFamily:'Nunito,sans-serif' },
header: { display:'flex', alignItems:'center', justifyContent:'space-between', gap:10, padding:'12px 16px',
paddingTop:'calc(12px + env(safe-area-inset-top))', background:'var(--bg-card)', borderBottom:'1px solid var(--border)', flexShrink:0 },
close: { background:'var(--bg)', border:'none', borderRadius:10, width:38, height:38, fontSize:20, cursor:'pointer', color:'var(--text)' },
toggle: { display:'flex', background:'var(--bg)', borderRadius:11, padding:3, gap:2 },
tBtn: (a) => ({ padding:'7px 16px', borderRadius:9, border:'none', cursor:'pointer', fontFamily:'Nunito,sans-serif',
fontSize:13, fontWeight:800, background: a ? 'var(--bg-card)' : 'transparent', color: a ? 'var(--primary)' : 'var(--text-2)',
boxShadow: a ? '0 1px 4px var(--shadow)' : 'none' }),
legend: { display:'flex', flexWrap:'wrap', gap:'6px 14px', padding:'10px 16px', background:'var(--bg-card)', borderBottom:'1px solid var(--border)', flexShrink:0 },
legItem: { display:'flex', alignItems:'center', gap:6, fontSize:11, fontWeight:600, color:'var(--text-2)' },
sw: (bg, brd) => ({ width:14, height:14, borderRadius:'50%', background:bg, border:brd || 'none', flexShrink:0 }),
scroll: { flex:1, overflowY:'auto', WebkitOverflowScrolling:'touch', padding:'0 16px 16px' },
yearNav: { display:'flex', alignItems:'center', justifyContent:'center', gap:24, padding:'12px 0 4px' },
yearBtn: { background:'none', border:'none', fontSize:22, cursor:'pointer', color:'var(--primary)', fontWeight:900 },
yearLbl: { fontSize:18, fontWeight:800, color:'var(--text)', minWidth:70, textAlign:'center' },
todayFab: { position:'absolute', left:'50%', transform:'translateX(-50%)', bottom:'calc(86px + env(safe-area-inset-bottom))',
background:'var(--bg-card)', border:'1px solid var(--border)', borderRadius:22, padding:'10px 18px', fontSize:13,
fontWeight:800, color:'var(--text)', cursor:'pointer', fontFamily:'Nunito,sans-serif', boxShadow:'0 4px 16px var(--shadow)', zIndex:5 },
footer: { flexShrink:0, padding:'12px 16px calc(14px + env(safe-area-inset-bottom))', background:'var(--bg-card)', borderTop:'1px solid var(--border)' },
editBtn: { width:'100%', padding:'15px 0', borderRadius:15, border:'none', background:'var(--primary)', color:'#fff',
fontSize:15, fontWeight:800, cursor:'pointer', fontFamily:'Nunito,sans-serif' },
weekdays: { display:'grid', gridTemplateColumns:'repeat(7, 1fr)', padding:'6px 0', position:'sticky', top:0,
background:'var(--bg)', zIndex:2 },
wd: { textAlign:'center', fontSize:11, fontWeight:700, color:'var(--text-2)' },
summary: { background:'var(--bg-card)', borderRadius:18, padding:'16px 18px', margin:'14px 0 6px',
boxShadow:'0 2px 12px var(--shadow)', display:'flex', flexDirection:'column', gap:12 },
sumTop: { display:'flex', alignItems:'center', justifyContent:'space-between', gap:12 },
sumDay: { fontSize:13, color:'var(--text-2)', fontWeight:600 },
sumDayBig: { fontSize:26, fontWeight:900, color:'var(--text)', lineHeight:1.1 },
sumPhase: (c) => ({ display:'inline-flex', alignItems:'center', gap:6, padding:'7px 13px', borderRadius:13,
border:'none', cursor:'pointer', fontFamily:'Nunito,sans-serif', fontSize:13, fontWeight:800,
background: c + '22', color: c }),
bar: { height:8, borderRadius:5, background:'var(--primary-light)', overflow:'hidden' },
barFill: (pct, c) => ({ height:'100%', width: Math.max(4, Math.min(100, pct)) + '%', background:c, borderRadius:5, transition:'width .3s' }),
sumRow: { display:'flex', justifyContent:'space-between', fontSize:13 },
hint: { fontSize:11, color:'var(--text-2)', textAlign:'center', fontWeight:600 },
};
const phaseC = info ? (PHASE_COLOR[info.phase] || 'var(--primary)') : 'var(--primary)';
function ovulationLine() {
if (!info) return null;
if (info.isOvulationToday) return '🌟 ' + t('calOvulationToday', lang);
if (info.daysToOvulation > 0) return t('calOvulationIn', lang) + ': ' + info.daysToOvulation + ' ' + t('days', lang);
return t('calOvulationDone', lang);
}
const summaryCard = info ? (
{t('dayCycleDay', lang)}
{info.cycleDay} {t('calCycleDayOf', lang)} {info.cycleLength}
{info.delayDays > 0 ? (
⏳
{ru ? `Задержка ${info.delayDays} ${pluralDayRu(info.delayDays)}` : `Period late by ${info.delayDays} ${info.delayDays === 1 ? 'day' : 'days'}`}
) : (
{t('calNextPeriodIn', lang)}
{info.daysToNextPeriod} {t('days', lang)} · {formatDayMonth(info.nextPeriod, lang)}
)}
{ovulationLine()}
{formatDayMonth(info.ovulation, lang)}
{t('calTapPhase', lang)}
) : null;
return (
{t('calLegPeriod', lang)}
{t('calLegFertile', lang)}
{t('calLegOvulation', lang)}
{t('calLegPredicted', lang)}
{t('calLegMark', lang)}
{view === 'month' ? (
{summaryCard}
{t('calWeekdays', lang).map((w, i) =>
{w}
)}
{months.map(({ y, m }) => {
const isCurrent = y === td.getFullYear() && m === td.getMonth();
return (
);
})}
) : (
{year}
{ setView('month'); }} />
)}
{view === 'month' && (
)}
{daySheet && (
setDaySheet(null)}
onPhaseInfo={ph => setPhaseSheet(ph)} />
)}
{phaseSheet && (
setPhaseSheet(null)} />
)}
);
}
/* ── Period editor (tap-to-toggle menstruation days) ───────────── */
function expandPeriodsToSet(periods, profile) {
const set = {};
const periodLen = (profile && profile.period_length) || 5;
// Не предзаполняем будущие дни: для периода без даты окончания разворачиваем
// только до сегодня. Иначе текущий период «без конца» помечал бы 4 будущих дня,
// и при снятии сегодняшнего дня они оставались бы как месячные на будущее.
const today = new Date(todayISO() + 'T00:00:00');
(periods || []).forEach(p => {
const s = (typeof p === 'string') ? p : (p && (p.start_date || p.start));
if (!s) return;
const sd = new Date(s + 'T00:00:00');
if (isNaN(sd.getTime())) return;
let endd;
if (p && p.end_date) { endd = new Date(p.end_date + 'T00:00:00'); }
else { endd = new Date(sd.getTime() + (periodLen - 1) * 86400000); }
if (endd.getTime() > today.getTime()) endd = today;
for (let d = new Date(sd); d.getTime() <= endd.getTime(); d = new Date(d.getTime() + 86400000)) {
set[iso(d)] = true;
}
});
return set;
}
function setToEpisodes(set) {
const dates = Object.keys(set).filter(k => set[k]).sort();
const episodes = [];
let cur = null;
dates.forEach(k => {
const d = new Date(k + 'T00:00:00');
if (cur && (d.getTime() - new Date(cur.end + 'T00:00:00').getTime()) === 86400000) {
cur.end = k;
} else {
cur = { start: k, end: k };
episodes.push(cur);
}
});
return episodes.map(e => ({ start_date: e.start, end_date: e.end }));
}
function PeriodEditScreen({ lang, periods, cycle, onSave, onClose }) {
const today = todayISO();
const td = new Date(today + 'T00:00:00');
const [sel, setSel] = useState(() => expandPeriodsToSet(periods, cycle));
const todayRef = useRef(null);
const months = useMemo(() => {
const list = [];
const base = new Date(td.getFullYear(), td.getMonth(), 1);
for (let i = -12; i <= 2; i++) {
const d = new Date(base.getFullYear(), base.getMonth() + i, 1);
list.push({ y: d.getFullYear(), m: d.getMonth() });
}
return list;
}, [today]); // eslint-disable-line
useEffect(() => { if (todayRef.current) todayRef.current.scrollIntoView({ block: 'center' }); }, []);
const toggle = key => setSel(s => { const n = { ...s }; if (n[key]) delete n[key]; else n[key] = true; return n; });
const st = {
root: { position:'fixed', inset:0, zIndex:260, background:'var(--bg)', display:'flex', flexDirection:'column', fontFamily:'Nunito,sans-serif' },
header: { display:'flex', alignItems:'center', justifyContent:'space-between', padding:'12px 18px',
paddingTop:'calc(12px + env(safe-area-inset-top))', background:'var(--bg-card)', borderBottom:'1px solid var(--border)', flexShrink:0 },
cancel: { background:'none', border:'none', fontSize:15, fontWeight:700, color:'var(--text-2)', cursor:'pointer', fontFamily:'Nunito,sans-serif' },
title: { fontSize:16, fontWeight:800, color:'var(--text)' },
save: { background:'none', border:'none', fontSize:15, fontWeight:800, color:'var(--primary)', cursor:'pointer', fontFamily:'Nunito,sans-serif' },
hint: { fontSize:12, color:'var(--text-2)', fontWeight:600, textAlign:'center', padding:'10px 16px', background:'var(--bg-card)', borderBottom:'1px solid var(--border)' },
scroll: { flex:1, overflowY:'auto', WebkitOverflowScrolling:'touch', padding:'0 16px 24px' },
weekdays: { display:'grid', gridTemplateColumns:'repeat(7, 1fr)', padding:'6px 0', position:'sticky', top:0, background:'var(--bg)', zIndex:2 },
wd: { textAlign:'center', fontSize:11, fontWeight:700, color:'var(--text-2)' },
mTitle: { fontSize:16, fontWeight:800, color:'var(--text)', textAlign:'center', margin:'12px 0 10px' },
grid: { display:'grid', gridTemplateColumns:'repeat(7, 1fr)', rowGap:6 },
cell: { aspectRatio:'1 / 1', display:'flex', alignItems:'center', justifyContent:'center', position:'relative' },
};
function dayCircle(on, isToday) {
return {
width:36, height:36, borderRadius:'50%', display:'flex', alignItems:'center', justifyContent:'center',
fontSize:14, fontWeight:700, cursor:'pointer', boxSizing:'border-box', lineHeight:1,
background: on ? C.period : 'transparent', color: on ? '#fff' : 'var(--text)',
border: on ? 'none' : '2px solid var(--border)',
boxShadow: isToday ? '0 0 0 2px var(--bg), 0 0 0 4px var(--text)' : 'none',
};
}
function monthGrid(y, m) {
const total = daysInMonth(y, m);
const lead = mondayIndex(startOfMonth(y, m));
const cells = [];
for (let i = 0; i < lead; i++) cells.push(null);
for (let day = 1; day <= total; day++) cells.push(day);
const isCurrent = y === td.getFullYear() && m === td.getMonth();
return (
{t('calMonths', lang)[m] + ', ' + y}
{cells.map((c, idx) => {
if (c === null) return
;
const key = iso(new Date(y, m, c));
const future = new Date(y, m, c).getTime() > td.getTime();
return (
{ if (!future) toggle(key); }}>{c}
);
})}
);
}
return (
{lang === 'ru' ? 'Нажмите на дни, когда шли месячные' : 'Tap the days you had your period'}
{t('calWeekdays', lang).map((w, i) =>
{w}
)}
{months.map(({ y, m }) => monthGrid(y, m))}
);
}
Object.assign(window, { CalendarScreen, PhaseInfoSheet, PeriodEditScreen });
})();