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:
MarekWo
2026-04-06 11:59:24 +02:00
parent 6c02220719
commit 60c698deb2
7 changed files with 292 additions and 15 deletions
+27 -15
View File
@@ -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;
+14
View File
@@ -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();
}
}
});
}
// =============================================================================
+13
View File
@@ -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
View 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);