mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-05-02 03:22:40 +02:00
feat: add Settings FAB button, drag-and-drop positioning, and size/spacing controls
- Add Settings quick-access button to both main chat and DM views - Make FAB container draggable via toggle button with position saved to localStorage - Add button size and spacing sliders in Settings > Appearance tab - Use CSS custom properties for dynamic FAB sizing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1038,19 +1038,20 @@ main {
|
||||
z-index: 900; /* Lower than offcanvas (1045) but higher than content */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: var(--fab-custom-gap, 12px);
|
||||
touch-action: none; /* Prevent scroll during drag */
|
||||
}
|
||||
|
||||
.fab {
|
||||
position: relative; /* For badge positioning */
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
width: var(--fab-custom-size, 56px);
|
||||
height: var(--fab-custom-size, 56px);
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
font-size: calc(var(--fab-custom-size, 56px) * 0.43);
|
||||
cursor: pointer;
|
||||
box-shadow: var(--fab-shadow);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
@@ -1110,14 +1111,19 @@ main {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* FAB toggle button (smaller, semi-transparent) */
|
||||
/* FAB toggle button (smaller, semi-transparent) — not affected by custom size */
|
||||
.fab-toggle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
background: rgba(108, 117, 125, 0.6);
|
||||
color: white;
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.85rem !important;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.fab-toggle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.fab-toggle:hover {
|
||||
@@ -1157,19 +1163,19 @@ main {
|
||||
.fab-container {
|
||||
right: 12px;
|
||||
top: 70px;
|
||||
gap: 10px;
|
||||
gap: var(--fab-custom-gap, 10px);
|
||||
}
|
||||
|
||||
.fab {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 1.25rem;
|
||||
width: var(--fab-custom-size, 48px);
|
||||
height: var(--fab-custom-size, 48px);
|
||||
font-size: calc(var(--fab-custom-size, 48px) * 0.43);
|
||||
}
|
||||
|
||||
.fab-toggle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 0.75rem;
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1485,6 +1491,12 @@ main {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Settings FAB button (warm orange) */
|
||||
.fab-settings {
|
||||
background: linear-gradient(135deg, #fd7e14 0%, #e06b0a 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Filter bar overlay - slides down from top of chat area */
|
||||
.filter-bar {
|
||||
position: absolute;
|
||||
|
||||
@@ -3931,6 +3931,20 @@ function initializeFabToggle() {
|
||||
const isCollapsed = container.classList.contains('collapsed');
|
||||
toggle.title = isCollapsed ? 'Show buttons' : 'Hide buttons';
|
||||
});
|
||||
|
||||
// Drag-and-drop support
|
||||
initFabDrag('fabContainer', 'fabToggle', 'mc-webui-fab-pos');
|
||||
|
||||
// Listen for settings open request from DM iframe
|
||||
window.addEventListener('message', (e) => {
|
||||
if (e.data && e.data.type === 'openSettings') {
|
||||
const modal = document.getElementById('settingsModal');
|
||||
if (modal) {
|
||||
const bsModal = bootstrap.Modal.getOrCreateInstance(modal);
|
||||
bsModal.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -236,6 +236,16 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
// Initialize FAB toggle
|
||||
initializeDmFabToggle();
|
||||
|
||||
// Settings FAB - open parent's settings modal
|
||||
const dmSettingsFab = document.getElementById('dmSettingsFab');
|
||||
if (dmSettingsFab) {
|
||||
dmSettingsFab.addEventListener('click', () => {
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({type: 'openSettings'}, '*');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load auto-retry config
|
||||
loadAutoRetryConfig();
|
||||
|
||||
@@ -1778,6 +1788,9 @@ function initializeDmFabToggle() {
|
||||
const isCollapsed = container.classList.contains('collapsed');
|
||||
toggle.title = isCollapsed ? 'Show buttons' : 'Hide buttons';
|
||||
});
|
||||
|
||||
// Drag-and-drop support
|
||||
initFabDrag('dmFabContainer', 'dmFabToggle', 'mc-webui-fab-pos-dm');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
154
app/static/js/fab-utils.js
Normal file
154
app/static/js/fab-utils.js
Normal file
@@ -0,0 +1,154 @@
|
||||
// =============================================================================
|
||||
// FAB Container — Drag-and-Drop & Customization Utilities
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Make a FAB container draggable via its toggle button.
|
||||
* Short click = toggle collapse, drag = reposition.
|
||||
* Position is persisted to localStorage.
|
||||
*
|
||||
* @param {string} containerId - e.g. 'fabContainer' or 'dmFabContainer'
|
||||
* @param {string} toggleId - e.g. 'fabToggle' or 'dmFabToggle'
|
||||
* @param {string} storageKey - localStorage key for position
|
||||
*/
|
||||
function initFabDrag(containerId, toggleId, storageKey) {
|
||||
const container = document.getElementById(containerId);
|
||||
const toggle = document.getElementById(toggleId);
|
||||
if (!container || !toggle) return;
|
||||
|
||||
const DRAG_THRESHOLD = 5; // px – movement before drag starts
|
||||
let dragging = false;
|
||||
let startX, startY, origLeft, origTop;
|
||||
let didDrag = false;
|
||||
|
||||
// --- Restore saved position ---
|
||||
restoreFabPosition(container, storageKey);
|
||||
|
||||
// --- Pointer events on toggle ---
|
||||
toggle.addEventListener('pointerdown', onPointerDown);
|
||||
|
||||
function onPointerDown(e) {
|
||||
// Only primary button
|
||||
if (e.button !== 0) return;
|
||||
|
||||
e.preventDefault();
|
||||
toggle.setPointerCapture(e.pointerId);
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
origLeft = rect.left;
|
||||
origTop = rect.top;
|
||||
dragging = false;
|
||||
didDrag = false;
|
||||
|
||||
toggle.addEventListener('pointermove', onPointerMove);
|
||||
toggle.addEventListener('pointerup', onPointerUp);
|
||||
}
|
||||
|
||||
function onPointerMove(e) {
|
||||
const dx = e.clientX - startX;
|
||||
const dy = e.clientY - startY;
|
||||
|
||||
if (!dragging && (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD)) {
|
||||
dragging = true;
|
||||
didDrag = true;
|
||||
// Switch container to left/top positioning for drag
|
||||
container.style.right = 'auto';
|
||||
}
|
||||
|
||||
if (dragging) {
|
||||
const newLeft = origLeft + dx;
|
||||
const newTop = origTop + dy;
|
||||
container.style.left = newLeft + 'px';
|
||||
container.style.top = newTop + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerUp(e) {
|
||||
toggle.removeEventListener('pointermove', onPointerMove);
|
||||
toggle.removeEventListener('pointerup', onPointerUp);
|
||||
|
||||
if (didDrag) {
|
||||
// Clamp to viewport
|
||||
clampFabPosition(container);
|
||||
// Save
|
||||
saveFabPosition(container, storageKey);
|
||||
}
|
||||
// If it was not a drag, let the click event fire naturally (toggle collapse)
|
||||
// If it was a drag, suppress the click
|
||||
if (didDrag) {
|
||||
toggle.addEventListener('click', suppressClick, {once: true, capture: true});
|
||||
}
|
||||
}
|
||||
|
||||
function suppressClick(e) {
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function clampFabPosition(container) {
|
||||
const rect = container.getBoundingClientRect();
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
|
||||
let left = rect.left;
|
||||
let top = rect.top;
|
||||
|
||||
// Keep at least 20px of the container visible on each edge
|
||||
if (left + rect.width < 20) left = 20 - rect.width;
|
||||
if (left > vw - 20) left = vw - 20;
|
||||
if (top < 0) top = 0;
|
||||
if (top > vh - 20) top = vh - 20;
|
||||
|
||||
container.style.left = left + 'px';
|
||||
container.style.top = top + 'px';
|
||||
}
|
||||
|
||||
function saveFabPosition(container, storageKey) {
|
||||
const rect = container.getBoundingClientRect();
|
||||
localStorage.setItem(storageKey, JSON.stringify({
|
||||
left: rect.left,
|
||||
top: rect.top
|
||||
}));
|
||||
}
|
||||
|
||||
function restoreFabPosition(container, storageKey) {
|
||||
const saved = localStorage.getItem(storageKey);
|
||||
if (!saved) return;
|
||||
|
||||
try {
|
||||
const pos = JSON.parse(saved);
|
||||
container.style.right = 'auto';
|
||||
container.style.left = pos.left + 'px';
|
||||
container.style.top = pos.top + 'px';
|
||||
// Re-clamp in case viewport changed
|
||||
clampFabPosition(container);
|
||||
} catch (e) {
|
||||
localStorage.removeItem(storageKey);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FAB Size & Spacing — apply from localStorage
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Apply saved FAB appearance settings (size, gap).
|
||||
* Called on page load from both main and DM pages.
|
||||
*/
|
||||
function applyFabAppearance() {
|
||||
const size = localStorage.getItem('mc-webui-fab-size');
|
||||
const gap = localStorage.getItem('mc-webui-fab-gap');
|
||||
|
||||
if (size) {
|
||||
document.documentElement.style.setProperty('--fab-custom-size', size + 'px');
|
||||
}
|
||||
if (gap) {
|
||||
document.documentElement.style.setProperty('--fab-custom-gap', gap + 'px');
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-apply on load
|
||||
document.addEventListener('DOMContentLoaded', applyFabAppearance);
|
||||
@@ -473,6 +473,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h6 class="text-muted mb-3">Quick Access Buttons</h6>
|
||||
<table class="table table-sm table-borderless mb-3 align-middle">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="ps-0">Button size (px)</td>
|
||||
<td class="pe-0 d-flex align-items-center gap-2" style="width:12rem">
|
||||
<input type="range" class="form-range flex-grow-1" id="settFabSize" min="28" max="72" step="2" value="56">
|
||||
<span class="text-muted small" id="settFabSizeVal" style="min-width:2.5rem;text-align:right">56</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-0">Spacing (px)</td>
|
||||
<td class="pe-0 d-flex align-items-center gap-2">
|
||||
<input type="range" class="form-range flex-grow-1" id="settFabGap" min="2" max="24" step="1" value="12">
|
||||
<span class="text-muted small" id="settFabGapVal" style="min-width:2.5rem;text-align:right">12</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-0">Position</td>
|
||||
<td class="pe-0">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="settFabResetPos">Reset to default</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -661,6 +688,9 @@
|
||||
<!-- SocketIO for real-time updates -->
|
||||
<script src="{{ url_for('static', filename='vendor/socket.io/socket.io.min.js') }}"></script>
|
||||
|
||||
<!-- FAB Utilities (drag, sizing — must load before app.js) -->
|
||||
<script src="{{ url_for('static', filename='js/fab-utils.js') }}"></script>
|
||||
|
||||
<!-- Custom JS -->
|
||||
<!-- QR Code generator (for Device Share) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
|
||||
@@ -716,6 +746,51 @@
|
||||
document.querySelectorAll('.theme-option').forEach(function(el) {
|
||||
el.classList.toggle('active', el.getAttribute('data-theme-value') === current);
|
||||
});
|
||||
|
||||
// --- FAB appearance controls ---
|
||||
var fabSizeSlider = document.getElementById('settFabSize');
|
||||
var fabSizeVal = document.getElementById('settFabSizeVal');
|
||||
var fabGapSlider = document.getElementById('settFabGap');
|
||||
var fabGapVal = document.getElementById('settFabGapVal');
|
||||
var fabResetPos = document.getElementById('settFabResetPos');
|
||||
|
||||
// Load saved values
|
||||
var savedSize = localStorage.getItem('mc-webui-fab-size');
|
||||
var savedGap = localStorage.getItem('mc-webui-fab-gap');
|
||||
if (savedSize && fabSizeSlider) { fabSizeSlider.value = savedSize; fabSizeVal.textContent = savedSize; }
|
||||
if (savedGap && fabGapSlider) { fabGapSlider.value = savedGap; fabGapVal.textContent = savedGap; }
|
||||
|
||||
// Live preview on slider change
|
||||
if (fabSizeSlider) {
|
||||
fabSizeSlider.addEventListener('input', function() {
|
||||
var v = this.value;
|
||||
fabSizeVal.textContent = v;
|
||||
document.documentElement.style.setProperty('--fab-custom-size', v + 'px');
|
||||
localStorage.setItem('mc-webui-fab-size', v);
|
||||
});
|
||||
}
|
||||
if (fabGapSlider) {
|
||||
fabGapSlider.addEventListener('input', function() {
|
||||
var v = this.value;
|
||||
fabGapVal.textContent = v;
|
||||
document.documentElement.style.setProperty('--fab-custom-gap', v + 'px');
|
||||
localStorage.setItem('mc-webui-fab-gap', v);
|
||||
});
|
||||
}
|
||||
|
||||
// Reset position button
|
||||
if (fabResetPos) {
|
||||
fabResetPos.addEventListener('click', function() {
|
||||
localStorage.removeItem('mc-webui-fab-pos');
|
||||
localStorage.removeItem('mc-webui-fab-pos-dm');
|
||||
var container = document.getElementById('fabContainer');
|
||||
if (container) {
|
||||
container.style.left = '';
|
||||
container.style.right = '16px';
|
||||
container.style.top = '80px';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -185,6 +185,9 @@
|
||||
<button class="fab fab-filter" id="dmFilterFab" title="Filter Messages">
|
||||
<i class="bi bi-funnel-fill"></i>
|
||||
</button>
|
||||
<button class="fab fab-settings" id="dmSettingsFab" title="Settings">
|
||||
<i class="bi bi-gear-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -356,6 +359,9 @@
|
||||
<!-- SocketIO for real-time updates -->
|
||||
<script src="{{ url_for('static', filename='vendor/socket.io/socket.io.min.js') }}"></script>
|
||||
|
||||
<!-- FAB Utilities (drag, sizing) -->
|
||||
<script src="{{ url_for('static', filename='js/fab-utils.js') }}"></script>
|
||||
|
||||
<!-- Custom JS -->
|
||||
<script src="{{ url_for('static', filename='js/dm.js') }}"></script>
|
||||
|
||||
|
||||
@@ -124,6 +124,9 @@
|
||||
<button class="fab fab-contacts" data-bs-toggle="modal" data-bs-target="#contactsModal" title="Contact Management">
|
||||
<i class="bi bi-person-fill"></i>
|
||||
</button>
|
||||
<button class="fab fab-settings" data-bs-toggle="modal" data-bs-target="#settingsModal" title="Settings">
|
||||
<i class="bi bi-gear-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- DM Modal (Full Screen) -->
|
||||
|
||||
Reference in New Issue
Block a user