Calendar
Qwen 3.6 27b MTP · 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 96,072 bytes · SHA-256 d822c7f4691a
<!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: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
font-size: 14px;
color: #202124;
background: #fff;
line-height: 1.5;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
#app {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 1400px;
margin: 0 auto;
}
/* ─── Toolbar / Header ─────────────────────────────────────────── */
#toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
border-bottom: 1px solid #e0e2e5;
background: #fff;
position: sticky;
top: 0;
z-index: 100;
flex-wrap: wrap;
gap: 8px;
}
.toolbar-left {
display: flex;
align-items: center;
gap: 4px;
}
.toolbar-center {
display: flex;
align-items: center;
gap: 12px;
}
.toolbar-right {
display: flex;
align-items: center;
}
#header-date {
font-size: 22px;
font-weight: 500;
margin: 0 8px;
white-space: nowrap;
color: #3c4043;
}
.year-input {
width: 64px;
padding: 4px 8px;
border: 1px solid #dadce0;
border-radius: 4px;
font-size: 13px;
text-align: center;
background: #fff;
}
.year-input:focus {
outline: none;
border-color: #1a73e8;
box-shadow: 0 0 0 2px rgba(26,115,232,0.15);
}
/* ─── Buttons ──────────────────────────────────────────────────── */
.icon-btn {
background: none;
border: none;
cursor: pointer;
padding: 8px;
border-radius: 50%;
font-size: 18px;
color: #444746;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s;
}
.icon-btn:hover { background: #f1f3f4; }
.btn {
padding: 6px 16px;
border-radius: 20px;
border: none;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: background 0.15s, box-shadow 0.15s;
}
.btn-primary { background: #1a73e8; color: #fff; }
.btn-primary:hover { background: #1765cc; }
.btn-secondary { background: #f1f3f4; color: #202124; }
.btn-secondary:hover { background: #e8eaed; }
.btn-danger { background: #d93025; color: #fff; }
.btn-danger:hover { background: #c5221f; }
.btn-today {
border: 1px solid #dadce0;
background: #fff;
color: #1a73e8;
font-weight: 600;
}
.btn-today:hover { background: #e8f0fe; }
/* ─── View Switcher ────────────────────────────────────────────── */
.view-switcher {
display: flex;
gap: 2px;
background: #f1f3f4;
border-radius: 8px;
padding: 2px;
}
.view-btn {
padding: 6px 14px;
border: none;
background: transparent;
cursor: pointer;
font-size: 13px;
font-weight: 500;
color: #5f6368;
border-radius: 6px;
transition: all 0.15s;
}
.view-btn:hover { background: rgba(0,0,0,0.04); }
.view-btn.active {
background: #fff;
color: #202124;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
/* ─── Settings Panel ───────────────────────────────────────────── */
.settings-panel {
position: absolute;
top: 56px;
right: 16px;
background: #fff;
border: 1px solid #dadce0;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
z-index: 200;
}
.settings-panel.hidden { display: none !important; }
.settings-panel h3 { font-size: 14px; margin-bottom: 8px; color: #3c4043; }
/* ─── Main Content ─────────────────────────────────────────────── */
#views-container {
flex: 1;
overflow-y: auto;
position: relative;
}
.view-container.hidden { display: none !important; }
/* ─── Responsive ──────────────────────────────────────────────── */
@media (max-width: 768px) {
#toolbar { padding: 6px 8px !important; gap: 4px !important; }
.toolbar-left, .toolbar-center { gap: 2px !important; }
#header-date { font-size: 16px !important; margin: 0 4px !important; }
.year-input { width: 52px !important; padding: 3px 4px !important; }
.view-btn { padding: 4px 8px !important; font-size: 12px !important; }
.btn-today { padding: 4px 10px !important; font-size: 12px !important; }
}
/* ── css: month-view ── */
/* ─── Month View Grid ──────────────────────────────────────────── */
#month-view-container {
display: flex;
flex-direction: column;
height: 100%;
}
.month-header-row {
display: grid;
grid-template-columns: repeat(7, 1fr);
border-bottom: 1px solid #dadce0;
}
.month-day-label {
text-align: center;
padding: 8px 4px;
font-size: 11px;
font-weight: 600;
color: #70757a;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.month-row {
display: grid;
grid-template-columns: repeat(7, 1fr);
flex: 1;
}
.month-cell {
border-right: 1px solid #e8eaed;
border-bottom: 1px solid #e8eaed;
min-height: 90px;
padding: 2px 4px;
cursor: pointer;
position: relative;
transition: background 0.1s;
}
.month-cell:hover { background: #f8f9fa; }
.month-cell.other-month {
background: #fafbfc;
}
.month-cell.other-month .day-number { color: #c4c7cc; }
.day-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
font-size: 13px;
font-weight: 500;
}
.month-cell.today .day-number {
background: #1a73e8;
color: #fff;
font-weight: 700;
}
.cell-events {
display: flex;
flex-direction: column;
gap: 1px;
margin-top: 2px;
}
.event-chip {
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
border-left: 3px solid #3b82f6;
background: rgba(59, 130, 246, 0.08);
color: #1a73e8;
transition: filter 0.1s;
}
.event-chip:hover { filter: brightness(0.92); }
.event-more {
font-size: 10px;
color: #70757a;
padding: 1px 6px;
cursor: pointer;
}
.month-cell.drag-over {
background: #e8f0fe !important;
outline: 2px solid #1a73e8;
outline-offset: -2px;
}
/* ─── Day Popover ──────────────────────────────────────────────── */
.day-popover {
position: fixed;
background: #fff;
border: 1px solid #dadce0;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
padding: 12px;
z-index: 300;
min-width: 220px;
}
.popover-header {
font-weight: 600;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
font-size: 13px;
}
.popover-event {
padding: 6px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
border-left: 3px solid #3b82f6;
margin-bottom: 4px;
}
.popover-event:hover { background: #f1f3f4; }
/* ─── Responsive ──────────────────────────────────────────────── */
@media (max-width: 768px) {
.month-cell { min-height: 60px !important; padding: 1px 2px !important; }
.day-number { width: 22px !important; height: 22px !important; font-size: 11px !important; }
.event-chip { font-size: 9px !important; padding: 0 3px !important; border-left-width: 2px !important; }
}
/* ── css: week-view ── */
/* ─── Week View ────────────────────────────────────────────────── */
#week-view-container {
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
}
.week-header {
display: grid;
grid-template-columns: 56px repeat(7, 1fr);
border-bottom: 1px solid #dadce0;
position: sticky;
top: 0;
background: #fff;
z-index: 10;
}
.week-day-header {
text-align: center;
padding: 8px 4px;
border-left: 1px solid #eee;
}
.day-label {
font-size: 11px;
color: #70757a;
font-weight: 600;
text-transform: uppercase;
}
.date-label {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
font-size: 20px;
font-weight: 400;
margin-top: 2px;
}
.today-date {
background: #1a73e8;
color: #fff;
font-weight: 700;
}
/* All-day strip */
.all-day-strip {
display: grid;
grid-template-columns: 56px repeat(7, 1fr);
border-bottom: 1px solid #dadce0;
min-height: 32px;
}
.all-day-slot {
padding: 4px;
border-left: 1px solid #eee;
}
.all-day-chip {
font-size: 11px;
padding: 2px 6px;
}
/* Time grid */
.time-grid {
display: flex;
position: relative;
}
.hour-labels {
width: 56px;
flex-shrink: 0;
position: relative;
}
.hour-label {
height: 240px; /* 60 min * 4 px/min */
font-size: 10px;
color: #9aa0a6;
text-align: right;
padding-right: 8px;
position: relative;
top: -6px;
}
.day-columns {
display: grid;
grid-template-columns: repeat(7, 1fr);
flex: 1;
position: relative;
}
.week-day-column {
border-left: 1px solid #eee;
position: relative;
min-height: 6480px; /* (22-6)*60*4 = 3840, but we need more for scroll */
}
.week-day-column.today {
background: rgba(26, 115, 232, 0.02);
}
.time-slot {
height: 60px; /* 15 min * 4 px/min */
border-bottom: 1px solid #f8f9fa;
cursor: pointer;
}
.time-slot:hover { background: rgba(26, 115, 232, 0.04); }
.week-event-block {
position: absolute;
border-left-width: 3px;
border-left-style: solid;
z-index: 5;
}
.event-time { font-size: 10px; opacity: 0.8; line-height: 1.2; }
.event-title { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.3; }
.current-time-indicator {
position: absolute;
left: -1px;
right: -1px;
height: 2px;
background: #d93025;
z-index: 6;
pointer-events: none;
}
.current-time-indicator::before {
content: '';
position: absolute;
left: -5px;
top: -4px;
width: 10px;
height: 10px;
border-radius: 50%;
background: #d93025;
}
/* ─── Responsive ──────────────────────────────────────────────── */
@media (max-width: 768px) {
.toolbar-left, .toolbar-center { gap: 4px !important; }
#header-date { font-size: 16px !important; margin: 0 4px !important; }
.year-input { width: 52px !important; }
.view-btn { padding: 4px 8px !important; font-size: 12px !important; }
.month-cell { min-height: 60px !important; padding: 1px 2px !important; }
.day-number { width: 22px !important; height: 22px !important; line-height: 22px !important; font-size: 11px !important; }
.event-chip { font-size: 9px !important; padding: 0 3px !important; border-left-width: 2px !important; }
.week-header { grid-template-columns: 40px repeat(7, 1fr) !important; }
.hour-labels { width: 40px !important; }
.date-label { width: 28px !important; height: 28px !important; font-size: 16px !important; }
}
/* ── css: agenda-view ── */
/* ─── Agenda View ──────────────────────────────────────────────── */
#agenda-view-container {
padding: 16px;
}
.agenda-day-header {
font-size: 13px;
font-weight: 600;
color: #70757a;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 12px 0 4px;
border-bottom: 1px solid #eee;
}
.agenda-event-item {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
margin-top: 2px;
}
.agenda-event-item:hover { background: #f1f3f4; }
.agenda-color-bar {
width: 4px;
min-height: 28px;
border-radius: 2px;
flex-shrink: 0;
align-self: stretch;
}
.agenda-event-content { flex: 1; }
.agenda-event-title { font-weight: 500; font-size: 14px; }
.agenda-event-time {
font-size: 12px;
color: #70757a;
}
.agenda-empty {
text-align: center;
padding: 48px;
color: #9aa0a6;
font-size: 15px;
}
.today-badge {
display: inline-block;
background: #e8f0fe;
color: #1a73e8;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
margin-left: 8px;
}
/* ── css: modal ── */
/* ─── Modal Overlay & Dialog ───────────────────────────────────── */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 500;
}
.modal-overlay.hidden { display: none !important; }
.modal {
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 40px rgba(0,0,0,0.2);
width: 90%;
max-width: 460px;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.modal-header h2 { font-size: 18px; font-weight: 500; }
.modal-body { padding: 20px; }
/* ─── Form Elements ────────────────────────────────────────────── */
.form-group { margin-bottom: 14px; }
.form-group label {
display: block;
font-size: 12px;
font-weight: 500;
color: #70757a;
margin-bottom: 4px;
}
.form-group input[type="text"],
.form-group input[type="date"],
.form-group input[type="time"],
.form-group select,
.form-group textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #dadce0;
border-radius: 6px;
font-size: 14px;
font-family: inherit;
transition: border-color 0.15s;
}
.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.15);
}
.form-row {
display: flex;
gap: 12px;
align-items: flex-end;
}
.flex-1 { flex: 1; }
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
}
.field-error {
display: block;
color: #d93025;
font-size: 12px;
margin-top: 2px;
min-height: 16px;
}
/* ─── Color Picker ─────────────────────────────────────────────── */
.color-picker {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.color-swatch {
width: 28px;
height: 28px;
border-radius: 50%;
border: 3px solid transparent;
cursor: pointer;
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);
}
/* ─── Weekday Picker ───────────────────────────────────────────── */
.weekday-picker {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.weekday-btn {
padding: 4px 10px;
border: 1px solid #dadce0;
border-radius: 16px;
background: #fff;
cursor: pointer;
font-size: 12px;
transition: all 0.15s;
}
.weekday-btn.selected {
background: #e8f0fe;
border-color: #1a73e8;
color: #1a73e8;
}
/* ─── Modal Actions ────────────────────────────────────────────── */
.modal-actions {
display: flex;
align-items: center;
gap: 8px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid #eee;
}
.spacer { flex: 1; }
/* ─── Scope Dialog (recurring edit) ────────────────────────────── */
.scope-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 600;
}
.scope-dialog {
background: #fff;
border-radius: 12px;
padding: 24px;
max-width: 380px;
width: 90%;
box-shadow: 0 8px 40px rgba(0,0,0,0.2);
}
.scope-dialog h3 { margin-bottom: 8px; }
.scope-dialog p { font-size: 13px; color: #5f6368; margin-bottom: 16px; }
.scope-options {
display: flex;
flex-direction: column;
gap: 8px;
}
/* ─── Confirm Dialog ───────────────────────────────────────────── */
.confirm-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 600;
}
.confirm-dialog {
background: #fff;
border-radius: 12px;
padding: 24px;
max-width: 340px;
width: 90%;
box-shadow: 0 8px 40px rgba(0,0,0,0.2);
}
.confirm-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 16px;
}
/* ─── Toast Notifications ──────────────────────────────────────── */
.toast-container {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 700;
}
.toast {
background: #323232;
color: #fff;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
margin-top: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
animation: toast-in 0.2s ease-out;
}
.toast.fade-out { opacity: 0; transition: opacity 0.3s; }
@keyframes toast-in {
from { transform: translateY(10px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* ─── Utility ──────────────────────────────────────────────────── */
.hidden { display: none !important; }
</style>
</head>
<body>
<div id="app">
<header id="toolbar">
<div class="toolbar-left">
<button id="nav-prev" class="icon-btn" title="Previous">‹</button>
<button id="nav-next" class="icon-btn" title="Next">›</button>
<h1 id="header-date"></h1>
<input type="number" id="year-jump" min="1900" max="2200" class="year-input">
</div>
<div class="toolbar-center">
<button id="nav-today" class="btn btn-today">Today</button>
<div class="view-switcher">
<button class="view-btn active" data-view="month">Month</button>
<button class="view-btn" data-view="week">Week</button>
<button class="view-btn" data-view="agenda">Agenda</button>
</div>
</div>
<div class="toolbar-right">
<button id="settings-btn" class="icon-btn" title="Settings">⚙</button>
</div>
</header>
<div id="settings-panel" class="settings-panel hidden">
<h3>Settings</h3>
<label class="checkbox-label">
<input type="checkbox" id="week-start-toggle">
<span>Week starts on Monday</span>
</label>
</div>
<main id="views-container">
<div id="month-view-container" class="view-container"></div>
<div id="week-view-container" class="view-container hidden"></div>
<div id="agenda-view-container" class="view-container hidden"></div>
</main>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./state":"src/state.ts","./date-util":"src/date-util.ts","./recurrence":"src/recurrence.ts","./modal":"src/modal.ts","./views/month-view":"src/views/month-view.ts","./views/week-view":"src/views/week-view.ts","./views/agenda-view":"src/views/agenda-view.ts","./render":"src/render.ts"},"src/date-util.ts":{"./types":"src/types.ts","./constants":"src/constants.ts"},"src/recurrence.ts":{"./types":"src/types.ts","./date-util":"src/date-util.ts"},"src/state.ts":{"./types":"src/types.ts","./date-util":"src/date-util.ts","./constants":"src/constants.ts"},"src/modal.ts":{"./types":"src/types.ts","./constants":"src/constants.ts","./date-util":"src/date-util.ts"},"src/render.ts":{"./types":"src/types.ts","./date-util":"src/date-util.ts"},"src/views/month-view.ts":{"../types":"src/types.ts","../date-util":"src/date-util.ts","../constants":"src/constants.ts","../render":"src/render.ts"},"src/views/week-view.ts":{"../types":"src/types.ts","../date-util":"src/date-util.ts","../constants":"src/constants.ts","../render":"src/render.ts"},"src/views/agenda-view.ts":{"../types":"src/types.ts","../date-util":"src/date-util.ts","../render":"src/render.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";
// ─── Main Entry Point ─────────────────────────────────────────────────
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const state = __importStar(require("./state"));
const dateUtil = __importStar(require("./date-util"));
const recurrence = __importStar(require("./recurrence"));
const modal = __importStar(require("./modal"));
const monthView = __importStar(require("./views/month-view"));
const weekView = __importStar(require("./views/week-view"));
const agendaView = __importStar(require("./views/agenda-view"));
const render_1 = require("./render");
// ── Initialization ────────────────────────────────────────────────────
function start() {
const appState = state.init();
// Initialize modal
modal.initModal();
// Initialize views
monthView.init({
containerId: 'month-view-container',
weekStart: appState.settings.weekStart,
onDayClick: (date) => openNewEventModal(date),
onEventClick: (evt, date) => openEditOccurrenceModal(evt, date),
onDragDrop: handleMonthDragDrop,
});
weekView.init({
containerId: 'week-view-container',
weekStart: appState.settings.weekStart,
onTimeSlotClick: (date, hour, minute) => {
const startTime = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
openNewEventModal(date, startTime);
},
onEventClick: (evt, date) => openEditOccurrenceModal(evt, date),
onDragDrop: handleWeekDragDrop,
});
agendaView.init({
containerId: 'agenda-view-container',
onEventClick: (evt) => openEditOccurrenceModal(evt, evt.date),
});
// Wire navigation controls
wireNavigation(appState);
// Subscribe to state changes for re-rendering
state.subscribe(() => renderCurrentView());
// Initial render
renderCurrentView();
}
// ── Rendering ─────────────────────────────────────────────────────────
function renderCurrentView() {
const appState = state.getState();
const { currentYear, currentMonth, settings } = appState;
// Update header
updateHeader(currentYear, currentMonth);
// Compute visible date range and get occurrences
let rangeStart, rangeEnd;
switch (settings.viewMode) {
case 'month':
rangeStart = { year: currentYear, month: currentMonth, day: 1 };
const lastDay = dateUtil.daysInMonth(currentYear, currentMonth);
rangeEnd = { year: currentYear, month: currentMonth, day: lastDay };
break;
case 'week':
rangeStart = dateUtil.startOfWeek({ year: currentYear, month: currentMonth, day: 1 }, settings.weekStart);
rangeEnd = dateUtil.addDays(rangeStart, 6);
break;
case 'agenda':
const today = dateUtil.today();
rangeStart = { ...today };
rangeEnd = dateUtil.addDays(today, 30);
break;
}
// Expand all events for the visible range
const occurrences = recurrence.getAllOccurrences(appState.events, rangeStart, rangeEnd, appState.exceptions, appState.deletedOccurrences);
// Render active view
switch (settings.viewMode) {
case 'month':
monthView.render(currentYear, currentMonth, occurrences, settings.weekStart);
break;
case 'week':
weekView.render({ year: currentYear, month: currentMonth, day: 1 }, occurrences, settings.weekStart);
break;
case 'agenda':
const today2 = dateUtil.today();
agendaView.render(today2, occurrences);
break;
}
// Show/hide view containers
['month', 'week', 'agenda'].forEach(view => {
const el = document.getElementById(`${view}-view-container`);
if (settings.viewMode === view) {
el.classList.remove('hidden');
el.style.display = '';
}
else {
el.classList.add('hidden');
el.style.display = 'none';
}
});
// Update view mode buttons
document.querySelectorAll('.view-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.view === settings.viewMode);
});
}
function updateHeader(year, month) {
const headerEl = document.getElementById('header-date');
if (headerEl) {
headerEl.textContent = `${dateUtil.monthName(month)} ${year}`;
}
}
// ── Navigation Wiring ─────────────────────────────────────────────────
function wireNavigation(appState) {
// Prev/Next buttons
document.getElementById('nav-prev').addEventListener('click', () => navigate(-1));
document.getElementById('nav-next').addEventListener('click', () => navigate(1));
// Today button
document.getElementById('nav-today').addEventListener('click', () => {
const today = dateUtil.today();
state.setCurrentMonth(today.year, today.month);
});
// Year jump input
const yearInput = document.getElementById('year-jump');
yearInput.value = String(appState.currentYear);
yearInput.addEventListener('change', () => {
const y = parseInt(yearInput.value);
if (!isNaN(y) && y > 0) {
state.setCurrentMonth(y, state.getState().currentMonth);
}
else {
yearInput.value = String(state.getState().currentYear);
}
});
// View mode buttons
document.querySelectorAll('.view-btn').forEach(btn => {
btn.addEventListener('click', () => {
const view = btn.dataset.view;
if (view)
state.setViewMode(view);
});
});
// Week start toggle
document.getElementById('week-start-toggle').addEventListener('click', () => {
const current = state.getState().settings.weekStart;
state.setWeekStart(current === 0 ? 1 : 0);
});
// Settings button (toggle settings panel)
document.getElementById('settings-btn').addEventListener('click', () => {
const panel = document.getElementById('settings-panel');
if (panel)
panel.classList.toggle('open');
});
}
function navigate(delta) {
const appState = state.getState();
let newMonth = appState.currentMonth + delta;
let newYear = appState.currentYear;
if (newMonth < 0) {
newMonth = 11;
newYear--;
}
if (newMonth > 11) {
newMonth = 0;
newYear++;
}
state.setCurrentMonth(newYear, newMonth);
}
// ── Event Modal Integration ───────────────────────────────────────────
function openNewEventModal(date, startTime) {
modal.openModal({
onSave: (evt) => {
if (!evt.title)
return; // Cancel signal
state.addEvent(evt);
(0, render_1.showToast)('Event created');
modal.closeModal();
},
onCancel: () => modal.closeModal(),
}, date);
// If startTime provided, set it after modal opens (small delay for DOM update)
if (startTime) {
setTimeout(() => {
const startEl = document.getElementById('evt-start');
const endEl = document.getElementById('evt-end');
if (startEl && endEl) {
startEl.value = startTime;
// Default 1-hour duration
const [h, m] = startTime.split(':').map(Number);
let endH = h + 1;
let endM = m;
if (endH > 23)
endH = 0;
endEl.value = `${String(endH).padStart(2, '0')}:${String(endM).padStart(2, '0')}`;
}
}, 50);
}
}
function openEditOccurrenceModal(evt, date) {
const master = state.getEvent(evt.eventId);
if (!master)
return;
// Check if this is a recurring event — show scope selector
if (master.recurrence.type !== 'none') {
openRecurringEditModal(master, evt, date);
return;
}
// Non-recurring: edit the master directly
modal.openEditModal({
onSave: (updated) => {
if (!updated.title) {
state.deleteEvent(evt.eventId);
(0, render_1.showToast)('Event deleted');
return;
}
state.updateEvent(evt.eventId, updated);
(0, render_1.showToast)('Event updated');
modal.closeModal();
},
onCancel: () => modal.closeModal(),
}, master);
}
function openRecurringEditModal(master, occurrence, date) {
// Show a scope selector before opening the edit modal
const overlay = document.createElement('div');
overlay.className = 'scope-overlay';
overlay.innerHTML = `
<div class="scope-dialog">
<h3>Edit recurring event</h3>
<p>This is part of a recurring series. Which occurrence do you want to edit?</p>
<div class="scope-options">
<button class="btn btn-secondary scope-btn" data-scope="this">This event only</button>
<button class="btn btn-secondary scope-btn" data-scope="following">This & following</button>
<button class="btn btn-primary scope-btn" data-scope="all">Entire series</button>
</div>
</div>
`;
document.body.appendChild(overlay);
overlay.querySelectorAll('.scope-btn').forEach(btn => {
btn.addEventListener('click', () => {
const scope = btn.dataset.scope;
overlay.remove();
if (scope === 'this') {
// Edit this occurrence only — create an exception
openEditOccurrenceOnly(master, occurrence, date);
}
else if (scope === 'following') {
// Split the series: set new anchor to this occurrence's date
const updatedMaster = { ...master, date: { ...date } };
modal.openEditModal({
onSave: (updated) => {
if (!updated.title)
return;
state.updateEvent(master.id, { ...updated, date: { ...date } });
(0, render_1.showToast)('Series updated from this point forward');
modal.closeModal();
},
onCancel: () => modal.closeModal(),
}, updatedMaster);
}
else {
// Edit entire series
modal.openEditModal({
onSave: (updated) => {
if (!updated.title) {
state.deleteEvent(master.id);
(0, render_1.showToast)('Series deleted');
return;
}
state.updateEvent(master.id, updated);
(0, render_1.showToast)('Entire series updated');
modal.closeModal();
},
onCancel: () => modal.closeModal(),
}, master);
}
});
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay)
overlay.remove();
});
}
function openEditOccurrenceOnly(master, occurrence, date) {
// Open modal pre-filled with the occurrence data
modal.openModal({
onSave: (updated) => {
if (!updated.title)
return;
// Create an exception for this specific occurrence
state.addException(master.id, date, updated);
(0, render_1.showToast)('Occurrence updated');
modal.closeModal();
},
onCancel: () => modal.closeModal(),
}, date);
// Pre-fill the form with occurrence data
setTimeout(() => {
document.getElementById('evt-title').value = occurrence.title;
document.getElementById('evt-allday').checked = occurrence.isAllDay;
document.getElementById('evt-start').value = occurrence.startTime || '';
document.getElementById('evt-end').value = occurrence.endTime || '';
document.getElementById('evt-notes').value = occurrence.notes || '';
// Color selection
document.querySelectorAll('.color-swatch').forEach(s => {
s.classList.toggle('selected', s.dataset.color === occurrence.color);
});
// Hide recurrence options for single-occurrence edit
document.getElementById('evt-recurrence-type').value = 'none';
}, 50);
}
// ── Drag & Drop Handlers ──────────────────────────────────────────────
function handleMonthDragDrop(eventId, fromDate, toDate) {
const master = state.getEvent(eventId);
if (!master)
return;
// Check if this is a recurring event — ask for scope
if (master.recurrence.type !== 'none') {
(0, render_1.showToast)(`Dragged "${master.title}" to ${dateUtil.monthName(toDate.month)} ${toDate.day}`);
// For simplicity, move the entire series anchor date
state.updateEvent(eventId, { date: { ...toDate } });
}
else {
state.updateEvent(eventId, { date: { ...toDate } });
}
(0, render_1.showToast)(`Moved to ${dateUtil.monthName(toDate.month)} ${toDate.day}`);
}
// ── Auto-start ────────────────────────────────────────────────────────
start();
function handleWeekDragDrop(eventId, fromDate, fromTime, toDate, toTime) {
const master = state.getEvent(eventId);
if (!master)
return;
if (dateUtil.sameDay(fromDate, toDate)) {
// Same day — just update time
state.updateEvent(eventId, { startTime: toTime });
(0, render_1.showToast)('Time updated');
}
else {
// Different day — update date and time
state.updateEvent(eventId, { date: { ...toDate }, startTime: toTime });
(0, render_1.showToast)(`Moved to ${dateUtil.monthName(toDate.month)} ${toDate.day}`);
}
}
};
// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
// ─── Tunable Constants ────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.TRANSITION_MS = exports.MAX_CELL_EVENTS = exports.DEFAULT_WEEK_START = exports.SNAP_MINUTES = exports.HOUR_END = exports.HOUR_START = exports.DEFAULT_COLOR = exports.EVENT_COLORS = void 0;
exports.EVENT_COLORS = [
{ name: 'Blue', value: '#3b82f6' },
{ name: 'Red', value: '#ef4444' },
{ name: 'Green', value: '#22c55e' },
{ name: 'Purple', value: '#a855f7' },
{ name: 'Orange', value: '#f97316' },
{ name: 'Teal', value: '#14b8a6' },
{ name: 'Pink', value: '#ec4899' },
{ name: 'Yellow', value: '#eab308' },
];
exports.DEFAULT_COLOR = exports.EVENT_COLORS[0].value;
// Week view hour range (24h)
exports.HOUR_START = 6; // 6:00 AM
exports.HOUR_END = 22; // 10:00 PM
exports.SNAP_MINUTES = 15; // drag snap interval in minutes
// Default week start day (0=Sunday, 1=Monday)
exports.DEFAULT_WEEK_START = 1;
// Max events shown per month cell before "+N more"
exports.MAX_CELL_EVENTS = 3;
// Animation duration for view transitions (ms)
exports.TRANSITION_MS = 250;
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ─── Type Definitions ────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
};
// ── module: src/date-util.ts ──
__mods["src/date-util.ts"] = function (exports, require, module) {
"use strict";
// ─── Date Engine (Pure Functions) ─────────────────────────────────────
// All arithmetic on year/month/day components — never ms-based day hacks.
Object.defineProperty(exports, "__esModule", { value: true });
exports.isLeapYear = isLeapYear;
exports.daysInMonth = daysInMonth;
exports.dayOfWeek = dayOfWeek;
exports.addDays = addDays;
exports.addMonths = addMonths;
exports.addYears = addYears;
exports.sameDay = sameDay;
exports.compareDays = compareDays;
exports.today = today;
exports.startOfWeek = startOfWeek;
exports.getWeekDays = getWeekDays;
exports.buildMonthGrid = buildMonthGrid;
exports.monthName = monthName;
exports.dayNameShort = dayNameShort;
exports.dayNameFull = dayNameFull;
exports.formatDate = formatDate;
exports.parseDate = parseDate;
exports.formatTime = formatTime;
exports.formatDayHeader = formatDayHeader;
exports.generateId = generateId;
const constants_1 = require("./constants");
// ── Leap Year & Month Lengths ─────────────────────────────────────────
function isLeapYear(year) {
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
}
function daysInMonth(year, month) {
// month: 0=Jan .. 11=Dec
const table = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
if (month === 1 && isLeapYear(year))
return 29; // Feb leap
return table[month];
}
// ── Day-of-Week ───────────────────────────────────────────────────────
// Returns 0=Sunday .. 6=Saturday. Uses Zeller-like formula on components.
function dayOfWeek(y, m, d) {
// Use native Date only for the day-of-week lookup (no arithmetic)
const dt = new Date(y, m, d);
return dt.getDay(); // 0=Sun .. 6=Sat
}
// ── Component Arithmetic ──────────────────────────────────────────────
function addDays(dc, n) {
let { year, month, day } = dc;
const delta = n >= 0 ? n : -n;
const sign = n >= 0 ? 1 : -1;
for (let i = 0; i < delta; i++) {
day += sign;
if (day > daysInMonth(year, month)) {
day = 1;
month++;
if (month > 11) {
month = 0;
year++;
}
}
else if (day < 1) {
month--;
if (month < 0) {
month = 11;
year--;
}
day = daysInMonth(year, month);
}
}
return { year, month, day };
}
function addMonths(dc, n) {
let { year, month, day } = dc;
const totalMonths = (year * 12 + month) + n;
year = Math.floor(totalMonths / 12);
month = ((totalMonths % 12) + 12) % 12; // handle negative wrap
// Clamp day to valid range for the target month
const maxDay = daysInMonth(year, month);
if (day > maxDay)
day = maxDay;
return { year, month, day };
}
function addYears(dc, n) {
let { year, month, day } = dc;
year += n;
// Handle Feb 29 -> Feb 28 in non-leap years
if (month === 1 && day === 29 && !isLeapYear(year)) {
day = 28;
}
return { year, month, day };
}
// ── Comparison & Equality ─────────────────────────────────────────────
function sameDay(a, b) {
return a.year === b.year && a.month === b.month && a.day === b.day;
}
function compareDays(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;
}
// ── Today ─────────────────────────────────────────────────────────────
function today() {
const d = new Date();
return { year: d.getFullYear(), month: d.getMonth(), day: d.getDate() };
}
// ── Week Helpers ──────────────────────────────────────────────────────
/** Get the first day of the week containing `dc`, using given weekStart. */
function startOfWeek(dc, weekStart = constants_1.DEFAULT_WEEK_START) {
const dow = dayOfWeek(dc.year, dc.month, dc.day); // 0=Sun..6=Sat
let diff;
if (weekStart === 1) {
// Monday start: Mon=0 .. Sun=6
diff = dow === 0 ? -6 : 1 - dow;
}
else {
// Sunday start: Sun=0 .. Sat=6
diff = -dow;
}
return addDays(dc, diff);
}
/** Get all 7 days of the week containing `dc`. */
function getWeekDays(dc, weekStart = constants_1.DEFAULT_WEEK_START) {
const start = startOfWeek(dc, weekStart);
const result = [];
for (let i = 0; i < 7; i++) {
result.push(addDays(start, i));
}
return result;
}
function buildMonthGrid(year, month, weekStart = constants_1.DEFAULT_WEEK_START) {
const firstDay = { year, month, day: 1 };
const weekStartDay = startOfWeek(firstDay, weekStart);
const grid = [];
let current = { ...weekStartDay };
for (let row = 0; row < 6; row++) {
const rowCells = [];
for (let col = 0; col < 7; col++) {
rowCells.push({
date: { year: current.year, month: current.month, day: current.day },
isCurrentMonth: current.month === month,
});
// Advance to next day — but stop after filling the grid if we've gone past 6 rows
const next = addDays(current, 1);
// If we're on row 5 (last) and already in a different month, check if we need more
current = next;
}
grid.push(rowCells);
// Early exit: if the last cell of this row is still in our target month's week range
// Actually, standard calendar grids always show 6 rows. But some months only need 5.
// We'll check: if we've passed the end of the month and are now into next month,
// and all remaining cells would be from next month...
// Standard approach: just fill 6 rows always for consistency.
}
return grid;
}
// ── Formatting ────────────────────────────────────────────────────────
const MONTH_NAMES = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
const DAY_NAMES_SHORT = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const DAY_NAMES_FULL = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
function monthName(month) { return MONTH_NAMES[month]; }
function dayNameShort(dow) { return DAY_NAMES_SHORT[dow]; }
function dayNameFull(dow) { return DAY_NAMES_FULL[dow]; }
function formatDate(dc) {
return `${dc.year}-${String(dc.month + 1).padStart(2, '0')}-${String(dc.day).padStart(2, '0')}`;
}
function parseDate(s) {
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!m)
return null;
return { year: +m[1], month: +m[2] - 1, day: +m[3] };
}
function formatTime(hhmm) {
if (!hhmm)
return '';
const [h, m] = hhmm.split(':').map(Number);
const ampm = h >= 12 ? 'PM' : 'AM';
const hour12 = h % 12 || 12;
return `${hour12}:${String(m).padStart(2, '0')} ${ampm}`;
}
function formatDayHeader(dc) {
return `${monthName(dc.month)} ${dc.day}, ${dc.year}`;
}
// ── ID Generation ─────────────────────────────────────────────────────
let _idCounter = Date.now();
function generateId() {
return `evt_${++_idCounter}_${Math.random().toString(36).slice(2, 8)}`;
}
};
// ── module: src/recurrence.ts ──
__mods["src/recurrence.ts"] = function (exports, require, module) {
"use strict";
// ─── Recurrence Engine ────────────────────────────────────────────────
// Expands recurrence rules into concrete occurrences within a given date range.
// Never materializes infinite series — only computes for the requested window.
Object.defineProperty(exports, "__esModule", { value: true });
exports.expandEvent = expandEvent;
exports.getAllOccurrences = getAllOccurrences;
exports.getOccurrencesForDate = getOccurrencesForDate;
const date_util_1 = require("./date-util");
/**
* Expand a single event into all its occurrences within [rangeStart, rangeEnd].
* Respects exceptions and deleted occurrences.
*/
function expandEvent(evt, rangeStart, rangeEnd, exceptions, deletedOccurrences) {
const results = [];
// Generate candidate dates from recurrence rule
const candidates = generateCandidateDates(evt, rangeStart, rangeEnd);
for (const date of candidates) {
// Check if this occurrence was deleted
if (deletedOccurrences.some(d => d.eventId === evt.id && (0, date_util_1.sameDay)(d.date, date))) {
continue;
}
// Build the base occurrence from the master event
const exception = exceptions.find(e => e.eventId === evt.id && (0, date_util_1.sameDay)(e.date, date));
const override = exception?.override ?? {};
results.push({
eventId: evt.id,
date,
startTime: override.startTime !== undefined ? override.startTime : evt.startTime,
endTime: override.endTime !== undefined ? override.endTime : evt.endTime,
isAllDay: override.isAllDay !== undefined ? override.isAllDay : evt.isAllDay,
title: override.title ?? evt.title,
color: override.color ?? evt.color,
notes: override.notes ?? evt.notes,
});
}
return results;
}
/** Generate all candidate dates for an event within the range. */
function generateCandidateDates(evt, rangeStart, rangeEnd) {
const { recurrence, date: anchorDate } = evt;
const results = [];
if (recurrence.type === 'none') {
// Non-recurring: only include if within range
if ((0, date_util_1.compareDays)(anchorDate, rangeStart) >= 0 && (0, date_util_1.compareDays)(anchorDate, rangeEnd) <= 0) {
results.push({ ...anchorDate });
}
return results;
}
const interval = recurrence.interval ?? 1;
switch (recurrence.type) {
case 'daily':
generateDaily(anchorDate, rangeStart, rangeEnd, interval, results);
break;
case 'weekly':
generateWeekly(evt, anchorDate, rangeStart, rangeEnd, interval, results);
break;
case 'monthly':
generateMonthly(anchorDate, rangeStart, rangeEnd, interval, results);
break;
case 'yearly':
generateYearly(anchorDate, rangeStart, rangeEnd, interval, results);
break;
}
return results;
}
function generateDaily(anchor, rangeStart, rangeEnd, interval, out) {
// Start from the first occurrence on or after rangeStart
let current = { ...anchor };
if ((0, date_util_1.compareDays)(current, rangeStart) < 0) {
const diff = daysBetween(anchor, rangeStart);
const steps = Math.ceil(diff / interval);
current = (0, date_util_1.addDays)(anchor, steps * interval);
}
while ((0, date_util_1.compareDays)(current, rangeEnd) <= 0) {
if ((0, date_util_1.compareDays)(current, anchor) >= 0) { // Don't go before anchor
out.push({ ...current });
}
current = (0, date_util_1.addDays)(current, interval);
}
}
function generateWeekly(evt, anchor, rangeStart, rangeEnd, interval, out) {
const weekdays = evt.recurrence.weekdays ?? [anchorDow(anchor)];
// Find the first week to start checking from
let currentWeekAnchor = { ...anchor };
if ((0, date_util_1.compareDays)(currentWeekAnchor, rangeStart) < 0) {
const diff = daysBetween(anchor, rangeStart);
const weeks = Math.ceil(diff / interval);
currentWeekAnchor = (0, date_util_1.addDays)(anchor, weeks * interval);
}
while (true) {
// For this week anchor, check each selected weekday
let foundInRange = false;
for (const wd of weekdays) {
const dow = anchorDow(currentWeekAnchor);
const dayOffset = ((wd - dow) + 7) % 7;
const candidate = (0, date_util_1.addDays)(currentWeekAnchor, dayOffset);
if ((0, date_util_1.compareDays)(candidate, rangeStart) >= 0 && (0, date_util_1.compareDays)(candidate, rangeEnd) <= 0) {
out.push({ ...candidate });
foundInRange = true;
}
}
// Advance to next interval week
currentWeekAnchor = (0, date_util_1.addDays)(currentWeekAnchor, interval * 7);
// Stop if the earliest possible day in the next iteration is past rangeEnd
const minNextDow = Math.min(...weekdays);
const dow = anchorDow(currentWeekAnchor);
const minOffset = ((minNextDow - dow) + 7) % 7;
if ((0, date_util_1.compareDays)((0, date_util_1.addDays)(currentWeekAnchor, minOffset), rangeEnd) > 0)
break;
}
}
function generateMonthly(anchor, rangeStart, rangeEnd, interval, out) {
// Monthly on the same day-of-month. Skip months that don't have that day.
const targetDay = anchor.day;
let currentMonthOffset = 0;
if ((0, date_util_1.compareDays)(anchor, rangeStart) < 0) {
const monthDiff = (rangeStart.year - anchor.year) * 12 + (rangeStart.month - anchor.month);
currentMonthOffset = Math.max(0, Math.ceil(monthDiff / interval) * interval);
}
for (let offset = currentMonthOffset;; offset += interval) {
const candidateDate = (0, date_util_1.addMonths)(anchor, offset);
// Skip if the target day doesn't exist in this month (e.g., Jan 31 -> Feb has no 31st)
if (candidateDate.day !== targetDay)
continue;
if ((0, date_util_1.compareDays)(candidateDate, rangeEnd) > 0)
break;
if ((0, date_util_1.compareDays)(candidateDate, anchor) >= 0 && (0, date_util_1.compareDays)(candidateDate, rangeStart) >= 0) {
out.push({ ...candidateDate });
}
}
}
function generateYearly(anchor, rangeStart, rangeEnd, interval, out) {
let current = { ...anchor };
if ((0, date_util_1.compareDays)(current, rangeStart) < 0) {
const yearDiff = rangeStart.year - anchor.year;
const steps = Math.max(0, Math.ceil(yearDiff / interval));
current = (0, date_util_1.addYears)(anchor, steps * interval);
}
while ((0, date_util_1.compareDays)(current, rangeEnd) <= 0 && (0, date_util_1.compareDays)(current, anchor) >= 0) {
out.push({ ...current });
current = (0, date_util_1.addYears)(current, interval);
}
}
// ── Helpers ───────────────────────────────────────────────────────────
function daysBetween(a, b) {
// Use native Date for this calculation only (no arithmetic on components)
const da = new Date(a.year, a.month, a.day).getTime();
const db = new Date(b.year, b.month, b.day).getTime();
return Math.round((db - da) / 86400000);
}
function anchorDow(dc) {
// dayOfWeek returns 0=Sun..6=Sat
const dt = new Date(dc.year, dc.month, dc.day);
return dt.getDay();
}
/** Get all occurrences for a set of events within a range. */
function getAllOccurrences(events, rangeStart, rangeEnd, exceptions, deletedOccurrences) {
const all = [];
for (const evt of events) {
all.push(...expandEvent(evt, rangeStart, rangeEnd, exceptions, deletedOccurrences));
}
// Sort by date, then start time
all.sort((a, b) => {
const dc = (0, date_util_1.compareDays)(a.date, b.date);
if (dc !== 0)
return dc;
if (a.isAllDay && !b.isAllDay)
return -1;
if (!a.isAllDay && b.isAllDay)
return 1;
if (a.startTime && b.startTime)
return a.startTime.localeCompare(b.startTime);
if (a.startTime)
return -1;
if (b.startTime)
return 1;
return 0;
});
return all;
}
/** Get occurrences for a specific date. */
function getOccurrencesForDate(events, date, exceptions, deletedOccurrences) {
const rangeStart = { ...date };
const rangeEnd = { ...date };
return getAllOccurrences(events, rangeStart, rangeEnd, exceptions, deletedOccurrences);
}
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
// ─── Application State & Persistence ──────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadState = loadState;
exports.saveState = saveState;
exports.init = init;
exports.getState = getState;
exports.subscribe = subscribe;
exports.addEvent = addEvent;
exports.updateEvent = updateEvent;
exports.deleteEvent = deleteEvent;
exports.getEvent = getEvent;
exports.addException = addException;
exports.deleteOccurrence = deleteOccurrence;
exports.undoDeleteOccurrence = undoDeleteOccurrence;
exports.setCurrentMonth = setCurrentMonth;
exports.setViewMode = setViewMode;
exports.setWeekStart = setWeekStart;
const date_util_1 = require("./date-util");
const constants_1 = require("./constants");
const STORAGE_KEY = 'calendar_planner_state_v1';
function defaultState() {
const t = (0, date_util_1.today)();
return {
events: [],
exceptions: [],
deletedOccurrences: [],
settings: { weekStart: constants_1.DEFAULT_WEEK_START, viewMode: 'month' },
currentYear: t.year,
currentMonth: t.month,
};
}
function loadState() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
// Merge with defaults for forward compatibility
return { ...defaultState(), ...parsed };
}
}
catch { /* ignore */ }
return defaultState();
}
function saveState(state) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
catch { /* quota exceeded — silently fail */ }
}
// ── Event CRUD ────────────────────────────────────────────────────────
let state;
let listeners = [];
function init() {
state = loadState();
return state;
}
function getState() {
return state;
}
function subscribe(fn) {
listeners.push(fn);
return () => { listeners = listeners.filter(l => l !== fn); };
}
function notify() {
saveState(state);
for (const fn of listeners)
fn();
}
function addEvent(evt) {
const newEvt = { ...evt, id: (0, date_util_1.generateId)() };
state.events.push(newEvt);
notify();
return newEvt;
}
function updateEvent(id, updates) {
const idx = state.events.findIndex(e => e.id === id);
if (idx < 0)
return;
state.events[idx] = { ...state.events[idx], ...updates };
notify();
}
function deleteEvent(id) {
state.events = state.events.filter(e => e.id !== id);
// Also clean up related exceptions and deleted occurrences
state.exceptions = state.exceptions.filter(e => e.eventId !== id);
state.deletedOccurrences = state.deletedOccurrences.filter(d => d.eventId !== id);
notify();
}
function getEvent(id) {
return state.events.find(e => e.id === id);
}
// ── Occurrence Exceptions ─────────────────────────────────────────────
function addException(evtId, date, override) {
// Remove any existing exception for this occurrence
state.exceptions = state.exceptions.filter(e => !(e.eventId === evtId && sameDaySimple(e.date, date)));
state.exceptions.push({ eventId: evtId, date: { ...date }, override });
notify();
}
function deleteOccurrence(evtId, date) {
// Remove from deleted list if already there (toggle)
const existing = state.deletedOccurrences.findIndex(d => d.eventId === evtId && sameDaySimple(d.date, date));
if (existing >= 0) {
state.deletedOccurrences.splice(existing, 1);
}
else {
state.deletedOccurrences.push({ eventId: evtId, date: { ...date } });
}
notify();
}
function undoDeleteOccurrence(evtId, date) {
state.deletedOccurrences = state.deletedOccurrences.filter(d => !(d.eventId === evtId && sameDaySimple(d.date, date)));
notify();
}
// ── Navigation ────────────────────────────────────────────────────────
function setCurrentMonth(year, month) {
state.currentYear = year;
state.currentMonth = month;
notify();
}
function setViewMode(mode) {
state.settings.viewMode = mode;
notify();
}
function setWeekStart(ws) {
state.settings.weekStart = ws;
notify();
}
// ── Helpers ───────────────────────────────────────────────────────────
function sameDaySimple(a, b) {
return a.year === b.year && a.month === b.month && a.day === b.day;
}
};
// ── module: src/modal.ts ──
__mods["src/modal.ts"] = function (exports, require, module) {
"use strict";
// ─── Event Editor Modal ───────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.initModal = initModal;
exports.openModal = openModal;
exports.openEditModal = openEditModal;
exports.closeModal = closeModal;
const constants_1 = require("./constants");
let modalEl = null;
let currentCallback = null;
let editingId = null;
function initModal() {
// Build the modal DOM once
const overlay = document.createElement('div');
overlay.id = 'event-modal-overlay';
overlay.className = 'modal-overlay hidden';
const modal = document.createElement('div');
modal.id = 'event-modal';
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-header">
<h2 id="modal-title-text">New Event</h2>
<button id="modal-close" class="icon-btn" title="Close">×</button>
</div>
<div class="modal-body">
<form id="event-form" novalidate>
<!-- Title -->
<div class="form-group">
<label for="evt-title">Title</label>
<input type="text" id="evt-title" required placeholder="Event title" autocomplete="off" />
<span class="field-error" id="err-title"></span>
</div>
<!-- Date -->
<div class="form-group">
<label for="evt-date">Date</label>
<input type="date" id="evt-date" required />
<span class="field-error" id="err-date"></span>
</div>
<!-- All-day toggle -->
<div class="form-group form-row">
<label class="checkbox-label">
<input type="checkbox" id="evt-allday" />
<span>All day</span>
</label>
</div>
<!-- Time range (hidden when all-day) -->
<div class="form-row time-row" id="time-group">
<div class="form-group flex-1">
<label for="evt-start">Start</label>
<input type="time" id="evt-start" step="900" />
</div>
<div class="form-group flex-1">
<label for="evt-end">End</label>
<input type="time" id="evt-end" step="900" />
<span class="field-error" id="err-time"></span>
</div>
</div>
<!-- Color -->
<div class="form-group">
<label>Color</label>
<div class="color-picker" id="color-picker"></div>
</div>
<!-- Recurrence -->
<div class="form-group">
<label for="evt-recurrence-type">Repeats</label>
<select id="evt-recurrence-type">
<option value="none">Does not repeat</option>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
<option value="yearly">Yearly</option>
</select>
</div>
<!-- Weekly weekday selection -->
<div class="form-group hidden" id="weekday-group">
<label>On these days:</label>
<div class="weekday-picker" id="weekday-picker"></div>
</div>
<!-- Notes -->
<div class="form-group">
<label for="evt-notes">Notes</label>
<textarea id="evt-notes" rows="2" placeholder="Optional notes…"></textarea>
</div>
<!-- Actions -->
<div class="modal-actions">
<button type="button" id="modal-delete" class="btn btn-danger hidden">Delete Event</button>
<div class="spacer"></div>
<button type="button" id="modal-cancel" class="btn btn-secondary">Cancel</button>
<button type="submit" id="modal-save" class="btn btn-primary">Save</button>
</div>
</form>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
modalEl = overlay;
// Build color picker
const colorPicker = document.getElementById('color-picker');
for (const c of constants_1.EVENT_COLORS) {
const swatch = document.createElement('button');
swatch.type = 'button';
swatch.className = 'color-swatch';
swatch.style.backgroundColor = c.value;
swatch.dataset.color = c.value;
swatch.title = c.name;
colorPicker.appendChild(swatch);
}
// Build weekday picker
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const weekdayPicker = document.getElementById('weekday-picker');
for (let i = 0; i < 7; i++) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'weekday-btn';
btn.textContent = dayNames[i];
btn.dataset.day = String(i);
weekdayPicker.appendChild(btn);
}
// Wire events
wireModalEvents(overlay, modal);
}
function wireModalEvents(overlay, modal) {
const closeBtn = document.getElementById('modal-close');
const cancelBtn = document.getElementById('modal-cancel');
const deleteBtn = document.getElementById('modal-delete');
const form = document.getElementById('event-form');
const alldayCheck = document.getElementById('evt-allday');
const timeGroup = document.getElementById('time-group');
const recurrenceType = document.getElementById('evt-recurrence-type');
const weekdayGroup = document.getElementById('weekday-group');
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
overlay.addEventListener('click', (e) => { if (e.target === overlay)
closeModal(); });
alldayCheck.addEventListener('change', () => {
timeGroup.classList.toggle('hidden', alldayCheck.checked);
});
recurrenceType.addEventListener('change', () => {
weekdayGroup.classList.toggle('hidden', recurrenceType.value !== 'weekly');
});
// Color picker selection
const colorPicker = document.getElementById('color-picker');
let selectedColor = constants_1.DEFAULT_COLOR;
colorPicker.addEventListener('click', (e) => {
const swatch = e.target.closest('.color-swatch');
if (!swatch)
return;
selectedColor = swatch.dataset.color || constants_1.DEFAULT_COLOR;
document.querySelectorAll('.color-swatch').forEach(s => s.classList.remove('selected'));
swatch.classList.add('selected');
});
// Weekday picker selection
const weekdayPicker = document.getElementById('weekday-picker');
let selectedWeekdays = [];
weekdayPicker.addEventListener('click', (e) => {
const btn = e.target.closest('.weekday-btn');
if (!btn)
return;
const day = parseInt(btn.dataset.day || '0');
if (selectedWeekdays.includes(day)) {
selectedWeekdays = selectedWeekdays.filter(d => d !== day);
btn.classList.remove('selected');
}
else {
selectedWeekdays.push(day);
btn.classList.add('selected');
}
});
// Form submit
form.addEventListener('submit', (e) => {
e.preventDefault();
clearErrors();
const title = document.getElementById('evt-title').value.trim();
const dateVal = document.getElementById('evt-date').value;
const isAllDay = alldayCheck.checked;
const startTime = document.getElementById('evt-start').value || undefined;
const endTime = document.getElementById('evt-end').value || undefined;
const notes = document.getElementById('evt-notes').value.trim() || undefined;
// Validation
let valid = true;
if (!title) {
setFieldError('err-title', 'Title is required');
valid = false;
}
if (!dateVal) {
setFieldError('err-date', 'Date is required');
valid = false;
}
if (!isAllDay && !startTime) {
setFieldError('err-time', 'Start time is required');
valid = false;
}
if (!isAllDay && startTime && endTime && endTime <= startTime) {
setFieldError('err-time', 'End time must be after start time');
valid = false;
}
if (!valid)
return;
const [y, m, d] = dateVal.split('-').map(Number);
const recurrenceTypeVal = recurrenceType.value;
const evt = {
title,
date: { year: y, month: m - 1, day: d },
isAllDay,
startTime,
endTime,
color: selectedColor,
notes,
recurrence: {
type: recurrenceTypeVal,
weekdays: recurrenceTypeVal === 'weekly' ? [...selectedWeekdays].sort() : undefined,
},
};
currentCallback?.onSave(evt);
});
// Delete button
deleteBtn.addEventListener('click', () => {
if (editingId && confirm('Delete this event?')) {
// Signal deletion via a special save with null title — handled by caller
const evt = {
title: '', date: { year: 0, month: 0, day: 1 }, isAllDay: false,
color: constants_1.DEFAULT_COLOR, recurrence: { type: 'none' },
};
currentCallback?.onSave(evt); // Caller checks for empty title = delete signal
}
});
// Store getters for form data access
modalEl._getSelectedColor = () => selectedColor;
modalEl._getSelectedWeekdays = () => [...selectedWeekdays];
}
function setFieldError(id, msg) {
const el = document.getElementById(id);
if (el)
el.textContent = msg;
}
function clearErrors() {
document.querySelectorAll('.field-error').forEach(el => el.textContent = '');
}
function openModal(callbacks, initialDate) {
currentCallback = callbacks;
editingId = null;
const titleText = document.getElementById('modal-title-text');
titleText.textContent = 'New Event';
// Reset form
document.getElementById('evt-title').value = '';
document.getElementById('evt-date').value = initialDate
? `${initialDate.year}-${String(initialDate.month + 1).padStart(2, '0')}-${String(initialDate.day).padStart(2, '0')}`
: new Date().toISOString().slice(0, 10);
document.getElementById('evt-allday').checked = false;
document.getElementById('evt-start').value = '09:00';
document.getElementById('evt-end').value = '10:00';
document.getElementById('evt-notes').value = '';
document.getElementById('evt-recurrence-type').value = 'none';
// Reset color selection
document.querySelectorAll('.color-swatch').forEach(s => s.classList.remove('selected'));
const firstSwatch = document.querySelector('.color-swatch');
if (firstSwatch)
firstSwatch.classList.add('selected');
// Reset weekday selection
document.querySelectorAll('.weekday-btn').forEach(b => b.classList.remove('selected'));
// Show/hide groups
document.getElementById('time-group').classList.remove('hidden');
document.getElementById('weekday-group').classList.add('hidden');
document.getElementById('modal-delete').classList.add('hidden');
clearErrors();
modalEl.classList.remove('hidden');
}
function openEditModal(callbacks, evt) {
currentCallback = callbacks;
editingId = evt.id;
const titleText = document.getElementById('modal-title-text');
titleText.textContent = 'Edit Event';
document.getElementById('evt-title').value = evt.title;
document.getElementById('evt-date').value =
`${evt.date.year}-${String(evt.date.month + 1).padStart(2, '0')}-${String(evt.date.day).padStart(2, '0')}`;
document.getElementById('evt-allday').checked = evt.isAllDay;
document.getElementById('evt-start').value = evt.startTime || '';
document.getElementById('evt-end').value = evt.endTime || '';
document.getElementById('evt-notes').value = evt.notes || '';
document.getElementById('evt-recurrence-type').value = evt.recurrence.type;
// Color selection
document.querySelectorAll('.color-swatch').forEach(s => {
s.classList.toggle('selected', s.dataset.color === evt.color);
});
// Weekday selection
const selectedWeekdays = evt.recurrence.weekdays ?? [];
document.querySelectorAll('.weekday-btn').forEach(b => {
b.classList.toggle('selected', selectedWeekdays.includes(parseInt(b.dataset.day || '0')));
});
// Show/hide groups
document.getElementById('time-group').classList.toggle('hidden', evt.isAllDay);
document.getElementById('weekday-group').classList.toggle('hidden', evt.recurrence.type !== 'weekly');
document.getElementById('modal-delete').classList.remove('hidden');
clearErrors();
modalEl.classList.remove('hidden');
}
function closeModal() {
if (modalEl)
modalEl.classList.add('hidden');
currentCallback = null;
editingId = null;
}
};
// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
// ─── Shared Rendering Helpers ─────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.createEl = createEl;
exports.renderEventChip = renderEventChip;
exports.today = today;
exports.isToday = isToday;
exports.showToast = showToast;
exports.showConfirm = showConfirm;
const date_util_1 = require("./date-util");
function createEl(tag, className, text) {
const el = document.createElement(tag);
if (className)
el.className = className;
if (text !== undefined)
el.textContent = text;
return el;
}
// ── Event Chip Rendering ──────────────────────────────────────────────
/** Render a compact event chip for month view cells. */
function renderEventChip(evt) {
const chip = createEl('div', 'event-chip');
chip.style.borderLeftColor = evt.color || '#3b82f6';
chip.textContent = evt.title;
return chip;
}
// ── Today Detection ───────────────────────────────────────────────────
let _todayCache = null;
function today() {
if (!_todayCache)
_todayCache = (0, date_util_1.today)();
return _todayCache;
}
function isToday(dc) {
return (0, date_util_1.sameDay)(dc, today());
}
// ── Toast Notifications ───────────────────────────────────────────────
let toastContainerEl = null;
function ensureToastContainer() {
if (!toastContainerEl) {
toastContainerEl = createEl('div', 'toast-container');
document.body.appendChild(toastContainerEl);
}
return toastContainerEl;
}
function showToast(message, duration = 3000) {
const container = ensureToastContainer();
const toast = createEl('div', 'toast', message);
container.appendChild(toast);
setTimeout(() => {
toast.classList.add('fade-out');
setTimeout(() => toast.remove(), 300);
}, duration);
}
// ── Confirm Dialog ────────────────────────────────────────────────────
function showConfirm(message) {
return new Promise((resolve) => {
const overlay = createEl('div', 'confirm-overlay');
const dialog = createEl('div', 'confirm-dialog');
dialog.innerHTML = `
<p>${message}</p>
<div class="confirm-actions">
<button class="btn btn-secondary" id="confirm-cancel">Cancel</button>
<button class="btn btn-danger" id="confirm-ok">Delete</button>
</div>
`;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
const cancelBtn = dialog.querySelector('#confirm-cancel');
const okBtn = dialog.querySelector('#confirm-ok');
cancelBtn.addEventListener('click', () => { resolve(false); cleanup(); });
okBtn.addEventListener('click', () => { resolve(true); cleanup(); });
overlay.addEventListener('click', (e) => { if (e.target === overlay) {
resolve(false);
cleanup();
} });
function cleanup() {
setTimeout(() => overlay.remove(), 200);
}
});
}
};
// ── module: src/views/month-view.ts ──
__mods["src/views/month-view.ts"] = function (exports, require, module) {
"use strict";
// ─── Month View ───────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.init = init;
exports.render = render;
const date_util_1 = require("../date-util");
const constants_1 = require("../constants");
const render_1 = require("../render");
let options = null;
let currentEvents = new Map(); // dateStr -> events[]
function init(opts) {
options = opts;
}
function render(year, month, allOccurrences, weekStart) {
if (!options)
return;
const container = document.getElementById(options.containerId);
if (!container)
return;
// Build grid - use passed weekStart or fall back to options
const ws = (weekStart !== undefined ? weekStart : options.weekStart);
const grid = (0, date_util_1.buildMonthGrid)(year, month, ws);
// Index events by date string for quick lookup
currentEvents.clear();
for (const evt of allOccurrences) {
const key = `${evt.date.year}-${evt.date.month}-${evt.date.day}`;
if (!currentEvents.has(key))
currentEvents.set(key, []);
currentEvents.get(key).push(evt);
}
// Render header row with day names
container.innerHTML = '';
const headerRow = (0, render_1.createEl)('div', 'month-header-row');
for (let i = 0; i < 7; i++) {
// Calculate which day of week this column represents
let dow;
if (ws === 1) {
dow = (i + 1) % 7; // Mon=1, Tue=2, ..., Sun=0
}
else {
dow = i; // Sun=0, Mon=1, ...
}
const dayLabel = (0, render_1.createEl)('div', 'month-day-label');
dayLabel.textContent = (0, date_util_1.dayNameShort)(dow);
headerRow.appendChild(dayLabel);
}
container.appendChild(headerRow);
// Render grid rows
for (const row of grid) {
const rowEl = (0, render_1.createEl)('div', 'month-row');
for (const cell of row) {
const cellEl = renderCell(cell.date, cell.isCurrentMonth);
rowEl.appendChild(cellEl);
}
container.appendChild(rowEl);
}
}
function renderCell(date, isCurrentMonth) {
const cell = (0, render_1.createEl)('div', 'month-cell');
if (!isCurrentMonth)
cell.classList.add('other-month');
if ((0, render_1.isToday)(date))
cell.classList.add('today');
// Day number
const dayNum = (0, render_1.createEl)('span', 'day-number');
dayNum.textContent = String(date.day);
cell.appendChild(dayNum);
// Events for this date
const key = `${date.year}-${date.month}-${date.day}`;
const events = currentEvents.get(key) || [];
if (events.length > 0) {
const eventList = (0, render_1.createEl)('div', 'cell-events');
const showCount = Math.min(events.length, constants_1.MAX_CELL_EVENTS);
for (let i = 0; i < showCount; i++) {
const chip = (0, render_1.renderEventChip)(events[i]);
chip.addEventListener('click', (e) => {
e.stopPropagation();
options.onEventClick(events[i], date);
});
// Make chips draggable
makeDraggable(chip, events[i].eventId, date);
eventList.appendChild(chip);
}
if (events.length > constants_1.MAX_CELL_EVENTS) {
const more = (0, render_1.createEl)('div', 'event-chip event-more');
more.textContent = `+${events.length - constants_1.MAX_CELL_EVENTS} more`;
more.addEventListener('click', (e) => {
e.stopPropagation();
showDayPopover(date, events);
});
eventList.appendChild(more);
}
cell.appendChild(eventList);
}
// Click to create new event
cell.addEventListener('click', () => {
options.onDayClick(date);
});
// Drag drop target
makeDropTarget(cell, date);
return cell;
}
// ── Drag & Drop ───────────────────────────────────────────────────────
let dragData = null;
function makeDraggable(chip, eventId, fromDate) {
chip.draggable = true;
chip.addEventListener('dragstart', (e) => {
dragData = { eventId, fromDate };
e.dataTransfer.effectAllowed = 'move';
// Set a ghost image
const ghost = (0, render_1.createEl)('div', 'event-chip');
ghost.textContent = 'Move event';
document.body.appendChild(ghost);
e.dataTransfer.setDragImage?.(ghost, 0, 0);
setTimeout(() => ghost.remove(), 0);
});
chip.addEventListener('dragend', () => { dragData = null; });
}
function makeDropTarget(cell, toDate) {
cell.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
cell.classList.add('drag-over');
});
cell.addEventListener('dragleave', () => {
cell.classList.remove('drag-over');
});
cell.addEventListener('drop', (e) => {
e.preventDefault();
cell.classList.remove('drag-over');
if (!dragData)
return;
options.onDragDrop(dragData.eventId, dragData.fromDate, toDate);
dragData = null;
});
}
// ── Day Popover ───────────────────────────────────────────────────────
function showDayPopover(date, events) {
// Remove existing popover
const existing = document.querySelector('.day-popover');
if (existing)
existing.remove();
const popover = (0, render_1.createEl)('div', 'day-popover');
const header = (0, render_1.createEl)('div', 'popover-header');
header.textContent = `${date.month + 1}/${date.day}/${date.year} — ${events.length} event${events.length !== 1 ? 's' : ''}`;
popover.appendChild(header);
for (const evt of events) {
const item = (0, render_1.createEl)('div', 'popover-event');
item.style.borderLeftColor = evt.color || '#3b82f6';
item.textContent = evt.title;
item.addEventListener('click', () => {
options.onEventClick(evt, date);
popover.remove();
});
popover.appendChild(item);
}
document.body.appendChild(popover);
// Position near the cell (approximate)
const container = document.getElementById(options.containerId);
const rect = container.getBoundingClientRect();
popover.style.position = 'fixed';
popover.style.top = `${rect.top + 100}px`;
popover.style.left = `${rect.left + 200}px`;
// Close on outside click
setTimeout(() => {
document.addEventListener('click', (e) => {
if (!popover.contains(e.target))
popover.remove();
}, { once: true });
}, 10);
}
};
// ── module: src/views/week-view.ts ──
__mods["src/views/week-view.ts"] = function (exports, require, module) {
"use strict";
// ─── Week View ────────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.init = init;
exports.render = render;
const date_util_1 = require("../date-util");
const constants_1 = require("../constants");
const render_1 = require("../render");
let options = null;
const PIXELS_PER_MINUTE = 4; // pixels per minute for event height calculation
function init(opts) {
options = opts;
}
function render(date, allOccurrences, weekStart) {
if (!options)
return;
const container = document.getElementById(options.containerId);
if (!container)
return;
const ws = (weekStart !== undefined ? weekStart : options.weekStart);
const weekDays = (0, date_util_1.getWeekDays)(date, ws);
const totalMinutes = (constants_1.HOUR_END - constants_1.HOUR_START) * 60;
// Build the week view layout
container.innerHTML = '';
container.style.position = 'relative';
// Header row with day names
const headerRow = (0, render_1.createEl)('div', 'week-header');
for (const wd of weekDays) {
const dayHeader = (0, render_1.createEl)('div', 'week-day-header');
if ((0, render_1.isToday)(wd))
dayHeader.classList.add('today');
const dowLabel = (0, render_1.createEl)('div', 'day-label');
dowLabel.textContent = (0, date_util_1.dayNameShort)(getDow(wd));
dayHeader.appendChild(dowLabel);
const dateLabel = (0, render_1.createEl)('div', 'date-label');
dateLabel.textContent = String(wd.day);
if ((0, render_1.isToday)(wd)) {
dateLabel.className += ' today-date';
}
dayHeader.appendChild(dateLabel);
headerRow.appendChild(dayHeader);
}
container.appendChild(headerRow);
// All-day events strip
const allDayStrip = (0, render_1.createEl)('div', 'all-day-strip');
for (const wd of weekDays) {
const key = `${wd.year}-${wd.month}-${wd.day}`;
const dayEvents = allOccurrences.filter(e => (0, date_util_1.sameDay)(e.date, wd) && e.isAllDay);
const slot = (0, render_1.createEl)('div', 'all-day-slot');
if ((0, render_1.isToday)(wd))
slot.classList.add('today');
for (const evt of dayEvents) {
const chip = (0, render_1.createEl)('div', 'event-chip all-day-chip');
chip.style.borderLeftColor = evt.color || '#3b82f6';
chip.textContent = evt.title;
chip.addEventListener('click', (e) => { e.stopPropagation(); options.onEventClick(evt, wd); });
slot.appendChild(chip);
}
allDayStrip.appendChild(slot);
}
container.appendChild(allDayStrip);
// Time grid
const timeGrid = (0, render_1.createEl)('div', 'time-grid');
timeGrid.style.height = `${totalMinutes * PIXELS_PER_MINUTE}px`;
// Hour labels (left column)
const hourLabels = (0, render_1.createEl)('div', 'hour-labels');
for (let h = constants_1.HOUR_START; h <= constants_1.HOUR_END; h++) {
const label = (0, render_1.createEl)('div', 'hour-label');
label.textContent = formatHourLabel(h);
hourLabels.appendChild(label);
}
timeGrid.appendChild(hourLabels);
// Day columns with events
const dayColumns = (0, render_1.createEl)('div', 'day-columns');
for (const wd of weekDays) {
const col = (0, render_1.createEl)('div', 'week-day-column');
if ((0, render_1.isToday)(wd))
col.classList.add('today');
// Hour slots (clickable)
for (let h = constants_1.HOUR_START; h <= constants_1.HOUR_END; h++) {
for (let m = 0; m < 60; m += constants_1.SNAP_MINUTES) {
const slot = (0, render_1.createEl)('div', 'time-slot');
slot.addEventListener('click', () => options.onTimeSlotClick(wd, h, m));
col.appendChild(slot);
}
}
// Render events as positioned blocks
const dayEvents = allOccurrences.filter(e => (0, date_util_1.sameDay)(e.date, wd) && !e.isAllDay);
for (const evt of dayEvents) {
const block = renderEventBlock(evt, totalMinutes);
col.appendChild(block);
}
// Current time indicator
if ((0, render_1.isToday)(wd)) {
const indicator = (0, render_1.createEl)('div', 'current-time-indicator');
const now = getNow();
const minutesFromStart = (now.getHours() - constants_1.HOUR_START) * 60 + now.getMinutes();
if (minutesFromStart >= 0 && minutesFromStart <= totalMinutes) {
indicator.style.top = `${minutesFromStart * PIXELS_PER_MINUTE}px`;
}
else {
indicator.remove();
}
col.appendChild(indicator);
}
dayColumns.appendChild(col);
}
timeGrid.appendChild(dayColumns);
container.appendChild(timeGrid);
}
function renderEventBlock(evt, totalMinutes) {
const block = (0, render_1.createEl)('div', 'week-event-block');
block.style.borderLeftColor = evt.color || '#3b82f6';
block.style.backgroundColor = (evt.color || '#3b82f6') + '1a'; // ~10% opacity
const startMin = timeToMinutes(evt.startTime);
const endMin = timeToMinutes(evt.endTime);
if (startMin < 0 || endMin <= startMin)
return block;
const topPx = Math.max(0, (startMin - constants_1.HOUR_START * 60)) * PIXELS_PER_MINUTE;
const heightPx = Math.max(20, (endMin - startMin) * PIXELS_PER_MINUTE);
block.style.position = 'absolute';
block.style.top = `${topPx}px`;
block.style.height = `${heightPx}px`;
block.style.left = '4px';
block.style.right = '4px';
block.style.borderRadius = '4px';
block.style.padding = '2px 6px';
block.style.fontSize = '11px';
block.style.overflow = 'hidden';
block.style.cursor = 'pointer';
const timeStr = `${evt.startTime} – ${evt.endTime}`;
block.innerHTML = `<div class="event-time">${timeStr}</div><div class="event-title">${evt.title}</div>`;
// Click to edit
block.addEventListener('click', (e) => {
e.stopPropagation();
options.onEventClick(evt, evt.date);
});
return block;
}
// ── Helpers ───────────────────────────────────────────────────────────
function getDow(dc) {
const d = new Date(dc.year, dc.month, dc.day);
return d.getDay(); // 0=Sun..6=Sat
}
function formatHourLabel(h) {
if (h === 0)
return '12 AM';
if (h < 12)
return `${h} AM`;
if (h === 12)
return '12 PM';
return `${h - 12} PM`;
}
function timeToMinutes(t) {
if (!t)
return constants_1.HOUR_START * 60;
const [h, m] = t.split(':').map(Number);
return h * 60 + m;
}
function getNow() {
return new Date();
}
};
// ── module: src/views/agenda-view.ts ──
__mods["src/views/agenda-view.ts"] = function (exports, require, module) {
"use strict";
// ─── Agenda View ──────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.init = init;
exports.render = render;
const date_util_1 = require("../date-util");
const render_1 = require("../render");
let options = null;
function init(opts) {
options = opts;
}
/** Render agenda view starting from `fromDate` for the next `numDays` days. */
function render(fromDate, allOccurrences, numDays = 30) {
if (!options)
return;
const container = document.getElementById(options.containerId);
if (!container)
return;
container.innerHTML = '';
// Group events by date
const grouped = new Map();
for (const evt of allOccurrences) {
const key = `${evt.date.year}-${evt.date.month}-${evt.date.day}`;
if (!grouped.has(key))
grouped.set(key, []);
grouped.get(key).push(evt);
}
// Iterate through each day and show events
let hasEvents = false;
for (let i = 0; i < numDays; i++) {
const date = (0, date_util_1.addDays)(fromDate, i);
const key = `${date.year}-${date.month}-${date.day}`;
const events = grouped.get(key) || [];
if (events.length === 0)
continue;
hasEvents = true;
// Day header
const dayHeader = createEl('div', 'agenda-day-header');
let label;
if (i === 0) {
label = (0, render_1.isToday)(date) ? 'Today' : `${(0, date_util_1.monthName)(date.month)} ${date.day}`;
}
else if (i === 1) {
label = 'Tomorrow';
}
else {
label = `${(0, date_util_1.monthName)(date.month)} ${date.day}`;
}
dayHeader.textContent = label;
if ((0, render_1.isToday)(date)) {
const todayBadge = createEl('span', 'today-badge');
todayBadge.textContent = '';
dayHeader.appendChild(todayBadge);
}
container.appendChild(dayHeader);
// Event items
for (const evt of events) {
const item = createEl('div', 'agenda-event-item');
if (evt.isAllDay) {
item.classList.add('all-day');
}
// Color indicator
const colorBar = createEl('div', 'agenda-color-bar');
colorBar.style.backgroundColor = evt.color || '#3b82f6';
item.appendChild(colorBar);
// Content
const content = createEl('div', 'agenda-event-content');
const titleEl = createEl('div', 'agenda-event-title', evt.title);
content.appendChild(titleEl);
if (evt.isAllDay) {
const timeEl = createEl('div', 'agenda-event-time', 'All day');
content.appendChild(timeEl);
}
else {
const timeEl = createEl('div', 'agenda-event-time', `${(0, date_util_1.formatTime)(evt.startTime)} – ${(0, date_util_1.formatTime)(evt.endTime)}`);
content.appendChild(timeEl);
}
item.appendChild(content);
item.addEventListener('click', () => options.onEventClick(evt));
container.appendChild(item);
}
}
if (!hasEvents) {
const empty = createEl('div', 'agenda-empty');
empty.textContent = 'No upcoming events';
container.appendChild(empty);
}
}
function createEl(tag, className, text) {
const el = document.createElement(tag);
if (className)
el.className = className;
if (text !== undefined)
el.textContent = text;
return el;
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"typescript","sessionId":"c5bfb438-2623-433b-ae81-94e7aa721278","tokensIn":5030518,"tokensOut":56898,"tokensTotal":5087416,"turns":105,"toolCalls":105,"failedToolCalls":3,"timestamp":"2026-07-30T16:23:17.597Z"} -->