release(v1.6.7): Folder Move feature, stable DnD persistence, safer uploads, and ACL/UI polish

This commit is contained in:
Ryan
2025-10-25 02:16:01 -04:00
committed by GitHub
parent b5610cf156
commit 2739925f0b
15 changed files with 915 additions and 224 deletions

View File

@@ -8,7 +8,7 @@
const MEDIUM_MIN = 1205; // matches your small-screen cutoff
const MEDIUM_MAX = 1600; // tweak as you like
const TOGGLE_TOP_PX = 10;
const TOGGLE_TOP_PX = 8;
const TOGGLE_LEFT_PX = 100;
const TOGGLE_ICON_OPEN = 'view_sidebar';
@@ -19,6 +19,13 @@ const KNOWN_CARD_IDS = ['uploadCard', 'folderManagementCard'];
const CARD_IDS = ['uploadCard', 'folderManagementCard'];
// --- NEW: separate user snapshot so refresh restores *manual* placements only ---
const USER_SNAPSHOT_KEY = 'userZonesSnapshot'; // { cardId: 'sidebarDropArea'|'leftCol'|'rightCol' }
function hasUserSnapshot() {
try { const s = JSON.parse(localStorage.getItem(USER_SNAPSHOT_KEY) || '{}'); return !!s && Object.keys(s).length > 0; } catch { return false; }
}
function isDarkMode() {
return document.body.classList.contains('dark-mode');
}
@@ -54,6 +61,16 @@ function snapshotZoneLocations() {
localStorage.setItem('zonesSnapshot', JSON.stringify(snap));
}
// NEW: Save where the user *manually* placed cards (used on normal refresh).
function snapshotUserZones() {
const snap = {};
getKnownCards().forEach(card => {
const p = card.parentNode;
snap[card.id] = p && p.id ? p.id : '';
});
localStorage.setItem(USER_SNAPSHOT_KEY, JSON.stringify(snap));
}
// Move a card to default expanded spot (your request: sidebar is default).
function moveCardToSidebarDefault(card) {
const sidebar = getSidebar();
@@ -75,6 +92,56 @@ function stripHeaderArtifacts(card) {
}
}
// Kill the 50px ghost gutter when the sidebar isn't participating in layout.
function clampSidebarWhenEmpty() {
const sidebar = getSidebar();
if (!sidebar) return;
const sidebarHasCards = hasSidebarCards();
const collapsed = isZonesCollapsed();
// Sidebar should not take space if it's collapsed OR has no cards.
const shouldHideSidebarSpace = collapsed || !sidebarHasCards;
if (shouldHideSidebarSpace) {
// Make sure it takes absolutely no horizontal space.
sidebar.style.display = 'none'; // don't render at all
sidebar.style.width = '0px';
sidebar.style.minWidth = '0px';
sidebar.style.margin = '0';
sidebar.style.padding = '0';
sidebar.style.flex = '0 0 0px';
// if you add/remove highlight elsewhere, also ensure it's not forcing size
sidebar.classList.remove('active');
} else {
// Let your CSS control it when it's actually visible/has cards.
sidebar.style.width = '';
sidebar.style.minWidth = '';
sidebar.style.margin = '';
sidebar.style.padding = '';
sidebar.style.flex = '';
// display is already decided by updateSidebarVisibility/applyZonesCollapsed
}
}
// Let the sidebar become a real drop target during drag, even if empty.
function unclampSidebarForDrag() {
const sidebar = getSidebar();
if (!sidebar) return;
// only un-clamp if panels are not collapsed
if (!isZonesCollapsed()) {
sidebar.style.display = 'block';
// give it a sensible min width so the highlight looks right
sidebar.style.minWidth = '280px';
// never force a fixed height here; let CSS layout handle it
sidebar.style.width = '';
sidebar.style.flex = ''; // don't lock flex while dragging
sidebar.style.margin = '';
sidebar.style.padding = '';
}
}
// Restore cards after “expand” (toggle off) or after refresh.
// - If we have a snapshot, use it.
// - If not, put all cards in the sidebar (your default).
@@ -152,8 +219,10 @@ function removeHeaderIconForCard(card) {
}
}
// Apply *user* snapshot on normal load (not expand) to preserve manual placement.
function applySnapshotIfPresent() {
const snap = readZonesSnapshot();
let snap = {};
try { snap = JSON.parse(localStorage.getItem(USER_SNAPSHOT_KEY) || '{}'); } catch { snap = {}; }
const keys = Object.keys(snap || {});
if (!keys.length) return false;
@@ -231,21 +300,39 @@ function moveAllSidebarCardsToTop() {
}
// New: enforce responsive behavior (sidebar disabled on small screens)
// Add hysteresis to avoid flapping near threshold
let __lastIsSmall = null;
let __lastWidth = null;
const SMALL_ENTER = MEDIUM_MIN - 16; // enter small below this
const SMALL_EXIT = MEDIUM_MIN + 16; // leave small above this
function enforceResponsiveZones() {
const isSmall = isSmallScreen();
const w = window.innerWidth;
const prevSmall = __lastIsSmall;
let nowSmall;
if (__lastWidth == null) {
nowSmall = w < MEDIUM_MIN;
} else if (prevSmall === true) {
nowSmall = !(w >= SMALL_EXIT);
} else if (prevSmall === false) {
nowSmall = (w < SMALL_ENTER);
} else {
nowSmall = w < MEDIUM_MIN;
}
__lastWidth = w;
const sidebar = getSidebar();
const topZone = getTopZone();
if (isSmall && __lastIsSmall !== true) {
if (nowSmall && prevSmall !== true) {
// entering small: remember what was in sidebar, move them up, hide sidebar
snapshotSidebarCardsForResponsive();
moveAllSidebarCardsToTop();
if (sidebar) sidebar.style.display = 'none';
if (topZone) topZone.style.display = ''; // ensure visible
__lastIsSmall = true;
} else if (!isSmall && __lastIsSmall !== false) {
} else if (!nowSmall && prevSmall !== false) {
// leaving small: restore only what used to be in the sidebar
const ids = readResponsiveSnapshot();
const sb = getSidebar();
@@ -309,10 +396,10 @@ function setZonesCollapsed(collapsed) {
if (collapsed) {
// Remember where cards were, then show them as header icons
snapshotZoneLocations();
collapseCardsToHeader(); // your existing helper that calls insertCardInHeader(...)
snapshotZoneLocations(); // original snapshot used only for collapse/expand
collapseCardsToHeader(); // your existing helper that calls insertCardInHeader(...)
} else {
// Expand: bring cards back
// Expand: bring cards back (from the collapse snapshot)
restoreCardsFromSnapshot();
// Ensure zones are visible right away after expand
@@ -324,6 +411,7 @@ function setZonesCollapsed(collapsed) {
ensureZonesToggle();
updateZonesToggleUI();
clampSidebarWhenEmpty();
}
function applyZonesCollapsed() {
@@ -570,6 +658,7 @@ export function loadSidebarOrder() {
const orderStr = localStorage.getItem('sidebarOrder');
const headerOrderStr = localStorage.getItem('headerOrder');
// Restore user's last manual placement on normal load
if (applySnapshotIfPresent()) {
updateTopZoneLayout();
updateSidebarVisibility();
@@ -577,6 +666,23 @@ export function loadSidebarOrder() {
ensureZonesToggle();
return;
}
// If no user snapshot exists and we're not on small screens,
// but the cards are currently in the top zone, default them to the sidebar once.
if (!hasUserSnapshot() && !isSmallScreen() && hasTopZoneCards() && !hasSidebarCards()) {
const sb = getSidebar();
if (sb) {
['uploadCard','folderManagementCard'].forEach(id => {
const c = document.getElementById(id);
if (c && !sb.contains(c)) {
sb.appendChild(c);
c.style.width = '100%';
}
});
snapshotUserZones(); // persist this as the user's baseline
updateSidebarVisibility();
updateTopZoneLayout();
}
}
// Only apply the one-time default if *not* initialized yet
if (!defaultAlready &&
@@ -692,6 +798,7 @@ function updateSidebarVisibility() {
localStorage.setItem('layoutDefaultApplied_v1', '1');
ensureZonesToggle();
clampSidebarWhenEmpty();
}
// NEW: Save header order to localStorage.
@@ -733,6 +840,7 @@ function updateTopZoneLayout() {
// hide whole top row when empty (kills the gap)
if (topZone) topZone.style.display = (hasUpload || hasFolder) ? '' : 'none';
clampSidebarWhenEmpty();
}
// When a card is being dragged, if the top drop zone is empty, set its min-height.
@@ -799,6 +907,10 @@ function insertCardInSidebar(card, event) {
// SAVE order & refresh minimal UI, but DO NOT collapse/restore here:
saveSidebarOrder();
// NEW: persist user manual placement (used on normal refresh)
snapshotUserZones();
updateSidebarVisibility();
ensureZonesToggle();
updateZonesToggleUI();
@@ -1081,13 +1193,15 @@ export function initDragAndDrop() {
showTopZoneWhileDragging();
const sidebar = getSidebar();
if (sidebar) {
sidebar.classList.add('active');
sidebar.style.display = isZonesCollapsed() ? 'none' : 'block';
sidebar.classList.add('highlight');
sidebar.style.height = '800px';
sidebar.style.minWidth = '280px';
}
if (sidebar) {
unclampSidebarForDrag(); // <— NEW
sidebar.classList.add('active');
sidebar.classList.add('highlight');
// keep it visible, but don't force a fixed height
sidebar.style.removeProperty('height'); // <— no 800px box
// ensure it's actually visible if not collapsed
if (!isZonesCollapsed()) sidebar.style.display = 'block';
}
showHeaderDropZone();
const topZone = getTopZone();
@@ -1169,7 +1283,6 @@ export function initDragAndDrop() {
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 &&
@@ -1208,6 +1321,9 @@ export function initDragAndDrop() {
setTimeout(() => {
card.style.removeProperty('width');
}, 210);
// NEW: persist user manual placement
snapshotUserZones();
}
}
}
@@ -1224,6 +1340,7 @@ export function initDragAndDrop() {
) {
insertCardInHeader(card, e);
droppedInHeader = true;
// header mode is transient; do not overwrite userZones here
}
}
@@ -1257,10 +1374,16 @@ export function initDragAndDrop() {
updateTopZoneLayout();
updateSidebarVisibility();
clampSidebarWhenEmpty();
if (sidebar) {
sidebar.classList.remove('highlight');
sidebar.style.height = '';
sidebar.style.minWidth = '';
}
hideHeaderDropZone();
cleanupTopZoneAfterDrop();
snapshotZoneLocations();
snapshotZoneLocations(); // keep original (collapse/expand)
const tz = getTopZone();
if (tz) tz.style.minHeight = '';
}