From e6b8198385b930939154bc8bc3661d067d8d4aef Mon Sep 17 00:00:00 2001 From: maxlamagna Date: Tue, 21 Jul 2026 23:11:50 +0100 Subject: [PATCH] feat(ui): floating day indicator (WhatsApp-style) + fix orphaned date dividers on per-channel clear --- static/channels.js | 7 ++++- static/chat.js | 68 +++++++++++++++++++++++++++++++++++++++++++++- static/index.html | 7 +++-- static/style.css | 32 ++++++++++++++++++++++ 4 files changed, 109 insertions(+), 5 deletions(-) diff --git a/static/channels.js b/static/channels.js index fd577d1d..943cc9a8 100644 --- a/static/channels.js +++ b/static/channels.js @@ -308,9 +308,14 @@ function switchChannel(name) { const savedId = _channelScrollMsg[name]; if (savedId) { const el = document.querySelector(`.message[data-id="${savedId}"]`); - if (el) { el.scrollIntoView({ block: 'start' }); return; } + if (el) { + el.scrollIntoView({ block: 'start' }); + if (typeof dayFloatRefresh === 'function') dayFloatRefresh(); + return; + } } window.scrollToBottom(); + if (typeof dayFloatRefresh === 'function') dayFloatRefresh(); } function filterMessagesByChannel() { diff --git a/static/chat.js b/static/chat.js index df48a039..f12c2868 100644 --- a/static/chat.js +++ b/static/chat.js @@ -577,7 +577,8 @@ function connectWebSocket() { const container = document.getElementById('messages'); const toRemove = []; for (const el of container.children) { - if (el.dataset.id && (el.dataset.channel || 'general') === clearChannel) { + const isDivider = el.classList.contains('date-divider'); + if ((el.dataset.id || isDivider) && (el.dataset.channel || 'general') === clearChannel) { toRemove.push(el); } } @@ -591,6 +592,7 @@ function connectWebSocket() { lastMessageDate = null; lastMessageDates = {}; } + dayFloatRefresh(); requestAnimationFrame(() => { const _clearDbgAfter = _clearDbgList ? _clearDbgList.children.length : -1; console.log('CLEAR_DEBUG after clear (next frame), jobs-panel-children=' + _clearDbgAfter); @@ -666,6 +668,69 @@ function maybeInsertDateDivider(container, msg) { } } +// --- Floating day indicator --- + +let dayFloatDividers = null; // live HTMLCollection — follows divider add/remove +let dayFloatFadeTimer = null; +let dayFloatRafPending = false; + +function dayFloatLabel() { + if (!dayFloatDividers) { + const container = document.getElementById('messages'); + if (!container) return null; + dayFloatDividers = container.getElementsByClassName('date-divider'); + } + const scroll = document.getElementById('timeline'); + if (!scroll) return null; + const top = scroll.getBoundingClientRect().top; + let label = null; + for (const d of dayFloatDividers) { + if (d.style.display === 'none') continue; + if (d.getBoundingClientRect().top <= top) { + label = d.textContent; + } else { + break; + } + } + return label; +} + +function dayFloatHide() { + const float = document.getElementById('day-float'); + if (!float) return; + if (dayFloatFadeTimer) { clearTimeout(dayFloatFadeTimer); dayFloatFadeTimer = null; } + float.classList.remove('visible'); +} + +function dayFloatOnScroll() { + if (dayFloatRafPending) return; + dayFloatRafPending = true; + requestAnimationFrame(() => { + dayFloatRafPending = false; + const float = document.getElementById('day-float'); + if (!float) return; + const label = dayFloatLabel(); + if (!label) { dayFloatHide(); return; } + float.querySelector('span').textContent = label; + float.classList.add('visible'); + if (dayFloatFadeTimer) clearTimeout(dayFloatFadeTimer); + dayFloatFadeTimer = setTimeout(() => { + dayFloatFadeTimer = null; + float.classList.remove('visible'); + }, 1200); + }); +} + +function dayFloatRefresh() { + // Recompute without starting the show/fade cycle: swap the label if the + // pill is relevant, hide (cancelling any fade) if no day is above the top. + const float = document.getElementById('day-float'); + if (!float) return; + const label = dayFloatLabel(); + if (!label) { dayFloatHide(); return; } + float.querySelector('span').textContent = label; +} + // --- Messages --- function appendMessage(msg) { @@ -2499,6 +2564,7 @@ function setupScroll() { unreadCount = 0; } updateScrollAnchor(); + dayFloatOnScroll(); }); // Keep pinned to bottom when content changes (e.g. images load) diff --git a/static/index.html b/static/index.html index 955feb87..0cf03c21 100644 --- a/static/index.html +++ b/static/index.html @@ -4,7 +4,7 @@ agentchattr - + @@ -172,6 +172,7 @@

agentchattr

+
Loading messages... @@ -342,8 +343,8 @@

agentchattr

- + - + diff --git a/static/style.css b/static/style.css index 0871d58c..3064f090 100644 --- a/static/style.css +++ b/static/style.css @@ -2553,6 +2553,38 @@ body.channels-in-sidebar #channel-bar { background: var(--border); } +/* --- Floating day indicator --- */ + +#day-float { + position: sticky; + top: 8px; + height: 0; + display: flex; + align-items: flex-start; + justify-content: center; + z-index: 6; + pointer-events: none; +} + +#day-float span { + background: var(--bg-msg); + border: 1px solid var(--border); + border-radius: var(--radius-full); + padding: 3px 10px; + color: var(--text-dim); + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + text-indent: 0.06em; + opacity: 0; + transition: opacity 0.25s ease; +} + +#day-float.visible span { + opacity: 1; +} + /* --- Code copy button --- */ .code-copy-btn {