diff --git a/CHANGELOG.md b/CHANGELOG.md index f45d7a2..379af8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## Changes 10/23/2025 (v1.6.1) + +feat(ui): unified zone toggle + polished interactions for sidebar/top cards + +- Add floating toggle button styling (hover lift, press, focus ring, ripple) + for #zonesToggleFloating and #sidebarToggleFloating (CSS). +- Ensure icons are visible and centered; enforce consistent sizing/color. +- Introduce unified “zones collapsed” state persisted via `localStorage.zonesCollapsed`. +- Update dragAndDrop.js to: + - manage a single floating toggle for both Sidebar and Top Zone + - keep toggle visible when cards are in Top Zone; hide only when both cards are in Header + - rotate icon 90° when both cards are in Top Zone and panels are open + - respect collapsed state during DnD flows and on load + - preserve original DnD behaviors and saved orders (sidebar/header) +- Minor layout/visibility fixes during drag (clear temp heights; honor collapsed). + +Notes: + +- No breaking API changes; existing `sidebarOrder` / `headerOrder` continue to work. +- New key: `zonesCollapsed` (string '0'/'1') controls visibility of Sidebar + Top Zone. + +UX: + +- Floating toggle feels more “material”: subtle hover elevation, press feedback, + focus ring, and click ripple to restore the prior interactive feel. +- Icons remain legible on white (explicit color set), centered in the circular button. + +--- + ## Changes 10/22/2025 (v1.6.0) feat(acl): granular per-folder permissions + stricter gates; WebDAV & UI aligned diff --git a/public/css/styles.css b/public/css/styles.css index 1d3ce30..5c37e39 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -2308,4 +2308,69 @@ body.dark-mode .user-dropdown .user-menu .item:hover { } :root { --perm-caret: #444; } /* light */ -body.dark-mode { --perm-caret: #ccc; } /* dark */ \ No newline at end of file +body.dark-mode { --perm-caret: #ccc; } /* dark */ + +#zonesToggleFloating, +#sidebarToggleFloating { + transition: + transform 160ms cubic-bezier(.2,.0,.2,1), + box-shadow 160ms cubic-bezier(.2,.0,.2,1), + border-color 160ms cubic-bezier(.2,.0,.2,1), + background-color 160ms cubic-bezier(.2,.0,.2,1); +} + +#zonesToggleFloating .material-icons, +#zonesToggleFloating .material-icons-outlined, +#sidebarToggleFloating .material-icons, +#sidebarToggleFloating .material-icons-outlined { + color: #333 !important; + font-size: 22px; + line-height: 1; + display: block; +} + +#zonesToggleFloating:hover, +#sidebarToggleFloating:hover { + transform: translateY(-1px); + box-shadow: 0 6px 16px rgba(0,0,0,.14); + border-color: #cfcfcf; +} + +#zonesToggleFloating:active, +#sidebarToggleFloating:active { + transform: translateY(0) scale(.96); + box-shadow: 0 3px 8px rgba(0,0,0,.12); +} + +#zonesToggleFloating:focus-visible, +#sidebarToggleFloating:focus-visible { + outline: none; + box-shadow: + 0 6px 16px rgba(0,0,0,.14), + 0 0 0 3px rgba(25,118,210,.25); /* soft brandy ring */ +} + +#zonesToggleFloating::after, +#sidebarToggleFloating::after { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + background: radial-gradient(circle, rgba(0,0,0,.12) 0%, rgba(0,0,0,0) 60%); + transform: scale(0); + opacity: 0; + transition: transform 300ms ease, opacity 450ms ease; + pointer-events: none; +} + +#zonesToggleFloating:active::after, +#sidebarToggleFloating:active::after { + transform: scale(1.4); + opacity: 1; +} + +#zonesToggleFloating.is-collapsed, +#sidebarToggleFloating.is-collapsed { + background: #fafafa; + border-color: #e2e2e2; +} \ No newline at end of file diff --git a/public/js/adminPanel.js b/public/js/adminPanel.js index f6d6547..b9ffee7 100644 --- a/public/js/adminPanel.js +++ b/public/js/adminPanel.js @@ -4,7 +4,7 @@ import { loadAdminConfigFunc } from './auth.js'; import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js'; import { sendRequest } from './networkUtils.js'; -const version = "v1.6.0"; +const version = "v1.6.1"; const adminTitle = `${t("admin_panel")} ${version}`; diff --git a/public/js/dragAndDrop.js b/public/js/dragAndDrop.js index 8260186..89a3a16 100644 --- a/public/js/dragAndDrop.js +++ b/public/js/dragAndDrop.js @@ -1,29 +1,234 @@ // dragAndDrop.js -// This file handles drag-and-drop functionality for cards in the sidebar, header and top drop zones. -// It also manages the visibility of the sidebar and header drop zones based on the current state of the application. -// It includes functions to save and load the order of cards in the sidebar and header from localStorage. -// It also includes functions to handle the drag-and-drop events, including mouse movements and drop zones. -// It uses CSS classes to manage the appearance of the sidebar and header drop zones during drag-and-drop operations. +// Enhances the dashboard with drag-and-drop functionality for cards: +// - injects a tiny floating toggle btn +// - remembers collapsed state in localStorage +// - keeps the original card DnD + order logic intact // ---- responsive defaults ---- const MEDIUM_MIN = 1205; // matches your small-screen cutoff const MEDIUM_MAX = 1600; // tweak as you like +const TOGGLE_TOP_PX = 10; +const TOGGLE_LEFT_PX = 100; + +const TOGGLE_ICON_OPEN = 'view_sidebar'; +const TOGGLE_ICON_CLOSED = 'menu'; + +function updateSidebarToggleUI() { + const btn = document.getElementById('sidebarToggleFloating'); + const sidebar = getSidebar(); + if (!btn || !sidebar) return; + + if (!hasSidebarCards()) { btn.remove(); return; } + + const collapsed = isSidebarCollapsed(); + btn.innerHTML = ``; + btn.title = collapsed ? 'Show sidebar' : 'Hide sidebar'; + btn.style.display = 'block'; + btn.classList.toggle('toggle-ping', collapsed); +} + + +function hasSidebarCards() { + const sb = getSidebar(); + return !!sb && sb.querySelectorAll('#uploadCard, #folderManagementCard').length > 0; +} + +function hasTopZoneCards() { + const tz = getTopZone(); + return !!tz && tz.querySelectorAll('#uploadCard, #folderManagementCard').length > 0; +} + +// Both cards are in the top zone (upload + folder) +function allCardsInTopZone() { + const tz = getTopZone(); + if (!tz) return false; + const hasUpload = !!tz.querySelector('#uploadCard'); + const hasFolder = !!tz.querySelector('#folderManagementCard'); + return hasUpload && hasFolder; + } + +function isZonesCollapsed() { + return localStorage.getItem('zonesCollapsed') === '1'; +} +function setZonesCollapsed(collapsed) { + localStorage.setItem('zonesCollapsed', collapsed ? '1' : '0'); + applyZonesCollapsed(); + updateZonesToggleUI(); +} +function applyZonesCollapsed() { + const collapsed = isZonesCollapsed(); + const sidebar = getSidebar(); + const topZone = getTopZone(); + + if (sidebar) sidebar.style.display = collapsed ? 'none' : (hasSidebarCards() ? 'block' : 'none'); + if (topZone) topZone.style.display = collapsed ? 'none' : (hasTopZoneCards() ? '' : ''); +} + function isMediumScreen() { const w = window.innerWidth; return w >= MEDIUM_MIN && w < MEDIUM_MAX; } +// ----- Sidebar collapse state helpers ----- +function getSidebar() { + return document.getElementById('sidebarDropArea'); +} +function getTopZone() { + return document.getElementById('uploadFolderRow'); +} + +function isSidebarCollapsed() { + return localStorage.getItem('sidebarCollapsed') === '1'; +} + +function setSidebarCollapsed(collapsed) { + localStorage.setItem('sidebarCollapsed', collapsed ? '1' : '0'); + applySidebarCollapsed(); + updateSidebarToggleUI(); +} + +function applySidebarCollapsed() { + const sidebar = getSidebar(); + if (!sidebar) return; + + // We avoid hard-coding layout assumptions: simply hide/show the sidebar area. + // If you want a sliding effect, add CSS later; JS will just toggle display. + const collapsed = isSidebarCollapsed(); + sidebar.style.display = collapsed ? 'none' : 'block'; +} + +function ensureZonesToggle() { + // show only if at least one zone *can* show a card + const shouldShow = hasSidebarCards() || hasTopZoneCards(); + + let btn = document.getElementById('sidebarToggleFloating'); + if (!shouldShow) { + if (btn) btn.remove(); + return; + } + if (!btn) { + btn = document.createElement('button'); + btn.id = 'sidebarToggleFloating'; + btn.type = 'button'; + btn.setAttribute('aria-label', 'Toggle panels'); + Object.assign(btn.style, { + position: 'fixed', + left: `${TOGGLE_LEFT_PX}px`, + top: `${TOGGLE_TOP_PX}px`, + zIndex: '1000', + width: '38px', + height: '38px', + borderRadius: '19px', + border: '1px solid #ccc', + background: '#fff', + cursor: 'pointer', + boxShadow: '0 2px 6px rgba(0,0,0,.15)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: '0', + lineHeight: '0', + }); + btn.addEventListener('click', () => { + setZonesCollapsed(!isZonesCollapsed()); + }); + document.body.appendChild(btn); + } + updateZonesToggleUI(); +} +function updateZonesToggleUI() { + const btn = document.getElementById('sidebarToggleFloating'); + if (!btn) return; + + // if neither zone has cards, remove the toggle + if (!hasSidebarCards() && !hasTopZoneCards()) { + btn.remove(); + return; + } + + const collapsed = isZonesCollapsed(); + const iconName = collapsed ? TOGGLE_ICON_CLOSED : TOGGLE_ICON_OPEN; + btn.innerHTML = ``; + btn.title = collapsed ? 'Show panels' : 'Hide panels'; + btn.style.display = 'block'; + + // Rotate the icon 90° when BOTH cards are in the top zone and panels are open + const iconEl = btn.querySelector('.toggle-icon'); + if (iconEl) { + iconEl.style.transition = 'transform 0.2s ease'; + iconEl.style.display = 'inline-flex'; + iconEl.style.alignItems = 'center'; + if (!collapsed && allCardsInTopZone()) { + iconEl.style.transform = 'rotate(90deg)'; + } else { + iconEl.style.transform = 'rotate(0deg)'; + } + } +} + +// create a small floating toggle button (no HTML edits needed) +function ensureSidebarToggle() { + const sidebar = getSidebar(); + if (!sidebar) return; + + // Only show if there are cards + if (!hasSidebarCards()) { + const existing = document.getElementById('sidebarToggleFloating'); + if (existing) existing.remove(); + return; + } + + let btn = document.getElementById('sidebarToggleFloating'); + if (!btn) { + btn = document.createElement('button'); + btn.id = 'sidebarToggleFloating'; + btn.type = 'button'; + btn.setAttribute('aria-label', 'Toggle sidebar'); + + Object.assign(btn.style, { + position: 'fixed', + left: `${TOGGLE_LEFT_PX}px`, + top: `${TOGGLE_TOP_PX}px`, + zIndex: '10010', + width: '38px', + height: '38px', + borderRadius: '19px', + border: '1px solid #ccc', + background: '#fff', + cursor: 'pointer', + boxShadow: '0 2px 6px rgba(0,0,0,.15)', + display: 'block', + }); + + btn.addEventListener('click', () => { + setSidebarCollapsed(!isSidebarCollapsed()); + // icon/title/animation update after state change + updateSidebarToggleUI(); + }); + + document.body.appendChild(btn); + } + + // set correct icon/title right away + //updateSidebarToggleUI(); + //applySidebarCollapsed(); + updateZonesToggleUI(); + applyZonesCollapsed(); +} + // Moves cards into the sidebar based on the saved order in localStorage. export function loadSidebarOrder() { - const sidebar = document.getElementById('sidebarDropArea'); + const sidebar = getSidebar(); if (!sidebar) return; const orderStr = localStorage.getItem('sidebarOrder'); const headerOrderStr = localStorage.getItem('headerOrder'); - const defaultAppliedKey = 'layoutDefaultApplied_v1'; // bump the suffix if you ever change logic + const defaultAppliedKey = 'layoutDefaultApplied_v1'; // bump if logic changes - // If we have a saved order (sidebar or header), just honor it as before + // If we have a saved order (sidebar), honor it as before if (orderStr) { const order = JSON.parse(orderStr || '[]'); if (Array.isArray(order) && order.length > 0) { @@ -37,6 +242,11 @@ export function loadSidebarOrder() { } }); updateSidebarVisibility(); + //applySidebarCollapsed(); // NEW: honor collapsed state + //ensureSidebarToggle(); // NEW: inject toggle + applyZonesCollapsed(); + ensureZonesToggle(); + return; } } @@ -45,6 +255,10 @@ export function loadSidebarOrder() { const headerOrder = JSON.parse(headerOrderStr || '[]'); if (Array.isArray(headerOrder) && headerOrder.length > 0) { updateSidebarVisibility(); + //applySidebarCollapsed(); + //ensureSidebarToggle(); + applyZonesCollapsed(); + ensureZonesToggle(); return; } @@ -72,73 +286,85 @@ export function loadSidebarOrder() { } updateSidebarVisibility(); + //applySidebarCollapsed(); + //ensureSidebarToggle(); + applyZonesCollapsed(); + ensureZonesToggle(); } - - export function loadHeaderOrder() { - const headerDropArea = document.getElementById('headerDropArea'); - if (!headerDropArea) return; - - // 1) Clear out any icons that might already be in the drop area - headerDropArea.innerHTML = ''; - - // 2) Read the saved array (or empty array if invalid/missing) - let stored; - try { - stored = JSON.parse(localStorage.getItem('headerOrder') || '[]'); - } catch { - stored = []; - } - - // 3) Deduplicate IDs - const uniqueIds = Array.from(new Set(stored)); - - // 4) Re-insert exactly one icon per saved card ID - uniqueIds.forEach(id => { - const card = document.getElementById(id); - if (card) insertCardInHeader(card, null); - }); - - // 5) Persist the cleaned, deduped list back to storage - localStorage.setItem('headerOrder', JSON.stringify(uniqueIds)); +export function loadHeaderOrder() { + const headerDropArea = document.getElementById('headerDropArea'); + if (!headerDropArea) return; + + // 1) Clear out any icons that might already be in the drop area + headerDropArea.innerHTML = ''; + + // 2) Read the saved array (or empty array if invalid/missing) + let stored; + try { + stored = JSON.parse(localStorage.getItem('headerOrder') || '[]'); + } catch { + stored = []; } - - // Internal helper: update sidebar visibility based on its content. - function updateSidebarVisibility() { - const sidebar = document.getElementById('sidebarDropArea'); - if (sidebar) { - const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard'); - if (cards.length > 0) { + + // 3) Deduplicate IDs + const uniqueIds = Array.from(new Set(stored)); + + // 4) Re-insert exactly one icon per saved card ID + uniqueIds.forEach(id => { + const card = document.getElementById(id); + if (card) insertCardInHeader(card, null); + }); + + // 5) Persist the cleaned, deduped list back to storage + localStorage.setItem('headerOrder', JSON.stringify(uniqueIds)); +} + +// Internal helper: update sidebar visibility based on its content. +// NOTE: do NOT auto-hide if user manually collapsed; that is separate. +function updateSidebarVisibility() { + const sidebar = getSidebar(); + if (!sidebar) return; + + const anyCards = hasSidebarCards(); + + // clear any leftover drag height + sidebar.style.height = ''; + + if (anyCards) { sidebar.classList.add('active'); - sidebar.style.display = 'block'; + // respect the unified zones-collapsed switch + sidebar.style.display = isZonesCollapsed() ? 'none' : 'block'; } else { sidebar.classList.remove('active'); sidebar.style.display = 'none'; } - // Save the current order in localStorage. - saveSidebarOrder(); - } + + // Save order and update toggle visibility + saveSidebarOrder(); + ensureZonesToggle(); // will hide/remove the button if no cards +} + +// NEW: Save header order to localStorage. +function saveHeaderOrder() { + const headerDropArea = document.getElementById('headerDropArea'); + if (headerDropArea) { + const icons = Array.from(headerDropArea.children); + // Each header icon stores its associated card in the property cardElement. + const order = icons.map(icon => icon.cardElement.id); + localStorage.setItem('headerOrder', JSON.stringify(order)); } - - // NEW: Save header order to localStorage. - function saveHeaderOrder() { - const headerDropArea = document.getElementById('headerDropArea'); - if (headerDropArea) { - const icons = Array.from(headerDropArea.children); - // Each header icon stores its associated card in the property cardElement. - const order = icons.map(icon => icon.cardElement.id); - localStorage.setItem('headerOrder', JSON.stringify(order)); - } - } - - // Internal helper: update top zone layout (center a card if one column is empty). - function updateTopZoneLayout() { - const leftCol = document.getElementById('leftCol'); - const rightCol = document.getElementById('rightCol'); - - const leftIsEmpty = !leftCol.querySelector('#uploadCard'); - const rightIsEmpty = !rightCol.querySelector('#folderManagementCard'); - +} + +// Internal helper: update top zone layout (center a card if one column is empty). +function updateTopZoneLayout() { + const leftCol = document.getElementById('leftCol'); + const rightCol = document.getElementById('rightCol'); + + const leftIsEmpty = !leftCol?.querySelector('#uploadCard'); + const rightIsEmpty = !rightCol?.querySelector('#folderManagementCard'); + + if (leftCol && rightCol) { if (leftIsEmpty && !rightIsEmpty) { leftCol.style.display = 'none'; rightCol.style.margin = '0 auto'; @@ -152,385 +378,316 @@ export function loadSidebarOrder() { rightCol.style.margin = ''; } } - - // When a card is being dragged, if the top drop zone is empty, set its min-height. - function addTopZoneHighlight() { - const topZone = document.getElementById('uploadFolderRow'); - if (topZone) { - topZone.classList.add('highlight'); - if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) { - topZone.style.minHeight = '375px'; - } - } - } - - // When the drag ends, remove the extra min-height. - function removeTopZoneHighlight() { - const topZone = document.getElementById('uploadFolderRow'); - if (topZone) { - topZone.classList.remove('highlight'); - topZone.style.minHeight = ''; - } - } - - // Vertical slide/fade animation helper. - function animateVerticalSlide(card) { - card.style.transform = 'translateY(30px)'; - card.style.opacity = '0'; - // Force reflow. - card.offsetWidth; - requestAnimationFrame(() => { - card.style.transition = 'transform 0.3s ease, opacity 0.3s ease'; - card.style.transform = 'translateY(0)'; - card.style.opacity = '1'; - }); - setTimeout(() => { - card.style.transition = ''; - card.style.transform = ''; - card.style.opacity = ''; - }, 310); - } - - // Internal helper: insert card into sidebar at a proper position based on event.clientY. - function insertCardInSidebar(card, event) { - const sidebar = document.getElementById('sidebarDropArea'); - if (!sidebar) return; - const existingCards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard')); - let inserted = false; - for (const currentCard of existingCards) { - const rect = currentCard.getBoundingClientRect(); - const midY = rect.top + rect.height / 2; - if (event.clientY < midY) { - sidebar.insertBefore(card, currentCard); - inserted = true; - break; - } - } - if (!inserted) { - sidebar.appendChild(card); - } - // Ensure card fills the sidebar. - card.style.width = '100%'; - animateVerticalSlide(card); - } - - // Internal helper: save the current sidebar card order to localStorage. - function saveSidebarOrder() { - const sidebar = document.getElementById('sidebarDropArea'); - if (sidebar) { - const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard'); - const order = Array.from(cards).map(card => card.id); - localStorage.setItem('sidebarOrder', JSON.stringify(order)); - } - } - - // Helper: move cards from sidebar back to the top drop area when on small screens. - function moveSidebarCardsToTop() { - if (window.innerWidth < 1205) { - const sidebar = document.getElementById('sidebarDropArea'); - if (!sidebar) return; - const cards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard')); - cards.forEach(card => { - const orig = document.getElementById(card.dataset.originalContainerId); - if (orig) { - orig.appendChild(card); - animateVerticalSlide(card); - } - }); - updateSidebarVisibility(); - updateTopZoneLayout(); - } - } - - // Listen for window resize to automatically move sidebar cards back to top on small screens. - window.addEventListener('resize', function () { - if (window.innerWidth < 1205) { - moveSidebarCardsToTop(); - } - }); - - // This function ensures the top drop zone (#uploadFolderRow) has a stable width when empty. - function ensureTopZonePlaceholder() { - const topZone = document.getElementById('uploadFolderRow'); - if (!topZone) return; +} + +// When a card is being dragged, if the top drop zone is empty, set its min-height. +function addTopZoneHighlight() { + const topZone = document.getElementById('uploadFolderRow'); + if (topZone) { + topZone.classList.add('highlight'); if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) { - let placeholder = topZone.querySelector('.placeholder'); - if (!placeholder) { - placeholder = document.createElement('div'); - placeholder.className = 'placeholder'; - placeholder.style.visibility = 'hidden'; - placeholder.style.display = 'block'; - placeholder.style.width = '100%'; - placeholder.style.height = '375px'; - topZone.appendChild(placeholder); - } - } else { - const placeholder = topZone.querySelector('.placeholder'); - if (placeholder) placeholder.remove(); + topZone.style.minHeight = '375px'; } } - - // --- NEW HELPER FUNCTIONS FOR HEADER DROP ZONE --- - - // Show header drop zone and add a "drag-active" class so that the pseudo-element appears. - function showHeaderDropZone() { - const headerDropArea = document.getElementById('headerDropArea'); - if (headerDropArea) { - headerDropArea.style.display = 'inline-flex'; - headerDropArea.classList.add('drag-active'); +} + +// When the drag ends, remove the extra min-height. +function removeTopZoneHighlight() { + const topZone = document.getElementById('uploadFolderRow'); + if (topZone) { + topZone.classList.remove('highlight'); + topZone.style.minHeight = ''; + } +} + +// Vertical slide/fade animation helper. +function animateVerticalSlide(card) { + card.style.transform = 'translateY(30px)'; + card.style.opacity = '0'; + // Force reflow. + card.offsetWidth; + requestAnimationFrame(() => { + card.style.transition = 'transform 0.3s ease, opacity 0.3s ease'; + card.style.transform = 'translateY(0)'; + card.style.opacity = '1'; + }); + setTimeout(() => { + card.style.transition = ''; + card.style.transform = ''; + card.style.opacity = ''; + }, 310); +} + +// Internal helper: insert card into sidebar at a proper position based on event.clientY. +function insertCardInSidebar(card, event) { + const sidebar = getSidebar(); + if (!sidebar) return; + const existingCards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard')); + let inserted = false; + for (const currentCard of existingCards) { + const rect = currentCard.getBoundingClientRect(); + const midY = rect.top + rect.height / 2; + if (event.clientY < midY) { + sidebar.insertBefore(card, currentCard); + inserted = true; + break; } } - - // Hide header drop zone by removing the "drag-active" class. - // If a header icon is present (i.e. a card was dropped), the drop zone remains visible without the dashed border. - function hideHeaderDropZone() { - const headerDropArea = document.getElementById('headerDropArea'); - if (headerDropArea) { - headerDropArea.classList.remove('drag-active'); - if (headerDropArea.children.length === 0) { - headerDropArea.style.display = 'none'; + if (!inserted) { + sidebar.appendChild(card); + } + // Ensure card fills the sidebar. + card.style.width = '100%'; + animateVerticalSlide(card); + // if user dropped into sidebar, auto-un-collapse if currently collapsed + if (isZonesCollapsed()) setZonesCollapsed(false); +} + +// Internal helper: save the current sidebar card order to localStorage. +function saveSidebarOrder() { + const sidebar = getSidebar(); + if (sidebar) { + const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard'); + const order = Array.from(cards).map(card => card.id); + localStorage.setItem('sidebarOrder', JSON.stringify(order)); + } +} + +// Helper: move cards from sidebar back to the top drop area when on small screens. +function moveSidebarCardsToTop() { + if (window.innerWidth < 1205) { + const sidebar = getSidebar(); + if (!sidebar) return; + const cards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard')); + cards.forEach(card => { + const orig = document.getElementById(card.dataset.originalContainerId); + if (orig) { + orig.appendChild(card); + animateVerticalSlide(card); } + }); + updateSidebarVisibility(); + updateTopZoneLayout(); + } +} + +// Listen for window resize to automatically move sidebar cards back to top on small screens. +window.addEventListener('resize', function () { + if (window.innerWidth < 1205) { + moveSidebarCardsToTop(); + } +}); + +// This function ensures the top drop zone (#uploadFolderRow) has a stable width when empty. +function ensureTopZonePlaceholder() { + const topZone = document.getElementById('uploadFolderRow'); + if (!topZone) return; + if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) { + let placeholder = topZone.querySelector('.placeholder'); + if (!placeholder) { + placeholder = document.createElement('div'); + placeholder.className = 'placeholder'; + placeholder.style.visibility = 'hidden'; + placeholder.style.display = 'block'; + placeholder.style.width = '100%'; + placeholder.style.height = '375px'; + topZone.appendChild(placeholder); + } + } else { + const placeholder = topZone.querySelector('.placeholder'); + if (placeholder) placeholder.remove(); + } +} + +// --- Header drop zone helpers --- + +function showHeaderDropZone() { + const headerDropArea = document.getElementById('headerDropArea'); + if (headerDropArea) { + headerDropArea.style.display = 'inline-flex'; + headerDropArea.classList.add('drag-active'); + } +} + +function hideHeaderDropZone() { + const headerDropArea = document.getElementById('headerDropArea'); + if (headerDropArea) { + headerDropArea.classList.remove('drag-active'); + if (headerDropArea.children.length === 0) { + headerDropArea.style.display = 'none'; } } - - // === NEW FUNCTION: Insert card into header drop zone as a material icon === - function insertCardInHeader(card, event) { - const headerDropArea = document.getElementById('headerDropArea'); - if (!headerDropArea) return; - - // For folder management and upload cards, preserve the original by moving it to a hidden container. - if (card.id === 'folderManagementCard' || card.id === 'uploadCard') { - let hiddenContainer = document.getElementById('hiddenCardsContainer'); - if (!hiddenContainer) { - hiddenContainer = document.createElement('div'); - hiddenContainer.id = 'hiddenCardsContainer'; - hiddenContainer.style.display = 'none'; - document.body.appendChild(hiddenContainer); - } - // Move the original card to the hidden container if it's not already there. - if (card.parentNode.id !== 'hiddenCardsContainer') { - hiddenContainer.appendChild(card); - } - } else { - // For other cards, simply remove from current container. - if (card.parentNode) { - card.parentNode.removeChild(card); - } - } - - // Create the header icon button. - const iconButton = document.createElement('button'); - iconButton.className = 'header-card-icon'; - // Remove default button styling. - iconButton.style.border = 'none'; - iconButton.style.background = 'none'; - iconButton.style.outline = 'none'; - iconButton.style.cursor = 'pointer'; - - // Choose an icon based on the card type with 24px size. - if (card.id === 'uploadCard') { - iconButton.innerHTML = 'cloud_upload'; - } else if (card.id === 'folderManagementCard') { - iconButton.innerHTML = 'folder'; - } else { - iconButton.innerHTML = 'insert_drive_file'; - } - - // Save a reference to the card in the icon button. - iconButton.cardElement = card; - // Associate this icon with the card for future removal. - card.headerIconButton = iconButton; - - let modal = null; - let isLocked = false; - let hoverActive = false; - - // showModal: When triggered, ensure the card is attached to the modal. - function showModal() { - if (!modal) { - modal = document.createElement('div'); - modal.className = 'header-card-modal'; - modal.style.position = 'fixed'; - modal.style.top = '55px'; - modal.style.right = '80px'; - modal.style.zIndex = '11000'; - // Render the modal but initially keep it hidden. - modal.style.display = 'block'; - modal.style.visibility = 'hidden'; - modal.style.opacity = '0'; - modal.style.background = 'none'; - modal.style.border = 'none'; - modal.style.padding = '0'; - modal.style.boxShadow = 'none'; - document.body.appendChild(modal); - // Attach modal hover events. - modal.addEventListener('mouseover', handleMouseOver); - modal.addEventListener('mouseout', handleMouseOut); - iconButton.modalInstance = modal; - } - // If the card isn't already in the modal, remove it from the hidden container and attach it. - if (!modal.contains(card)) { - const hiddenContainer = document.getElementById('hiddenCardsContainer'); - if (hiddenContainer && hiddenContainer.contains(card)) { - hiddenContainer.removeChild(card); - } - modal.appendChild(card); - } - // Reveal the modal. - modal.style.visibility = 'visible'; - modal.style.opacity = '1'; - } - - // hideModal: Hide the modal and return the card to the hidden container. - function hideModal() { - if (modal && !isLocked && !hoverActive) { - modal.style.visibility = 'hidden'; - modal.style.opacity = '0'; - // Return the card to the hidden container. - const hiddenContainer = document.getElementById('hiddenCardsContainer'); - if (hiddenContainer && modal.contains(card)) { - hiddenContainer.appendChild(card); - } - } - } - - function handleMouseOver() { - hoverActive = true; - showModal(); - } - - function handleMouseOut() { - hoverActive = false; - setTimeout(() => { - if (!hoverActive && !isLocked) { - hideModal(); - } - }, 300); - } - - // Attach hover events to the icon. - iconButton.addEventListener('mouseover', handleMouseOver); - iconButton.addEventListener('mouseout', handleMouseOut); - - // Toggle the locked state on click so the modal stays open. - iconButton.addEventListener('click', (e) => { - isLocked = !isLocked; - if (isLocked) { - showModal(); - } else { - hideModal(); - } - e.stopPropagation(); +} + +// Insert card into header drop zone as a material icon +function insertCardInHeader(card, event) { + const headerDropArea = document.getElementById('headerDropArea'); + if (!headerDropArea) return; + + // Preserve the original by moving it to a hidden container. + if (card.id === 'folderManagementCard' || card.id === 'uploadCard') { + let hiddenContainer = document.getElementById('hiddenCardsContainer'); + if (!hiddenContainer) { + hiddenContainer = document.createElement('div'); + hiddenContainer.id = 'hiddenCardsContainer'; + hiddenContainer.style.display = 'none'; + document.body.appendChild(hiddenContainer); + } + if (card.parentNode?.id !== 'hiddenCardsContainer') { + hiddenContainer.appendChild(card); + } + } else if (card.parentNode) { + card.parentNode.removeChild(card); + } + + const iconButton = document.createElement('button'); + iconButton.className = 'header-card-icon'; + iconButton.style.border = 'none'; + iconButton.style.background = 'none'; + iconButton.style.outline = 'none'; + iconButton.style.cursor = 'pointer'; + + if (card.id === 'uploadCard') { + iconButton.innerHTML = 'cloud_upload'; + } else if (card.id === 'folderManagementCard') { + iconButton.innerHTML = 'folder'; + } else { + iconButton.innerHTML = 'insert_drive_file'; + } + + iconButton.cardElement = card; + card.headerIconButton = iconButton; + + let modal = null; + let isLocked = false; + let hoverActive = false; + + function showModal() { + if (!modal) { + modal = document.createElement('div'); + modal.className = 'header-card-modal'; + Object.assign(modal.style, { + position: 'fixed', + top: '55px', + right: '80px', + zIndex: '11000', + display: 'block', + visibility: 'hidden', + opacity: '0', + background: 'none', + border: 'none', + padding: '0', + boxShadow: 'none', }); - - // Append the header icon button into the header drop zone. - headerDropArea.appendChild(iconButton); - // Save the updated header order. - saveHeaderOrder(); + document.body.appendChild(modal); + modal.addEventListener('mouseover', handleMouseOver); + modal.addEventListener('mouseout', handleMouseOut); + iconButton.modalInstance = modal; } - - // === Main Drag and Drop Initialization === - export function initDragAndDrop() { - function run() { - const draggableCards = document.querySelectorAll('#uploadCard, #folderManagementCard'); - draggableCards.forEach(card => { - if (!card.dataset.originalContainerId) { - card.dataset.originalContainerId = card.parentNode.id; - } - const header = card.querySelector('.card-header'); - if (header) { - header.classList.add('drag-header'); - } - - let isDragging = false; - let dragTimer = null; - let offsetX = 0, offsetY = 0; - let initialLeft, initialTop; - - if (header) { - header.addEventListener('mousedown', function (e) { - e.preventDefault(); - const card = this.closest('.card'); - // Capture the card's initial bounding rectangle. - const initialRect = card.getBoundingClientRect(); - const originX = ((e.clientX - initialRect.left) / initialRect.width) * 100; - const originY = ((e.clientY - initialRect.top) / initialRect.height) * 100; - card.style.transformOrigin = `${originX}% ${originY}%`; - - // Store the initial rect so we use it later. - dragTimer = setTimeout(() => { - isDragging = true; - card.classList.add('dragging'); - card.style.pointerEvents = 'none'; - addTopZoneHighlight(); - - const sidebar = document.getElementById('sidebarDropArea'); - if (sidebar) { - sidebar.classList.add('active'); - sidebar.style.display = 'block'; - sidebar.classList.add('highlight'); - sidebar.style.height = '800px'; - } - - // Show header drop zone while dragging. - showHeaderDropZone(); - - // Use the stored initialRect. - initialLeft = initialRect.left + window.pageXOffset; - initialTop = initialRect.top + window.pageYOffset; - offsetX = e.pageX - initialLeft; - offsetY = e.pageY - initialTop; - - // Remove any associated header icon if present. - if (card.headerIconButton) { - if (card.headerIconButton.parentNode) { - card.headerIconButton.parentNode.removeChild(card.headerIconButton); - } - if (card.headerIconButton.modalInstance && card.headerIconButton.modalInstance.parentNode) { - card.headerIconButton.modalInstance.parentNode.removeChild(card.headerIconButton.modalInstance); - } - card.headerIconButton = null; - saveHeaderOrder(); - } - - // Append card to body and fix its dimensions. - document.body.appendChild(card); - card.style.position = 'absolute'; - card.style.left = initialLeft + 'px'; - card.style.top = initialTop + 'px'; - card.style.width = initialRect.width + 'px'; - card.style.height = initialRect.height + 'px'; - card.style.minWidth = initialRect.width + 'px'; - card.style.flexShrink = '0'; - card.style.zIndex = '10000'; - }, 500); - }); - header.addEventListener('mouseup', function () { - clearTimeout(dragTimer); - }); - } - - document.addEventListener('mousemove', function (e) { - if (isDragging) { - card.style.left = (e.pageX - offsetX) + 'px'; - card.style.top = (e.pageY - offsetY) + 'px'; - } - }); - - document.addEventListener('mouseup', function (e) { - if (isDragging) { - isDragging = false; - card.style.pointerEvents = ''; - card.classList.remove('dragging'); - removeTopZoneHighlight(); - - const sidebar = document.getElementById('sidebarDropArea'); + if (!modal.contains(card)) { + const hiddenContainer = document.getElementById('hiddenCardsContainer'); + if (hiddenContainer && hiddenContainer.contains(card)) { + hiddenContainer.removeChild(card); + } + modal.appendChild(card); + } + modal.style.visibility = 'visible'; + modal.style.opacity = '1'; + } + + function hideModal() { + if (modal && !isLocked && !hoverActive) { + modal.style.visibility = 'hidden'; + modal.style.opacity = '0'; + const hiddenContainer = document.getElementById('hiddenCardsContainer'); + if (hiddenContainer && modal.contains(card)) { + hiddenContainer.appendChild(card); + } + } + } + + function handleMouseOver() { + hoverActive = true; + showModal(); + } + + function handleMouseOut() { + hoverActive = false; + setTimeout(() => { + if (!hoverActive && !isLocked) { + hideModal(); + } + }, 300); + } + + iconButton.addEventListener('mouseover', handleMouseOver); + iconButton.addEventListener('mouseout', handleMouseOut); + + iconButton.addEventListener('click', (e) => { + isLocked = !isLocked; + if (isLocked) showModal(); + else hideModal(); + e.stopPropagation(); + }); + + headerDropArea.appendChild(iconButton); + saveHeaderOrder(); +} + +// === Main Drag and Drop Initialization === +export function initDragAndDrop() { + function run() { + // make sure toggle exists even if user hasn't dragged yet + // ensureSidebarToggle(); + //applySidebarCollapsed(); + applyZonesCollapsed(); + ensureZonesToggle(); + + const draggableCards = document.querySelectorAll('#uploadCard, #folderManagementCard'); + draggableCards.forEach(card => { + if (!card.dataset.originalContainerId && card.parentNode) { + card.dataset.originalContainerId = card.parentNode.id; + } + const header = card.querySelector('.card-header'); + if (header) { + header.classList.add('drag-header'); + } + + let isDragging = false; + let dragTimer = null; + let offsetX = 0, offsetY = 0; + let initialLeft, initialTop; + + if (header) { + header.addEventListener('mousedown', function (e) { + e.preventDefault(); + const card = this.closest('.card'); + const initialRect = card.getBoundingClientRect(); + const originX = ((e.clientX - initialRect.left) / initialRect.width) * 100; + const originY = ((e.clientY - initialRect.top) / initialRect.height) * 100; + card.style.transformOrigin = `${originX}% ${originY}%`; + + dragTimer = setTimeout(() => { + isDragging = true; + card.classList.add('dragging'); + card.style.pointerEvents = 'none'; + addTopZoneHighlight(); + + const sidebar = getSidebar(); if (sidebar) { - sidebar.classList.remove('highlight'); - sidebar.style.height = ''; + sidebar.classList.add('active'); + sidebar.style.display = isSidebarCollapsed() ? 'none' : 'block'; + sidebar.classList.add('highlight'); + sidebar.style.height = '800px'; } - - // Remove any existing header icon if present. + + showHeaderDropZone(); + + initialLeft = initialRect.left + window.pageXOffset; + initialTop = initialRect.top + window.pageYOffset; + offsetX = e.pageX - initialLeft; + offsetY = e.pageY - initialTop; + if (card.headerIconButton) { if (card.headerIconButton.parentNode) { card.headerIconButton.parentNode.removeChild(card.headerIconButton); @@ -541,111 +698,159 @@ export function loadSidebarOrder() { card.headerIconButton = null; saveHeaderOrder(); } - - let droppedInSidebar = false; - let droppedInTop = false; - let droppedInHeader = false; - - // Check if dropped in sidebar drop zone. - const sidebarElem = document.getElementById('sidebarDropArea'); - if (sidebarElem) { - const rect = sidebarElem.getBoundingClientRect(); - const dropZoneBottom = rect.top + 800; // Virtual drop zone height. - if ( - e.clientX >= rect.left && - e.clientX <= rect.right && - e.clientY >= rect.top && - e.clientY <= dropZoneBottom - ) { - insertCardInSidebar(card, e); - droppedInSidebar = true; - } - } - // Check the top drop zone. - const topRow = document.getElementById('uploadFolderRow'); - if (!droppedInSidebar && topRow) { - const rect = topRow.getBoundingClientRect(); - if ( - e.clientX >= rect.left && - e.clientX <= rect.right && - e.clientY >= rect.top && - e.clientY <= rect.bottom - ) { - let container; - if (card.id === 'uploadCard') { - container = document.getElementById('leftCol'); - } else if (card.id === 'folderManagementCard') { - container = document.getElementById('rightCol'); - } - if (container) { - ensureTopZonePlaceholder(); - updateTopZoneLayout(); - container.appendChild(card); - droppedInTop = true; - // Set a fixed width during animation. - card.style.width = "363px"; - animateVerticalSlide(card); - setTimeout(() => { - card.style.removeProperty('width'); - }, 210); - } - } - } - // Check the header drop zone. - const headerDropArea = document.getElementById('headerDropArea'); - if (!droppedInSidebar && !droppedInTop && headerDropArea) { - const rect = headerDropArea.getBoundingClientRect(); - if ( - e.clientX >= rect.left && - e.clientX <= rect.right && - e.clientY >= rect.top && - e.clientY <= rect.bottom - ) { - insertCardInHeader(card, e); - droppedInHeader = true; - } - } - // If card was not dropped in any zone, return it to its original container. - if (!droppedInSidebar && !droppedInTop && !droppedInHeader) { - const orig = document.getElementById(card.dataset.originalContainerId); - if (orig) { - orig.appendChild(card); - card.style.removeProperty('width'); - } - } - - // Clear inline drag-related styles. - [ - 'position', - 'left', - 'top', - 'z-index', - 'height', - 'min-width', - 'flex-shrink', - 'transition', - 'transform', - 'opacity' - ].forEach(prop => card.style.removeProperty(prop)); - - // For sidebar drops, force width to 100%. - if (droppedInSidebar) { - card.style.width = '100%'; - } - - updateTopZoneLayout(); - updateSidebarVisibility(); - - // Hide header drop zone if no icon is present. - hideHeaderDropZone(); - } + + document.body.appendChild(card); + card.style.position = 'absolute'; + card.style.left = initialLeft + 'px'; + card.style.top = initialTop + 'px'; + card.style.width = initialRect.width + 'px'; + card.style.height = initialRect.height + 'px'; + card.style.minWidth = initialRect.width + 'px'; + card.style.flexShrink = '0'; + card.style.zIndex = '10000'; + }, 500); }); + + header.addEventListener('mouseup', function () { + clearTimeout(dragTimer); + }); + } + + document.addEventListener('mousemove', function (e) { + if (isDragging) { + card.style.left = (e.pageX - offsetX) + 'px'; + card.style.top = (e.pageY - offsetY) + 'px'; + } }); - } - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', run); - } else { - run(); - } - } \ No newline at end of file + + document.addEventListener('mouseup', function (e) { + if (isDragging) { + isDragging = false; + card.style.pointerEvents = ''; + card.classList.remove('dragging'); + removeTopZoneHighlight(); + + const sidebar = getSidebar(); + if (sidebar) { + sidebar.classList.remove('highlight'); + sidebar.style.height = ''; + } + + if (card.headerIconButton) { + if (card.headerIconButton.parentNode) { + card.headerIconButton.parentNode.removeChild(card.headerIconButton); + } + if (card.headerIconButton.modalInstance && card.headerIconButton.modalInstance.parentNode) { + card.headerIconButton.modalInstance.parentNode.removeChild(card.headerIconButton.modalInstance); + } + card.headerIconButton = null; + saveHeaderOrder(); + } + + let droppedInSidebar = false; + let droppedInTop = false; + let droppedInHeader = false; + + // Check if dropped in sidebar drop zone. + const sidebarElem = getSidebar(); + if (sidebarElem) { + const rect = sidebarElem.getBoundingClientRect(); + const dropZoneBottom = rect.top + 800; // Virtual drop zone height. + if ( + e.clientX >= rect.left && + e.clientX <= rect.right && + e.clientY >= rect.top && + e.clientY <= dropZoneBottom + ) { + insertCardInSidebar(card, e); + droppedInSidebar = true; + } + } + + // Check the top drop zone. + const topRow = document.getElementById('uploadFolderRow'); + if (!droppedInSidebar && topRow) { + const rect = topRow.getBoundingClientRect(); + if ( + e.clientX >= rect.left && + e.clientX <= rect.right && + e.clientY >= rect.top && + e.clientY <= rect.bottom + ) { + let container; + if (card.id === 'uploadCard') { + container = document.getElementById('leftCol'); + } else if (card.id === 'folderManagementCard') { + container = document.getElementById('rightCol'); + } + if (container) { + ensureTopZonePlaceholder(); + updateTopZoneLayout(); + container.appendChild(card); + droppedInTop = true; + card.style.width = "363px"; + animateVerticalSlide(card); + setTimeout(() => { + card.style.removeProperty('width'); + }, 210); + } + } + } + + // Check the header drop zone. + const headerDropArea = document.getElementById('headerDropArea'); + if (!droppedInSidebar && !droppedInTop && headerDropArea) { + const rect = headerDropArea.getBoundingClientRect(); + if ( + e.clientX >= rect.left && + e.clientX <= rect.right && + e.clientY >= rect.top && + e.clientY <= rect.bottom + ) { + insertCardInHeader(card, e); + droppedInHeader = true; + } + } + + // If card was not dropped in any zone, return it to its original container. + if (!droppedInSidebar && !droppedInTop && !droppedInHeader) { + const orig = document.getElementById(card.dataset.originalContainerId); + if (orig) { + orig.appendChild(card); + card.style.removeProperty('width'); + } + } + + // Clear inline drag-related styles. + [ + 'position', + 'left', + 'top', + 'z-index', + 'height', + 'min-width', + 'flex-shrink', + 'transition', + 'transform', + 'opacity' + ].forEach(prop => card.style.removeProperty(prop)); + + // For sidebar drops, force width to 100%. + if (droppedInSidebar) { + card.style.width = '100%'; + } + + updateTopZoneLayout(); + updateSidebarVisibility(); + hideHeaderDropZone(); + } + }); + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', run); + } else { + run(); + } +} \ No newline at end of file