summaryrefslogtreecommitdiff
path: root/web/templates/partials
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-02-01 10:52:28 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-02-01 10:52:28 -1000
commit1c6552117038cb7c01e016dbf1ac062e1d9f9c73 (patch)
treeff65c67a40e08a14f89fe3057a8ac4886d94b75b /web/templates/partials
parente0e0dc11195c0e0516b45975de51df1dc98f83de (diff)
Improve timeline view with dynamic bounds, now line, and overlap handling
- Add dynamic calendar clipping: show 1 hour before/after events instead of hardcoded 6am-10pm - Add "NOW" line indicator showing current time position - Improve time label readability with larger font and better contrast - Add overlap detection with column-based indentation for concurrent events - Apply calendar view to Tomorrow section (matching Today's layout) - Fix auto-refresh switching to tasks tab (default was 'tasks' instead of 'timeline') Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'web/templates/partials')
-rw-r--r--web/templates/partials/timeline-tab.html281
1 files changed, 232 insertions, 49 deletions
diff --git a/web/templates/partials/timeline-tab.html b/web/templates/partials/timeline-tab.html
index 8745d1d..bdf2007 100644
--- a/web/templates/partials/timeline-tab.html
+++ b/web/templates/partials/timeline-tab.html
@@ -2,24 +2,51 @@
<style>
.calendar-grid {
position: relative;
- border-left: 2px solid rgba(255,255,255,0.15);
- margin-left: 50px;
- min-height: 640px;
+ border-left: 2px solid rgba(255,255,255,0.2);
+ margin-left: 56px;
}
.calendar-hour {
position: relative;
height: 40px;
- border-bottom: 1px solid rgba(255,255,255,0.05);
+ border-bottom: 1px solid rgba(255,255,255,0.08);
}
.calendar-hour-label {
position: absolute;
- left: -50px;
- top: -8px;
- font-size: 0.7em;
- color: rgba(255,255,255,0.4);
- width: 44px;
+ left: -56px;
+ top: -9px;
+ font-size: 0.8rem;
+ font-weight: 500;
+ color: rgba(255,255,255,0.6);
+ width: 50px;
text-align: right;
}
+ .now-line {
+ position: absolute;
+ left: -8px;
+ right: 0;
+ height: 2px;
+ background: #ef4444;
+ z-index: 5;
+ }
+ .now-line::before {
+ content: 'NOW';
+ position: absolute;
+ left: -48px;
+ top: -7px;
+ font-size: 0.65rem;
+ font-weight: 600;
+ color: #ef4444;
+ letter-spacing: 0.05em;
+ }
+ .now-dot {
+ position: absolute;
+ left: -5px;
+ top: -4px;
+ width: 10px;
+ height: 10px;
+ background: #ef4444;
+ border-radius: 50%;
+ }
.calendar-event {
position: absolute;
left: 8px;
@@ -125,26 +152,18 @@
{{end}}
</div>
- <!-- Calendar Grid (6am-10pm) -->
- <div class="calendar-grid" id="today-calendar">
+ <!-- Calendar Grid (dynamic hours) -->
+ <div class="calendar-grid" id="today-calendar" data-start-hour="{{.TodayStartHour}}" data-now-hour="{{.NowHour}}" data-now-minute="{{.NowMinute}}">
<!-- Hour rows -->
- <div class="calendar-hour" data-hour="6"><span class="calendar-hour-label">6am</span></div>
- <div class="calendar-hour" data-hour="7"><span class="calendar-hour-label">7am</span></div>
- <div class="calendar-hour" data-hour="8"><span class="calendar-hour-label">8am</span></div>
- <div class="calendar-hour" data-hour="9"><span class="calendar-hour-label">9am</span></div>
- <div class="calendar-hour" data-hour="10"><span class="calendar-hour-label">10am</span></div>
- <div class="calendar-hour" data-hour="11"><span class="calendar-hour-label">11am</span></div>
- <div class="calendar-hour" data-hour="12"><span class="calendar-hour-label">12pm</span></div>
- <div class="calendar-hour" data-hour="13"><span class="calendar-hour-label">1pm</span></div>
- <div class="calendar-hour" data-hour="14"><span class="calendar-hour-label">2pm</span></div>
- <div class="calendar-hour" data-hour="15"><span class="calendar-hour-label">3pm</span></div>
- <div class="calendar-hour" data-hour="16"><span class="calendar-hour-label">4pm</span></div>
- <div class="calendar-hour" data-hour="17"><span class="calendar-hour-label">5pm</span></div>
- <div class="calendar-hour" data-hour="18"><span class="calendar-hour-label">6pm</span></div>
- <div class="calendar-hour" data-hour="19"><span class="calendar-hour-label">7pm</span></div>
- <div class="calendar-hour" data-hour="20"><span class="calendar-hour-label">8pm</span></div>
- <div class="calendar-hour" data-hour="21"><span class="calendar-hour-label">9pm</span></div>
- <div class="calendar-hour" data-hour="22"><span class="calendar-hour-label">10pm</span></div>
+ {{range .TodayHours}}
+ <div class="calendar-hour" data-hour="{{.}}">
+ <span class="calendar-hour-label">{{if eq . 0}}12am{{else if lt . 12}}{{.}}am{{else if eq . 12}}12pm{{else}}{{subtract . 12}}pm{{end}}</span>
+ </div>
+ {{end}}
+ <!-- Now line (positioned by JS) -->
+ <div class="now-line" id="now-line" style="display:none;">
+ <div class="now-dot"></div>
+ </div>
<!-- Timed events positioned by JavaScript -->
{{range .TodayItems}}
@@ -183,40 +202,57 @@
<script>
(function() {
const calendar = document.getElementById('today-calendar');
+ if (!calendar) return;
const events = calendar.querySelectorAll('.calendar-event');
const hourHeight = 40;
- const startHour = 6;
- const endHour = 22;
+ const startHour = parseInt(calendar.dataset.startHour) || 8;
+ const nowHour = parseInt(calendar.dataset.nowHour);
+ const nowMinute = parseInt(calendar.dataset.nowMinute);
+ // Build event data for overlap detection
+ const eventData = [];
events.forEach(function(el) {
const hour = parseInt(el.dataset.hour);
const minute = parseInt(el.dataset.minute);
const endHourVal = parseInt(el.dataset.endHour);
const endMinute = parseInt(el.dataset.endMinute);
-
- // Skip events outside the visible range
- if (hour < startHour || hour > endHour) {
- el.remove();
- return;
- }
-
- // Calculate position
- const top = (hour - startHour) * hourHeight + (minute / 60) * hourHeight;
-
- // Calculate height based on duration
- let durationMinutes;
+ const startMin = hour * 60 + minute;
+ let endMin;
if (endHourVal > hour || (endHourVal === hour && endMinute > minute)) {
- durationMinutes = (endHourVal - hour) * 60 + (endMinute - minute);
+ endMin = endHourVal * 60 + endMinute;
} else {
- durationMinutes = 55; // Default ~1 hour for tasks without duration
+ endMin = startMin + 55;
}
+ eventData.push({ el, startMin, endMin, column: 0 });
+ });
+
+ // Assign columns for overlapping events
+ eventData.sort((a, b) => a.startMin - b.startMin);
+ for (let i = 0; i < eventData.length; i++) {
+ const ev = eventData[i];
+ const overlaps = eventData.filter((other, j) =>
+ j < i && other.endMin > ev.startMin && other.startMin < ev.endMin
+ );
+ const usedCols = overlaps.map(o => o.column);
+ let col = 0;
+ while (usedCols.includes(col)) col++;
+ ev.column = col;
+ }
+
+ // Position events with column-based indentation
+ eventData.forEach(function(ev) {
+ const el = ev.el;
+ const hour = parseInt(el.dataset.hour);
+ const minute = parseInt(el.dataset.minute);
+ const top = (hour - startHour) * hourHeight + (minute / 60) * hourHeight;
+ const durationMinutes = ev.endMin - ev.startMin;
const height = Math.max(28, (durationMinutes / 60) * hourHeight - 4);
el.style.top = top + 'px';
el.style.height = height + 'px';
+ el.style.left = (8 + ev.column * 16) + 'px';
el.style.display = 'block';
- // Click to open URL
const url = el.dataset.url;
if (url) {
el.style.cursor = 'pointer';
@@ -228,9 +264,19 @@
}
});
+ // Position the "now" line
+ const nowLine = document.getElementById('now-line');
+ if (nowLine && !isNaN(nowHour)) {
+ const nowTop = (nowHour - startHour) * hourHeight + (nowMinute / 60) * hourHeight;
+ if (nowTop >= 0) {
+ nowLine.style.top = nowTop + 'px';
+ nowLine.style.display = 'block';
+ }
+ }
+
// Hide untimed section if empty
const untimedSection = document.getElementById('untimed-section');
- if (untimedSection && untimedSection.children.length === 0) {
+ if (untimedSection && untimedSection.querySelectorAll('.untimed-item').length === 0) {
untimedSection.style.display = 'none';
}
})();
@@ -238,7 +284,7 @@
</details>
{{end}}
- <!-- Tomorrow Section (Expanded) -->
+ <!-- Tomorrow Section (Calendar View) -->
{{if .TomorrowItems}}
<details class="group" open>
<summary class="text-lg font-semibold mb-3 flex items-center gap-2 text-white/70 cursor-pointer hover:text-white/90 sticky top-0 bg-black/20 backdrop-blur-md py-2 z-10 rounded-lg px-2 list-none">
@@ -248,11 +294,148 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</summary>
- <div class="space-y-2 relative pl-4 border-l border-white/10 ml-2">
+
+ <!-- Overdue + All-Day + Untimed Items Section -->
+ <div class="mb-4 flex flex-wrap gap-1 p-2 bg-black/20 rounded-lg" id="tomorrow-untimed-section">
{{range .TomorrowItems}}
- {{template "timeline-item" .}}
+ {{if or .IsOverdue .IsAllDay}}
+ <div class="untimed-item source-{{.Source}} {{if .IsOverdue}}overdue{{end}} {{if .IsCompleted}}opacity-50{{end}}">
+ {{if or (eq .Type "task") (eq .Type "card") (eq .Type "gtask")}}
+ <input type="checkbox"
+ {{if .IsCompleted}}checked{{end}}
+ hx-post="{{if .IsCompleted}}/uncomplete-atom{{else}}/complete-atom{{end}}"
+ hx-vals='{"id": "{{.ID}}", "source": "{{.Source}}"{{if .ListID}}, "listId": "{{.ListID}}"{{end}}}'
+ hx-target="#tab-content"
+ hx-swap="innerHTML"
+ class="h-4 w-4 rounded bg-black/40 border-white/30 text-white/80 focus:ring-white/30 cursor-pointer flex-shrink-0">
+ {{end}}
+ <span class="{{if .IsCompleted}}line-through text-white/50{{end}}">{{.Title}}</span>
+ {{if .IsOverdue}}<span class="overdue-badge">overdue</span>{{end}}
+ {{if .URL}}
+ <a href="{{.URL}}" target="_blank" class="text-white/50 hover:text-white">
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
+ </svg>
+ </a>
+ {{end}}
+ </div>
+ {{end}}
{{end}}
</div>
+
+ <!-- Calendar Grid (dynamic hours) -->
+ <div class="calendar-grid" id="tomorrow-calendar" data-start-hour="{{.TomorrowStartHour}}">
+ <!-- Hour rows -->
+ {{range .TomorrowHours}}
+ <div class="calendar-hour" data-hour="{{.}}">
+ <span class="calendar-hour-label">{{if eq . 0}}12am{{else if lt . 12}}{{.}}am{{else if eq . 12}}12pm{{else}}{{subtract . 12}}pm{{end}}</span>
+ </div>
+ {{end}}
+
+ <!-- Timed events positioned by JavaScript -->
+ {{range .TomorrowItems}}
+ {{if and (not .IsOverdue) (not .IsAllDay)}}
+ <div class="calendar-event source-{{.Source}} {{if .IsCompleted}}opacity-50{{end}}"
+ data-hour="{{.Time.Hour}}"
+ data-minute="{{.Time.Minute}}"
+ data-end-hour="{{if .EndTime}}{{.EndTime.Hour}}{{else}}{{.Time.Hour}}{{end}}"
+ data-end-minute="{{if .EndTime}}{{.EndTime.Minute}}{{else}}59{{end}}"
+ data-id="{{.ID}}"
+ data-source="{{.Source}}"
+ data-completed="{{.IsCompleted}}"
+ data-type="{{.Type}}"
+ data-url="{{.URL}}"
+ {{if .ListID}}data-list-id="{{.ListID}}"{{end}}
+ style="display:none;">
+ <div class="flex items-center gap-2">
+ {{if or (eq .Type "task") (eq .Type "card") (eq .Type "gtask")}}
+ <input type="checkbox"
+ {{if .IsCompleted}}checked{{end}}
+ hx-post="{{if .IsCompleted}}/uncomplete-atom{{else}}/complete-atom{{end}}"
+ hx-vals='{"id": "{{.ID}}", "source": "{{.Source}}"{{if .ListID}}, "listId": "{{.ListID}}"{{end}}}'
+ hx-target="#tab-content"
+ hx-swap="innerHTML"
+ onclick="event.stopPropagation();"
+ class="h-4 w-4 rounded bg-black/40 border-white/30 text-white/80 focus:ring-white/30 cursor-pointer flex-shrink-0">
+ {{end}}
+ <span class="calendar-event-title {{if .IsCompleted}}line-through text-white/50{{end}}">{{.Title}}</span>
+ </div>
+ <div class="calendar-event-time">{{.Time.Format "3:04 PM"}}{{if .EndTime}} - {{.EndTime.Format "3:04 PM"}}{{end}}</div>
+ </div>
+ {{end}}
+ {{end}}
+ </div>
+
+ <script>
+ (function() {
+ const calendar = document.getElementById('tomorrow-calendar');
+ if (!calendar) return;
+ const events = calendar.querySelectorAll('.calendar-event');
+ const hourHeight = 40;
+ const startHour = parseInt(calendar.dataset.startHour) || 8;
+
+ // Build event data for overlap detection
+ const eventData = [];
+ events.forEach(function(el) {
+ const hour = parseInt(el.dataset.hour);
+ const minute = parseInt(el.dataset.minute);
+ const endHourVal = parseInt(el.dataset.endHour);
+ const endMinute = parseInt(el.dataset.endMinute);
+ const startMin = hour * 60 + minute;
+ let endMin;
+ if (endHourVal > hour || (endHourVal === hour && endMinute > minute)) {
+ endMin = endHourVal * 60 + endMinute;
+ } else {
+ endMin = startMin + 55;
+ }
+ eventData.push({ el, startMin, endMin, column: 0 });
+ });
+
+ // Assign columns for overlapping events
+ eventData.sort((a, b) => a.startMin - b.startMin);
+ for (let i = 0; i < eventData.length; i++) {
+ const ev = eventData[i];
+ const overlaps = eventData.filter((other, j) =>
+ j < i && other.endMin > ev.startMin && other.startMin < ev.endMin
+ );
+ const usedCols = overlaps.map(o => o.column);
+ let col = 0;
+ while (usedCols.includes(col)) col++;
+ ev.column = col;
+ }
+
+ // Position events with column-based indentation
+ eventData.forEach(function(ev) {
+ const el = ev.el;
+ const hour = parseInt(el.dataset.hour);
+ const minute = parseInt(el.dataset.minute);
+ const top = (hour - startHour) * hourHeight + (minute / 60) * hourHeight;
+ const durationMinutes = ev.endMin - ev.startMin;
+ const height = Math.max(28, (durationMinutes / 60) * hourHeight - 4);
+
+ el.style.top = top + 'px';
+ el.style.height = height + 'px';
+ el.style.left = (8 + ev.column * 16) + 'px';
+ el.style.display = 'block';
+
+ const url = el.dataset.url;
+ if (url) {
+ el.style.cursor = 'pointer';
+ el.addEventListener('click', function(e) {
+ if (e.target.tagName !== 'INPUT') {
+ window.open(url, '_blank');
+ }
+ });
+ }
+ });
+
+ // Hide untimed section if empty
+ const untimedSection = document.getElementById('tomorrow-untimed-section');
+ if (untimedSection && untimedSection.querySelectorAll('.untimed-item').length === 0) {
+ untimedSection.style.display = 'none';
+ }
+ })();
+ </script>
</details>
{{end}}