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();