diff --git a/CHANGELOG.md b/CHANGELOG.md index 626be1f..3474b88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## Changes 11/8/2025 (v1.8.13) + +release(v1.8.13): ui(dnd): stabilize zones, lock sidebar width, and keep header dock in sync + +- dnd: fix disappearing/overlapping cards when moving between sidebar/top; return to origin on failed drop +- layout: placeCardInZone now live-updates top layout, sidebar visibility, and toggle icon +- toggle/collapse: move ALL cards to header on collapse, restore saved layout on expand; keep icon state synced; add body.sidebar-hidden for proper file list expansion; emit `zones:collapsed-changed` +- header dock: show dock whenever icons exist (and on collapse); hide when empty +- responsive: enforceResponsiveZones also updates toggle icon; stash/restore behavior unchanged +- sidebar: hard-lock width to 350px (CSS) and remove runtime 280px minWidth; add placeholder when empty to make dropping back easy +- CSS: right-align header dock buttons, centered “Drop Zone” label, sensible min-height; dark-mode safe +- refactor: small renames/ordering; remove redundant z-index on toggle; minor formatting + +--- + ## Changes 11/8/2025 (v1.8.12) release(v1.8.12): auth UI & DnD polish — show OIDC, auto-SSO, right-aligned header icons diff --git a/public/css/styles.css b/public/css/styles.css index 396a8b5..b302b27 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -142,13 +142,13 @@ body { border-radius: 4px !important; padding: 6px 10px !important; } - /* make the drop zone fill leftover space and right-align its own icons */ -#headerDropArea.header-drop-zone{ - display: flex; - justify-content: flex-end; - min-width: 100px; -} + #headerDropArea.header-drop-zone{ + display: flex; + justify-content: flex-end; /* buttons to the right */ + align-items: center; + min-height: 40px; /* so the label has room */ + } .header-buttons button:hover { background-color: rgba(255, 255, 255, 0.2); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); @@ -1532,7 +1532,16 @@ body { .drag-header.active { width: 350px; height: 750px; - }.main-column { + } + /* Fixed-width sidebar (always 350px) */ +#sidebarDropArea{ + width: 350px; + min-width: 350px; + max-width: 350px; + flex: 0 0 350px; + box-sizing: border-box; +} + .main-column { flex: 1; transition: margin-left 0.3s ease; }#uploadFolderRow { @@ -1600,8 +1609,8 @@ body { }#sidebarDropArea, #uploadFolderRow { background-color: transparent; - - }.dark-mode #sidebarDropArea, + } + .dark-mode #sidebarDropArea, .dark-mode #uploadFolderRow { background-color: transparent; }.dark-mode #sidebarDropArea.highlight, @@ -1615,8 +1624,6 @@ body { border: none !important; }.dragging:focus { outline: none; - }#sidebarDropArea > .card { - margin-bottom: 1rem; }.card { background-color: #fff; color: #000; @@ -1713,8 +1720,9 @@ body { border: 2px dashed #555; color: #fff; }.header-drop-zone.drag-active:empty::before { - content: "Drop"; + content: "Drop Zone"; font-size: 10px; + padding-right: 6px; color: #aaa; }/* Disable text selection on rows to prevent accidental copying when shift-clicking */ #fileList tbody tr.clickable-row { diff --git a/public/js/dragAndDrop.js b/public/js/dragAndDrop.js index 26d8e94..45dde8e 100644 --- a/public/js/dragAndDrop.js +++ b/public/js/dragAndDrop.js @@ -41,7 +41,6 @@ function readLayout() { function writeLayout(layout) { localStorage.setItem(LAYOUT_KEY, JSON.stringify(layout || {})); } - function setLayoutFor(cardId, zoneId) { const layout = readLayout(); layout[cardId] = zoneId; @@ -93,6 +92,7 @@ function removeHeaderIconForCard(card) { function insertCardInHeader(card) { const host = getHeaderDropArea(); if (!host) return; + // Ensure hidden container exists to park real cards while icon-visible. let hidden = $('hiddenCardsContainer'); if (!hidden) { @@ -110,10 +110,10 @@ function insertCardInHeader(card) { iconButton.style.border = 'none'; iconButton.style.background = 'none'; iconButton.style.cursor = 'pointer'; - iconButton.innerHTML = `${card.id === 'uploadCard' ? 'cloud_upload' - : card.id === 'folderManagementCard' ? 'folder' - : 'insert_drive_file' - }`; + iconButton.innerHTML = `${ + card.id === 'uploadCard' ? 'cloud_upload' : + card.id === 'folderManagementCard' ? 'folder' : 'insert_drive_file' + }`; iconButton.cardElement = card; card.headerIconButton = iconButton; @@ -150,8 +150,8 @@ function insertCardInHeader(card) { function showModal() { ensureModal(); if (!modal.contains(card)) { - let hidden = $('hiddenCardsContainer'); - if (hidden && hidden.contains(card)) hidden.removeChild(card); + const hiddenNow = $('hiddenCardsContainer'); + if (hiddenNow && hiddenNow.contains(card)) hiddenNow.removeChild(card); card.style.width = ''; card.style.minWidth = ''; modal.appendChild(card); @@ -163,8 +163,8 @@ function insertCardInHeader(card) { if (!modal) return; modal.style.visibility = 'hidden'; modal.style.opacity = '0'; - const hidden = $('hiddenCardsContainer'); - if (hidden && modal.contains(card)) hidden.appendChild(card); + const hiddenNow = $('hiddenCardsContainer'); + if (hiddenNow && modal.contains(card)) hiddenNow.appendChild(card); } function maybeHide() { setTimeout(() => { @@ -181,6 +181,8 @@ function insertCardInHeader(card) { }); host.appendChild(iconButton); + // make sure the dock is visible when icons exist + showHeaderDockPersistent(); saveHeaderOrder(); } @@ -227,6 +229,10 @@ function placeCardInZone(card, zoneId, { animate = true } = {}) { break; } } + + updateTopZoneLayout(); + updateSidebarVisibility(); + updateZonesToggleUI(); // live update when zones change } function currentZoneForCard(card) { @@ -234,7 +240,6 @@ function currentZoneForCard(card) { const pid = card.parentNode.id || ''; if (pid === 'hiddenCardsContainer' && card.headerIconButton) return ZONES.HEADER; if ([ZONES.SIDEBAR, ZONES.TOP_LEFT, ZONES.TOP_RIGHT, ZONES.HEADER].includes(pid)) return pid; - // If card is temporarily in modal (header), treat as header if (card.headerIconButton && card.headerIconButton.modalInstance?.contains(card)) return ZONES.HEADER; return pid || null; } @@ -248,44 +253,6 @@ function saveCurrentLayout() { writeLayout(layout); } -function applyUserLayoutOrDefault() { - const layout = readLayout(); - const hasAny = Object.keys(layout).length > 0; - - // If we have saved user layout, honor it - if (hasAny) { - getCards().forEach(card => { - const targetZone = layout[card.id]; - if (!targetZone) return; - // On small screens: if saved zone is the sidebar, temporarily place in top cols - if (isSmallScreen() && targetZone === ZONES.SIDEBAR) { - const target = (card.id === 'uploadCard') ? ZONES.TOP_LEFT : ZONES.TOP_RIGHT; - placeCardInZone(card, target, { animate: false }); - } else { - placeCardInZone(card, targetZone, { animate: false }); - } - }); - updateTopZoneLayout(); - updateSidebarVisibility(); - return; - } - - // No saved layout yet: apply defaults - if (!isSmallScreen()) { - // Wide: default both to sidebar (if not already) - getCards().forEach(c => placeCardInZone(c, ZONES.SIDEBAR, { animate: false })); - } else { - // Small: deterministic mapping - getCards().forEach(c => { - const zone = (c.id === 'uploadCard') ? ZONES.TOP_LEFT : ZONES.TOP_RIGHT; - placeCardInZone(c, zone, { animate: false }); - }); - } - updateTopZoneLayout(); - updateSidebarVisibility(); - saveCurrentLayout(); // initialize baseline so future moves persist -} - // -------------------- responsive stash -------------------- function stashSidebarCardsBeforeSmall() { const sb = getSidebar(); @@ -339,21 +306,62 @@ function enforceResponsiveZones() { __wasSmall = nowSmall; updateTopZoneLayout(); updateSidebarVisibility(); + updateZonesToggleUI(); // keep icon in sync when responsive flips +} + +// -------------------- header dock visibility helpers -------------------- +function showHeaderDockPersistent() { + const h = getHeaderDropArea(); + if (h) { + h.style.display = 'inline-flex'; + h.classList.add('dock-visible'); + } +} +function hideHeaderDockPersistent() { + const h = getHeaderDropArea(); + if (h) { + h.classList.remove('dock-visible'); + if (h.children.length === 0) h.style.display = 'none'; + } } // -------------------- zones toggle (collapse to header) -------------------- function isZonesCollapsed() { return localStorage.getItem('zonesCollapsed') === '1'; } + +function applyCollapsedBodyClass() { + // helps grid/containers expand the file list area when sidebar is hidden + document.body.classList.toggle('sidebar-hidden', isZonesCollapsed()); + const main = document.querySelector('.main-wrapper') || document.querySelector('#main') || document.querySelector('main'); + if (main) { + main.style.contain = 'size'; + void main.offsetHeight; + setTimeout(() => { main.style.removeProperty('contain'); }, 0); + } +} + function setZonesCollapsed(collapsed) { localStorage.setItem('zonesCollapsed', collapsed ? '1' : '0'); + if (collapsed) { - // Move ALL cards to header icons (transient). Do not overwrite saved layout. + // Move ALL cards to header icons (transient) regardless of where they were. getCards().forEach(insertCardInHeader); + showHeaderDockPersistent(); + const sb = getSidebar(); + if (sb) sb.style.display = 'none'; } else { - // Restore the saved user layout. + // Restore saved layout + rebuild header icons only for HEADER-assigned cards applyUserLayoutOrDefault(); + loadHeaderOrder(); + hideHeaderDockPersistent(); } + + updateSidebarVisibility(); + updateTopZoneLayout(); ensureZonesToggle(); updateZonesToggleUI(); + applyCollapsedBodyClass(); + + document.dispatchEvent(new CustomEvent('zones:collapsed-changed', { detail: { collapsed: isZonesCollapsed() } })); } function getHeaderHost() { @@ -379,7 +387,6 @@ function ensureZonesToggle() { position: 'absolute', top: `${TOGGLE_TOP_PX}px`, left: `${TOGGLE_LEFT_PX}px`, - zIndex: '10010', width: '38px', height: '38px', borderRadius: '19px', @@ -424,7 +431,7 @@ function updateZonesToggleUI() { iconEl.style.transition = 'transform 0.2s ease'; iconEl.style.display = 'inline-flex'; iconEl.style.alignItems = 'center'; - // fun rotate if both cards are in top zone + // rotate if both cards are in top zone (only when not collapsed) const tz = getTopZone(); const allTop = !!tz?.querySelector('#uploadCard') && !!tz?.querySelector('#folderManagementCard'); iconEl.style.transform = (!collapsed && allTop) ? 'rotate(90deg)' : 'rotate(0deg)'; @@ -486,7 +493,31 @@ function updateTopZoneLayout() { if (top) top.style.display = (hasUpload || hasFolder) ? '' : 'none'; } -// drag visual helpers +// --- sidebar placeholder while dragging (only when empty) --- +function ensureSidebarPlaceholder() { + const sb = getSidebar(); + if (!sb) return; + if (hasSidebarCards()) return; // only when empty + let ph = sb.querySelector('.sb-dnd-placeholder'); + if (!ph) { + ph = document.createElement('div'); + ph.className = 'sb-dnd-placeholder'; + Object.assign(ph.style, { + height: '340px', + width: '100%', + visibility: 'hidden' + }); + sb.appendChild(ph); + } +} +function removeSidebarPlaceholder() { + const sb = getSidebar(); + if (!sb) return; + const ph = sb.querySelector('.sb-dnd-placeholder'); + if (ph) ph.remove(); +} + +// -------------------- DnD core -------------------- function addTopZoneHighlight() { const top = getTopZone(); if (!top) return; @@ -525,6 +556,7 @@ function cleanupTopZoneAfterDrop() { if (ph) ph.remove(); top.classList.remove('highlight'); top.style.minHeight = ''; + // ✅ fixed selector string here const hasAny = top.querySelectorAll('#uploadCard, #folderManagementCard').length > 0; top.style.display = hasAny ? '' : 'none'; } @@ -539,11 +571,10 @@ function hideHeaderDropZone() { const h = getHeaderDropArea(); if (h) { h.classList.remove('drag-active'); - if (h.children.length === 0) h.style.display = 'none'; + if (h.children.length === 0 && !isZonesCollapsed()) h.style.display = 'none'; } } -// -------------------- DnD core -------------------- function makeCardDraggable(card) { if (!card) return; const header = card.querySelector('.card-header'); @@ -573,11 +604,9 @@ function makeCardDraggable(card) { const sb = getSidebar(); if (sb) { - sb.classList.add('active'); - sb.classList.add('highlight'); + sb.classList.add('active', 'highlight'); if (!isZonesCollapsed()) sb.style.display = 'block'; - sb.style.removeProperty('height'); - sb.style.minWidth = '280px'; + ensureSidebarPlaceholder(); // make empty sidebar easy to drop into } showHeaderDropZone(); @@ -597,9 +626,8 @@ function makeCardDraggable(card) { top: initialTop + 'px', width: rect.width + 'px', height: rect.height + 'px', - minWidth: rect.width + 'px', - flexShrink: '0', - zIndex: '10000' + zIndex: '10000', + pointerEvents: 'none' }); }, 450); }); @@ -623,8 +651,7 @@ function makeCardDraggable(card) { const sb = getSidebar(); if (sb) { sb.classList.remove('highlight'); - sb.style.height = ''; - sb.style.minWidth = ''; + removeSidebarPlaceholder(); } let dropped = null; @@ -663,22 +690,20 @@ function makeCardDraggable(card) { } } - // If not dropped anywhere, return to original container if (!dropped) { + // return to original container const orig = $(card.dataset.originalContainerId); if (orig) { orig.appendChild(card); card.style.removeProperty('width'); animateVerticalSlide(card); - // keep previous zone in layout (no change) } } else { - // Persist user layout on manual move (including header) setLayoutFor(card.id, dropped); } // Clear inline drag styles - ['position', 'left', 'top', 'z-index', 'height', 'min-width', 'flex-shrink', 'transition', 'transform', 'opacity', 'width'] + ['position', 'left', 'top', 'z-index', 'height', 'min-width', 'flex-shrink', 'transition', 'transform', 'opacity', 'width', 'pointer-events'] .forEach(prop => card.style.removeProperty(prop)); removeTopZoneHighlight(); @@ -686,15 +711,52 @@ function makeCardDraggable(card) { cleanupTopZoneAfterDrop(); updateTopZoneLayout(); updateSidebarVisibility(); + updateZonesToggleUI(); }); } +// -------------------- defaults + layout -------------------- +function applyUserLayoutOrDefault() { + const layout = readLayout(); + const hasAny = Object.keys(layout).length > 0; + + if (hasAny) { + getCards().forEach(card => { + const targetZone = layout[card.id]; + if (!targetZone) return; + // On small screens: if saved zone is the sidebar, temporarily place in top cols + if (isSmallScreen() && targetZone === ZONES.SIDEBAR) { + const target = (card.id === 'uploadCard') ? ZONES.TOP_LEFT : ZONES.TOP_RIGHT; + placeCardInZone(card, target, { animate: false }); + } else { + placeCardInZone(card, targetZone, { animate: false }); + } + }); + updateTopZoneLayout(); + updateSidebarVisibility(); + return; + } + + // No saved layout yet: apply defaults + if (!isSmallScreen()) { + getCards().forEach(c => placeCardInZone(c, ZONES.SIDEBAR, { animate: false })); + } else { + getCards().forEach(c => { + const zone = (c.id === 'uploadCard') ? ZONES.TOP_LEFT : ZONES.TOP_RIGHT; + placeCardInZone(c, zone, { animate: false }); + }); + } + updateTopZoneLayout(); + updateSidebarVisibility(); + saveCurrentLayout(); // initialize baseline so future moves persist +} + // -------------------- public API -------------------- export function loadSidebarOrder() { - // Backward compat: act as "apply layout" applyUserLayoutOrDefault(); ensureZonesToggle(); updateZonesToggleUI(); + applyCollapsedBodyClass(); } export function loadHeaderOrder() { @@ -704,9 +766,9 @@ export function loadHeaderOrder() { const layout = readLayout(); - // If collapsed: all cards appear as header icons if (isZonesCollapsed()) { getCards().forEach(insertCardInHeader); + showHeaderDockPersistent(); saveHeaderOrder(); return; } @@ -715,6 +777,7 @@ export function loadHeaderOrder() { getCards().forEach(card => { if (layout[card.id] === ZONES.HEADER) insertCardInHeader(card); }); + if (header.children.length === 0) header.style.display = 'none'; saveHeaderOrder(); } @@ -727,6 +790,7 @@ export function initDragAndDrop() { // 2) Paint controls/UI ensureZonesToggle(); updateZonesToggleUI(); + applyCollapsedBodyClass(); // 3) Make cards draggable getCards().forEach(makeCardDraggable); @@ -735,9 +799,7 @@ export function initDragAndDrop() { let raf = null; const onResize = () => { if (raf) cancelAnimationFrame(raf); - raf = requestAnimationFrame(() => { - enforceResponsiveZones(); - }); + raf = requestAnimationFrame(() => enforceResponsiveZones()); }; window.addEventListener('resize', onResize); enforceResponsiveZones();