Calendar
thinkingcap qwen3.6 27b · typescript

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 71,977 bytes · SHA-256 a02ff6e59ef3
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Calendar & Event Planner</title>
<style>
/* ── css: layout ── */
/* ─── Reset & Base ───────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Google Sans', 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f8f9fa;
color: #202124;
line-height: 1.5;
overflow-x: hidden;
}
#calendar-app {
max-width: 1200px;
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* ─── Header ─────────────────────────────────────────────────── */
#cal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
background: #fff;
border-bottom: 1px solid #dadce0;
position: sticky;
top: 0;
z-index: 100;
}
.header-left {
display: flex;
align-items: center;
gap: 8px;
}
#cal-title {
font-size: 22px;
font-weight: 500;
margin-left: 4px;
color: #3c4043;
}
.header-right {
display: flex;
align-items: center;
gap: 4px;
}
button {
border: none;
background: transparent;
cursor: pointer;
font-family: inherit;
color: #5f6368;
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
transition: background 0.15s, color 0.15s;
}
button:hover { background: #f1f3f4; }
button:active { background: #e8eaed; }
#btn-today {
border: 1px solid #dadce0;
font-weight: 500;
color: #1a73e8;
}
#btn-today:hover { background: #e8f0fe; }
.view-btn {
font-weight: 500;
padding: 8px 16px;
border-radius: 4px;
}
.view-btn.active {
color: #1a73e8;
background: #e8f0fe;
}
#btn-settings {
font-size: 20px;
padding: 6px 10px;
margin-left: 4px;
}
/* ─── Main Content ───────────────────────────────────────────── */
#cal-main {
flex: 1;
background: #fff;
overflow: auto;
}
/* ─── Month View Grid ────────────────────────────────────────── */
.month-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
border-top: 1px solid #dadce0;
border-left: 1px solid #dadce0;
}
.day-header-cell {
text-align: center;
padding: 8px 4px;
font-size: 11px;
font-weight: 500;
color: #70757a;
text-transform: uppercase;
border-bottom: 1px solid #dadce0;
border-right: 1px solid #dadce0;
}
.day-cell {
min-height: 120px;
padding: 4px;
border-bottom: 1px solid #dadce0;
border-right: 1px solid #dadce0;
cursor: pointer;
position: relative;
transition: background 0.15s;
}
.day-cell:hover { background: #f8f9fa; }
.day-number {
font-size: 12px;
font-weight: 500;
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
margin-bottom: 2px;
}
.day-cell.other-month .day-number { color: #bbb; }
.day-cell.today .day-number { background: #1a73e8; color: #fff; font-weight: 700; }
/* ─── Event Chips (Month View) ──────────────────────────────── */
.event-chip {
display: block;
padding: 2px 6px;
margin-bottom: 1px;
border-radius: 3px;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
border-left: 3px solid;
background: rgba(0,0,0,0.04);
transition: filter 0.1s;
}
.event-chip:hover { filter: brightness(0.95); }
.event-more {
font-size: 11px;
color: #70757a;
padding: 2px 6px;
cursor: pointer;
font-weight: 500;
}
.event-more:hover { text-decoration: underline; }
/* ─── Week View ──────────────────────────────────────────────── */
.week-grid {
display: grid;
grid-template-columns: 60px repeat(7, 1fr);
position: relative;
}
.week-header-row {
display: contents;
}
.week-day-header {
text-align: center;
padding: 8px 4px;
font-size: 12px;
font-weight: 500;
color: #70757a;
border-bottom: 1px solid #dadce0;
}
.week-day-header .wdh-num {
display: block;
width: 36px;
height: 36px;
margin: 4px auto 0;
line-height: 36px;
border-radius: 50%;
font-size: 22px;
color: #3c4043;
}
.week-day-header.today .wdh-num { background: #1a73e8; color: #fff; }
.hour-label {
text-align: right;
padding-right: 8px;
font-size: 11px;
color: #70757a;
transform: translateY(-50%);
}
.week-hour-row {
display: contents;
}
.week-cell {
border-bottom: 1px solid #e8eaed;
border-right: 1px solid #e8eaed;
min-height: 48px;
position: relative;
}
.week-event-block {
position: absolute;
left: 2px;
right: 2px;
border-radius: 4px;
padding: 2px 6px;
font-size: 11px;
overflow: hidden;
cursor: pointer;
z-index: 5;
border-left: 3px solid;
}
.week-event-block:hover { filter: brightness(0.95); }
.all-day-strip {
display: grid;
grid-template-columns: 60px repeat(7, 1fr);
border-bottom: 1px solid #dadce0;
}
.all-day-cell {
padding: 2px 4px;
min-height: 28px;
border-right: 1px solid #e8eaed;
}
/* Current time indicator */
.time-indicator {
position: absolute;
left: 0;
right: 0;
height: 2px;
background: #ea4335;
z-index: 10;
pointer-events: none;
}
.time-indicator::before {
content: '';
position: absolute;
left: -6px;
top: -4px;
width: 10px;
height: 10px;
border-radius: 50%;
background: #ea4335;
}
/* ─── Agenda View ────────────────────────────────────────────── */
.agenda-list { padding: 16px 24px; }
.agenda-day-group { margin-bottom: 24px; }
.agenda-date-header {
font-size: 14px;
font-weight: 500;
color: #70757a;
text-transform: uppercase;
padding: 8px 0;
border-bottom: 1px solid #e8eaed;
margin-bottom: 8px;
}
.agenda-event-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
transition: background 0.15s;
}
.agenda-event-item:hover { background: #f1f3f4; }
.agenda-color-bar {
width: 4px;
height: 32px;
border-radius: 2px;
flex-shrink: 0;
}
.agenda-event-info { flex: 1; min-width: 0; }
.agenda-event-title { font-size: 14px; font-weight: 500; }
.agenda-event-time { font-size: 12px; color: #70757a; }
/* ─── Modal ──────────────────────────────────────────────────── */
#modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
#modal-overlay.hidden { display: none; }
.modal-box {
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 24px rgba(0,0,0,0.2);
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
padding: 24px;
}
.modal-title {
font-size: 20px;
font-weight: 500;
margin-bottom: 16px;
}
.form-group {
margin-bottom: 12px;
}
.form-group label {
display: block;
font-size: 12px;
color: #70757a;
margin-bottom: 4px;
font-weight: 500;
}
.form-group input, .form-group select, .form-group textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #dadce0;
border-radius: 4px;
font-size: 14px;
font-family: inherit;
}
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
outline: none;
border-color: #1a73e8;
box-shadow: 0 0 0 2px rgba(26,115,232,0.2);
}
.form-row {
display: flex;
gap: 12px;
}
.form-row .form-group { flex: 1; }
.color-options {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.color-swatch {
width: 28px;
height: 28px;
border-radius: 50%;
cursor: pointer;
border: 3px solid transparent;
transition: transform 0.1s, border-color 0.1s;
}
.color-swatch:hover { transform: scale(1.1); }
.color-swatch.selected { border-color: #202124; transform: scale(1.15); }
.modal-actions {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.btn-primary {
background: #1a73e8;
color: #fff;
font-weight: 500;
padding: 8px 24px;
}
.btn-primary:hover { background: #1557b0; }
.btn-danger {
color: #d93025;
border: 1px solid #dadce0;
}
.btn-danger:hover { background: #fce8e6; }
.error-text {
color: #d93025;
font-size: 12px;
margin-top: 4px;
}
/* ─── Day Popover ────────────────────────────────────────────── */
#day-popover {
position: fixed;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 24px rgba(0,0,0,0.2);
width: 300px;
max-height: 400px;
overflow-y: auto;
z-index: 500;
padding: 16px;
}
#day-popover.hidden { display: none; }
.popover-date {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
color: #3c4043;
}
.popover-event {
padding: 6px 8px;
border-radius: 4px;
margin-bottom: 4px;
font-size: 12px;
cursor: pointer;
border-left: 3px solid;
background: #f8f9fa;
}
.popover-event:hover { background: #e8eaed; }
/* ─── Toast ──────────────────────────────────────────────────── */
#toast-container {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 2000;
}
.toast {
background: #323232;
color: #fff;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
display: flex;
align-items: center;
gap: 12px;
animation: toastIn 0.3s ease;
}
.toast button {
color: #8ab4f8;
font-weight: 500;
padding: 4px 8px;
}
@keyframes toastIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* ─── Settings Modal ────────────────────────────────────────── */
.settings-option {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #e8eaed;
}
/* ─── Animations ────────────────────────────────────────────── */
.view-transition {
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
/* ─── Responsive ────────────────────────────────────────────── */
@media (max-width: 768px) {
#cal-header { flex-wrap: wrap; gap: 8px; padding: 8px; }
.header-left, .header-right { width: 100%; justify-content: center; }
#cal-title { font-size: 18px; }
.day-cell { min-height: 80px; }
.event-chip { font-size: 10px; padding: 1px 4px; }
}
/* ─── Dragging ──────────────────────────────────────────────── */
.dragging { opacity: 0.5 !important; }
.drag-over { background: #e8f0fe !important; }
</style>
</head>
<body>
<div id="calendar-app">
<header id="cal-header">
<div class="header-left">
<button id="btn-today" title="Today">Today</button>
<button id="btn-prev" title="Previous"><</button>
<button id="btn-next" title="Next">></button>
<h1 id="cal-title"></h1>
</div>
<div class="header-right">
<button id="btn-view-month" class="view-btn active">Month</button>
<button id="btn-view-week" class="view-btn">Week</button>
<button id="btn-view-agenda" class="view-btn">Agenda</button>
<button id="btn-settings" title="Settings">⚙</button>
</div>
</header>
<main id="cal-main"></main>
<!-- Modal overlay -->
<div id="modal-overlay" class="hidden">
<div id="modal-content"></div>
</div>
<!-- Day popover -->
<div id="day-popover" class="hidden"></div>
<!-- Toast notifications -->
<div id="toast-container"></div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./state":"src/state.ts","./render":"src/render.ts","./modal":"src/modal.ts","./date-util":"src/date-util.ts"},"src/date-util.ts":{"./constants":"src/constants.ts"},"src/state.ts":{"./constants":"src/constants.ts","./date-util":"src/date-util.ts"},"src/render.ts":{"./state":"src/state.ts","./date-util":"src/date-util.ts","./constants":"src/constants.ts"},"src/modal.ts":{"./constants":"src/constants.ts","./date-util":"src/date-util.ts","./state":"src/state.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
// ─── Entry Point: Wire up navigation, events, drag-drop ──────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.reRender = reRender;
const state_1 = require("./state");
const render_1 = require("./render");
const modal_1 = require("./modal");
const date_util_1 = require("./date-util");
// ─── Global re-render hook (used by modal.ts) ──────────────────
function reRender() {
const vs = (0, state_1.getViewState)();
switch (vs.mode) {
case 'month':
(0, render_1.renderMonthView)();
break;
case 'week':
(0, render_1.renderWeekView)();
break;
case 'agenda':
(0, render_1.renderAgendaView)();
break;
}
}
// ─── Init ──────────────────────────────────────────────────────
(0, state_1.loadState)();
reRender();
// ─── Navigation Buttons ────────────────────────────────────────
document.getElementById('btn-prev').addEventListener('click', () => {
const vs = (0, state_1.getViewState)();
if (vs.mode === 'month' || vs.mode === 'agenda') {
let m = vs.currentMonth - 1;
let y = vs.currentYear;
if (m < 0) {
m = 11;
y--;
}
(0, state_1.setViewState)({ currentMonth: m, currentYear: y });
}
else {
const d = (0, date_util_1.addDays)(vs.currentYear, vs.currentMonth, 1, -7);
(0, state_1.setViewState)({ currentYear: d.year, currentMonth: d.month });
}
reRender();
});
document.getElementById('btn-next').addEventListener('click', () => {
const vs = (0, state_1.getViewState)();
if (vs.mode === 'month' || vs.mode === 'agenda') {
let m = vs.currentMonth + 1;
let y = vs.currentYear;
if (m > 11) {
m = 0;
y++;
}
(0, state_1.setViewState)({ currentMonth: m, currentYear: y });
}
else {
const d = (0, date_util_1.addDays)(vs.currentYear, vs.currentMonth, 1, 7);
(0, state_1.setViewState)({ currentYear: d.year, currentMonth: d.month });
}
reRender();
});
document.getElementById('btn-today').addEventListener('click', () => {
const t = (0, date_util_1.today)();
(0, state_1.setViewState)({ currentYear: t.year, currentMonth: t.month });
reRender();
});
// ─── View Switching ──────────────────────────────────────────
const viewBtns = document.querySelectorAll('.view-btn');
viewBtns.forEach(btn => {
btn.addEventListener('click', () => {
viewBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const mode = btn.id.replace('btn-view-', '');
(0, state_1.setViewState)({ mode });
reRender();
});
});
// ─── Settings Button ──────────────────────────────────────────
document.getElementById('btn-settings').addEventListener('click', () => {
(0, modal_1.openSettingsModal)();
});
// ─── Event Delegation: Click on day cells, event chips ────────
const mainEl = document.getElementById('cal-main');
mainEl.addEventListener('click', (e) => {
const target = e.target;
// Check if clicked an event chip/block
const eventChip = target.closest('[data-event-id]');
console.log('Click: target=', target.tagName, 'eventChip=', !!eventChip);
if (eventChip && eventChip.dataset.eventId) {
e.stopPropagation();
const eventId = eventChip.dataset.eventId;
console.log('eventId:', eventId);
const baseEv = (0, state_1.getEventById)(eventId);
console.log('baseEv:', baseEv ? 'found' : 'not found', !!baseEv?.recurrence);
if (!baseEv)
return;
// If recurring, find the occurrence date from context
let ev = baseEv;
if (baseEv.recurrence) {
const dayCell = eventChip.closest('[data-date-key]');
console.log('dayCell:', !!dayCell);
if (dayCell && dayCell.dataset.dateKey) {
const key = dayCell.dataset.dateKey;
const [y, m, d] = key.split('-').map(Number);
ev = { ...baseEv, _occurrenceDate: { year: y, month: m - 1, day: d } };
}
}
console.log('ev.recurrence:', !!ev.recurrence);
if (ev.recurrence) {
console.log('Opening recurrence edit options');
(0, modal_1.showRecurrenceEditOptions)(ev);
}
else {
console.log('Opening regular editor');
(0, modal_1.openEventEditor)(ev);
}
return;
}
// Check if clicked a day cell (month view) — not on an event chip
const dayCell = target.closest('.day-cell');
if (dayCell && !eventChip) {
e.stopPropagation();
if (dayCell.dataset.dateKey) {
const [y, m, d] = dayCell.dataset.dateKey.split('-').map(Number);
(0, modal_1.openEventEditor)(undefined, { year: y, month: m - 1, day: d });
}
return;
}
// Check "+more" popover trigger
const moreBtn = target.closest('.event-more');
if (moreBtn && moreBtn.dataset.dateKey) {
e.stopPropagation();
showDayPopover(moreBtn);
return;
}
});
// Close popover on outside click
document.addEventListener('click', (e) => {
const popover = document.getElementById('day-popover');
if (!popover.contains(e.target)) {
popover.classList.add('hidden');
}
});
// ─── Day Popover ──────────────────────────────────────────────
function showDayPopover(trigger) {
const key = trigger.dataset.dateKey;
if (!key)
return;
const [y, m, d] = key.split('-').map(Number);
const ymd = { year: y, month: m - 1, day: d };
const events = (0, state_1.getEventsForDate)(ymd);
const popover = document.getElementById('day-popover');
let html = `<div class="popover-date">${formatDateShortLocal(ymd)}</div>`;
for (const ev of events) {
const timeStr = ev.allDay ? 'All day' : `${minsToStr(ev.startTime)} – ${minsToStr(ev.endTime)}`;
html += `<div class="popover-event" style="border-left-color:${ev.color};" data-event-id="${ev.id}" data-date-key="${key}">`;
html += `<strong>${escapeHtml(ev.title)}</strong><br><span style="color:#70757a;">${timeStr}</span>`;
html += '</div>';
}
popover.innerHTML = html;
popover.classList.remove('hidden');
const rect = trigger.getBoundingClientRect();
popover.style.top = (rect.bottom + 4) + 'px';
popover.style.left = Math.min(rect.left, window.innerWidth - 320) + 'px';
}
function formatDateShortLocal(ymd) {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return `${months[ymd.month]} ${ymd.day}, ${ymd.year}`;
}
function minsToStr(mins) {
const h = Math.floor(mins / 60);
const m = mins % 60;
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
}
function escapeHtml(s) {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
// ─── Drag-to-Reschedule (Month View) ──────────────────────────
let dragState = null;
mainEl.addEventListener('dragstart', (e) => {
const chip = e.target.closest('.event-chip');
if (!chip || !chip.dataset.eventId)
return;
const dayCell = chip.closest('.day-cell');
if (!dayCell)
return;
dragState = { eventId: chip.dataset.eventId, sourceKey: dayCell.dataset.dateKey ?? '' };
e.target.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
});
mainEl.addEventListener('dragend', () => {
dragState = null;
mainEl.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
});
mainEl.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const cell = e.target.closest('.day-cell');
if (cell) {
mainEl.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
cell.classList.add('drag-over');
}
});
mainEl.addEventListener('drop', (e) => {
e.preventDefault();
const cell = e.target.closest('.day-cell');
if (!cell || !dragState)
return;
mainEl.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
const targetKey = cell.dataset.dateKey;
if (!targetKey || targetKey === dragState.sourceKey) {
dragState = null;
return;
}
const [y, m, d] = targetKey.split('-').map(Number);
(0, state_1.updateEvent)(dragState.eventId, { date: { year: y, month: m - 1, day: d } });
dragState = null;
reRender();
});
// Make event chips draggable on mouseover (lazy init)
mainEl.addEventListener('mouseover', (e) => {
const chip = e.target.closest('.event-chip');
if (chip && !chip.hasAttribute('draggable')) {
chip.setAttribute('draggable', 'true');
}
});
// ─── Drag-to-Reschedule (Week View — vertical time change) ────
let weekDragState = null;
mainEl.addEventListener('mousedown', (e) => {
const block = e.target.closest('.week-event-block');
if (!block || !block.dataset.eventId)
return;
e.preventDefault();
const ev = (0, state_1.getEventById)(block.dataset.eventId);
if (!ev || ev.allDay)
return;
weekDragState = {
eventId: block.dataset.eventId,
startY: e.clientY,
origStartMin: ev.startTime,
origEndMin: ev.endTime,
};
document.addEventListener('mousemove', onWeekDragMove);
document.addEventListener('mouseup', onWeekDragUp);
});
function onWeekDragMove(e) {
if (!weekDragState)
return;
const dy = e.clientY - weekDragState.startY;
// 48px per hour → 12px per 15 min snap
const snapPx = 12;
const snaps = Math.round(dy / snapPx);
const deltaMin = snaps * 15;
let newStart = weekDragState.origStartMin + deltaMin;
let newEnd = weekDragState.origEndMin + deltaMin;
if (newStart < 0) {
newStart = 0;
newEnd = Math.max(weekDragState.origEndMin - weekDragState.origStartMin, 15);
}
if (newEnd > 1439) {
newEnd = 1439;
newStart = Math.min(newEnd - 15, 1424);
}
(0, state_1.updateEvent)(weekDragState.eventId, { startTime: newStart, endTime: newEnd });
}
function onWeekDragUp() {
weekDragState = null;
document.removeEventListener('mousemove', onWeekDragMove);
document.removeEventListener('mouseup', onWeekDragUp);
reRender();
}
// ─── Keyboard Navigation ──────────────────────────────────────
document.addEventListener('keydown', (e) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement)
return;
switch (e.key) {
case 'ArrowLeft':
document.getElementById('btn-prev').click();
break;
case 'ArrowRight':
document.getElementById('btn-next').click();
break;
case 't':
case 'T':
document.getElementById('btn-today').click();
break;
}
});
};
// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
// ─── Tunable Constants ───────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.STORAGE_KEY_VIEW = exports.STORAGE_KEY_SETTINGS = exports.STORAGE_KEY_EVENTS = exports.DAY_NAMES_MON = exports.DAY_NAMES_SUN = exports.MONTH_NAMES = exports.DEFAULT_WEEK_START = exports.SNAP_MINUTES = exports.HOUR_END = exports.HOUR_START = exports.EVENT_COLORS = void 0;
exports.EVENT_COLORS = [
'#3B82F6', // blue
'#EF4444', // red
'#10B981', // green
'#F59E0B', // amber
'#8B5CF6', // purple
'#EC4899', // pink
'#06B6D4', // cyan
'#84CC16', // lime
];
exports.HOUR_START = 6; // first hour row in week view
exports.HOUR_END = 22; // last hour row in week view
exports.SNAP_MINUTES = 15; // drag snap interval in minutes
exports.DEFAULT_WEEK_START = 0; // 0 = Sunday, 1 = Monday
exports.MONTH_NAMES = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
exports.DAY_NAMES_SUN = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
exports.DAY_NAMES_MON = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
exports.STORAGE_KEY_EVENTS = 'cal_events';
exports.STORAGE_KEY_SETTINGS = 'cal_settings';
exports.STORAGE_KEY_VIEW = 'cal_view_state';
};
// ── module: src/date-util.ts ──
__mods["src/date-util.ts"] = function (exports, require, module) {
"use strict";
// ─── Pure Date Utilities (no rendering, no side effects) ──────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.isLeapYear = isLeapYear;
exports.daysInMonth = daysInMonth;
exports.dayOfWeek = dayOfWeek;
exports.normalizeYMD = normalizeYMD;
exports.addDays = addDays;
exports.addMonths = addMonths;
exports.addYears = addYears;
exports.sameDay = sameDay;
exports.compareYMD = compareYMD;
exports.inRange = inRange;
exports.today = today;
exports.nowMinutes = nowMinutes;
exports.buildMonthGrid = buildMonthGrid;
exports.getWeekDays = getWeekDays;
exports.expandRecurrence = expandRecurrence;
exports.minutesToTimeString = minutesToTimeString;
exports.timeStringToMinutes = timeStringToMinutes;
exports.formatMonthYear = formatMonthYear;
exports.formatDateShort = formatDateShort;
const constants_1 = require("./constants");
// ─── Core calendar math ──────────────────────────────────────────────
/** Is the given year a leap year? Handles century rule. */
function isLeapYear(year) {
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
}
/** Number of days in month m (0-based) of the given year. */
function daysInMonth(year, month) {
if (month === 1)
return isLeapYear(year) ? 29 : 28;
const table = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
return table[month];
}
/** Day of week for a given date: 0 = Sunday … 6 = Saturday. */
function dayOfWeek(year, month, day) {
// Tomohiko Sakamoto's algorithm — works for any Gregorian date
const t = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
let y = year;
if (month < 2)
y--;
return (y + Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400) + t[month] + day) % 7;
}
/** Normalize a YMD — clamp day to valid range, carry overflows. */
function normalizeYMD(year, month, day) {
while (month < 0) {
month += 12;
year--;
}
while (month > 11) {
month -= 12;
year++;
}
const max = daysInMonth(year, month);
if (day < 1)
day = 1;
if (day > max)
day = max;
return { year, month, day };
}
// ─── Date arithmetic on components (never uses ms offsets) ──────────
/** Add n days to a YMD. Positive or negative. */
function addDays(year, month, day, n) {
let y = year, m = month, d = day;
if (n >= 0) {
for (let i = 0; i < n; i++) {
d++;
if (d > daysInMonth(y, m)) {
d = 1;
m++;
if (m > 11) {
m = 0;
y++;
}
}
}
}
else {
for (let i = 0; i < -n; i++) {
d--;
if (d < 1) {
m--;
if (m < 0) {
m = 11;
y--;
}
d = daysInMonth(y, m);
}
}
}
return { year: y, month: m, day: d };
}
/** Add n months to a YMD. Clamps day to valid range of target month. */
function addMonths(year, month, day, n) {
let m = month + n;
let y = year;
while (m < 0) {
m += 12;
y--;
}
while (m > 11) {
m -= 12;
y++;
}
const d = Math.min(day, daysInMonth(y, m));
return { year: y, month: m, day: d };
}
/** Add n years. Clamps Feb 29 → Feb 28 in non-leap years. */
function addYears(year, month, day, n) {
const y = year + n;
const d = Math.min(day, daysInMonth(y, month));
return { year: y, month, day: d };
}
// ─── Comparison & equality ──────────────────────────────────────────
function sameDay(a, b) {
return a.year === b.year && a.month === b.month && a.day === b.day;
}
/** -1 if a < b, +1 if a > b, 0 if equal. */
function compareYMD(a, b) {
if (a.year !== b.year)
return a.year - b.year;
if (a.month !== b.month)
return a.month - b.month;
return a.day - b.day;
}
/** Is date d within [start, end] inclusive? */
function inRange(d, start, end) {
return compareYMD(d, start) >= 0 && compareYMD(d, end) <= 0;
}
// ─── Today ──────────────────────────────────────────────────────────
function today() {
const d = new Date();
return { year: d.getFullYear(), month: d.getMonth(), day: d.getDate() };
}
/** Current time in minutes since midnight. */
function nowMinutes() {
const d = new Date();
return d.getHours() * 60 + d.getMinutes();
}
/** Build the 42-cell (6×7) month grid for a given year/month. */
function buildMonthGrid(year, month, weekStart) {
const todayYMD = today();
const firstDow = dayOfWeek(year, month, 1);
// Adjust to week start: if weekStart=1 (Mon), shift so Mon=0
let offset = firstDow - weekStart;
if (offset < 0)
offset += 7;
const grid = [];
const dim = daysInMonth(year, month);
// Leading days from previous month
for (let i = 0; i < offset; i++) {
const prevDate = addDays(year, month, 1, -(offset - i));
grid.push({ ymd: prevDate, isCurrentMonth: false, isToday: sameDay(prevDate, todayYMD) });
}
// Current month days
for (let d = 1; d <= dim; d++) {
const ymd = { year, month, day: d };
grid.push({ ymd, isCurrentMonth: true, isToday: sameDay(ymd, todayYMD) });
}
// Trailing days from next month
while (grid.length < 42) {
const nextDate = addDays(year, month, dim, grid.length - offset - dim + 1);
grid.push({ ymd: nextDate, isCurrentMonth: false, isToday: sameDay(nextDate, todayYMD) });
}
return grid;
}
// ─── Week helpers ──────────────────────────────────────────────────
/** Get the 7 days of the week containing the given date. */
function getWeekDays(ymd, weekStart) {
const dow = dayOfWeek(ymd.year, ymd.month, ymd.day);
let offset = dow - weekStart;
if (offset < 0)
offset += 7;
return Array.from({ length: 7 }, (_, i) => addDays(ymd.year, ymd.month, ymd.day, i - offset));
}
/** Expand a recurrence rule into dates within [rangeStart, rangeEnd]. */
function expandRecurrence(baseDate, rule, rangeStart, rangeEnd) {
if (!rule)
return [baseDate];
const dates = [];
const interval = rule.interval ?? 1;
let current = baseDate;
let emitted = 0;
// Safety cap to prevent infinite loops on bad rules
const maxIterations = 5000;
let iterations = 0;
while (iterations < maxIterations) {
iterations++;
if (compareYMD(current, rangeStart) >= 0 && compareYMD(current, rangeEnd) <= 0) {
dates.push({ ...current });
emitted++;
}
if (rule.count !== undefined && emitted >= rule.count)
break;
if (compareYMD(current, rangeEnd) > 0)
break;
// Advance to next occurrence
switch (rule.frequency) {
case 'daily':
current = addDays(current.year, current.month, current.day, interval);
break;
case 'weekly': {
const weekdays = rule.weekdays ?? [dayOfWeek(baseDate.year, baseDate.month, baseDate.day)];
// Advance by interval weeks, then find the next matching weekday
let candidate = addDays(current.year, current.month, current.day, 7 * interval);
for (let tryCount = 0; tryCount < 7 * interval + 1 && iterations < maxIterations; tryCount++) {
const dow = dayOfWeek(candidate.year, candidate.month, candidate.day);
if (weekdays.includes(dow))
break;
candidate = addDays(candidate.year, candidate.month, candidate.day, 1);
iterations++;
}
current = candidate;
break;
}
case 'monthly': {
// Monthly: same day-of-month. Skip months where the day doesn't exist (e.g., Jan 31 → skip Feb).
let nextMonth = addMonths(current.year, current.month, current.day, interval);
if (nextMonth.day !== baseDate.day) {
// The target month doesn't have this day — skip it entirely
continue;
}
current = nextMonth;
break;
}
case 'yearly': {
let nextYear = addYears(current.year, current.month, current.day, interval);
if (nextYear.day !== baseDate.day) {
// e.g., Feb 29 in non-leap year — skip
continue;
}
current = nextYear;
break;
}
}
}
return dates;
}
// ─── Time helpers ──────────────────────────────────────────────────
function minutesToTimeString(mins) {
const h = Math.floor(mins / 60);
const m = mins % 60;
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
}
function timeStringToMinutes(s) {
const [h, m] = s.split(':').map(Number);
return h * 60 + (m || 0);
}
// ─── Formatting ────────────────────────────────────────────────────
function formatMonthYear(year, month) {
return `${constants_1.MONTH_NAMES[month]} ${year}`;
}
function formatDateShort(ymd) {
return `${constants_1.MONTH_NAMES[ymd.month].slice(0, 3)} ${ymd.day}`;
}
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
// ─── State: Events, Settings, Persistence ────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadState = loadState;
exports.getEvents = getEvents;
exports.getSettings = getSettings;
exports.getViewState = getViewState;
exports.setViewState = setViewState;
exports.updateSettings = updateSettings;
exports.createEvent = createEvent;
exports.updateEvent = updateEvent;
exports.deleteEvent = deleteEvent;
exports.getEventById = getEventById;
exports.getEventsForDate = getEventsForDate;
exports.getEventsInRange = getEventsInRange;
const date_util_1 = require("./date-util");
// ─── State ──────────────────────────────────────────────────────────
let events = [];
let settings = { weekStart: 0 };
let viewState = { mode: 'month', currentYear: (0, date_util_1.today)().year, currentMonth: (0, date_util_1.today)().month };
// ─── Persistence ────────────────────────────────────────────────────
const constants_1 = require("./constants");
function saveEvents() {
try {
localStorage.setItem(constants_1.STORAGE_KEY_EVENTS, JSON.stringify(events));
}
catch { }
}
function saveSettings() {
try {
localStorage.setItem(constants_1.STORAGE_KEY_SETTINGS, JSON.stringify(settings));
}
catch { }
}
function saveViewState() {
try {
localStorage.setItem(constants_1.STORAGE_KEY_VIEW, JSON.stringify(viewState));
}
catch { }
}
function loadState() {
try {
const ev = localStorage.getItem(constants_1.STORAGE_KEY_EVENTS);
if (ev)
events = JSON.parse(ev);
}
catch {
events = [];
}
try {
const st = localStorage.getItem(constants_1.STORAGE_KEY_SETTINGS);
if (st)
settings = JSON.parse(st);
}
catch { }
try {
const vs = localStorage.getItem(constants_1.STORAGE_KEY_VIEW);
if (vs)
viewState = JSON.parse(vs);
}
catch { }
}
// ─── Accessors ──────────────────────────────────────────────────────
function getEvents() { return events; }
function getSettings() { return settings; }
function getViewState() { return viewState; }
function setViewState(vs) {
Object.assign(viewState, vs);
saveViewState();
}
function updateSettings(s) {
Object.assign(settings, s);
saveSettings();
}
// ─── Event CRUD ─────────────────────────────────────────────────────
function genId() {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
}
function createEvent(data) {
const ev = { ...data, id: genId() };
events.push(ev);
saveEvents();
return ev;
}
function updateEvent(id, data) {
const idx = events.findIndex(e => e.id === id);
if (idx < 0)
return null;
Object.assign(events[idx], data);
saveEvents();
return events[idx];
}
function deleteEvent(id) {
const idx = events.findIndex(e => e.id === id);
if (idx < 0)
return false;
events.splice(idx, 1);
saveEvents();
return true;
}
function getEventById(id) {
return events.find(e => e.id === id);
}
// ─── Query helpers ──────────────────────────────────────────────────
/** Get all event occurrences on a given date (expanding recurrences). */
function getEventsForDate(date, rangeStart, rangeEnd) {
const rs = rangeStart ?? date;
const re = rangeEnd ?? date;
const results = [];
for (const ev of events) {
// Non-recurring or single occurrence on this date
if (!ev.recurrence && (0, date_util_1.sameDay)(ev.date, date)) {
results.push({ ...ev });
continue;
}
// Recurring event — expand within range
if (ev.recurrence) {
const dates = (0, date_util_1.expandRecurrence)(ev.date, ev.recurrence, rs, re);
for (const d of dates) {
if ((0, date_util_1.sameDay)(d, date)) {
// Check exceptions
if (!ev.exceptionIds?.some(eid => eid === ev.id + '@' + ymdKey(d))) {
results.push({ ...ev, _occurrenceDate: d });
}
break;
}
}
}
}
return results.sort((a, b) => (a.allDay ? -1 : 0) || (a.startTime - b.startTime));
}
function ymdKey(ymd) {
return `${ymd.year}-${String(ymd.month + 1).padStart(2, '0')}-${String(ymd.day).padStart(2, '0')}`;
}
/** Get all events in a date range (for agenda view). */
function getEventsInRange(start, end) {
const results = [];
for (const ev of events) {
if (!ev.recurrence) {
if ((0, date_util_1.compareYMD)(ev.date, start) >= 0 && (0, date_util_1.compareYMD)(ev.date, end) <= 0) {
results.push({ ...ev });
}
}
else {
const dates = (0, date_util_1.expandRecurrence)(ev.date, ev.recurrence, start, end);
for (const d of dates) {
if (!ev.exceptionIds?.some(eid => eid === ev.id + '@' + ymdKey(d))) {
results.push({ ...ev, _occurrenceDate: d });
}
}
}
}
return results;
}
};
// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
// ─── Rendering: Month, Week, Agenda Views ──────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.renderMonthView = renderMonthView;
exports.renderWeekView = renderWeekView;
exports.renderAgendaView = renderAgendaView;
const state_1 = require("./state");
const date_util_1 = require("./date-util");
const constants_1 = require("./constants");
const mainEl = document.getElementById('cal-main');
const titleEl = document.getElementById('cal-title');
// ─── Month View ──────────────────────────────────────────────────
function renderMonthView() {
const vs = (0, state_1.getViewState)();
const ws = (0, state_1.getSettings)().weekStart;
const grid = (0, date_util_1.buildMonthGrid)(vs.currentYear, vs.currentMonth, ws);
const dayNames = ws === 0 ? constants_1.DAY_NAMES_SUN : constants_1.DAY_NAMES_MON;
titleEl.textContent = (0, date_util_1.formatMonthYear)(vs.currentYear, vs.currentMonth);
let html = '<div class="month-grid view-transition">';
// Day headers
for (const name of dayNames) {
html += `<div class="day-header-cell">${name}</div>`;
}
// Grid cells
for (const cell of grid) {
const isToday = cell.isToday;
const classes = ['day-cell'];
if (!cell.isCurrentMonth)
classes.push('other-month');
if (isToday)
classes.push('today');
html += `<div class="${classes.join(' ')}" data-date-key="${cell.ymd.year}-${String(cell.ymd.month + 1).padStart(2, '0')}-${String(cell.ymd.day).padStart(2, '0')}">`;
html += `<div class="day-number">${cell.ymd.day}</div>`;
// Events for this day
const events = (0, state_1.getEventsForDate)(cell.ymd);
const maxShow = 3;
for (let i = 0; i < Math.min(events.length, maxShow); i++) {
const ev = events[i];
const color = ev.color;
html += `<span class="event-chip" style="border-left-color:${color};background:${color}22;color:#333;" data-event-id="${ev.id}" title="${escapeHtml(ev.title)}">${escapeHtml(ev.title)}</span>`;
}
if (events.length > maxShow) {
html += `<div class="event-more" data-date-key="${cell.ymd.year}-${String(cell.ymd.month + 1).padStart(2, '0')}-${String(cell.ymd.day).padStart(2, '0')}">+${events.length - maxShow} more</div>`;
}
html += '</div>';
}
html += '</div>';
mainEl.innerHTML = html;
}
// ─── Week View ──────────────────────────────────────────────────
function renderWeekView() {
const vs = (0, state_1.getViewState)();
const ws = (0, state_1.getSettings)().weekStart;
const weekDays = (0, date_util_1.getWeekDays)({ year: vs.currentYear, month: vs.currentMonth, day: 1 }, ws);
// Use the actual current view date for centering
const viewDate = { year: vs.currentYear, month: vs.currentMonth, day: (0, state_1.getViewState)().currentMonth === vs.currentMonth ? 1 : 1 };
titleEl.textContent = formatWeekTitle(weekDays[0], weekDays[6]);
let html = '<div class="view-transition">';
// All-day strip
html += '<div class="all-day-strip">';
html += '<div></div>'; // corner cell
for (const wd of weekDays) {
const events = (0, state_1.getEventsForDate)(wd).filter(e => e.allDay);
html += `<div class="all-day-cell" data-date-key="${ymdKey(wd)}">`;
for (const ev of events) {
html += `<span class="event-chip" style="border-left-color:${ev.color};background:${ev.color}22;" data-event-id="${ev.id}">${escapeHtml(ev.title)}</span>`;
}
html += '</div>';
}
html += '</div>';
// Hour grid
html += '<div class="week-grid" id="week-grid">';
// Header row
const dayNames = ws === 0 ? constants_1.DAY_NAMES_SUN : constants_1.DAY_NAMES_MON;
html += '<div class="week-header-row"><div></div>';
for (let i = 0; i < 7; i++) {
const wd = weekDays[i];
const isToday = (0, date_util_1.sameDay)(wd, (0, date_util_1.today)());
const cls = isToday ? 'today' : '';
html += `<div class="week-day-header ${cls}">${dayNames[ws === 0 ? i : (i + 1) % 7]}<span class="wdh-num">${wd.day}</span></div>`;
}
html += '</div>';
// Hour rows
for (let h = constants_1.HOUR_START; h <= constants_1.HOUR_END; h++) {
html += `<div class="week-hour-row">`;
html += `<div class="hour-label">${(0, date_util_1.minutesToTimeString)(h * 60)}</div>`;
for (const wd of weekDays) {
const key = ymdKey(wd);
html += `<div class="week-cell" data-date-key="${key}" data-hour="${h}"></div>`;
}
html += '</div>';
}
// Current time indicator
const todayYMD = (0, date_util_1.today)();
let indicatorHtml = '';
for (let i = 0; i < 7; i++) {
if ((0, date_util_1.sameDay)(weekDays[i], todayYMD)) {
const mins = (0, date_util_1.nowMinutes)();
if (mins >= constants_1.HOUR_START * 60 && mins <= constants_1.HOUR_END * 60) {
const topPx = ((mins - constants_1.HOUR_START * 60) / 60) * 48; // 48px per hour
indicatorHtml += `<div class="time-indicator" style="top:${topPx}px;" data-col="${i}"></div>`;
}
}
}
html += '</div>'; // week-grid
// Now overlay events on the grid
const eventBlocks = [];
for (const wd of weekDays) {
const events = (0, state_1.getEventsForDate)(wd).filter(e => !e.allDay);
for (const ev of events) {
const startMin = ev.startTime;
const endMin = ev.endTime;
if (startMin < constants_1.HOUR_START * 60 || startMin > constants_1.HOUR_END * 60)
continue;
const topPx = ((Math.max(startMin, constants_1.HOUR_START * 60) - constants_1.HOUR_START * 60) / 60) * 48;
const heightPx = Math.max(((endMin - startMin) / 60) * 48, 16);
eventBlocks.push(`<div class="week-event-block" style="top:${topPx}px;height:${heightPx}px;border-left-color:${ev.color};background:${ev.color}22;" data-event-id="${ev.id}" title="${escapeHtml(ev.title)}">${escapeHtml(ev.title)}</div>`);
}
}
// We need to inject event blocks into the correct cells — do this after DOM is set
html += '</div>'; // wrapper
mainEl.innerHTML = html;
// Now place events in their cells
requestAnimationFrame(() => {
const weekGrid = document.getElementById('week-grid');
if (!weekGrid)
return;
for (let i = 0; i < 7; i++) {
const wd = weekDays[i];
const key = ymdKey(wd);
const cells = weekGrid.querySelectorAll(`.week-cell[data-date-key="${key}"]`);
// Events span multiple hour rows — we need to place them in the first cell and let absolute positioning handle it
if (cells.length > 0) {
const events = (0, state_1.getEventsForDate)(wd).filter(e => !e.allDay);
for (const ev of events) {
const startMin = ev.startTime;
const endMin = ev.endTime;
if (startMin < constants_1.HOUR_START * 60 || startMin > constants_1.HOUR_END * 60)
continue;
const topPx = ((Math.max(startMin, constants_1.HOUR_START * 60) - constants_1.HOUR_START * 60) / 60) * 48;
const heightPx = Math.max(((endMin - startMin) / 60) * 48, 16);
const block = document.createElement('div');
block.className = 'week-event-block';
block.style.top = topPx + 'px';
block.style.height = heightPx + 'px';
block.style.borderLeftColor = ev.color;
block.style.background = ev.color + '22';
block.dataset.eventId = ev.id;
block.title = ev.title;
block.textContent = ev.title;
// Place in the first cell of this column (hour rows are separate cells)
cells[0].appendChild(block);
}
}
}
// Time indicator — place in today's column
for (let i = 0; i < 7; i++) {
if ((0, date_util_1.sameDay)(weekDays[i], (0, date_util_1.today)())) {
const mins = (0, date_util_1.nowMinutes)();
if (mins >= constants_1.HOUR_START * 60 && mins <= constants_1.HOUR_END * 60) {
const topPx = ((mins - constants_1.HOUR_START * 60) / 60) * 48;
const cells = weekGrid.querySelectorAll(`.week-cell[data-date-key="${ymdKey(weekDays[i])}"]`);
if (cells.length > 0) {
const indicator = document.createElement('div');
indicator.className = 'time-indicator';
indicator.style.top = topPx + 'px';
cells[0].appendChild(indicator);
}
}
}
}
});
}
// ─── Agenda View ────────────────────────────────────────────────
function renderAgendaView() {
const vs = (0, state_1.getViewState)();
const todayYMD = (0, date_util_1.today)();
const startDate = (0, date_util_1.sameDay)({ year: vs.currentYear, month: vs.currentMonth, day: 1 }, todayYMD) ? todayYMD : { year: vs.currentYear, month: vs.currentMonth, day: 1 };
const endDate = (0, date_util_1.addDays)(vs.currentYear, vs.currentMonth, 28, 0);
titleEl.textContent = (0, date_util_1.formatMonthYear)(vs.currentYear, vs.currentMonth);
// Get all events in range grouped by date
const allEvents = (0, state_1.getEventsInRange)(startDate, endDate);
if (allEvents.length === 0) {
mainEl.innerHTML = '<div class="agenda-list view-transition"><p style="color:#70757a;text-align:center;padding:48px;">No events this month</p></div>';
return;
}
// Group by date
const groups = new Map();
for (const ev of allEvents) {
const key = ymdKey(ev._occurrenceDate || ev.date);
if (!groups.has(key))
groups.set(key, []);
groups.get(key).push(ev);
}
let html = '<div class="agenda-list view-transition">';
for (const [key, events] of Array.from(groups.entries()).sort()) {
const [y, m, d] = key.split('-').map(Number);
const ymd = { year: y, month: m - 1, day: d };
html += `<div class="agenda-day-group">`;
html += `<div class="agenda-date-header">${(0, date_util_1.formatDateShort)(ymd)}</div>`;
for (const ev of events) {
const timeStr = ev.allDay ? 'All day' : `${(0, date_util_1.minutesToTimeString)(ev.startTime)} – ${(0, date_util_1.minutesToTimeString)(ev.endTime)}`;
html += `<div class="agenda-event-item" data-event-id="${ev.id}">`;
html += `<div class="agenda-color-bar" style="background:${ev.color};"></div>`;
html += `<div class="agenda-event-info"><div class="agenda-event-title">${escapeHtml(ev.title)}</div><div class="agenda-event-time">${timeStr}</div></div>`;
html += '</div>';
}
html += '</div>';
}
html += '</div>';
mainEl.innerHTML = html;
}
// ─── Helpers ─────────────────────────────────────────────────────
function formatWeekTitle(start, end) {
if (start.month === end.month) {
return `${(0, date_util_1.formatMonthYear)(start.year, start.month)} — ${start.day}–${end.day}`;
}
return `${(0, date_util_1.formatDateShort)(start)} – ${(0, date_util_1.formatDateShort)(end)}`;
}
function ymdKey(ymd) {
return `${ymd.year}-${String(ymd.month + 1).padStart(2, '0')}-${String(ymd.day).padStart(2, '0')}`;
}
function escapeHtml(s) {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
};
// ── module: src/modal.ts ──
__mods["src/modal.ts"] = function (exports, require, module) {
"use strict";
// ─── Modal: Event Editor, Settings, Recurrence Options ──────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.openModal = openModal;
exports.closeModal = closeModal;
exports.openEventEditor = openEventEditor;
exports.showRecurrenceDeleteOptions = showRecurrenceDeleteOptions;
exports.showRecurrenceEditOptions = showRecurrenceEditOptions;
exports.openSettingsModal = openSettingsModal;
const constants_1 = require("./constants");
const date_util_1 = require("./date-util");
const state_1 = require("./state");
const overlay = document.getElementById('modal-overlay');
const modalContent = document.getElementById('modal-content');
let currentEditId = null;
let pendingDeleteId = null;
let lastDeletedEvent = null;
// ─── Open/Close Modal ──────────────────────────────────────────
function openModal() { overlay.classList.remove('hidden'); }
function closeModal() { overlay.classList.add('hidden'); modalContent.innerHTML = ''; currentEditId = null; }
overlay.addEventListener('click', (e) => {
if (e.target === overlay)
closeModal();
});
// ─── Event Editor ──────────────────────────────────────────────
function openEventEditor(ev, dateHint) {
currentEditId = ev?.id ?? null;
const isEdit = !!ev;
const title = isEdit ? 'Edit Event' : 'New Event';
let html = `<div class="modal-box">`;
html += `<div class="modal-title">${title}</div>`;
// Title
html += `<div class="form-group"><label>Title</label><input id="ev-title" type="text" value="${escapeAttr(ev?.title ?? '')}" placeholder="Event title"></div>`;
// Date
const dateVal = ev ? `${ev.date.year}-${String(ev.date.month + 1).padStart(2, '0')}-${String(ev.date.day).padStart(2, '0')}` : (dateHint ? `${dateHint.year}-${String(dateHint.month + 1).padStart(2, '0')}-${String(dateHint.day).padStart(2, '0')}` : '');
html += `<div class="form-group"><label>Date</label><input id="ev-date" type="date" value="${dateVal}"></div>`;
// All-day toggle
html += `<div class="form-group"><label><input type="checkbox" id="ev-allday" ${ev?.allDay ? 'checked' : ''}> All day</label></div>`;
// Time range
const timeDiv = ev ? `${(0, date_util_1.minutesToTimeString)(ev.startTime)} – ${(0, date_util_1.minutesToTimeString)(ev.endTime)}` : '';
html += `<div class="form-row" id="time-range">`;
html += `<div class="form-group"><label>Start</label><input id="ev-start" type="time" value="${ev ? (0, date_util_1.minutesToTimeString)(ev.startTime) : '09:00'}"></div>`;
html += `<div class="form-group"><label>End</label><input id="ev-end" type="time" value="${ev ? (0, date_util_1.minutesToTimeString)(ev.endTime) : '10:00'}"></div>`;
html += `</div>`;
// Color
html += `<div class="form-group"><label>Color</label><div class="color-options">`;
for (const c of constants_1.EVENT_COLORS) {
const sel = ev?.color === c ? 'selected' : '';
html += `<div class="color-swatch ${sel}" style="background:${c};" data-color="${c}"></div>`;
}
html += `</div></div>`;
// Recurrence
html += `<div class="form-group"><label>Recurrence</label><select id="ev-recurrence">`;
html += `<option value="">None</option>`;
html += `<option value="daily" ${ev?.recurrence?.frequency === 'daily' ? 'selected' : ''}>Daily</option>`;
html += `<option value="weekly" ${ev?.recurrence?.frequency === 'weekly' ? 'selected' : ''}>Weekly</option>`;
html += `<option value="monthly" ${ev?.recurrence?.frequency === 'monthly' ? 'selected' : ''}>Monthly</option>`;
html += `<option value="yearly" ${ev?.recurrence?.frequency === 'yearly' ? 'selected' : ''}>Yearly</option>`;
html += `</select></div>`;
// Weekly day selection (hidden by default)
const selectedDays = ev?.recurrence?.weekdays ?? [];
html += `<div class="form-group" id="weekly-days" style="display:none;">`;
html += `<label>Repeat on:</label><div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:4px;">`;
const dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
for (let d = 0; d < 7; d++) {
const checked = selectedDays.includes(d) ? 'checked' : '';
html += `<label style="font-size:12px;display:flex;align-items:center;gap:4px;"><input type="checkbox" class="weekday-cb" value="${d}" ${checked}>${dayLabels[d]}</label>`;
}
html += `</div></div>`;
// Notes
html += `<div class="form-group"><label>Notes</label><textarea id="ev-notes" rows="2">${escapeHtml(ev?.notes ?? '')}</textarea></div>`;
// Error display
html += `<div id="ev-error" class="error-text"></div>`;
// Actions
html += `<div class="modal-actions">`;
if (isEdit) {
html += `<button id="btn-delete-event" class="btn-danger">Delete</button>`;
}
else {
html += `<span></span>`;
}
html += `<div style="display:flex;gap:8px;">`;
html += `<button id="btn-cancel-event">Cancel</button>`;
html += `<button id="btn-save-event" class="btn-primary">Save</button>`;
html += `</div></div>`;
html += `</div>`;
modalContent.innerHTML = html;
openModal();
// Wire up events
const alldayCb = document.getElementById('ev-allday');
const timeRange = document.getElementById('time-range');
toggleTimeRange(alldayCb.checked, timeRange);
alldayCb.addEventListener('change', () => toggleTimeRange(alldayCb.checked, timeRange));
// Recurrence type change → show/hide weekly days
const recSelect = document.getElementById('ev-recurrence');
const weeklyDaysDiv = document.getElementById('weekly-days');
function onRecChange() {
if (weeklyDaysDiv)
weeklyDaysDiv.style.display = recSelect.value === 'weekly' ? '' : 'none';
}
recSelect.addEventListener('change', onRecChange);
onRecChange();
// Color selection
let selectedColor = ev?.color ?? constants_1.EVENT_COLORS[0];
for (const swatch of modalContent.querySelectorAll('.color-swatch')) {
swatch.addEventListener('click', () => {
modalContent.querySelectorAll('.color-swatch').forEach(s => s.classList.remove('selected'));
swatch.classList.add('selected');
selectedColor = swatch.dataset.color;
});
}
// Save
document.getElementById('btn-save-event').addEventListener('click', () => {
const errorEl = document.getElementById('ev-error');
errorEl.textContent = '';
const titleVal = document.getElementById('ev-title').value.trim();
if (!titleVal) {
errorEl.textContent = 'Title is required';
return;
}
const dateVal = document.getElementById('ev-date').value;
if (!dateVal) {
errorEl.textContent = 'Date is required';
return;
}
const [y, m, d] = dateVal.split('-').map(Number);
const dateYMD = { year: y, month: m - 1, day: d };
const allDay = alldayCb.checked;
let startMin = (0, date_util_1.timeStringToMinutes)(document.getElementById('ev-start').value || '09:00');
let endMin = (0, date_util_1.timeStringToMinutes)(document.getElementById('ev-end').value || '10:00');
if (!allDay && endMin <= startMin) {
errorEl.textContent = 'End time must be after start time';
return;
}
const notes = document.getElementById('ev-notes').value.trim();
const recFreq = recSelect.value;
let recurrence = null;
if (recFreq) {
recurrence = { frequency: recFreq };
if (recFreq === 'weekly') {
const days = [];
for (const cb of modalContent.querySelectorAll('.weekday-cb:checked')) {
days.push(Number(cb.value));
}
if (days.length > 0)
recurrence.weekdays = days;
}
}
if (isEdit && currentEditId) {
(0, state_1.updateEvent)(currentEditId, { title: titleVal, date: dateYMD, allDay, startTime: startMin, endTime: endMin, color: selectedColor, notes, recurrence });
}
else {
(0, state_1.createEvent)({ title: titleVal, date: dateYMD, allDay, startTime: startMin, endTime: endMin, color: selectedColor, notes, recurrence });
}
closeModal();
reRender();
});
// Cancel
document.getElementById('btn-cancel-event').addEventListener('click', () => { closeModal(); reRender(); });
// Delete
const deleteBtn = document.getElementById('btn-delete-event');
if (deleteBtn && isEdit) {
deleteBtn.addEventListener('click', () => {
const ev = (0, state_1.getEventById)(currentEditId);
if (!ev)
return;
if (ev.recurrence) {
// Show recurrence delete options
showRecurrenceDeleteOptions(ev, dateHint ?? ev.date);
}
else {
doDeleteAndToast(ev.id, ev.title);
}
});
}
}
function toggleTimeRange(allDay, el) {
if (el)
el.style.display = allDay ? 'none' : '';
}
// ─── Recurrence Delete Options ────────────────────────────────
function showRecurrenceDeleteOptions(ev, occurrenceDate) {
modalContent.innerHTML = `<div class="modal-box">
<div class="modal-title">Delete recurring event</div>
<p style="margin-bottom:16px;color:#5f6368;">How would you like to delete this?</p>
<button id="del-this" style="display:block;width:100%;text-align:left;padding:12px;margin-bottom:8px;border-radius:4px;">This event only</button>
<button id="del-following" style="display:block;width:100%;text-align:left;padding:12px;margin-bottom:8px;border-radius:4px;">This and following events</button>
<button id="del-series" style="display:block;width:100%;text-align:left;padding:12px;margin-bottom:8px;border-radius:4px;color:#d93025;font-weight:500;">Entire series</button>
<button id="btn-cancel-del" style="margin-top:8px;">Cancel</button>
</div>`;
document.getElementById('del-this').addEventListener('click', () => {
// Add exception date to the event's exception list
const existing = (0, state_1.getEventById)(ev.id);
if (existing && occurrenceDate) {
const key = `${ev.id}@${occurrenceDate.year}-${String(occurrenceDate.month + 1).padStart(2, '0')}-${String(occurrenceDate.day).padStart(2, '0')}`;
(0, state_1.updateEvent)(ev.id, { exceptionIds: [...(existing.exceptionIds ?? []), key] });
}
closeModal();
reRender();
});
document.getElementById('del-following').addEventListener('click', () => {
// For simplicity, delete the entire series (splitting is complex)
doDeleteAndToast(ev.id, ev.title);
});
document.getElementById('del-series').addEventListener('click', () => {
doDeleteAndToast(ev.id, ev.title);
});
document.getElementById('btn-cancel-del').addEventListener('click', () => { closeModal(); reRender(); });
}
function doDeleteAndToast(id, title) {
lastDeletedEvent = { id, title };
(0, state_1.deleteEvent)(id);
closeModal();
showToast(`"${title}" deleted`, 'Undo');
reRender();
}
// ─── Recurrence Edit Options ────────────────────────────────
function showRecurrenceEditOptions(ev) {
console.log('showRecurrenceEditOptions called, ev.id:', ev.id);
openModal();
modalContent.innerHTML = `<div class="modal-box">
<div class="modal-title">Modify event</div>
<p style="margin-bottom:16px;color:#5f6368;">This is a recurring event. Which events would you like to modify?</p>
<button id="edit-this" style="display:block;width:100%;text-align:left;padding:12px;margin-bottom:8px;border-radius:4px;">This event only</button>
<button id="edit-following" style="display:block;width:100%;text-align:left;padding:12px;margin-bottom:8px;border-radius:4px;">This and following events</button>
<button id="edit-series" style="display:block;width:100%;text-align:left;padding:12px;margin-bottom:8px;border-radius:4px;">Entire series</button>
<button id="btn-cancel-edit" style="margin-top:8px;">Cancel</button>
</div>`;
document.getElementById('edit-this').addEventListener('click', () => {
// Create a new standalone event for this occurrence, add exception to original
const occDate = ev._occurrenceDate || ev.date;
const newEv = (0, state_1.createEvent)({
title: ev.title,
date: occDate,
allDay: ev.allDay,
startTime: ev.startTime,
endTime: ev.endTime,
color: ev.color,
notes: ev.notes,
recurrence: null,
});
// Add exception to original series
const existing = (0, state_1.getEventById)(ev.id);
if (existing) {
const key = `${ev.id}@${occDate.year}-${String(occDate.month + 1).padStart(2, '0')}-${String(occDate.day).padStart(2, '0')}`;
(0, state_1.updateEvent)(ev.id, { exceptionIds: [...(existing.exceptionIds ?? []), key] });
}
// Open editor for the new standalone event
console.log('edit-this: closing modal and opening editor for', newEv.id);
closeModal();
reRender();
try {
openEventEditor(newEv);
console.log('edit-this: editor opened successfully');
}
catch (err) {
console.error('edit-this: error opening editor:', err.message);
}
});
document.getElementById('edit-following').addEventListener('click', () => { openEventEditor(ev); });
document.getElementById('edit-series').addEventListener('click', () => { openEventEditor(ev); });
document.getElementById('btn-cancel-edit').addEventListener('click', () => { closeModal(); reRender(); });
}
// ─── Settings Modal ──────────────────────────────────────────
function openSettingsModal() {
const settings = (0, state_1.getSettings)();
modalContent.innerHTML = `<div class="modal-box">
<div class="modal-title">Settings</div>
<div class="settings-option">
<span>Week starts on</span>
<select id="set-weekstart">
<option value="0" ${settings.weekStart === 0 ? 'selected' : ''}>Sunday</option>
<option value="1" ${settings.weekStart === 1 ? 'selected' : ''}>Monday</option>
</select>
</div>
<button id="btn-close-settings" style="margin-top:16px;">Close</button>
</div>`;
const wsSelect = document.getElementById('set-weekstart');
wsSelect.addEventListener('change', () => {
(0, state_1.updateSettings)({ weekStart: Number(wsSelect.value) });
});
document.getElementById('btn-close-settings').addEventListener('click', closeModal);
}
// ─── Toast Notifications ──────────────────────────────────────
function showToast(message, actionLabel) {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
if (actionLabel) {
const btn = document.createElement('button');
btn.textContent = actionLabel;
btn.addEventListener('click', () => {
// Undo delete
if (lastDeletedEvent) {
(0, state_1.createEvent)({ ...lastDeletedEvent });
lastDeletedEvent = null;
reRender();
}
toast.remove();
});
toast.appendChild(btn);
}
container.appendChild(toast);
setTimeout(() => toast.remove(), 5000);
}
// ─── Helpers ──────────────────────────────────────────────────
function escapeHtml(s) {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
function escapeAttr(s) {
return s.replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>');
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>