Lines Matching +full:star +full:- +full:light

6  * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
15 * Get the meta directory path (farm-safe)
38 * Get sync config file path (farm-safe)
39 * Checks per-animal metadir first, falls back to shared plugin dir
42 $perAnimal = $this->metaDir() . 'calendar/sync_config.php';
60 $this->Lexer->addSpecialPattern('\{\{calendar(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
61 $this->Lexer->addSpecialPattern('\{\{eventlist(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
62 $this->Lexer->addSpecialPattern('\{\{eventpanel(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
70 $match = substr($match, 12, -2);
72 $match = substr($match, 13, -2);
74 $match = substr($match, 10, -2);
134 // Disable caching - theme can change via admin without page edit
135 $renderer->nocache();
138 $html = $this->renderStandaloneEventList($data);
140 $html = $this->renderEventPanelOnly($data);
142 $html = $this->renderStaticCalendar($data);
144 $html = $this->renderCompactCalendar($data);
147 $renderer->doc .= $html;
156 $excludeList = $this->parseExcludeList($exclude);
158 // Get theme - prefer inline theme= parameter, fall back to admin default
159 $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme();
160 $themeStyles = $this->getSidebarThemeStyles($theme);
161 $themeClass = 'calendar-theme-' . $theme;
170 $events = $this->loadEventsMultiNamespace($namespace, $year, $month, $excludeList);
172 $events = $this->loadEvents($namespace, $year, $month);
178 $prevMonth = $month - 1;
182 $prevYear--;
193 $configFile = $this->syncConfigPath();
202 // Container - all styling via CSS variables
203-compact-container ' . $themeClass . '" id="' . $calId . '" data-namespace="' . htmlspecialchars($…
205 // Inject CSS variables for this calendar instance - all theming flows from here
208 --background-site: ' . $themeStyles['bg'] . ';
209 --background-alt: ' . $themeStyles['cell_bg'] . ';
210 --background-header: ' . $themeStyles['header_bg'] . ';
211 --text-primary: ' . $themeStyles['text_primary'] . ';
212 --text-dim: ' . $themeStyles['text_dim'] . ';
213 --text-bright: ' . $themeStyles['text_bright'] . ';
214 --border-color: ' . $themeStyles['grid_border'] . ';
215 --border-main: ' . $themeStyles['border'] . ';
216 --cell-bg: ' . $themeStyles['cell_bg'] . ';
217 --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
218 --shadow-color: ' . $themeStyles['shadow'] . ';
219 --header-border: ' . $themeStyles['header_border'] . ';
220 --header-shadow: ' . $themeStyles['header_shadow'] . ';
221 --grid-bg: ' . $themeStyles['grid_bg'] . ';
222 --btn-text: ' . $btnTextColor . ';
223 --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
224 --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
225 --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
226 --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
227 --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
228 --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
229 --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
231 … #event-search-' . $calId . '::placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
232 …#event-search-' . $calId . '::-webkit-input-placeholder { color: ' . $themeStyles['text_dim'] . ';…
233 …#event-search-' . $calId . '::-moz-placeholder { color: ' . $themeStyles['text_dim'] . '; opacity:…
234 …#event-search-' . $calId . ':-ms-input-placeholder { color: ' . $themeStyles['text_dim'] . '; opac…
238 $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
244 …$html .= '<script type="application/json" id="events-data-' . $calId . '">' . json_encode($events)…
247 $html .= '<div class="calendar-compact-left">';
250 $html .= '<div class="calendar-compact-header">';
251 …$html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $prevYear . ', …
252 …$html .= '<h3 class="calendar-month-picker" onclick="openMonthPicker(\'' . $calId . '\', ' . $year…
253 …$html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $nextYear . ', …
254 …$html .= '<button class="cal-today-btn" onclick="jumpToToday(\'' . $calId . '\', \'' . $namespace …
257 // Calendar grid - day name headers as a separate div (avoids Firefox th height issues)
258 $html .= '<div class="calendar-day-headers">';
261 $html .= '<table class="calendar-compact-grid">';
279 $monthStart = new DateTime(sprintf('%04d-%02d-01', $year, $month));
280 $monthEnd = new DateTime(sprintf('%04d-%02d-%02d', $year, $month, $daysInMonth));
290 $currentKey = $current->format('Y-m-d');
293 $currentDate = DateTime::createFromFormat('Y-m-d', $currentKey);
294 … if ($currentDate && $currentDate->format('Y-m') === sprintf('%04d-%02d', $year, $month)) {
313 $current->modify('+1 day');
318 // Assign stable row slots to multi-day events (same algorithm as JS rebuildCalendar)
319 $slotAssignments = array(); // dateKey -> array of {id, slot}
320 $eventSlots = array(); // eventId -> slot number
325 // First pass: multi-day events get stable slots across all days they span
340 $checkKey = $checkCurrent->format('Y-m-d');
350 $checkCurrent->modify('+1 day');
360 $resKey = $resCurrent->format('Y-m-d');
363 $resCurrent->modify('+1 day');
369 // Second pass: single-day events fill remaining slots
378 // Sort single-day by time
382 if (empty($timeA) && !empty($timeB)) return -1;
412 $html .= '<td class="cal-empty"></td>';
414 $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $currentDay);
415 $isToday = ($dateKey === date('Y-m-d'));
418 $classes = 'cal-day';
419 if ($isToday) $classes .= ' cal-today';
420 if ($hasEvents) $classes .= ' cal-has-events';
422 …$html .= '<td class="' . $classes . '" data-date="' . $dateKey . '" tabindex="0" role="gridcell" a…
424 $dayNumClass = $isToday ? 'day-num day-num-today' : 'day-num';
430 $maxSlot = -1;
443 $html .= '<div class="event-indicators">';
447 …$html .= '<span style="display:block;width:100%;height:6px;min-height:6px;flex-shrink:0;"></span>';
470 $barClass = empty($eventTime) ? 'event-bar-no-time' : 'event-bar-timed';
471 if (!$isFirstDay) $barClass .= ' event-bar-continues';
472 if (!$isLastDay) $barClass .= ' event-bar-continuing';
474 $barClass .= ' event-bar-important';
475 if ($isFirstDay) $barClass .= ' event-bar-has-star';
480 $html .= '<span class="event-bar ' . $barClass . '" ';
497 $html .= '</div>'; // End calendar-left
500 $html .= '<div class="calendar-compact-right">';
501 $html .= '<div class="event-list-header">';
502 $html .= '<div class="event-list-header-content">';
503 $html .= '<h4 id="eventlist-title-' . $calId . '">Events</h4>';
505 $html .= '<span class="namespace-badge">' . htmlspecialchars($namespace) . '</span>';
510 $searchDefault = $this->getSearchDefault();
511 $searchAllClass = $searchDefault === 'all' ? ' all-dates' : '';
515 $html .= '<div class="event-search-container-inline">';
516 …$html .= '<input type="text" class="event-search-input-inline" id="event-search-' . $calId . '" pl…
517 …$html .= '<button class="event-search-clear-inline" id="search-clear-' . $calId . '" onclick="clea…
518 …$html .= '<button class="event-search-mode-inline' . $searchAllClass . '" id="search-mode-' . $cal…
521 …$html .= '<button class="add-event-compact" onclick="openAddEvent(\'' . $calId . '\', \'' . $names…
524 $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '">';
525 $html .= $this->renderEventListContent($events, $calId, $namespace, $themeStyles);
528 $html .= '</div>'; // End calendar-right
531 $html .= $this->renderEventDialog($calId, $namespace, $theme);
534 $html .= $this->renderMonthPicker($calId, $year, $month, $namespace, $theme, $themeStyles);
542 * Render a static/read-only calendar for presentation and printing
543 * No edit buttons, clean layout, print-friendly itinerary
550 $excludeList = $this->parseExcludeList($exclude);
557 $calId = 'static-cal-' . substr(md5($namespace . $year . $month . uniqid()), 0, 8);
560 … in_array($themeOverride, ['matrix', 'pink', 'purple', 'professional', 'wiki', 'dark', 'light'])) {
563 $theme = $this->getSidebarTheme();
565 $themeStyles = $this->getSidebarThemeStyles($theme);
568 $importantNsList = $this->getImportantNamespaces();
570 // Load events - check for multi-namespace or wildcard
573 $events = $this->loadEventsMultiNamespace($namespace, $year, $month, $excludeList);
575 $events = $this->loadEvents($namespace, $year, $month);
584 // Display title - custom or default month/year
588 $themeClass = 'static-theme-' . $theme;
591-static ' . $themeClass . '" id="' . $calId . '" data-year="' . $year . '" data-month="' . $month …
594 $html .= '<div class="static-screen-view">';
597 $html .= '<div class="static-header">';
599 …$html .= '<button class="static-nav-btn" onclick="navStaticCalendar(\'' . $calId . '\', -1)" title…
601 $html .= '<h2 class="static-month-title">' . hsc($displayTitle) . '</h2>';
603 …$html .= '<button class="static-nav-btn" onclick="navStaticCalendar(\'' . $calId . '\', 1)" title=…
606 …$html .= '<button class="static-print-btn" onclick="printStaticCalendar(\'' . $calId . '\')" title…
611 $html .= '<table class="static-calendar-grid">';
630 $html .= '<td class="static-day-empty"></td>';
632 $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $dayCount);
634 $isToday = ($dateKey === date('Y-m-d'));
637 $cellClass = 'static-day';
638 if ($isToday) $cellClass .= ' static-day-today';
639 if ($isWeekend) $cellClass .= ' static-day-weekend';
640 if (!empty($dayEvents)) $cellClass .= ' static-day-has-events';
643 $html .= '<div class="static-day-number">' . $dayCount . '</div>';
646 $html .= '<div class="static-day-events">';
663 // Build tooltip - plain text with basic formatting indicators
666 $tooltipText .= "\n�� " . $this->formatTime12Hour($time);
668 … $tooltipText .= ' - ' . $this->formatTime12Hour($event['endTime']);
682 $eventClass = 'static-event';
683 if ($isImportant) $eventClass .= ' static-event-important';
685 …$html .= '<div class="' . $eventClass . '" style="border-left-color: ' . $color . ';" title="' . h…
687 $html .= '<span class="static-event-star">⭐</span>';
690 … $html .= '<span class="static-event-time">' . $this->formatTime12Hour($time) . '</span> ';
692 $html .= '<span class="static-event-title">' . $title . '</span>';
710 $html .= '<div class="static-print-view">';
711 $html .= '<h2 class="static-print-title">' . hsc($displayTitle) . '</h2>';
714 …$html .= '<p class="static-print-namespace">' . $this->getLang('calendar_label') . ': ' . hsc($nam…
736 … $html .= '<p class="static-print-empty">' . $this->getLang('no_events_scheduled') . '</p>';
738 $html .= '<table class="static-itinerary">';
746 $dateDisplay = $dateObj->format('D, M j');
758 $rowClass = $isImportant ? 'static-itinerary-important' : '';
764 $html .= '<td class="static-itinerary-date">' . $dateDisplay . '</td>';
771 …$time = isset($event['time']) && $event['time'] ? $this->formatTime12Hour($event['time']) : $this-
773 $time .= ' - ' . $this->formatTime12Hour($event['endTime']);
775 $html .= '<td class="static-itinerary-time">' . $time . '</td>';
777 // Title with star for important
778 $html .= '<td class="static-itinerary-title">';
785 // Description - with formatting
786 … $desc = isset($event['description']) ? $this->renderDescription($event['description']) : '';
787 $html .= '<td class="static-itinerary-desc">' . $desc . '</td>';
804 * Format time to 12-hour format
812 $hour12 = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour);
820 $configFile = $this->syncConfigPath();
833 return '<p class="no-events-msg">No events this month</p>';
838 $theme = $this->getSidebarTheme();
839 $themeStyles = $this->getSidebarThemeStyles($theme);
841 $theme = $this->getSidebarTheme();
845 $configFile = $this->syncConfigPath();
855 $events = $this->checkTimeConflicts($events);
857 // Sort by date ascending (chronological order - oldest first)
866 // All-day events (no time) go to the TOP
867 if ($timeA === null && $timeB !== null) return -1; // A before B
869 if ($timeA === null && $timeB === null) return 0; // Both all-day, equal
878 $today = date('Y-m-d');
881 // Helper function to check if event is past (with 15-minute grace period for timed events)
893 // Event is today - check time with grace period
899 // Add 15-minute grace period
900 $eventDateTime->modify('+15 minutes');
905 // If time parsing fails, fall back to date-only comparison
914 // Build HTML for each event - separate past/completed from future
922 // Track first future/today event for auto-scroll
952 $renderedDescription = $this->renderDescription($description, $themeStyles);
954 // Convert to 12-hour format and handle time ranges
959 $displayTime = $timeObj->format('g:i A');
965 $displayTime .= ' - ' . $endTimeObj->format('g:i A');
974 // Use originalStartDate if this is a multi-month event continuation
977 $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Jan 24"
979 // Multi-day indicator
983 $multiDay = ' → ' . $endObj->format('D, M j');
986 $completedClass = $completed ? ' event-completed' : '';
987 // Don't grey out past due tasks - they need attention!
988 $pastClass = ($isPast && !$isPastDue) ? ' event-past' : '';
989 $pastDueClass = $isPastDue ? ' event-pastdue' : '';
990 … $firstFutureAttr = ($firstFutureEventId === $eventId) ? ' data-first-future="true"' : '';
1004 $importantClass = $isImportantNs ? ' event-important' : '';
1006 // For all themes: use CSS variables, only keep border-left-color as inline
1008-compact-item' . $completedClass . $pastClass . $pastDueClass . $importantClass . '" data-event-id…
1009 $eventHtml .= '<div class="event-info">';
1011 $eventHtml .= '<div class="event-title-row">';
1012 // Add star for important namespace events
1014 $eventHtml .= '<span class="event-important-star" title="Important">⭐</span> ';
1016 $eventHtml .= '<span class="event-title-compact">' . $title . '</span>';
1022 $eventHtml .= '<div class="event-meta-compact">';
1023 $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay;
1029 … class="event-pastdue-badge" style="background:' . $themeStyles['pastdue_color'] . ' !important; c…
1031 …="event-today-badge" style="background:' . $themeStyles['border'] . ' !important; color:' . $theme…
1033 // Add namespace badge - ALWAYS show if event has a namespace
1040-namespace-badge" onclick="filterCalendarByNamespace(\'' . $calId . '\', \'' . htmlspecialchars($e…
1051 … $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time'];
1055 … $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime'];
1056 … $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')';
1065 …$eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmousee…
1072 … $eventHtml .= '<div class="event-desc-compact">' . $renderedDescription . '</div>';
1075 // Past events: render with display:none for click-to-expand
1076 $eventHtml .= '<div class="event-meta-compact" style="display:none;">';
1077 $eventHtml .= '<span class="event-date-time">' . $displayDate . $multiDay;
1086-namespace-badge" onclick="filterCalendarByNamespace(\'' . $calId . '\', \'' . htmlspecialchars($e…
1096 … $startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time'];
1100 … $endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime'];
1101 … $conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')';
1110 …$eventHtml .= ' <span class="event-conflict-badge" data-conflicts="' . $conflictJson . '" onmousee…
1117 …$eventHtml .= '<div class="event-desc-compact" style="display:none;">' . $renderedDescription . '<…
1121 $eventHtml .= '</div>'; // event-info
1126 $eventHtml .= '<div class="event-actions-compact">';
1127 …$eventHtml .= '<button class="event-action-btn" onclick="deleteEvent(\'' . $calId . '\', \'' . $ev…
1128 …$eventHtml .= '<button class="event-action-btn" onclick="editEvent(\'' . $calId . '\', \'' . $even…
1131 // Checkbox for tasks - ON THE FAR RIGHT
1134 …$eventHtml .= '<input type="checkbox" class="task-checkbox" ' . $checked . ' onclick="toggleTaskCo…
1153 $html .= '<div class="past-events-section">';
1154 … $html .= '<div class="past-events-toggle" onclick="togglePastEvents(\'' . $calId . '\')">';
1155 $html .= '<span class="past-events-arrow" id="past-arrow-' . $calId . '">▶</span> ';
1156 $html .= '<span class="past-events-label">Past Events (' . $pastCount . ')</span>';
1158 …$html .= '<div class="past-events-content" id="past-events-' . $calId . '" style="display:none;">';
1180 if (empty($evt['time'])) continue; // Skip all-day events
1193 if ($this->eventsOverlap($dateEvents[$i], $dateEvents[$j])) {
1247 return false; // All-day events don't conflict
1257 $start1Mins = $this->timeToMinutes($start1);
1258 $end1Mins = $this->timeToMinutes($end1);
1259 $start2Mins = $this->timeToMinutes($start2);
1260 $end2Mins = $this->timeToMinutes($end2);
1281 $excludeList = $this->parseExcludeList($exclude);
1289 // Get theme - prefer inline theme= parameter, fall back to admin default
1290 …me = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme(); $themeStyles = $th…
1296 $events = $this->loadEventsMultiNamespace($namespace, $year, $month, $excludeList);
1298 $events = $this->loadEvents($namespace, $year, $month);
1304 $prevMonth = $month - 1;
1308 $prevYear--;
1322 $configFile = $this->syncConfigPath();
1331-panel-standalone" id="' . $calId . '" data-height="' . htmlspecialchars($height) . '" data-namesp…
1333 // Inject CSS variables for this panel instance - same as main calendar
1336 --background-site: ' . $themeStyles['bg'] . ';
1337 --background-alt: ' . $themeStyles['cell_bg'] . ';
1338 --background-header: ' . $themeStyles['header_bg'] . ';
1339 --text-primary: ' . $themeStyles['text_primary'] . ';
1340 --text-dim: ' . $themeStyles['text_dim'] . ';
1341 --text-bright: ' . $themeStyles['text_bright'] . ';
1342 --border-color: ' . $themeStyles['grid_border'] . ';
1343 --border-main: ' . $themeStyles['border'] . ';
1344 --cell-bg: ' . $themeStyles['cell_bg'] . ';
1345 --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
1346 --shadow-color: ' . $themeStyles['shadow'] . ';
1347 --header-border: ' . $themeStyles['header_border'] . ';
1348 --header-shadow: ' . $themeStyles['header_shadow'] . ';
1349 --grid-bg: ' . $themeStyles['grid_bg'] . ';
1350 --btn-text: ' . $btnTextColor . ';
1351 --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
1352 --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
1353 --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
1354 --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
1355 --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
1356 --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
1357 --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
1359 … #event-search-' . $calId . '::placeholder { color: ' . $themeStyles['text_dim'] . '; opacity: 1; }
1363 $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
1368 // Compact two-row header designed for ~500px width
1369 $html .= '<div class="panel-header-compact">';
1372 $html .= '<div class="panel-header-row-1">';
1373 …$html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $prevYear .…
1377 …$html .= '<h3 class="panel-month-title" onclick="openMonthPickerPanel(\'' . $calId . '\', ' . $yea…
1379 …$html .= '<button class="panel-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $nextYear .…
1385 …ss="panel-ns-badge" style="background:var(--cell-today-bg) !important; color:var(--text-bright) !i…
1389 …ss="panel-ns-badge" style="background:var(--cell-today-bg) !important; color:var(--text-bright) !i…
1394-ns-badge filter-on" style="background:var(--text-bright) !important; color:var(--background-site)…
1396 …ss="panel-ns-badge" style="background:var(--cell-today-bg) !important; color:var(--text-bright) !i…
1401 …$html .= '<button class="panel-today-btn" onclick="jumpTodayPanel(\'' . $calId . '\', \'' . $names…
1405 $searchDefault = $this->getSearchDefault();
1406 $searchAllClass = $searchDefault === 'all' ? ' all-dates' : '';
1410 $html .= '<div class="panel-header-row-2">';
1411 $html .= '<div class="panel-search-box">';
1412 …$html .= '<input type="text" class="panel-search-input" id="event-search-' . $calId . '" placehold…
1413 …$html .= '<button class="panel-search-clear" id="search-clear-' . $calId . '" onclick="clearEventS…
1414 …$html .= '<button class="panel-search-mode' . $searchAllClass . '" id="search-mode-' . $calId . '"…
1416 …$html .= '<button class="panel-add-btn" onclick="openAddEventPanel(\'' . $calId . '\', \'' . $name…
1421 …$html .= '<div class="event-list-compact" id="eventlist-' . $calId . '" style="max-height: ' . htm…
1422 $html .= $this->renderEventListContent($events, $calId, $namespace);
1425 $html .= $this->renderEventDialog($calId, $namespace, $theme);
1428 $html .= $this->renderMonthPicker($calId, $year, $month, $namespace, $theme, $themeStyles);
1442 $excludeList = $this->parseExcludeList($exclude);
1451 // Handle "range" parameter - day, week, month, or extended format (>3m, >100d, >2w, >1y)
1453 … $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks
1454 $endDate = date('Y-m-d');
1457 … $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks
1459 $endDateTime->modify('+7 days');
1460 $endDate = $endDateTime->format('Y-m-d');
1463 … $startDate = date('Y-m-01', strtotime('-1 month')); // Include previous month for past due tasks
1464 $endDate = date('Y-m-t'); // Last of current month
1466 $headerText = $dt->format('F Y');
1472 … $startDate = date('Y-m-d', strtotime('-30 days')); // Include past 30 days for past due tasks
1477 $endDateTime->modify('+' . $rangeNum . ' days');
1478 …$headerText = $this->getLang('next') . ' ' . $rangeNum . ' ' . ($rangeNum === 1 ? $this->getLang('…
1481 $endDateTime->modify('+' . $rangeNum . ' weeks');
1482 …$headerText = $this->getLang('next') . ' ' . $rangeNum . ' ' . ($rangeNum === 1 ? $this->getLang('…
1485 $endDateTime->modify('+' . $rangeNum . ' months');
1486 …$headerText = $this->getLang('next') . ' ' . $rangeNum . ' ' . ($rangeNum === 1 ? $this->getLang('…
1489 $endDateTime->modify('+' . $rangeNum . ' years');
1490 …$headerText = $this->getLang('next') . ' ' . $rangeNum . ' ' . ($rangeNum === 1 ? $this->getLang('…
1493 $endDate = $endDateTime->format('Y-m-d');
1495 // NEW: Sidebar widget - load current week's events
1496 $weekStartDay = $this->getWeekStartDay(); // Get saved preference
1500 $weekStart = date('Y-m-d', strtotime('monday this week'));
1501 $weekEnd = date('Y-m-d', strtotime('sunday this week'));
1503 // Sunday start (default - US/Canada standard)
1507 $weekStart = date('Y-m-d');
1509 // Monday-Saturday: go back to last Sunday
1510 $weekStart = date('Y-m-d', strtotime('-' . $today . ' days'));
1512 $weekEnd = date('Y-m-d', strtotime($weekStart . ' +6 days'));
1521 $tomorrowDate = date('Y-m-d', strtotime('+1 day'));
1528 $twoWeeksOut = date('Y-m-d', strtotime($weekEnd . ' +14 days'));
1531 $end->modify('+1 day'); // DatePeriod excludes end date
1540 $year = (int)$dt->format('Y');
1541 $month = (int)$dt->format('n');
1542 $dateKey = $dt->format('Y-m-d');
1544 $monthKey = $year . '-' . $month . '-' . $namespace;
1548 …$loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month, $excludeList…
1550 $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month);
1562 $allEvents = $this->checkTimeConflicts($allEvents);
1564 $calId = 'sidebar-' . substr(md5($namespace . $weekStart), 0, 8);
1568 return $this->renderSidebarWidget($allEvents, $namespace, $calId, $themeOverride);
1570 $startDate = date('Y-m-d');
1571 $endDate = date('Y-m-d');
1577 $headerText = $start->format('M j') . ' - ' . $end->format('M j, Y');
1582 $headerText = $dt->format('l, F j, Y');
1584 $startDate = date('Y-m-01');
1585 $endDate = date('Y-m-t');
1587 $headerText = $dt->format('F Y');
1594 $end->modify('+1 day');
1605 $year = (int)$dt->format('Y');
1606 $month = (int)$dt->format('n');
1607 $dateKey = $dt->format('Y-m-d');
1609 $monthKey = $year . '-' . $month . '-' . $namespace;
1613 …$loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month, $excludeList…
1615 $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month);
1632 // All-day events (no time) go to the TOP
1633 if ($timeA === null && $timeB !== null) return -1; // A before B
1635 if ($timeA === null && $timeB === null) return 0; // Both all-day, equal
1643 // Simple 2-line display widget
1645 $theme = !empty($data['theme']) ? $data['theme'] : $this->getSidebarTheme();
1646 $themeStyles = $this->getSidebarThemeStyles($theme);
1651 $themeClass = 'eventlist-theme-' . $theme;
1653 // Container styling - dark themes get border + glow, light themes get subtle border
1657 $containerStyle .= ' border-radius:4px;';
1658 $containerStyle .= ' box-shadow:0 0 10px ' . $themeStyles['shadow'] . ';';
1661 $containerStyle .= ' border-radius:4px;';
1664 …$html = '<div class="eventlist-simple ' . $themeClass . '" id="' . $calId . '" style="' . $contain…
1669 --background-site: ' . $themeStyles['bg'] . ';
1670 --background-alt: ' . $themeStyles['cell_bg'] . ';
1671 --text-primary: ' . $themeStyles['text_primary'] . ';
1672 --text-dim: ' . $themeStyles['text_dim'] . ';
1673 --text-bright: ' . $themeStyles['text_bright'] . ';
1674 --border-color: ' . $themeStyles['grid_border'] . ';
1675 --border-main: ' . $themeStyles['border'] . ';
1676 --cell-bg: ' . $themeStyles['cell_bg'] . ';
1677 --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
1678 --shadow-color: ' . $themeStyles['shadow'] . ';
1679 --grid-bg: ' . $themeStyles['grid_bg'] . ';
1680 --btn-text: ' . $btnTextColor . ';
1681 --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
1682 --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
1683 --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
1684 --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
1685 --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
1686 --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
1687 --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
1692 $html .= '<script src="' . DOKU_BASE . 'lib/plugins/calendar/calendar-main.js"></script>';
1700 $displayDate = $todayDate->format('D, M j, Y'); // "Fri, Jan 30, 2026"
1701 $currentTime = $todayDate->format('g:i:s A'); // "2:45:30 PM"
1703 $html .= '<div class="eventlist-today-header">';
1704 …$html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '">' . $currentTime . '</span…
1705 $html .= '<div class="eventlist-bottom-info">';
1706 ….= '<span class="eventlist-weather"><span id="weather-icon-' . $calId . '">��️</span> <span id="we…
1707 $html .= '<span class="eventlist-today-date">' . $displayDate . '</span>';
1724 const clockEl = document.getElementById("clock-' . $calId . '");
1729 // Fetch weather - uses default location, click weather to get local
1732 var userLon = -121.4944;
1735 …fetch("https://api.open-meteo.com/v1/forecast?latitude=" + lat + "&longitude=" + lon + "&current_w…
1742 const iconEl = document.getElementById("weather-icon-' . $calId . '");
1743 const tempEl = document.getElementById("weather-temp-' . $calId . '");
1775 var weatherEl = document.querySelector("#weather-icon-' . $calId . '");
1792 51: "��️", // Light drizzle
1822 $html .= '<div class="eventlist-simple-empty">';
1823 $html .= '<div class="eventlist-simple-header">' . htmlspecialchars($headerText);
1825 … $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($namespace) . '</span>';
1828 $html .= '<div class="eventlist-simple-body">No events</div>';
1832 $todayStr = date('Y-m-d');
1833 $tomorrow = date('Y-m-d', strtotime('+1 day'));
1837 $displayDate = $dateObj->format('D, M j');
1865 $todayClass = $isToday ? ' eventlist-simple-today' : '';
1866 $tomorrowClass = $isTomorrow ? ' eventlist-simple-tomorrow' : '';
1867 $pastDueClass = $isPastDue ? ' eventlist-simple-pastdue' : '';
1868 …$html .= '<div class="eventlist-simple-item' . $todayClass . $tomorrowClass . $pastDueClass . '">';
1869 $html .= '<div class="eventlist-simple-header">';
1872 … $html .= '<span class="eventlist-simple-title">' . htmlspecialchars($event['title']) . '</span>';
1874 // Time (12-hour format)
1883 … $html .= ' <span class="eventlist-simple-time">' . $displayTime . '</span>';
1888 $html .= ' <span class="eventlist-simple-date">' . $displayDate . '</span>';
1892 …"eventlist-simple-pastdue-badge" style="background:' . $themeStyles['pastdue_color'] . ' !importan…
1894 …list-simple-today-badge" style="background:' . $themeStyles['border'] . ' !important; color:' . $t…
1900 … $eventNamespace = $event['_namespace']; // Fallback to _namespace for multi-namespace loading
1903 …$html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($eventNamespace) . '</spa…
1908 // Line 2: Body (Description only) - only show if description exists
1910 …$html .= '<div class="eventlist-simple-body">' . $this->renderDescription($event['description']) .…
1918 $html .= '</div>'; // eventlist-simple
1926 $theme = $this->getSidebarTheme();
1928 $themeStyles = $this->getSidebarThemeStyles($theme);
1930 … $html = '<div class="event-dialog-compact" id="dialog-' . $calId . '" style="display:none;">';
1931 … $html .= '<div class="dialog-overlay" onclick="closeEventDialog(\'' . $calId . '\')"></div>';
1934 $html .= '<div class="dialog-content-sleek" id="dialog-content-' . $calId . '">';
1937 … $html .= '<div class="dialog-header-sleek dialog-drag-handle" id="drag-handle-' . $calId . '">';
1938 $html .= '<h3 id="dialog-title-' . $calId . '">Add Event</h3>';
1939 …$html .= '<button type="button" class="dialog-close-btn" onclick="closeEventDialog(\'' . $calId . …
1943 …form id="eventform-' . $calId . '" onsubmit="saveEventCompact(\'' . $calId . '\', \'' . $namespace…
1946 $html .= '<input type="hidden" id="event-id-' . $calId . '" name="eventId" value="">';
1949 $html .= '<div class="form-field">';
1950 $html .= '<label class="field-label">�� Title</label>';
1951 …$html .= '<input type="text" id="event-title-' . $calId . '" name="title" required class="input-sl…
1955 $html .= '<div class="form-field">';
1956 $html .= '<label class="field-label">�� Namespace</label>';
1959 … $html .= '<input type="hidden" id="event-namespace-' . $calId . '" name="namespace" value="">';
1962 $html .= '<div class="namespace-search-wrapper">';
1963 …l .= '<input type="text" id="event-namespace-search-' . $calId . '" class="input-sleek input-compa…
1964 …$html .= '<div class="namespace-dropdown" id="event-namespace-dropdown-' . $calId . '" style="disp…
1968 $allNamespaces = $this->getAllNamespaces();
1969 …$html .= '<script type="application/json" id="namespaces-data-' . $calId . '">' . json_encode($all…
1974 $html .= '<div class="form-field">';
1975 $html .= '<label class="field-label">�� Description</label>';
1976 …$html .= '<textarea id="event-desc-' . $calId . '" name="description" rows="2" class="input-sleek …
1979 // 3. START DATE - END DATE (inline)
1980 $html .= '<div class="form-row-group">';
1982 $html .= '<div class="form-field form-field-half">';
1983 $html .= '<label class="field-label-compact">�� Start Date</label>';
1984 $html .= '<div class="date-picker-wrapper">';
1985 … $html .= '<input type="hidden" id="event-date-' . $calId . '" name="date" required value="">';
1986 …n" class="custom-date-picker input-sleek input-compact" id="date-picker-btn-' . $calId . '" data-t…
1987 $html .= '<span class="date-display">Select date</span>';
1988 $html .= '<span class="date-arrow">▼</span>';
1990 $html .= '<div class="date-dropdown" id="date-dropdown-' . $calId . '"></div>';
1994 $html .= '<div class="form-field form-field-half">';
1995 $html .= '<label class="field-label-compact">�� End Date</label>';
1996 $html .= '<div class="date-picker-wrapper">';
1997 $html .= '<input type="hidden" id="event-end-date-' . $calId . '" name="endDate" value="">';
1998 …lass="custom-date-picker input-sleek input-compact" id="end-date-picker-btn-' . $calId . '" data-t…
1999 $html .= '<span class="date-display">Optional</span>';
2000 $html .= '<span class="date-arrow">▼</span>';
2002 $html .= '<div class="date-dropdown" id="end-date-dropdown-' . $calId . '"></div>';
2009 $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">';
2010 $html .= '<label class="checkbox-label checkbox-label-compact">';
2011 …$html .= '<input type="checkbox" id="event-recurring-' . $calId . '" name="isRecurring" class="rec…
2017-options-' . $calId . '" class="recurring-options" style="display:none; border:1px solid var(--bor…
2020 $html .= '<div class="form-row-group" style="margin-bottom:6px;">';
2022 $html .= '<div class="form-field" style="flex:0 0 auto; min-width:0;">';
2023 $html .= '<label class="field-label-compact">Repeat every</label>';
2024 … '<input type="number" id="event-recurrence-interval-' . $calId . '" name="recurrenceInterval" cla…
2027 $html .= '<div class="form-field" style="flex:1; min-width:0;">';
2028 $html .= '<label class="field-label-compact">&nbsp;</label>';
2029 …$html .= '<select id="event-recurrence-type-' . $calId . '" name="recurrenceType" class="input-sle…
2039 // Row 2: Weekly options - day of week checkboxes
2040 …$html .= '<div id="weekly-options-' . $calId . '" class="weekly-options" style="display:none; marg…
2041 …$html .= '<label class="field-label-compact" style="display:block; margin-bottom:4px;">On these da…
2042 $html .= '<div style="display:flex; flex-wrap:wrap; gap:2px;">';
2045-flex; align-items:center; padding:2px 6px; background:var(--cell-bg, #1a1a1a); border:1px solid v…
2046 …$html .= '<input type="checkbox" name="weekDays[]" value="' . $idx . '" style="margin-right:3px; w…
2053 // Row 3: Monthly options - day of month OR ordinal weekday
2054 …$html .= '<div id="monthly-options-' . $calId . '" class="monthly-options" style="display:none; ma…
2055 …$html .= '<label class="field-label-compact" style="display:block; margin-bottom:4px;">Repeat on:<…
2058 $html .= '<div style="margin-bottom:6px;">';
2059 …$html .= '<label style="display:inline-flex; align-items:center; margin-right:12px; cursor:pointer…
2060 …="dayOfMonth" checked onchange="updateMonthlyType(\'' . $calId . '\')" style="margin-right:4px;">';
2063 …$html .= '<label style="display:inline-flex; align-items:center; cursor:pointer; font-size:11px;">…
2064 …alue="ordinalWeekday" onchange="updateMonthlyType(\'' . $calId . '\')" style="margin-right:4px;">';
2070 …$html .= '<div id="monthly-day-' . $calId . '" style="display:flex; align-items:center; gap:6px;">…
2071 $html .= '<span style="font-size:11px;">Day</span>';
2072 …$html .= '<input type="number" id="event-month-day-' . $calId . '" name="monthDay" class="input-sl…
2073 $html .= '<span style="font-size:10px; color:var(--text-dim, #666);">of each month</span>';
2077 $html .= '<div id="monthly-ordinal-' . $calId . '" style="display:none;">';
2078 $html .= '<div style="display:flex; align-items:center; gap:4px; flex-wrap:wrap;">';
2079 …$html .= '<select id="event-ordinal-' . $calId . '" name="ordinalWeek" class="input-sleek input-co…
2085 $html .= '<option value="-1">Last</option>';
2087 …$html .= '<select id="event-ordinal-day-' . $calId . '" name="ordinalDay" class="input-sleek input
2096 $html .= '<span style="font-size:10px; color:var(--text-dim, #666);">of each month</span>';
2103 $html .= '<div class="form-row-group">';
2104 $html .= '<div class="form-field">';
2105 $html .= '<label class="field-label-compact">Repeat Until (optional)</label>';
2106 ….= '<input type="date" id="event-recurrence-end-' . $calId . '" name="recurrenceEnd" class="input-
2107 …$html .= '<div style="font-size:9px; color:var(--text-dim, #666); margin-top:2px;">Leave empty for…
2113 // 5. TIME (Start & End) - COLOR (inline)
2114 $html .= '<div class="form-row-group">';
2116 $html .= '<div class="form-field form-field-half">';
2117 $html .= '<label class="field-label-compact">�� Start Time</label>';
2118 $html .= '<div class="time-picker-wrapper">';
2120 $html .= '<input type="hidden" id="event-time-' . $calId . '" name="time" value="">';
2121 …n" class="custom-time-picker input-sleek input-compact" id="time-picker-btn-' . $calId . '" data-t…
2122 $html .= '<span class="time-display">All day</span>';
2123 $html .= '<span class="time-arrow">▼</span>';
2125 $html .= '<div class="time-dropdown" id="time-dropdown-' . $calId . '"></div>';
2129 $html .= '<div class="form-field form-field-half">';
2130 $html .= '<label class="field-label-compact">�� End Time</label>';
2131 $html .= '<div class="time-picker-wrapper">';
2133 $html .= '<input type="hidden" id="event-end-time-' . $calId . '" name="endTime" value="">';
2134 …lass="custom-time-picker input-sleek input-compact" id="end-time-picker-btn-' . $calId . '" data-t…
2135 $html .= '<span class="time-display">Same as start</span>';
2136 $html .= '<span class="time-arrow">▼</span>';
2138 $html .= '<div class="time-dropdown" id="end-time-dropdown-' . $calId . '"></div>';
2145 $html .= '<div class="form-row-group">';
2147 $html .= '<div class="form-field form-field-full">';
2148 $html .= '<label class="field-label-compact">�� Color</label>';
2149 $html .= '<div class="color-picker-wrapper">';
2150 …$html .= '<select id="event-color-' . $calId . '" name="color" class="input-sleek input-compact co…
2160 …$html .= '<input type="color" id="event-color-custom-' . $calId . '" class="color-picker-input col…
2167 $html .= '<div class="form-field form-field-checkbox form-field-checkbox-compact">';
2168 $html .= '<label class="checkbox-label checkbox-label-compact">';
2169 …$html .= '<input type="checkbox" id="event-is-task-' . $calId . '" name="isTask" class="task-toggl…
2175 $html .= '<div class="dialog-actions-sleek">';
2176 …$html .= '<button type="button" class="btn-sleek btn-cancel-sleek" onclick="closeEventDialog(\'' .…
2177 $html .= '<button type="submit" class="btn-sleek btn-save-sleek">�� Save</button>';
2190 $themeStyles = $this->getSidebarThemeStyles($theme);
2193 $themeClass = 'calendar-theme-' . $theme;
2195 …$html = '<div class="month-picker-overlay ' . $themeClass . '" id="month-picker-overlay-' . $calId…
2196 $html .= '<div class="month-picker-dialog" onclick="event.stopPropagation();">';
2199 $html .= '<div class="month-picker-selects">';
2200 $html .= '<select id="month-picker-month-' . $calId . '" class="month-picker-select">';
2204 … $html .= '<option value="' . $m . '"' . $selected . '>' . $monthNames[$m - 1] . '</option>';
2208 $html .= '<select id="month-picker-year-' . $calId . '" class="month-picker-select">';
2210 for ($y = $currentYear - 5; $y <= $currentYear + 5; $y++) {
2217 $html .= '<div class="month-picker-actions">';
2218 …$html .= '<button class="btn-sleek btn-cancel-sleek" onclick="closeMonthPicker(\'' . $calId . '\')…
2219 …$html .= '<button class="btn-sleek btn-save-sleek" onclick="jumpToSelectedMonth(\'' . $calId . '\'…
2235 $theme = $this->getSidebarTheme();
2236 $themeStyles = $this->getSidebarThemeStyles($theme);
2240 $linkStyle = ' class="cal-link"';
2242 // Token-based parsing to avoid escaping issues
2256 …' . htmlspecialchars($imagePath) . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />';
2260 …eHtml = '<img src="' . $imageUrl . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />';
2295 // Convert markdown-style links [text](url) to tokens
2363 $dataDir = $this->metaDir();
2369 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month);
2383 return $this->loadEventsWildcard($baseNamespace, $year, $month, $excludeList);
2388 return $this->loadEventsWildcard('', $year, $month, $excludeList);
2402 if ($this->isNamespaceExcluded($ns, $excludeList)) continue;
2405 if (!$this->checkNamespaceRead($ns)) continue;
2407 $events = $this->loadEvents($ns, $year, $month);
2425 $metaDir = $this->metaDir();
2435 $events = $this->loadEvents('', $year, $month);
2446 …if (!$this->isNamespaceExcluded($baseNamespace, $excludeList) && $this->checkNamespaceRead($baseNa…
2447 $events = $this->loadEvents($baseNamespace, $year, $month);
2462 $this->findCalendarNamespaces($dataDir, $metaDir, $year, $month, $allEvents, $excludeList);
2485 // No calendar dirs at this depth or deeper - stop early
2501 if ($this->isNamespaceExcluded($namespace, $excludeList)) continue;
2504 if (!$this->checkNamespaceRead($namespace)) continue;
2506 $events = $this->loadEvents($namespace, $year, $month);
2521 $dataDir = $this->metaDir();
2525 $this->scanForCalendarNamespaces($dataDir, '', $namespaces);
2556 $this->scanForCalendarNamespaces($path . '/', $namespace, $namespaces);
2562 * Render new sidebar widget - Week at a glance itinerary (200px wide)
2566 …return '<div style="width:200px; padding:12px; text-align:center; color:#999; font-size:11px;">No …
2570 $configFile = $this->syncConfigPath();
2580 $todayStr = date('Y-m-d');
2581 $tomorrowStr = date('Y-m-d', strtotime('+1 day'));
2584 $weekStartDay = $this->getWeekStartDay();
2588 $weekStart = date('Y-m-d', strtotime('monday this week'));
2589 $weekEnd = date('Y-m-d', strtotime('sunday this week'));
2591 // Sunday start (default - US/Canada standard)
2595 $weekStart = date('Y-m-d');
2597 // Monday-Saturday: go back to last Sunday
2598 $weekStart = date('Y-m-d', strtotime('-' . $today . ' days'));
2600 $weekEnd = date('Y-m-d', strtotime($weekStart . ' +6 days'));
2612 $eventsWithConflicts = $this->detectTimeConflicts($dayEvents);
2630 // Pre-render DokuWiki syntax to HTML for JavaScript display
2633 $eventWithHtml['title_html'] = $this->renderDokuWikiToHtml($event['title']);
2636 … $eventWithHtml['description_html'] = $this->renderDokuWikiToHtml($event['description']);
2665 // Same date - sort by time
2669 if (empty($timeA) && !empty($timeB)) return 1; // All-day events last
2670 if (!empty($timeA) && empty($timeB)) return -1;
2674 $aMinutes = $this->timeToMinutes($timeA);
2675 $bMinutes = $this->timeToMinutes($timeB);
2676 return $aMinutes - $bMinutes;
2682 // Get theme - prefer override from syntax parameter, fall back to admin default
2683 $theme = !empty($themeOverride) ? $themeOverride : $this->getSidebarTheme();
2684 $themeStyles = $this->getSidebarThemeStyles($theme);
2685 $themeClass = 'sidebar-' . $theme;
2687 // Start building HTML - Dynamic width with default font (overflow:visible for tooltips)
2688-widget ' . $themeClass . '" id="sidebar-widget-' . $calId . '" style="width:100%; max-width:100%;…
2693 #sidebar-widget-' . $calId . ' {
2694 --background-site: ' . $themeStyles['bg'] . ';
2695 --background-alt: ' . $themeStyles['cell_bg'] . ';
2696 --background-header: ' . $themeStyles['header_bg'] . ';
2697 --text-primary: ' . $themeStyles['text_primary'] . ';
2698 --text-dim: ' . $themeStyles['text_dim'] . ';
2699 --text-bright: ' . $themeStyles['text_bright'] . ';
2700 --border-color: ' . $themeStyles['grid_border'] . ';
2701 --border-main: ' . $themeStyles['border'] . ';
2702 --cell-bg: ' . $themeStyles['cell_bg'] . ';
2703 --cell-today-bg: ' . $themeStyles['cell_today_bg'] . ';
2704 --shadow-color: ' . $themeStyles['shadow'] . ';
2705 --header-border: ' . $themeStyles['header_border'] . ';
2706 --header-shadow: ' . $themeStyles['header_shadow'] . ';
2707 --grid-bg: ' . $themeStyles['grid_bg'] . ';
2708 --btn-text: ' . $btnTextColor . ';
2709 --pastdue-color: ' . $themeStyles['pastdue_color'] . ';
2710 --pastdue-bg: ' . $themeStyles['pastdue_bg'] . ';
2711 --pastdue-bg-strong: ' . $themeStyles['pastdue_bg_strong'] . ';
2712 --pastdue-bg-light: ' . $themeStyles['pastdue_bg_light'] . ';
2713 --tomorrow-bg: ' . $themeStyles['tomorrow_bg'] . ';
2714 --tomorrow-bg-strong: ' . $themeStyles['tomorrow_bg_strong'] . ';
2715 --tomorrow-bg-light: ' . $themeStyles['tomorrow_bg_light'] . ';
2722 @keyframes sparkle-' . $calId . ' {
2729 transform: translate(var(--tx), var(--ty)) scale(1) rotate(180deg);
2733 … transform: translate(calc(var(--tx) * 2), calc(var(--ty) * 2)) scale(0) rotate(360deg);
2737 @keyframes pulse-glow-' . $calId . ' {
2738 0%, 100% { box-shadow: 0 0 10px rgba(255, 20, 147, 0.4); }
2739 … 50% { box-shadow: 0 0 25px rgba(255, 20, 147, 0.8), 0 0 40px rgba(255, 20, 147, 0.4); }
2742 @keyframes shimmer-' . $calId . ' {
2743 0% { background-position: -200% center; }
2744 100% { background-position: 200% center; }
2747 .sidebar-pink {
2748 animation: pulse-glow-' . $calId . ' 3s ease-in-out infinite;
2751 .sidebar-pink:hover {
2752 … box-shadow: 0 0 30px rgba(255, 20, 147, 0.9), 0 0 50px rgba(255, 20, 147, 0.5) !important;
2755 .sparkle-' . $calId . ' {
2757 pointer-events: none;
2758 font-size: 20px;
2759 z-index: 1000;
2760 animation: sparkle-' . $calId . ' 1s ease-out forwards;
2761 filter: drop-shadow(0 0 3px rgba(255, 20, 147, 0.8));
2767 const container = document.getElementById("sidebar-widget-' . $calId . '");
2772 sparkle.className = "sparkle-' . $calId . '";
2780 sparkle.style.setProperty("--tx", Math.cos(angle) * distance + "px");
2781 sparkle.style.setProperty("--ty", Math.sin(angle) * distance + "px");
2791 const x = e.clientX - rect.left;
2792 const y = e.clientY - rect.top;
2797 const offsetX = x + (Math.random() - 0.5) * 30;
2798 const offsetY = y + (Math.random() - 0.5) * 30;
2804 // Random auto-sparkles for extra glamour
2815 $jsCalId = str_replace('-', '_', $calId);
2829 const clockEl = document.getElementById("clock-' . $calId . '");
2834 // Weather - uses default location, click weather to get local
2837 var userLon = -121.4944;
2840 …fetch("https://api.open-meteo.com/v1/forecast?latitude=" + lat + "&longitude=" + lon + "&current_w…
2847 const iconEl = document.getElementById("weather-icon-' . $calId . '");
2848 const tempEl = document.getElementById("weather-temp-' . $calId . '");
2876 var weatherEl = document.querySelector("#weather-icon-' . $calId . '");
2903 $displayDate = $todayDate->format('D, M j, Y');
2904 $currentTime = $todayDate->format('g:i:s A');
2906 …s="eventlist-today-header" style="background:' . $themeStyles['header_bg'] . '; border:2px solid '…
2907 …$html .= '<span class="eventlist-today-clock" id="clock-' . $calId . '" style="color:' . $themeSty…
2908 $html .= '<div class="eventlist-bottom-info">';
2909-weather"><span id="weather-icon-' . $calId . '">��️</span> <span id="weather-temp-' . $calId . '"…
2910 …$html .= '<span class="eventlist-today-date" style="color:' . $themeStyles['text_dim'] . ';">' . $…
2915 $todayStr = date('Y-m-d');
2917 // Thin "Add Event" bar between header and week grid - theme-aware colors
2927 …x; line-height:10px; text-align:center; cursor:pointer; border-top:1px solid rgba(0, 0, 0, 0.1); b…
2929 …nt-size:8px; font-weight:700; letter-spacing:0.4px; font-family:system-ui, sans-serif; text-shadow…
2933 $html .= $this->renderWeekGrid($weekEvents, $weekStart, $themeStyles, $theme);
2935 // Section colors - derived from theme palette
2948 $importantColor = '#ff85c1'; // Light pink
2954 // Wiki - section header backgrounds from template colors
2963 // Itinerary bar (collapsible toggle) - styled like +Add bar
2975 $jsCalId = str_replace('-', '_', $calId);
2978 $itineraryDefaultCollapsed = $this->getItineraryCollapsed();
2979 $arrowDefaultStyle = $itineraryDefaultCollapsed ? 'transform:rotate(-90deg);' : '';
2980 $contentDefaultStyle = $itineraryDefaultCollapsed ? 'max-height:0px; opacity:0;' : '';
2982-bar-' . $calId . '" style="background:' . $itineraryBg . '; padding:0; margin:0; height:12px; lin…
2983-arrow-' . $calId . '" style="color:' . $itineraryTextColor . '; font-size:6px; font-weight:700; f…
2984 …t-size:8px; font-weight:700; letter-spacing:0.4px; font-family:system-ui, sans-serif; text-shadow:…
2988 …$html .= '<div id="itinerary-content-' . $calId . '" style="transition:max-height 0.3s ease-out, o…
2992 …$html .= $this->renderSidebarSection('Today', $todayEvents, $todayColor, $calId, $themeStyles, $th…
2997 …$html .= $this->renderSidebarSection('Tomorrow', $tomorrowEvents, $tomorrowColor, $calId, $themeSt…
3002 …$html .= $this->renderSidebarSection('Important Events', $importantEvents, $importantColor, $calId…
3007 …iv style="padding:8px; text-align:center; color:' . $themeStyles['text_dim'] . '; font-size:10px; …
3010 $html .= '</div>'; // Close itinerary-content
3013 $itineraryDefaultCollapsed = $this->getItineraryCollapsed();
3015 $itineraryArrowDefault = $itineraryDefaultCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)';
3016 …$itineraryContentDefault = $itineraryDefaultCollapsed ? 'max-height:0px; opacity:0;' : 'max-height…
3024 const content = document.getElementById("itinerary-content-' . $calId . '");
3025 const arrow = document.getElementById("itinerary-arrow-' . $calId . '");
3031 arrow.style.transform = "rotate(-90deg)";
3050 const content = document.getElementById("itinerary-content-' . $calId . '");
3051 const arrow = document.getElementById("itinerary-arrow-' . $calId . '");
3059 arrow.style.transform = "rotate(-90deg)";
3068 $html .= $this->renderEventDialog($calId, $namespace, $theme);
3070 // Add JavaScript for positioning data-tooltip elements
3072 // Position data-tooltip elements to prevent cutoff (up and to the LEFT)
3074 const tooltipElements = document.querySelectorAll("[data-tooltip]");
3075 const isPinkTheme = document.querySelector(".sidebar-pink") !== null;
3083 element.style.setProperty("--tooltip-left", (rect.left - 150) + "px");
3084 element.style.setProperty("--tooltip-top", (rect.top - 30) + "px");
3088 element.style.setProperty("--heart-left", (rect.left - 150 + 210) + "px");
3089 element.style.setProperty("--heart-top", (rect.top - 30) + "px");
3098 [data-tooltip]:hover:before {
3099 left: var(--tooltip-left, 0) !important;
3100 top: var(--tooltip-top, 0) !important;
3102 .sidebar-pink [data-tooltip]:hover:after {
3103 left: var(--heart-left, 0) !important;
3104 top: var(--heart-top, 0) !important;
3114 * Render compact week grid (7 cells with event bars) - Theme-aware
3117 // Generate unique ID for this calendar instance - sanitize for JavaScript
3119 $jsCalId = str_replace('-', '_', $calId); // Sanitize for JS variable names
3121 …div style="display:grid; grid-template-columns:repeat(7, 1fr); gap:1px; background:' . $themeStyle…
3124 $weekStartDay = $this->getWeekStartDay();
3130 $today = date('Y-m-d');
3133 $date = date('Y-m-d', strtotime($weekStart . ' +' . $i . ' days'));
3144 // Theme-aware text shadow
3147 …$textShadow = $isToday ? 'text-shadow:0 0 3px ' . $glowColor . ';' : 'text-shadow:0 0 2px ' . $glo…
3150 …$textShadow = $isToday ? 'text-shadow:0 0 2px ' . $glowColor . ';' : 'text-shadow:0 0 1px ' . $glo…
3153 …$textShadow = $isToday ? 'text-shadow:0 0 2px ' . $glowColor . ';' : 'text-shadow:0 0 1px ' . $glo…
3165 … .= '<div style="background:' . $bgColor . '; padding:4px 2px; text-align:center; min-height:45px;…
3167 // Day letter - theme color
3169 …$html .= '<div style="font-size:9px; color:' . $dayLetterColor . '; font-weight:500; font-family:s…
3172 ….= '<div style="font-size:12px; color:' . $textColor . '; font-weight:' . $fontWeight . '; margin:…
3174 // Event bars (max 4 visible) with theme-aware glow
3181 …x; background:' . htmlspecialchars($color) . '; margin:1px 0; border-radius:1px; box-shadow:' . $b…
3184 // Show "+N more" if more than 4 - theme color
3187 ….= '<div style="font-size:7px; color:' . $moreTextColor . '; margin-top:1px; font-family:system-ui…
3196 // Add container for selected day events display (with unique ID) - theme-aware
3206 // Header text color - dark bg text for dark themes, white for light theme accent headers
3210-day-events-' . $calId . '" style="display:none; margin:8px 4px; border-left:3px solid ' . $panelB…
3212-size:9px; font-weight:700; letter-spacing:0.3px; font-family:system-ui, sans-serif; box-shadow:' …
3213 $html .= '<span id="selected-day-title-' . $calId . '"></span>';
3214 …mentById(\'selected-day-events-' . $calId . '\').style.display=\'none\';" style="cursor:pointer; f…
3216-webkit-text-fill-color:' . $panelHeaderColor . ' !important; padding:4px 6px; font-size:9px; font
3217 $html .= '<span id="selected-day-title-' . $calId . '"></span>';
3218-day-events-' . $calId . '\').style.display=\'none\';" style="cursor:pointer; font-size:12px; padd…
3221 …$html .= '<div id="selected-day-content-' . $calId . '" style="padding:4px 0; background:' . $pane…
3227 $jsCalId = str_replace('-', '_', $calId);
3235 … 'text_shadow' => ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $themeStyles['text_primary'] :
3236 …((in_array($theme, ['matrix', 'purple'])) ? 'text-shadow:0 0 1px ' . $themeStyles['text_primary'] …
3250 const container = document.getElementById("selected-day-events-' . $calId . '");
3251 const title = document.getElementById("selected-day-title-' . $calId . '");
3252 const content = document.getElementById("selected-day-content-' . $calId . '");
3258 const dayName = dateObj.toLocaleDateString("en-US", { weekday: "long" });
3259 … const monthDay = dateObj.toLocaleDateString("en-US", { month: "short", day: "numeric" });
3265 // Sort events by time (all-day events first, then timed events chronologically)
3267 // All-day events (no time) go to the beginning
3269 if (!a.time) return -1; // a is all-day, comes first
3270 if (!b.time) return 1; // b is all-day, comes first
3278 return minutesA - minutesB;
3281 // Build events HTML with single color bar (event color only) - theme-aware
3287-bottom:1px solid " + themeColors.border_color + "; font-size:10px; display:flex; align-items:stre…
3291 // Event assigned color bar (single bar on left) - theme-aware shadow
3293 …+= "<div style=\\"width:3px; align-self:stretch; background:" + eventColor + "; border-radius:1px;…
3296 …eventHTML += "<div style=\\"flex:1; min-width:0; display:flex; justify-content:space-between; alig…
3299 eventHTML += "<div style=\\"flex:1; min-width:0;\\">";
3300 …ML += "<div style=\\"font-weight:600; color:" + themeColors.text_primary + "; word-wrap:break-word…
3309 …HTML += "<span style=\\"color:" + themeColors.text_bright + "; font-weight:500; font-size:9px;\\">…
3312 // Title - use HTML version if available
3317 // Description if present - use HTML version - theme-aware color
3320 …eventHTML += "<div style=\\"font-size:9px; color:" + themeColors.text_dim + "; margin-top:2px;\\">…
3330 … const confTime = conf.time + (conf.end_time ? " - " + conf.end_time : "");
3335 …eventHTML += "<span class=\\"event-conflict-badge\\" style=\\"font-size:10px;\\" data-conflicts=\\…
3353 * Render a sidebar section (Today/Tomorrow/Important) - Matrix themed with colored borders
3369 // Different dates - sort by date
3374 // Same date - sort by time
3378 // All-day events last within same date
3380 if (!empty($aTime) && empty($bTime)) return -1;
3384 $aMinutes = $this->timeToMinutes($aTime);
3385 $bMinutes = $this->timeToMinutes($bTime);
3386 return $aMinutes - $bMinutes;
3394 // All-day events (no time) come first
3395 if (empty($aTime) && !empty($bTime)) return -1;
3399 // Both have times - convert to minutes for proper chronological sort
3400 $aMinutes = $this->timeToMinutes($aTime);
3401 $bMinutes = $this->timeToMinutes($bTime);
3403 return $aMinutes - $bMinutes;
3407 // Theme-aware section shadow
3413 // Wiki theme: use a background div for the left bar instead of border-left
3415 …$html = '<div style="display:flex; margin:8px 4px; box-shadow:' . $sectionShadow . '; background:'…
3416 … $html .= '<div style="width:3px; flex-shrink:0; background:' . $borderColor . ';"></div>';
3417 $html .= '<div style="flex:1; min-width:0;">';
3419 …$html = '<div style="border-left:3px solid ' . $borderColor . ' !important; margin:8px 4px; box-sh…
3422 // Section header with accent color background - theme-aware
3428 …Color . '; padding:4px 6px; font-size:9px; font-weight:700; letter-spacing:0.3px; font-family:syst…
3431-webkit-text-fill-color:' . $headerTextColor . ' !important; padding:4px 6px; font-size:9px; font-
3436 // Events - no background (transparent)
3440 …$html .= $this->renderSidebarEvent($event, $calId, $showDate, $accentColor, $themeStyles, $theme, …
3453 * Render individual event in sidebar - Theme-aware
3474 // Theme-aware colors
3477 $textShadow = ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $titleColor . ';' :
3478 … ((in_array($theme, ['matrix', 'purple'])) ? 'text-shadow:0 0 1px ' . $titleColor . ';' : '');
3488 …$confTime = $this->formatTimeDisplay($conf['time'], isset($conf['end_time']) ? $conf['end_time'] :…
3493 // No background on individual events (transparent) - unless important namespace
3497 // Important namespace highlighting - subtle themed background
3501 // Theme-specific important highlighting
3505 $importantBorder = 'border-right:2px solid rgba(0,204,7,0.4);';
3509 $importantBorder = 'border-right:2px solid rgba(156,39,176,0.4);';
3513 $importantBorder = 'border-right:2px solid rgba(255,105,180,0.5);';
3517 $importantBorder = 'border-right:2px solid rgba(33,150,243,0.4);';
3521 $importantBorder = 'border-right:2px solid rgba(0,102,204,0.3);';
3525 $importantBorder = 'border-right:2px solid rgba(0,204,7,0.4);';
3529 …ing:4px 6px; border-bottom:1px solid ' . $borderColor . ' !important; font-size:10px; display:flex…
3533 …l .= '<div style="width:3px; align-self:stretch; background:' . $eventColor . '; border-radius:1px…
3536 $html .= '<div style="flex:1; min-width:0;">';
3539 …$html .= '<div style="font-weight:600; color:' . $titleColor . '; word-wrap:break-word; font-famil…
3542 $displayTime = $this->formatTimeDisplay($time, $endTime);
3543 …$html .= '<span style="color:' . $timeColor . '; font-weight:500; font-size:9px;">' . htmlspecialc…
3550 … $html .= '<span style="font-size:11px; color:' . $checkColor . ';">' . $checkIcon . '</span> ';
3555 $html .= '<span style="font-size:9px;" title="Important">⭐</span> ';
3558 $html .= $title; // Already HTML-escaped on line 2625
3563 …$html .= ' <span class="event-conflict-badge" style="font-size:10px;" data-conflicts="' . $conflic…
3571 $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Feb 10"
3573 $dateShadow = ($theme === 'pink') ? 'text-shadow:0 0 2px ' . $dateColor . ';' :
3574 … ((in_array($theme, ['matrix', 'purple'])) ? 'text-shadow:0 0 1px ' . $dateColor . ';' : '');
3575 …$html .= '<div style="font-size:8px; color:' . $dateColor . '; font-weight:500; margin-top:2px; ' …
3585 * Format time display (12-hour format with optional end time)
3605 $display .= '-' . $endDisplayHour . ':' . $endMinute . ' ' . $endAmpm;
3631 // Skip all-day events (no time)
3646 // If no end time, use start time (zero duration) - matches main calendar logic
3653 if (empty($otherEvent['time'])) continue; // Skip all-day events
3667 $start1Min = $this->timeToMinutes($startTime);
3668 $end1Min = $this->timeToMinutes($endTime);
3669 $start2Min = $this->timeToMinutes($otherStart);
3670 $end2Min = $this->timeToMinutes($otherEnd);
3674 // e.g., 1:00-2:00 and 2:00-3:00 are NOT in conflict
3698 $totalMinutes = $this->timeToMinutes($time) + ($hours * 60);
3735 $this->scanForNamespaces($path . '/', $namespace, $namespaces);
3744 $configFile = $this->metaDir() . 'calendar_theme.txt';
3763 // Try multiple possible locations for style.ini (farm-safe)
3767 // Add farm-specific conf override path if available
3805 // __background_site__ → --background-site → Container, panel backgrounds
3806 … // __background__ → --cell-bg → Cell/input backgrounds (typically white)
3807 // __background_alt__ → --background-alt → Hover states, header backgrounds
3808 // → --background-header
3809 // __background_neu__ → --cell-today-bg → Today cell highlight
3810 // __text__ → --text-primary → Primary text, labels, titles
3811 // __text_neu__ → --text-dim → Secondary text, dates, descriptions
3813 // __border__ → --border-color → Grid lines, input borders
3814 …// → --border-main → Accent color: buttons, badges, active element…
3815 // → --header-border
3816 // __link__ → --text-bright → Links, accent text
3832 'cell_bg' => $background, // Cells use __background__ (white/light)
3846 * Get theme-specific color styles
3851 $wikiColors = $this->getWikiTemplateColors();
3863 'header_bg' => 'linear-gradient(180deg, #2a2a2a 0%, #242424 100%)',
3886 'header_bg' => 'linear-gradient(180deg, #2f2438 0%, #2a2030 100%)',
3909 'header_bg' => 'linear-gradient(180deg, #ffffff 0%, #f5f7fa 100%)',
3932 'header_bg' => 'linear-gradient(180deg, #2d1a24 0%, #1a0d14 100%)',
3983 $configFile = $this->metaDir() . 'calendar_week_start.txt';
3997 $configFile = $this->metaDir() . 'calendar_itinerary_collapsed.txt';
4008 $configFile = $this->metaDir() . 'calendar_search_default.txt';
4020 * Supports semicolon-separated list: "journal;drafts;personal:private"