mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-08-07 09:12:57 +02:00
feat(ui): FAB toggle, filter bar layout fix, and filter @mentions
- Add collapsible FAB container with chevron toggle button to temporarily hide floating action buttons that overlap messages - Make filter bar push messages down instead of overlaying the first matched message (CSS sibling selector adds padding-top) - Add @mentions autocomplete to filter search bar - typing @ shows contact list dropdown, selecting inserts plain name (not @[] format) so all messages from/mentioning that user are found Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -870,6 +870,48 @@ main {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* FAB toggle button (smaller, semi-transparent) */
|
||||
.fab-toggle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: rgba(108, 117, 125, 0.6);
|
||||
color: white;
|
||||
font-size: 0.85rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.fab-toggle:hover {
|
||||
background: rgba(108, 117, 125, 0.9);
|
||||
}
|
||||
|
||||
/* Collapsed state - hide all FABs except toggle */
|
||||
.fab-container.collapsed .fab:not(.fab-toggle) {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: scale(0);
|
||||
height: 0;
|
||||
width: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fab-container.collapsed {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* Smooth transitions for collapse */
|
||||
.fab-container .fab:not(.fab-toggle) {
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease, height 0.2s ease, width 0.2s ease;
|
||||
}
|
||||
|
||||
.fab-toggle i {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.fab-container.collapsed .fab-toggle i {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Mobile optimization */
|
||||
@media (max-width: 768px) {
|
||||
.fab-container {
|
||||
@@ -883,6 +925,12 @@ main {
|
||||
height: 48px;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.fab-toggle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
@@ -1220,6 +1268,15 @@ main {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Push messages container down when filter bar is visible */
|
||||
.messages-container {
|
||||
transition: padding-top 0.3s ease;
|
||||
}
|
||||
|
||||
.filter-bar.visible ~ .messages-container {
|
||||
padding-top: calc(1rem + 52px) !important; /* 52px ≈ filter bar height (0.75rem*2 padding + 36px input + border) */
|
||||
}
|
||||
|
||||
/* Filter bar inner layout */
|
||||
.filter-bar-inner {
|
||||
display: flex;
|
||||
@@ -1228,7 +1285,6 @@ main {
|
||||
}
|
||||
|
||||
.filter-bar-input {
|
||||
flex: 1;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid #ced4da;
|
||||
padding: 0.5rem 0.75rem;
|
||||
@@ -1307,6 +1363,24 @@ main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Filter input wrapper for mentions popup positioning */
|
||||
.filter-input-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.filter-input-wrapper .filter-bar-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Filter mentions popup - appears below input (not above like message input) */
|
||||
.filter-mentions-popup {
|
||||
bottom: auto !important;
|
||||
top: 100% !important;
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Mobile responsive filter bar */
|
||||
@media (max-width: 576px) {
|
||||
.filter-bar {
|
||||
|
||||
+207
-4
@@ -324,6 +324,9 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
// Initialize filter functionality
|
||||
initializeFilter();
|
||||
|
||||
// Initialize FAB toggle
|
||||
initializeFabToggle();
|
||||
|
||||
// Setup auto-refresh immediately after messages are displayed
|
||||
// Don't wait for geo cache - it's not needed for auto-refresh
|
||||
setupAutoRefresh();
|
||||
@@ -2808,6 +2811,22 @@ async function loadContactsForMentions() {
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FAB Toggle (Collapse/Expand)
|
||||
// =============================================================================
|
||||
|
||||
function initializeFabToggle() {
|
||||
const toggle = document.getElementById('fabToggle');
|
||||
const container = document.getElementById('fabContainer');
|
||||
if (!toggle || !container) return;
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
container.classList.toggle('collapsed');
|
||||
const isCollapsed = container.classList.contains('collapsed');
|
||||
toggle.title = isCollapsed ? 'Show buttons' : 'Hide buttons';
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Chat Filter Functionality
|
||||
// =============================================================================
|
||||
@@ -2834,9 +2853,14 @@ function initializeFilter() {
|
||||
openFilterBar();
|
||||
});
|
||||
|
||||
// Filter as user types (debounced)
|
||||
// Filter as user types (debounced) - also check for @mentions
|
||||
let filterTimeout = null;
|
||||
filterInput.addEventListener('input', () => {
|
||||
// Check for @mention trigger
|
||||
if (handleFilterMentionInput(filterInput)) {
|
||||
return; // Don't apply filter while picking a mention
|
||||
}
|
||||
|
||||
clearTimeout(filterTimeout);
|
||||
filterTimeout = setTimeout(() => {
|
||||
applyFilter(filterInput.value);
|
||||
@@ -2847,6 +2871,7 @@ function initializeFilter() {
|
||||
filterClearBtn.addEventListener('click', () => {
|
||||
filterInput.value = '';
|
||||
applyFilter('');
|
||||
hideFilterMentionsPopup();
|
||||
filterInput.focus();
|
||||
});
|
||||
|
||||
@@ -2855,11 +2880,30 @@ function initializeFilter() {
|
||||
closeFilterBar();
|
||||
});
|
||||
|
||||
// Keyboard shortcuts
|
||||
// Keyboard shortcuts (with mentions navigation support)
|
||||
filterInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeFilterBar();
|
||||
// If filter mentions popup is active, handle navigation
|
||||
if (filterMentionActive) {
|
||||
if (handleFilterMentionKeydown(e)) return;
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
if (filterMentionActive) {
|
||||
hideFilterMentionsPopup();
|
||||
e.preventDefault();
|
||||
} else {
|
||||
closeFilterBar();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Close filter mentions on blur
|
||||
filterInput.addEventListener('blur', () => {
|
||||
setTimeout(hideFilterMentionsPopup, 200);
|
||||
});
|
||||
|
||||
// Preload contacts when filter bar is focused
|
||||
filterInput.addEventListener('focus', () => {
|
||||
loadContactsForMentions();
|
||||
});
|
||||
|
||||
// Global keyboard shortcut: Ctrl+F to open filter
|
||||
@@ -2896,6 +2940,7 @@ function closeFilterBar() {
|
||||
|
||||
filterBar.classList.remove('visible');
|
||||
filterActive = false;
|
||||
hideFilterMentionsPopup();
|
||||
|
||||
// Reset filter
|
||||
filterInput.value = '';
|
||||
@@ -3026,6 +3071,164 @@ function getMessageId(messageEl) {
|
||||
return 'msg_' + children.indexOf(messageEl);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Filter Mentions Autocomplete
|
||||
// =============================================================================
|
||||
|
||||
let filterMentionActive = false;
|
||||
let filterMentionStartPos = -1;
|
||||
let filterMentionSelectedIndex = 0;
|
||||
|
||||
/**
|
||||
* Handle input in filter bar to detect @mention trigger
|
||||
* @returns {boolean} true if in mention mode (caller should skip filter apply)
|
||||
*/
|
||||
function handleFilterMentionInput(input) {
|
||||
const cursorPos = input.selectionStart;
|
||||
const text = input.value;
|
||||
const textBeforeCursor = text.substring(0, cursorPos);
|
||||
const lastAtPos = textBeforeCursor.lastIndexOf('@');
|
||||
|
||||
if (lastAtPos >= 0) {
|
||||
const textAfterAt = textBeforeCursor.substring(lastAtPos + 1);
|
||||
// No whitespace after @ means we're typing a mention
|
||||
if (!/[\s\n]/.test(textAfterAt)) {
|
||||
filterMentionStartPos = lastAtPos;
|
||||
filterMentionActive = true;
|
||||
showFilterMentionsPopup(textAfterAt);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (filterMentionActive) {
|
||||
hideFilterMentionsPopup();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle keyboard navigation in filter mentions popup
|
||||
* @returns {boolean} true if the key was handled
|
||||
*/
|
||||
function handleFilterMentionKeydown(e) {
|
||||
const popup = document.getElementById('filterMentionsPopup');
|
||||
const items = popup.querySelectorAll('.mention-item');
|
||||
if (items.length === 0) return false;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
filterMentionSelectedIndex = Math.min(filterMentionSelectedIndex + 1, items.length - 1);
|
||||
updateFilterMentionHighlight(items);
|
||||
return true;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
filterMentionSelectedIndex = Math.max(filterMentionSelectedIndex - 1, 0);
|
||||
updateFilterMentionHighlight(items);
|
||||
return true;
|
||||
case 'Enter':
|
||||
case 'Tab':
|
||||
if (items.length > 0 && filterMentionSelectedIndex < items.length) {
|
||||
e.preventDefault();
|
||||
const selected = items[filterMentionSelectedIndex];
|
||||
if (selected && selected.dataset.contact) {
|
||||
selectFilterMentionContact(selected.dataset.contact);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show filter mentions popup with filtered contacts
|
||||
*/
|
||||
function showFilterMentionsPopup(query) {
|
||||
const popup = document.getElementById('filterMentionsPopup');
|
||||
const list = document.getElementById('filterMentionsList');
|
||||
|
||||
// Ensure contacts are loaded
|
||||
loadContactsForMentions();
|
||||
|
||||
const filtered = filterContacts(query);
|
||||
|
||||
if (filtered.length === 0) {
|
||||
list.innerHTML = '<div class="mentions-empty">No contacts found</div>';
|
||||
popup.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
if (filterMentionSelectedIndex >= filtered.length) {
|
||||
filterMentionSelectedIndex = 0;
|
||||
}
|
||||
|
||||
list.innerHTML = filtered.map((contact, index) => {
|
||||
const highlighted = index === filterMentionSelectedIndex ? 'highlighted' : '';
|
||||
const escapedName = escapeHtml(contact);
|
||||
return `<div class="mention-item ${highlighted}" data-contact="${escapedName}" data-index="${index}">
|
||||
<span class="mention-item-name">${escapedName}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
list.querySelectorAll('.mention-item').forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
selectFilterMentionContact(this.dataset.contact);
|
||||
});
|
||||
});
|
||||
|
||||
popup.classList.remove('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide filter mentions popup
|
||||
*/
|
||||
function hideFilterMentionsPopup() {
|
||||
const popup = document.getElementById('filterMentionsPopup');
|
||||
if (popup) popup.classList.add('hidden');
|
||||
filterMentionActive = false;
|
||||
filterMentionStartPos = -1;
|
||||
filterMentionSelectedIndex = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update highlight in filter mentions popup
|
||||
*/
|
||||
function updateFilterMentionHighlight(items) {
|
||||
items.forEach((item, index) => {
|
||||
if (index === filterMentionSelectedIndex) {
|
||||
item.classList.add('highlighted');
|
||||
item.scrollIntoView({ block: 'nearest' });
|
||||
} else {
|
||||
item.classList.remove('highlighted');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a contact from filter mentions and insert plain name
|
||||
*/
|
||||
function selectFilterMentionContact(contactName) {
|
||||
const input = document.getElementById('filterInput');
|
||||
const text = input.value;
|
||||
|
||||
// Replace from @ position to cursor with plain contact name
|
||||
const beforeMention = text.substring(0, filterMentionStartPos);
|
||||
const afterCursor = text.substring(input.selectionStart);
|
||||
|
||||
input.value = beforeMention + contactName + afterCursor;
|
||||
|
||||
// Set cursor position after the name
|
||||
const newCursorPos = filterMentionStartPos + contactName.length;
|
||||
input.setSelectionRange(newCursorPos, newCursorPos);
|
||||
|
||||
hideFilterMentionsPopup();
|
||||
input.focus();
|
||||
|
||||
// Trigger filter with the new value
|
||||
applyFilter(input.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear filter state when messages are reloaded
|
||||
* Called from displayMessages()
|
||||
|
||||
@@ -76,7 +76,13 @@
|
||||
<!-- Filter bar overlay -->
|
||||
<div id="filterBar" class="filter-bar">
|
||||
<div class="filter-bar-inner">
|
||||
<input type="text" id="filterInput" class="filter-bar-input" placeholder="Filter messages..." autocomplete="off">
|
||||
<div class="filter-input-wrapper">
|
||||
<input type="text" id="filterInput" class="filter-bar-input" placeholder="Filter messages..." autocomplete="off">
|
||||
<!-- Filter mentions autocomplete popup -->
|
||||
<div id="filterMentionsPopup" class="mentions-popup filter-mentions-popup hidden">
|
||||
<div class="mentions-list" id="filterMentionsList"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span id="filterMatchCount" class="filter-match-count"></span>
|
||||
<button type="button" id="filterClearBtn" class="filter-bar-btn filter-bar-btn-clear" title="Clear">
|
||||
<i class="bi bi-x"></i>
|
||||
@@ -153,7 +159,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Floating Action Buttons -->
|
||||
<div class="fab-container">
|
||||
<div class="fab-container" id="fabContainer">
|
||||
<button class="fab fab-toggle" id="fabToggle" title="Hide buttons">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</button>
|
||||
<button class="fab fab-filter" id="filterFab" title="Filter Messages">
|
||||
<i class="bi bi-funnel-fill"></i>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user