Skip to main content

max / goingson

Delete the events calendar, which was replaced three months ago Problem 7d3cd131 found js/events-calendar.js loaded on every boot with nothing able to reach it, and left the call open: dead code, or a feature whose wiring was lost. The git log answers it. b79f301 (2026-05-18, mobile UX rework) removed #month-calendar-grid, #week-calendar-grid and the view-toggle that switched to them in the same pass that introduced the segmented list, so the calendar was replaced rather than dropped by accident. Goes with it: the script tag, and 286 lines of .cal-* CSS in one contiguous block plus the rules inside the compact media query. Every cal- reference in the tree was inside the module, so none of it was shared. --timeline-slot-h stays, since day-planning-render.js reads it. Also drops "70. Calendar Views" from the styles.css section index, which pointed at nothing either way: section 70 is Biometric App Lock. esc() call sites 338 -> 329, against the escape.js retirement. That is dead code leaving rather than a port, and the count still only starts falling for real at the quasi flip.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-12 00:20 UTC
Signed with PGP, not checked
Commit: 3df42ba374e565c4a989900821720ab75dfddc11
Parent: 1f79e5a
4 files changed, +9 insertions, -728 deletions
@@ -755,7 +755,6 @@
755 755 <script src="js/tasks.js"></script>
756 756 <script src="js/task-overview.js"></script>
757 757 <script src="js/events.js"></script>
758 - <script src="js/events-calendar.js"></script>
759 758 <script src="js/emails-render.js"></script>
760 759 <script src="js/emails-threads.js"></script>
761 760 <script src="js/emails-reader.js"></script>
@@ -123,7 +123,7 @@
123 123 62. Timer Widget 67. Task Overview
124 124 63. Focus Timer (Pomodoro) 68. Plan/Review Toggle Nudge Dot
125 125 64. Time Summary Panel 69. Finish & Review
126 - 65. Task Time Tracking UI 70. Calendar Views
126 + 65. Task Time Tracking UI
127 127
128 128 BAND: SCOPED STYLESHEETS
129 129 Compose window (compose.html standalone Tauri window)
@@ -9015,243 +9015,7 @@
9015 9015 margin-bottom: var(--gap-peer);
9016 9016 }
9017 9017
9018 - /* Month Grid */
9019 - .cal-month-grid {
9020 - border: var(--border-width) solid var(--border);
9021 - overflow: hidden;
9022 - }
9023 -
9024 - .cal-month-header,
9025 - .cal-month-cells {
9026 - display: grid;
9027 - grid-template-columns: repeat(7, 1fr);
9028 - }
9029 -
9030 - .cal-month-day-header {
9031 - font-family: var(--font-heading);
9032 - font-size: var(--font-size-sm);
9033 - font-weight: 600;
9034 - text-align: center;
9035 - padding: var(--gap-peer);
9036 - text-transform: uppercase;
9037 - background: var(--surface-overlay);
9038 - border-bottom: var(--border-width-sm) solid var(--border);
9039 - }
9040 -
9041 - .cal-month-cell {
9042 - min-height: 90px;
9043 - border: var(--border-width-sm) solid var(--border);
9044 - padding: var(--gap-bound);
9045 - cursor: pointer;
9046 - }
9047 -
9048 - .cal-month-cell:hover {
9049 - background: var(--surface-overlay);
9050 - }
9051 -
9052 - .cal-month-cell.other-month {
9053 - opacity: 0.4;
9054 - }
9055 -
9056 - .cal-month-cell.today {
9057 - border-color: var(--action);
9058 - border-width: 2px;
9059 - }
9060 -
9061 - .cal-month-cell-header {
9062 - margin-bottom: var(--step-hair);
9063 - }
9064 -
9065 - .cal-day-number {
9066 - font-family: var(--font-heading);
9067 - font-size: var(--font-size-md);
9068 - font-weight: 600;
9069 - }
9070 -
9071 - .cal-event-chip {
9072 - font-size: var(--font-size-xs);
9073 - padding: var(--step-hair) var(--step-tight);
9074 - margin-top: var(--step-hair);
9075 - border-radius: var(--radius-xs);
9076 - background: var(--action);
9077 - color: var(--content-on-action);
9078 - white-space: nowrap;
9079 - overflow: hidden;
9080 - text-overflow: ellipsis;
9081 - cursor: pointer;
9082 - }
9083 -
9084 - .cal-event-chip:hover {
9085 - opacity: 0.85;
9086 - }
9087 -
9088 - .cal-event-chip.block-focus { background: var(--category-one); }
9089 - .cal-event-chip.block-personal { background: var(--category-four); color: var(--content); }
9090 - .cal-event-chip.block-free_time { background: var(--category-two); color: var(--content); }
9091 -
9092 - /* A due task in a month cell. Outlined rather than filled, so a deadline reads
9093 - as a different kind of thing from an event without needing a legend: events
9094 - are blocks of time, tasks are dates something is owed. */
9095 - .cal-task-chip {
9096 - font-size: var(--font-size-xs);
9097 - padding: var(--step-hair) var(--step-tight);
9098 - margin-top: var(--step-hair);
9099 - border-radius: var(--radius-xs);
9100 - border: var(--border-width-sm) solid var(--action);
9101 - background: var(--surface-raised);
9102 - color: var(--content);
9103 - white-space: nowrap;
9104 - overflow: hidden;
9105 - text-overflow: ellipsis;
9106 - cursor: pointer;
9107 - }
9108 -
9109 - .cal-task-chip:hover {
9110 - background: var(--surface-overlay);
9111 - }
9112 -
9113 - .cal-task-chip.overdue {
9114 - border-color: var(--danger);
9115 - color: var(--danger);
9116 - font-weight: 600;
9117 - }
9118 -
9119 - .cal-task-chip.done {
9120 - border-color: var(--border);
9121 - color: var(--content-muted);
9122 - text-decoration: line-through;
9123 - }
9124 -
9125 - .cal-event-more {
9126 - font-size: var(--font-size-xxs);
9127 - color: var(--content-secondary);
9128 - padding: var(--step-hair) var(--step-tight);
9129 - }
9130 -
9131 - /* Week Grid */
9132 - .cal-week-grid {
9133 - border: var(--border-width) solid var(--border);
9134 - overflow: hidden;
9135 - }
9136 -
9137 - .cal-week-header {
9138 - display: grid;
9139 - grid-template-columns: 60px repeat(7, 1fr);
9140 - border-bottom: var(--border-width-sm) solid var(--border);
9141 - background: var(--surface-overlay);
9142 - }
9143 -
9144 - .cal-week-day-header {
9145 - text-align: center;
9146 - padding: var(--gap-peer);
9147 - font-family: var(--font-heading);
9148 - }
9149 -
9150 - .cal-week-day-header.today {
9151 - background: color-mix(in srgb, var(--action) 15%, transparent);
9152 - }
9153 -
9154 - .cal-week-day-name {
9155 - font-size: var(--font-size-sm);
9156 - text-transform: uppercase;
9157 - font-weight: 600;
9158 - }
9159 -
9160 - .cal-week-day-num {
9161 - font-size: var(--font-size-lg);
9162 - font-weight: 700;
9163 - display: block;
9164 - }
9165 -
9166 - .cal-week-allday-row {
9167 - display: grid;
9168 - grid-template-columns: 60px repeat(7, 1fr);
9169 - border-bottom: var(--border-width-sm) solid var(--border);
9170 - min-height: 28px;
9171 - }
9172 -
9173 - .cal-allday-label {
9174 - font-size: var(--font-size-xs);
9175 - color: var(--content-secondary);
9176 - display: flex;
9177 - align-items: center;
9178 - justify-content: center;
9179 - }
9180 -
9181 - .cal-week-allday-cell {
9182 - padding: var(--step-hair);
9183 - border-left: var(--border-width) solid var(--border);
9184 - }
9185 -
9186 - .cal-week-body {
9187 - display: grid;
9188 - grid-template-columns: 60px repeat(7, 1fr);
9189 - position: relative;
9190 - overflow-y: auto;
9191 - max-height: 70vh;
9192 - }
9193 -
9194 - .cal-week-time-gutter {
9195 - position: relative;
9196 - }
9197 -
9198 - .cal-week-hour-label {
9199 - position: absolute;
9200 - right: 0.5rem;
9201 - font-size: var(--font-size-xs);
9202 - color: var(--content-secondary);
9203 - transform: translateY(-0.5em);
9204 - }
9205 -
9206 - .cal-week-day-col {
9207 - position: relative;
9208 - border-left: var(--border-width) solid var(--border);
9209 - }
9210 -
9211 - .cal-week-day-col.today {
9212 - background: color-mix(in srgb, var(--action) 5%, transparent);
9213 - }
9214 -
9215 - .cal-week-hour-line {
9216 - position: absolute;
9217 - left: 0;
9218 - right: 0;
9219 - border-top: var(--border-width) dashed color-mix(in srgb, var(--border) 50%, transparent);
9220 - }
9221 -
9222 - .cal-week-event {
9223 - position: absolute;
9224 - left: 2px;
9225 - right: 2px;
9226 - border: var(--border-width-sm) solid var(--border);
9227 - border-radius: var(--radius-sm);
9228 - padding: var(--step-hair) var(--step-tight);
9229 - background: var(--action);
9230 - color: var(--content-on-action);
9231 - overflow: hidden;
9232 - cursor: pointer;
9233 - z-index: 10;
9234 - font-size: var(--font-size-xs);
9235 - }
9236 -
9237 - .cal-week-event:hover {
9238 - opacity: 0.85;
9239 - }
9240 -
9241 - .cal-week-event-title {
9242 - font-weight: 600;
9243 - }
9244 -
9245 - .cal-week-event-time {
9246 - font-size: var(--font-size-xxs);
9247 - opacity: 0.85;
9248 - }
9249 -
9250 - .cal-week-event.block-focus { background: var(--category-one); }
9251 - .cal-week-event.block-personal { background: var(--category-four); color: var(--content); }
9252 - .cal-week-event.block-free_time { background: var(--category-two); color: var(--content); }
9253 -
9254 - /* Calendar in mobile UI, single-day swipe carousel, denser timeline. */
9018 + /* Mobile UI: denser timeline, stacked settings page. */
9255 9019 @media (max-width: 599px) {
9256 9020 /* :root, not html: the default is declared on :root in §3, and a bare
9257 9021 html would lose to it on specificity no matter where it sits. */
@@ -9259,56 +9023,6 @@
9259 9023 --timeline-slot-h: 22px;
9260 9024 }
9261 9025
9262 - .cal-month-cell {
9263 - min-height: 64px;
9264 - padding: var(--step-tight);
9265 - }
9266 - .cal-day-number {
9267 - font-size: var(--font-size-lg);
9268 - font-weight: 600;
9269 - }
9270 - .cal-event-chip {
9271 - font-size: var(--font-size-xxs);
9272 - }
9273 - .cal-week-body {
9274 - max-height: 60vh;
9275 - }
9276 -
9277 - /* Single-day swipe carousel for week view */
9278 - .cal-mobile-day {
9279 - display: flex;
9280 - flex-direction: column;
9281 - height: 70vh;
9282 - user-select: none;
9283 - }
9284 - .cal-mobile-day-header {
9285 - padding: var(--gap-peer);
9286 - font-weight: 600;
9287 - text-align: center;
9288 - border-bottom: var(--border-width) solid var(--border);
9289 - }
9290 - .cal-mobile-day-header.today {
9291 - color: var(--action);
9292 - }
9293 - .cal-mobile-allday {
9294 - padding: var(--gap-peer);
9295 - display: flex;
9296 - flex-direction: column;
9297 - gap: var(--gap-bound);
9298 - border-bottom: var(--border-width) solid var(--border);
9299 - }
9300 - .cal-mobile-day-body {
9301 - position: relative;
9302 - flex: 1;
9303 - overflow-y: auto;
9304 - display: grid;
9305 - grid-template-columns: 48px 1fr;
9306 - }
9307 - .cal-mobile-day-col {
9308 - position: relative;
9309 - border-left: var(--border-width) solid var(--border);
9310 - }
9311 -
9312 9026 /* Settings Page, Mobile */
9313 9027 .settings-page-layout { flex-direction: column; }
9314 9028 .settings-sidebar {
@@ -7,9 +7,13 @@
7 7 //! second clause did the work here, because the four heavier candidates are all
8 8 //! waiting on something rather than on effort.
9 9 //!
10 - //! - `events.js` (993) and `events-calendar.js` (436). A month grid, a week
11 - //! grid and a mobile day column, which is [`RegionKind::Bespoke`]'s shape and
12 - //! was the weekly review's reason for skipping the same files.
10 + //! - `events.js` (993). A month grid, a week grid and a mobile day column,
11 + //! which is [`RegionKind::Bespoke`]'s shape and was the weekly review's
12 + //! reason for skipping the same file. `events-calendar.js` (436) was the
13 + //! other half of this candidate until it was deleted as dead code: its two
14 + //! grid containers left `index.html` in the 2026-05-18 mobile rework, which
15 + //! replaced the calendar with the segmented list, and nothing had reached it
16 + //! since.
13 17 //! - `day-planning` (~1,585 across four files). Drag-to-paint, which nothing in
14 18 //! the vocabulary names.
15 19 //! - `time-tracking.js` (564). A floating widget on a one-second `setInterval`,
@@ -1,436 +1,0 @@
1 - /**
2 - * GoingsOn - Events Calendar Module
3 - * Month grid and week grid views for events.
4 - */
5 -
6 - (function() {
7 - 'use strict';
8 - const esc = GoingsOn.utils.escapeHtml;
9 - const escAttr = GoingsOn.utils.escapeAttrValue;
10 - const escArg = GoingsOn.utils.escapeHandlerArg;
11 - const escAttrVal = GoingsOn.utils.escapeAttrValue;
12 -
13 - let currentMonthDate = new Date();
14 - let currentWeekDate = new Date();
15 - let monthEvents = [];
16 - let monthTasks = [];
17 - let weekEvents = [];
18 - let weekSwipeCleanup = null;
19 -
20 - // Width, and the same width the stylesheet uses: seven day columns either
21 - // fit or they do not, and that is a question about the window rather than
22 - // about the pointer. A tablet in landscape shows the week grid; a desktop
23 - // window dragged narrow shows one day.
24 - //
25 - // 599px is makeover-geometry's SizeClass::Compact upper bound, which is
26 - // where .cal-mobile-day is styled. The two have to agree, or this renders
27 - // a single-day DOM the stylesheet is laying out as a week; src-tauri's
28 - // build.rs fails the build if this string and SizeClass part ways.
29 - //
30 - // Held and watched rather than asked per call. The week view is two
31 - // different DOM trees, so crossing the boundary with one already on screen
32 - // leaves markup the stylesheet is not laying out; nothing else re-renders
33 - // on resize, and until this listener existed you got that until the next
34 - // navigation.
35 - const compact = window.matchMedia('(max-width: 599px)');
36 -
37 - function isCompactView() {
38 - return compact.matches;
39 - }
40 -
41 - compact.addEventListener('change', () => {
42 - // Only if the week view is the one on screen. Rebuilding it while the
43 - // month grid is up would drop the swipe handlers on a hidden node and
44 - // fight whatever the user is actually looking at.
45 - const container = document.getElementById('week-calendar-grid');
46 - if (container && container.offsetParent !== null) loadWeek();
47 - });
48 -
49 - // Date Helpers
50 -
51 - function getMonday(date) {
52 - const d = new Date(date);
53 - const day = d.getDay();
54 - const diff = (day + 6) % 7;
55 - d.setDate(d.getDate() - diff);
56 - d.setHours(0, 0, 0, 0);
57 - return d;
58 - }
59 -
60 - function toDateKey(date) {
61 - const y = date.getFullYear();
62 - const m = String(date.getMonth() + 1).padStart(2, '0');
63 - const d = String(date.getDate()).padStart(2, '0');
64 - return `${y}-${m}-${d}`;
65 - }
66 -
67 - function groupByDate(events) {
68 - const map = new Map();
69 - for (const e of events) {
70 - const d = new Date(e.startTimeEpoch || new Date(e.startTime).getTime());
71 - const key = toDateKey(d);
72 - if (!map.has(key)) map.set(key, []);
73 - map.get(key).push(e);
74 - }
75 - return map;
76 - }
77 -
78 - /**
79 - * Group tasks by the local day they are due on. A task with no due date is
80 - * dropped: the grid is a calendar, and an undated task has no cell.
81 - */
82 - function groupTasksByDueDate(tasks) {
83 - const map = new Map();
84 - for (const t of tasks) {
85 - if (!t.due) continue;
86 - const key = toDateKey(new Date(t.due));
87 - if (!map.has(key)) map.set(key, []);
88 - map.get(key).push(t);
89 - }
90 - return map;
91 - }
92 -
93 - function truncate(str, len) {
94 - if (!str || str.length <= len) return str || '';
95 - return str.substring(0, len - 1) + '\u2026';
96 - }
97 -
98 - function formatMonthLabel(date) {
99 - return date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
100 - }
101 -
102 - function formatWeekLabel(monday) {
103 - const sunday = new Date(monday);
104 - sunday.setDate(monday.getDate() + 6);
105 - const opts = { month: 'short', day: 'numeric' };
106 - const start = monday.toLocaleDateString('en-US', opts);
107 - const end = sunday.toLocaleDateString('en-US', { ...opts, year: 'numeric' });
108 - return `${start} \u2013 ${end}`;
109 - }
110 -
111 - // Month View
112 -
113 - async function loadMonth() {
114 - const first = new Date(currentMonthDate.getFullYear(), currentMonthDate.getMonth(), 1);
115 - const startOffset = (first.getDay() + 6) % 7;
116 - const gridStart = new Date(first);
117 - gridStart.setDate(first.getDate() - startOffset);
118 -
119 - const lastDay = new Date(first.getFullYear(), first.getMonth() + 1, 0);
120 - const gridEnd = new Date(lastDay);
121 - const endOffset = (7 - ((lastDay.getDay() + 6) % 7 + 1)) % 7;
122 - gridEnd.setDate(lastDay.getDate() + endOffset + 1);
123 -
124 - // Events and due tasks together: the month grid answers "what lands on
125 - // this day", and a deadline lands on a day exactly as an event does.
126 - // Fetched over the whole grid, so the leading and trailing days of the
127 - // neighbouring months are populated too.
128 - const [events, tasks] = await Promise.all([
129 - GoingsOn.api.events.listBetween(gridStart.toISOString(), gridEnd.toISOString())
130 - .catch(err => { console.error('Failed to load month events:', err); return []; }),
131 - GoingsOn.api.tasks.listDueBetween(gridStart.toISOString(), gridEnd.toISOString())
132 - .catch(err => { console.error('Failed to load month tasks:', err); return []; }),
133 - ]);
134 - monthEvents = events;
135 - monthTasks = tasks;
136 -
137 - renderMonthGrid(first, gridStart, gridEnd);
138 - const label = document.getElementById('month-calendar-label');
139 - if (label) label.textContent = formatMonthLabel(first);
140 - }
141 -
142 - function renderMonthGrid(firstOfMonth, gridStart, gridEnd) {
143 - const container = document.getElementById('month-calendar-grid');
144 - if (!container) return;
145 - const eventsByDate = groupByDate(monthEvents);
146 - const tasksByDate = groupTasksByDueDate(monthTasks);
147 - const today = toDateKey(new Date());
148 - const dayHeaders = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
149 -
150 - let html = '<div class="raised cal-month-grid">';
151 - html += '<div class="cal-month-header">';
152 - for (const d of dayHeaders) {
153 - html += `<div class="cal-month-day-header">${d}</div>`;
154 - }
155 - html += '</div><div class="cal-month-cells">';
156 -
157 - const cursor = new Date(gridStart);
158 - while (cursor < gridEnd) {
159 - const dateKey = toDateKey(cursor);
160 - const isCurrentMonth = cursor.getMonth() === firstOfMonth.getMonth();
161 - const isToday = dateKey === today;
162 - const dayEvents = eventsByDate.get(dateKey) || [];
163 - const dayTasks = tasksByDate.get(dateKey) || [];
164 -
165 - const classes = ['cal-month-cell'];
166 - if (!isCurrentMonth) classes.push('other-month');
167 - if (isToday) classes.push('today');
168 -
169 - html += `<div class="${classes.join(' ')}">`;
170 - html += `<div class="cal-month-cell-header"><span class="cal-day-number">${cursor.getDate()}</span></div>`;
171 -
172 - // Events first, then what is due: the day's fixed points, then the
173 - // work hanging off it. The cap is over both, so a busy day does not
174 - // grow a cell twice as tall as its neighbours.
175 - const maxShow = 3;
176 - let shown = 0;
177 - dayEvents.slice(0, maxShow).forEach(e => {
178 - const blockClass = e.blockType ? `block-${e.blockType}` : '';
179 - html += `<div class="cal-event-chip ${blockClass}" data-act="events.open" data-a1="${escAttr(e.id)}" title="${escAttrVal(e.title)}">${esc(truncate(e.title, 18))}</div>`;
180 - shown++;
181 - });
182 - dayTasks.slice(0, Math.max(0, maxShow - shown)).forEach(t => {
183 - const overdue = t.isOverdue ? ' overdue' : '';
184 - const done = t.status === 'Completed' ? ' done' : '';
185 - html += `<div class="cal-task-chip${overdue}${done}" data-act="taskOverview.open" data-a1="${escAttr(t.id)}" title="Due: ${escAttrVal(t.title)}">${esc(truncate(t.title, 18))}</div>`;
186 - shown++;
187 - });
188 - const hidden = dayEvents.length + dayTasks.length - shown;
189 - if (hidden > 0) {
190 - html += `<div class="cal-event-more">+${hidden} more</div>`;
191 - }
192 -
193 - html += '</div>';
194 - cursor.setDate(cursor.getDate() + 1);
195 - }
196 -
197 - html += '</div></div>';
198 - container.innerHTML = html;
199 - }
200 -
201 - function prevMonth() { currentMonthDate.setMonth(currentMonthDate.getMonth() - 1); loadMonth(); }
202 - function nextMonth() { currentMonthDate.setMonth(currentMonthDate.getMonth() + 1); loadMonth(); }
203 - function goToThisMonth() { currentMonthDate = new Date(); loadMonth(); }
204 -
205 - // Week View
206 -
207 - const SLOT_HEIGHT = 12;
208 - const HOURS_START = 6;
209 - const HOURS_END = 22;
210 -
211 - async function loadWeek() {
212 - const monday = getMonday(currentWeekDate);
213 - const sunday = new Date(monday);
214 - sunday.setDate(monday.getDate() + 7);
215 -
216 - try {
217 - weekEvents = await GoingsOn.api.events.listBetween(
218 - monday.toISOString(), sunday.toISOString()
219 - );
220 - } catch (err) {
221 - console.error('Failed to load week events:', err);
222 - weekEvents = [];
223 - }
224 -
225 - renderWeekGrid(monday);
226 - const label = document.getElementById('week-calendar-label');
227 - if (label) {
228 - label.textContent = isCompactView()
229 - ? currentWeekDate.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })
230 - : formatWeekLabel(monday);
231 - }
232 - }
233 -
234 - function renderWeekGrid(monday) {
235 - const container = document.getElementById('week-calendar-grid');
236 - if (!container) return;
237 - if (isCompactView()) {
238 - renderMobileDay(container);
239 - return;
240 - }
241 - const eventsByDate = groupByDate(weekEvents);
242 - const today = toDateKey(new Date());
243 - const totalSlots = (HOURS_END - HOURS_START) * 4;
244 - const gridHeight = totalSlots * SLOT_HEIGHT;
245 -
246 - let html = '<div class="raised cal-week-grid">';
247 -
248 - // Header row
249 - html += '<div class="cal-week-header"><div class="cal-week-time-gutter"></div>';
250 - const cursor = new Date(monday);
251 - for (let d = 0; d < 7; d++) {
252 - const dateKey = toDateKey(cursor);
253 - const dayName = cursor.toLocaleDateString('en-US', { weekday: 'short' });
254 - html += `<div class="cal-week-day-header ${dateKey === today ? 'today' : ''}">
255 - <span class="cal-week-day-name">${dayName}</span>
256 - <span class="cal-week-day-num">${cursor.getDate()}</span>
257 - </div>`;
258 - cursor.setDate(cursor.getDate() + 1);
259 - }
260 - html += '</div>';
261 -
262 - // All-day row
263 - html += '<div class="cal-week-allday-row"><div class="cal-week-time-gutter cal-allday-label">All Day</div>';
264 - const adCursor = new Date(monday);
265 - for (let d = 0; d < 7; d++) {
266 - const dateKey = toDateKey(adCursor);
267 - const dayEvts = (eventsByDate.get(dateKey) || []).filter(e => e.isAllDay);
268 - html += '<div class="cal-week-allday-cell">';
269 - dayEvts.forEach(e => {
270 - const blockClass = e.blockType ? `block-${e.blockType}` : '';
271 - html += `<div class="cal-event-chip ${blockClass}" data-act="events.open" data-a1="${escAttr(e.id)}" title="${escAttrVal(e.title)}">${esc(truncate(e.title, 14))}</div>`;
272 - });
273 - html += '</div>';
274 - adCursor.setDate(adCursor.getDate() + 1);
275 - }
276 - html += '</div>';
277 -
278 - // Time grid body
279 - html += `<div class="cal-week-body" style="height: ${gridHeight}px;">`;
280 -
281 - // Time gutter
282 - html += '<div class="cal-week-time-gutter">';
283 - for (let h = HOURS_START; h < HOURS_END; h++) {
284 - const label = h === 0 ? '12 AM' : h < 12 ? `${h} AM` : h === 12 ? '12 PM' : `${h - 12} PM`;
285 - html += `<div class="cal-week-hour-label" style="top: ${(h - HOURS_START) * 4 * SLOT_HEIGHT}px;">${label}</div>`;
286 - }
287 - html += '</div>';
288 -
289 - // Day columns
290 - const colCursor = new Date(monday);
291 - for (let d = 0; d < 7; d++) {
292 - const dateKey = toDateKey(colCursor);
293 - const dayEvts = (eventsByDate.get(dateKey) || []).filter(e => !e.isAllDay);
294 - html += `<div class="cal-week-day-col ${dateKey === today ? 'today' : ''}">`;
295 -
296 - // Hour lines
297 - for (let h = HOURS_START; h < HOURS_END; h++) {
298 - html += `<div class="cal-week-hour-line" style="top: ${(h - HOURS_START) * 4 * SLOT_HEIGHT}px;"></div>`;
299 - }
300 -
301 - // Positioned events
302 - dayEvts.forEach(e => {
303 - const startDate = new Date(e.startTimeEpoch || new Date(e.startTime).getTime());
304 - const endEpoch = e.endTimeEpoch || (e.endTime ? new Date(e.endTime).getTime() : (startDate.getTime() + 3600000));
305 - const endDate = new Date(endEpoch);
306 -
307 - const startMinutes = startDate.getHours() * 60 + startDate.getMinutes();
308 - const endMinutes = endDate.getHours() * 60 + endDate.getMinutes();
309 - const durationMinutes = Math.max(endMinutes - startMinutes, 15);
310 -
311 - const topPx = ((startMinutes - HOURS_START * 60) / 15) * SLOT_HEIGHT;
312 - const heightPx = Math.max((durationMinutes / 15) * SLOT_HEIGHT, SLOT_HEIGHT);
313 - const blockClass = e.blockType ? `block-${e.blockType}` : '';
314 -
315 - html += `<div class="cal-week-event ${blockClass}" style="top: ${topPx}px; height: ${heightPx}px;" data-act="events.open" data-a1="${escAttr(e.id)}" title="${escAttrVal(e.title + ' ' + e.timeFormatted)}">
316 - <div class="cal-week-event-title">${esc(truncate(e.title, 20))}</div>
317 - <div class="cal-week-event-time">${esc(e.timeFormatted)}</div>
318 - </div>`;
319 - });
320 -
321 - html += '</div>';
322 - colCursor.setDate(colCursor.getDate() + 1);
323 - }
324 -
325 - html += '</div></div>';
326 - container.innerHTML = html;
327 -
328 - // Auto-scroll to current hour
329 - const body = container.querySelector('.cal-week-body');
330 - if (body) {
331 - const now = new Date();
332 - const currentMinutes = now.getHours() * 60 + now.getMinutes();
333 - const scrollTarget = ((currentMinutes - HOURS_START * 60) / 15) * SLOT_HEIGHT - 100;
334 - body.scrollTop = Math.max(0, scrollTarget);
335 - }
336 - }
337 -
338 - function prevWeek() {
339 - const step = isCompactView() ? 1 : 7;
340 - currentWeekDate.setDate(currentWeekDate.getDate() - step);
341 - loadWeek();
342 - }
343 - function nextWeek() {
344 - const step = isCompactView() ? 1 : 7;
345 - currentWeekDate.setDate(currentWeekDate.getDate() + step);
346 - loadWeek();
347 - }
348 - function goToThisWeek() { currentWeekDate = new Date(); loadWeek(); }
349 -
350 - // Mobile: Single-day swipe view
351 -
352 - function renderMobileDay(container) {
353 - const dateKey = toDateKey(currentWeekDate);
354 - const today = toDateKey(new Date());
355 - const allDayEvts = (groupByDate(weekEvents).get(dateKey) || []).filter(e => e.isAllDay);
356 - const timedEvts = (groupByDate(weekEvents).get(dateKey) || []).filter(e => !e.isAllDay);
357 - const totalSlots = (HOURS_END - HOURS_START) * 4;
358 - const gridHeight = totalSlots * SLOT_HEIGHT;
359 -
360 - let html = '<div class="cal-mobile-day">';
361 - html += `<div class="cal-mobile-day-header${dateKey === today ? ' today' : ''}">
362 - ${esc(currentWeekDate.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' }))}
363 - </div>`;
364 -
365 - if (allDayEvts.length) {
366 - html += '<div class="cal-mobile-allday">';
367 - allDayEvts.forEach(e => {
368 - const blockClass = e.blockType ? `block-${e.blockType}` : '';
369 - html += `<div class="cal-event-chip ${blockClass}" data-act="events.open" data-a1="${escAttr(e.id)}">${esc(truncate(e.title, 30))}</div>`;
370 - });
371 - html += '</div>';
372 - }
373 -
374 - html += `<div class="cal-mobile-day-body" style="height: ${gridHeight}px;">`;
375 - html += '<div class="cal-week-time-gutter">';
376 - for (let h = HOURS_START; h < HOURS_END; h++) {
377 - const label = h === 0 ? '12 AM' : h < 12 ? `${h} AM` : h === 12 ? '12 PM' : `${h - 12} PM`;
378 - html += `<div class="cal-week-hour-label" style="top: ${(h - HOURS_START) * 4 * SLOT_HEIGHT}px;">${label}</div>`;
379 - }
380 - html += '</div>';
381 -
382 - html += `<div class="cal-mobile-day-col">`;
383 - for (let h = HOURS_START; h < HOURS_END; h++) {
384 - html += `<div class="cal-week-hour-line" style="top: ${(h - HOURS_START) * 4 * SLOT_HEIGHT}px;"></div>`;
385 - }
386 - timedEvts.forEach(e => {
387 - const startDate = new Date(e.startTimeEpoch || new Date(e.startTime).getTime());
388 - const endEpoch = e.endTimeEpoch || (e.endTime ? new Date(e.endTime).getTime() : (startDate.getTime() + 3600000));
389 - const endDate = new Date(endEpoch);
390 - const startMinutes = startDate.getHours() * 60 + startDate.getMinutes();
391 - const endMinutes = endDate.getHours() * 60 + endDate.getMinutes();
392 - const durationMinutes = Math.max(endMinutes - startMinutes, 15);
393 - const topPx = ((startMinutes - HOURS_START * 60) / 15) * SLOT_HEIGHT;
394 - const heightPx = Math.max((durationMinutes / 15) * SLOT_HEIGHT, SLOT_HEIGHT);
395 - const blockClass = e.blockType ? `block-${e.blockType}` : '';
396 - html += `<div class="cal-week-event ${blockClass}" style="top: ${topPx}px; height: ${heightPx}px;" data-act="events.open" data-a1="${escAttr(e.id)}">
397 - <div class="cal-week-event-title">${esc(truncate(e.title, 30))}</div>
398 - <div class="cal-week-event-time">${esc(e.timeFormatted)}</div>
399 - </div>`;
400 - });
401 - html += '</div></div></div>';
402 -
403 - container.innerHTML = html;
404 -
405 - // Auto-scroll to current hour if it's today
406 - const body = container.querySelector('.cal-mobile-day-body');
407 - if (body && dateKey === today) {
408 - const now = new Date();
409 - const minutes = now.getHours() * 60 + now.getMinutes();
410 - body.scrollTop = Math.max(0, ((minutes - HOURS_START * 60) / 15) * SLOT_HEIGHT - 100);
411 - }
412 -
413 - // Swipe to change day
414 - if (weekSwipeCleanup) weekSwipeCleanup();
415 - if (GoingsOn.touch?.isTouchDevice && GoingsOn.touch.addSwipeNavigation) {
416 - weekSwipeCleanup = GoingsOn.touch.addSwipeNavigation(container, {
417 - onLeft: nextWeek,
418 - onRight: prevWeek,
419 - });
420 - }
421 - }
422 -
423 - // Namespace
424 -
425 - GoingsOn.eventsCalendar = {
426 - loadMonth,
427 - loadWeek,
428 - prevMonth,
429 - nextMonth,
430 - goToThisMonth,
431 - prevWeek,
432 - nextWeek,
433 - goToThisWeek,
434 - };
435 -
436 - })();