From ae932a9aa9544fdb0abb02b4cfc41c5a31fec55e Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 24 Oct 2025 03:21:39 -0400 Subject: [PATCH] release(v1.6.6): header-mounted toggle, dark-mode polish, persistent layout, and ACL fix --- CHANGELOG.md | 16 ++ public/api/folder/capabilities.php | 4 +- public/css/styles.css | 5 +- public/js/dragAndDrop.js | 258 ++++++++++++++++++++++------- 4 files changed, 219 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ade0e5..db9a0b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## Changes 10/24/2025 (v1.6.6) + +release(v1.6.6): header-mounted toggle, dark-mode polish, persistent layout, and ACL fix + +- dragAndDrop: mount zones toggle beside header logo (absolute, non-scrolling); + stop click propagation so it doesn’t trigger the logo link; theme-aware styling + - live updates via MutationObserver; snapshot card locations on drop and restore + on load (prevents sidebar reset); guard first-run defaults with + `layoutDefaultApplied_v1`; small/medium layout tweaks & refactors. +- CSS: switch toggle icon to CSS variable (`--toggle-icon-color`) with dark-mode + override; remove hardcoded `!important`. +- API (capabilities.php): remove unused `disableUpload` flag from `canUpload` + and flags payload to resolve undefined variable warning. + +--- + ## Changes 10/24/2025 (v1.6.5) release(v1.6.5): fix PHP warning and upload-flag check in capabilities.php diff --git a/public/api/folder/capabilities.php b/public/api/folder/capabilities.php index 7e9a4d6..4952bd0 100644 --- a/public/api/folder/capabilities.php +++ b/public/api/folder/capabilities.php @@ -153,7 +153,6 @@ if ($folder !== 'root') { $perms = loadPermsFor($username); $isAdmin = ACL::isAdmin($perms); $readOnly = !empty($perms['readOnly']); -$disableUpload = (bool)($perms['disableUpload'] ?? false); $inScope = inUserFolderScope($folder, $username, $perms, $isAdmin); // --- ACL base abilities --- @@ -178,7 +177,7 @@ $gShareFolder = $isAdmin || ACL::canShareFolder($username, $perms, $folder); // --- Apply scope + flags to effective UI actions --- $canView = $canViewBase && $inScope; // keep scope for folder-only -$canUpload = $gUploadBase && !$readOnly && !$disableUpload && $inScope; +$canUpload = $gUploadBase && !$readOnly && $inScope; $canCreate = $canManageBase && !$readOnly && $inScope; // Create **folder** $canRename = $canManageBase && !$readOnly && $inScope; // Rename **folder** $canDelete = $gDeleteBase && !$readOnly && $inScope; @@ -213,7 +212,6 @@ echo json_encode([ 'flags' => [ //'folderOnly' => !empty($perms['folderOnly']) || !empty($perms['userFolderOnly']) || !empty($perms['UserFolderOnly']), 'readOnly' => $readOnly, - 'disableUpload' => $disableUpload, ], 'owner' => $owner, diff --git a/public/css/styles.css b/public/css/styles.css index 0de09fa..686f699 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -2318,11 +2318,14 @@ body.dark-mode { --perm-caret: #ccc; } /* dark */ background-color 160ms cubic-bezier(.2,.0,.2,1); } +:root { --toggle-icon-color: #333; } +body.dark-mode { --toggle-icon-color: #eee; } + #zonesToggleFloating .material-icons, #zonesToggleFloating .material-icons-outlined, #sidebarToggleFloating .material-icons, #sidebarToggleFloating .material-icons-outlined { - color: #333 !important; + color: var(--toggle-icon-color); font-size: 22px; line-height: 1; display: block; diff --git a/public/js/dragAndDrop.js b/public/js/dragAndDrop.js index ccf855a..e5348f6 100644 --- a/public/js/dragAndDrop.js +++ b/public/js/dragAndDrop.js @@ -19,6 +19,25 @@ const KNOWN_CARD_IDS = ['uploadCard', 'folderManagementCard']; const CARD_IDS = ['uploadCard', 'folderManagementCard']; +function isDarkMode() { + return document.body.classList.contains('dark-mode'); +} + +function themeToggleButton(btn) { + if (!btn) return; + if (isDarkMode()) { + btn.style.background = '#2c2c2c'; + btn.style.border = '1px solid #555'; + btn.style.boxShadow = '0 2px 6px rgba(0,0,0,.35)'; + btn.style.color = '#e0e0e0'; // <- material icon inherits this + } else { + btn.style.background = '#fff'; + btn.style.border = '1px solid #ccc'; + btn.style.boxShadow = '0 2px 6px rgba(0,0,0,.15)'; + btn.style.color = '#222'; // <- material icon inherits this + } +} + function getKnownCards() { return CARD_IDS .map(id => document.getElementById(id)) @@ -133,6 +152,34 @@ function removeHeaderIconForCard(card) { } } +function applySnapshotIfPresent() { + const snap = readZonesSnapshot(); + const keys = Object.keys(snap || {}); + if (!keys.length) return false; + + const sidebar = getSidebar(); + const leftCol = document.getElementById('leftCol'); + const rightCol = document.getElementById('rightCol'); + + getKnownCards().forEach(card => { + const destId = snap[card.id]; + const dest = + destId === 'leftCol' ? leftCol : + destId === 'rightCol' ? rightCol : + destId === 'sidebarDropArea' ? sidebar : null; + if (dest) { + // clear sticky widths if coming from sidebar/header + card.style.width = ''; + card.style.minWidth = ''; + dest.appendChild(card); + } + }); + + // prevent first-run default from stomping this on reload + localStorage.setItem('layoutDefaultApplied_v1', '1'); + return true; +} + // New: small-screen detector function isSmallScreen() { return window.innerWidth < MEDIUM_MIN; } @@ -158,12 +205,12 @@ function clearResponsiveSnapshot() { // New: deterministic mapping from card -> top column function moveCardToTopByMapping(card) { - const leftCol = document.getElementById('leftCol'); + const leftCol = document.getElementById('leftCol'); const rightCol = document.getElementById('rightCol'); if (!leftCol || !rightCol) return; const target = (card.id === 'uploadCard') ? leftCol : - (card.id === 'folderManagementCard') ? rightCol : leftCol; + (card.id === 'folderManagementCard') ? rightCol : leftCol; // clear any sticky widths from sidebar/header card.style.width = ''; @@ -196,7 +243,7 @@ function enforceResponsiveZones() { snapshotSidebarCardsForResponsive(); moveAllSidebarCardsToTop(); if (sidebar) sidebar.style.display = 'none'; - if (topZone) topZone.style.display = ''; // ensure visible + if (topZone) topZone.style.display = ''; // ensure visible __lastIsSmall = true; } else if (!isSmall && __lastIsSmall !== false) { // leaving small: restore only what used to be in the sidebar @@ -321,17 +368,70 @@ function applySidebarCollapsed() { sidebar.style.display = collapsed ? 'none' : 'block'; } +function getHeaderHost() { + // 1) exact structure you shared + let host = document.querySelector('.header-container .header-left'); + // 2) fallback to header root + if (!host) host = document.querySelector('.header-container'); + // 3) last resort + if (!host) host = document.querySelector('header'); + return host || document.body; +} + +function mountHeaderToggle(btn) { + const host = document.querySelector('.header-left'); + const logoA = host?.querySelector('a'); + if (!host) return; + + // ensure positioning context + if (getComputedStyle(host).position === 'static') host.style.position = 'relative'; + + if (logoA) { + logoA.insertAdjacentElement('afterend', btn); // sibling of , not inside it + } else { + host.appendChild(btn); + } + + Object.assign(btn.style, { + position: 'absolute', + left: '100px', // adjust position beside the logo + top: '10px', + zIndex: '10010', + pointerEvents: 'auto' + }); +} + function ensureZonesToggle() { let btn = document.getElementById('sidebarToggleFloating'); + const host = getHeaderHost(); + if (!host) return; + + // ensure the host is a positioning context + const hostStyle = getComputedStyle(host); + if (hostStyle.position === 'static') { + host.style.position = 'relative'; + } + if (!btn) { btn = document.createElement('button'); + btn.id = 'sidebarToggleFloating'; - btn.type = 'button'; + btn.type = 'button'; // not a submit +btn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); // don't bubble into the + setSidebarCollapsed(!isSidebarCollapsed()); + updateSidebarToggleUI(); // refresh icon/title +}); +['mousedown','mouseup','pointerdown','pointerup'].forEach(evt => + btn.addEventListener(evt, (e) => e.stopPropagation()) +); btn.setAttribute('aria-label', 'Toggle panels'); + Object.assign(btn.style, { - position: 'fixed', - left: `${TOGGLE_LEFT_PX}px`, - top: `${TOGGLE_TOP_PX}px`, + position: 'absolute', // <-- key change (was fixed) + top: '8px', // adjust to line up with header content + left: '100px', // place to the right of your logo; tweak as needed zIndex: '1000', width: '38px', height: '38px', @@ -344,13 +444,31 @@ function ensureZonesToggle() { alignItems: 'center', justifyContent: 'center', padding: '0', - lineHeight: '0', + lineHeight: '0' }); + + // dark-mode polish (optional) + if (document.body.classList.contains('dark-mode')) { + btn.style.background = '#2c2c2c'; + btn.style.border = '1px solid #555'; + btn.style.boxShadow = '0 2px 6px rgba(0,0,0,.35)'; + btn.style.color = '#e0e0e0'; + } + btn.addEventListener('click', () => { setZonesCollapsed(!isZonesCollapsed()); }); - document.body.appendChild(btn); + + // Insert right after the logo if present, else just append to host + const afterLogo = host.querySelector('.header-logo'); + if (afterLogo && afterLogo.parentNode) { + afterLogo.parentNode.insertBefore(btn, afterLogo.nextSibling); + } else { + host.appendChild(btn); + } + themeToggleButton(btn); } + updateZonesToggleUI(); } @@ -376,8 +494,21 @@ function updateZonesToggleUI() { iconEl.style.transform = 'rotate(0deg)'; } } + themeToggleButton(btn); } +(function watchThemeChanges() { + const obs = new MutationObserver((muts) => { + for (const m of muts) { + if (m.type === 'attributes' && m.attributeName === 'class') { + const btn = document.getElementById('sidebarToggleFloating'); + if (btn) themeToggleButton(btn); + } + } + }); + obs.observe(document.body, { attributes: true }); +})(); + // create a small floating toggle button (no HTML edits needed) function ensureSidebarToggle() { const sidebar = getSidebar(); @@ -433,55 +564,61 @@ export function loadSidebarOrder() { const sidebar = getSidebar(); if (!sidebar) return; + const defaultAppliedKey = 'layoutDefaultApplied_v1'; + const defaultAlready = localStorage.getItem(defaultAppliedKey) === '1'; + const orderStr = localStorage.getItem('sidebarOrder'); const headerOrderStr = localStorage.getItem('headerOrder'); - const defaultAppliedKey = 'layoutDefaultApplied_v1'; // bump if logic changes - - // One-time default: if no saved order and no header order, -// put cards into the sidebar on all ≥ MEDIUM_MIN screens. -if ((!orderStr || !JSON.parse(orderStr || '[]').length) && -(!headerOrderStr || !JSON.parse(headerOrderStr || '[]').length)) { - -const isLargeEnough = window.innerWidth >= MEDIUM_MIN; -if (isLargeEnough) { -const mainWrapper = document.querySelector('.main-wrapper'); -if (mainWrapper) mainWrapper.style.display = 'flex'; - -const moved = []; -['uploadCard', 'folderManagementCard'].forEach(id => { - const card = document.getElementById(id); - if (card && card.parentNode?.id !== 'sidebarDropArea') { - // clear any sticky widths from header/top - card.style.width = ''; - card.style.minWidth = ''; - getSidebar().appendChild(card); - animateVerticalSlide(card); - moved.push(id); - } -}); - -if (moved.length) { - localStorage.setItem('sidebarOrder', JSON.stringify(moved)); -} -} - - } - - // No sidebar order saved yet: if user has header icons saved, do nothing (they've customized) - const headerOrder = JSON.parse(headerOrderStr || '[]'); - if (Array.isArray(headerOrder) && headerOrder.length > 0) { + if (applySnapshotIfPresent()) { + updateTopZoneLayout(); updateSidebarVisibility(); - //applySidebarCollapsed(); - //ensureSidebarToggle(); applyZonesCollapsed(); ensureZonesToggle(); return; } - // One-time default: on medium screens, start cards in the sidebar - const alreadyApplied = localStorage.getItem(defaultAppliedKey) === '1'; - if (!alreadyApplied && isMediumScreen()) { + // Only apply the one-time default if *not* initialized yet + if (!defaultAlready && + ((!orderStr || !JSON.parse(orderStr || '[]').length) && + (!headerOrderStr || !JSON.parse(headerOrderStr || '[]').length))) { + + const isLargeEnough = window.innerWidth >= MEDIUM_MIN; + if (isLargeEnough) { + const mainWrapper = document.querySelector('.main-wrapper'); + if (mainWrapper) mainWrapper.style.display = 'flex'; + + const moved = []; + ['uploadCard', 'folderManagementCard'].forEach(id => { + const card = document.getElementById(id); + if (card && card.parentNode?.id !== 'sidebarDropArea') { + card.style.width = ''; + card.style.minWidth = ''; + getSidebar().appendChild(card); + animateVerticalSlide(card); + moved.push(id); + } + }); + + if (moved.length) { + localStorage.setItem('sidebarOrder', JSON.stringify(moved)); + } + } + + // Mark initialized so this default never fires again + localStorage.setItem(defaultAppliedKey, '1'); + } + + // If user has header icons saved, honor that and bail + const headerOrder = JSON.parse(headerOrderStr || '[]'); + if (Array.isArray(headerOrder) && headerOrder.length > 0) { + updateSidebarVisibility(); + applyZonesCollapsed(); + ensureZonesToggle(); + return; + } + + if (!defaultAlready && isMediumScreen()) { const mainWrapper = document.querySelector('.main-wrapper'); if (mainWrapper) mainWrapper.style.display = 'flex'; @@ -498,13 +635,11 @@ if (moved.length) { if (moved.length) { localStorage.setItem('sidebarOrder', JSON.stringify(moved)); - localStorage.setItem(defaultAppliedKey, '1'); + localStorage.setItem(defaultAppliedKey, '1'); // mark initialized } } updateSidebarVisibility(); - //applySidebarCollapsed(); - //ensureSidebarToggle(); applyZonesCollapsed(); ensureZonesToggle(); } @@ -553,7 +688,10 @@ function updateSidebarVisibility() { // Save order and update toggle visibility saveSidebarOrder(); - ensureZonesToggle(); // will hide/remove the button if no cards + // Mark layout initialized so the first-run default won't fire on reload + localStorage.setItem('layoutDefaultApplied_v1', '1'); + + ensureZonesToggle(); } // NEW: Save header order to localStorage. @@ -573,8 +711,8 @@ function updateTopZoneLayout() { const leftCol = document.getElementById('leftCol'); const rightCol = document.getElementById('rightCol'); - const hasUpload = !!topZone?.querySelector('#uploadCard'); - const hasFolder = !!topZone?.querySelector('#folderManagementCard'); + const hasUpload = !!topZone?.querySelector('#uploadCard'); + const hasFolder = !!topZone?.querySelector('#folderManagementCard'); if (leftCol && rightCol) { if (hasUpload && !hasFolder) { @@ -953,11 +1091,10 @@ export function initDragAndDrop() { showHeaderDropZone(); const topZone = getTopZone(); - if (topZone) - { - topZone.style.display = ''; - ensureTopZonePlaceholder(); - } + if (topZone) { + topZone.style.display = ''; + ensureTopZonePlaceholder(); + } initialLeft = initialRect.left + window.pageXOffset; initialTop = initialRect.top + window.pageYOffset; @@ -1123,6 +1260,7 @@ export function initDragAndDrop() { hideHeaderDropZone(); cleanupTopZoneAfterDrop(); + snapshotZoneLocations(); const tz = getTopZone(); if (tz) tz.style.minHeight = ''; }