From e51dc5da043b2f70262927209c466efc6e549c37 Mon Sep 17 00:00:00 2001 From: Microck Date: Sat, 21 Feb 2026 08:54:18 +0000 Subject: [PATCH] feat: add island right-dock feature --- celstomp/css/components/island.css | 110 +++++++++++++++++++++++++++++ celstomp/js/ui/island-helper.js | 70 +++++++++++++++++- 2 files changed, 177 insertions(+), 3 deletions(-) diff --git a/celstomp/css/components/island.css b/celstomp/css/components/island.css index b764d09..7642c0e 100644 --- a/celstomp/css/components/island.css +++ b/celstomp/css/components/island.css @@ -351,3 +351,113 @@ #islandSidePanel #clearAllBtn.danger:hover{ background: rgba(255,107,107,0.10); } + +/* Right-docked island styles */ +.islandDock.right-docked { + left: auto !important; + right: 0; + top: var(--header-h) !important; + width: 280px; + height: calc(100vh - var(--header-h) - var(--timeline-h)) !important; + max-height: calc(100vh - var(--header-h) - var(--timeline-h)); + border-radius: 0; + border-right: none; + border-top: none; +} + +.islandDock.right-docked .islandResizeHandle { + display: none !important; +} + +.islandDock.right-docked #islandSidePanel { + display: none !important; +} + +/* Drop zone indicator */ +.island-drop-zone { + position: fixed; + right: 0; + top: var(--header-h); + width: 60px; + height: calc(100vh - var(--header-h) - var(--timeline-h)); + background: rgba(0, 229, 255, 0.1); + border: 2px dashed rgba(0, 229, 255, 0.5); + pointer-events: none; + opacity: 0; + transition: opacity 0.15s ease; + z-index: 35; +} + +.island-drop-zone.visible { + opacity: 1; +} + +/* Lock hint */ +.island-lock-hint { + position: fixed; + right: 70px; + top: calc(var(--header-h) + 50%); + transform: translateY(-50%); + background: rgba(0, 229, 255, 0.18); + border: 1px solid rgba(0, 229, 255, 0.5); + color: #9fd5ff; + padding: 8px 12px; + border-radius: 6px; + font-size: 12px; + pointer-events: none; + opacity: 0; + transition: opacity 0.15s ease; + z-index: 36; +} + +.island-lock-hint.visible { + opacity: 1; +} + +/* Custom scrollbar for layers */ +#islandLayersSlot { + scrollbar-width: thin; + scrollbar-color: rgba(255,255,255,0.22) rgba(0,0,0,0.18); +} + +#islandLayersSlot::-webkit-scrollbar { + width: 8px; +} + +#islandLayersSlot::-webkit-scrollbar-track { + background: rgba(0,0,0,0.18); + border-radius: 4px; +} + +#islandLayersSlot::-webkit-scrollbar-thumb { + background: rgba(255,255,255,0.20); + border-radius: 4px; +} + +#islandLayersSlot::-webkit-scrollbar-thumb:hover { + background: rgba(255,255,255,0.30); +} + +/* Custom scrollbar for side panel */ +.islandSideBody { + scrollbar-width: thin; + scrollbar-color: rgba(255,255,255,0.22) rgba(0,0,0,0.18); +} + +.islandSideBody::-webkit-scrollbar { + width: 8px; +} + +.islandSideBody::-webkit-scrollbar-track { + background: rgba(0,0,0,0.18); + border-radius: 4px; +} + +.islandSideBody::-webkit-scrollbar-thumb { + background: rgba(255,255,255,0.20); + border-radius: 4px; +} + +.islandSideBody::-webkit-scrollbar-thumb:hover { + background: rgba(255,255,255,0.30); +} diff --git a/celstomp/js/ui/island-helper.js b/celstomp/js/ui/island-helper.js index ff6e029..af60505 100644 --- a/celstomp/js/ui/island-helper.js +++ b/celstomp/js/ui/island-helper.js @@ -175,6 +175,49 @@ function wireFloatingIslandDrag() { if (!head) return; if (dock._dragWired) return; dock._dragWired = true; + + let dropZone = document.querySelector(".island-drop-zone"); + if (!dropZone) { + dropZone = document.createElement("div"); + dropZone.className = "island-drop-zone"; + document.body.appendChild(dropZone); + } + + let lockHint = document.querySelector(".island-lock-hint"); + if (!lockHint) { + lockHint = document.createElement("div"); + lockHint.className = "island-lock-hint"; + lockHint.textContent = "Release to dock"; + document.body.appendChild(lockHint); + } + + const RIGHT_DOCK_KEY = "celstomp_island_right_docked"; + const RIGHT_DOCK_THRESHOLD = 80; + + function loadRightDocked() { + try { + const saved = localStorage.getItem(RIGHT_DOCK_KEY); + if (saved === "1") { + dock.classList.add("right-docked"); + dock.style.left = ""; + dock.style.top = ""; + } + } catch {} + } + loadRightDocked(); + + function setRightDocked(v) { + const yes = !!v; + dock.classList.toggle("right-docked", yes); + try { + localStorage.setItem(RIGHT_DOCK_KEY, yes ? "1" : "0"); + } catch {} + if (yes) { + dock.style.left = ""; + dock.style.top = ""; + } + } + let dragging = false; let pid = null; let offX = 0; @@ -185,6 +228,7 @@ function wireFloatingIslandDrag() { let cachedW = 0; let cachedH = 0; const pad = 8; + const updateCache = () => { cachedHeaderH = typeof nowCSSVarPx === "function" ? nowCSSVarPx("--header-h", 48) : 48; cachedVW = window.innerWidth; @@ -193,18 +237,23 @@ function wireFloatingIslandDrag() { cachedW = r.width; cachedH = r.height; }; + const clampPos = (x, y) => { - // O(1) calculation using cached values to prevent Layout Thrashing x = Math.max(pad, Math.min(cachedVW - cachedW - pad, x)); y = Math.max(cachedHeaderH + pad, Math.min(cachedVH - cachedH - pad, y)); return { x, y }; }; + head.addEventListener("pointerdown", e => { if (e.pointerType === "mouse" && e.button !== 0) return; if (e.target.closest(".islandBtn, .islandBtns, .islandResizeHandle")) return; - + + if (dock.classList.contains("right-docked")) { + setRightDocked(false); + } + updateCache(); - + const r = dock.getBoundingClientRect(); offX = e.clientX - r.left; offY = e.clientY - r.top; @@ -218,19 +267,34 @@ function wireFloatingIslandDrag() { }, { passive: false }); + window.addEventListener("pointermove", e => { if (!dragging || e.pointerId !== pid) return; const pos = clampPos(e.clientX - offX, e.clientY - offY); dock.style.left = pos.x + "px"; dock.style.top = pos.y + "px"; + + const nearRight = cachedVW - e.clientX < RIGHT_DOCK_THRESHOLD; + dropZone.classList.toggle("visible", nearRight); + lockHint.classList.toggle("visible", nearRight); + e.preventDefault(); }, { passive: false }); + const end = e => { if (!dragging || pid != null && e.pointerId !== pid) return; dragging = false; dock.classList.remove("dragging"); + dropZone.classList.remove("visible"); + lockHint.classList.remove("visible"); + + const nearRight = cachedVW - e.clientX < RIGHT_DOCK_THRESHOLD; + if (nearRight) { + setRightDocked(true); + } + try { head.releasePointerCapture(pid); } catch {}