From b985aea80dfc2fe6c3be09f19ca9d5ccc1ecfa2a Mon Sep 17 00:00:00 2001 From: KingKDot Date: Sat, 1 Aug 2026 02:11:52 -0500 Subject: [PATCH] slightly more UI animations --- Overlord-Server/public/assets/build.js | 37 ++++++-- Overlord-Server/public/assets/deploy.js | 21 +++- .../assets/filebrowser-transfer-panel.js | 11 ++- Overlord-Server/public/assets/filebrowser.js | 89 ++++++++--------- Overlord-Server/public/assets/main.css | 23 +++++ Overlord-Server/public/assets/motion.js | 95 +++++++++++++++++++ Overlord-Server/public/assets/settings.js | 30 +++++- Overlord-Server/public/build.html | 7 +- Overlord-Server/public/filebrowser.html | 23 ++++- Overlord-Server/public/settings.html | 8 +- 10 files changed, 273 insertions(+), 71 deletions(-) create mode 100644 Overlord-Server/public/assets/motion.js diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js index 1f467b6..6f02b8a 100644 --- a/Overlord-Server/public/assets/build.js +++ b/Overlord-Server/public/assets/build.js @@ -3,6 +3,11 @@ import { createBuildHistoryManager, formatFileSize, } from "./build-history-manager.js"; +import { + animateElement, + revealElement, + staggerChildren, +} from "./motion.js"; const form = document.getElementById("build-form"); const buildBtn = document.getElementById("build-btn"); @@ -195,12 +200,22 @@ function updateServerUrlCurrentButton() { let isBuilding = false; +function revealBuildResults() { + if (buildResults.classList.contains("hidden")) { + revealElement(buildResults, { duration: 240, offset: 8 }); + } + requestAnimationFrame(() => { + staggerChildren(buildFilesDiv, { limit: 10, interval: 28, duration: 220, offset: 6 }); + }); +} + function initAccordions() { document.querySelectorAll(".accordion-section").forEach((section) => { const header = section.querySelector(".accordion-header"); const body = section.querySelector(".accordion-body"); const chevron = section.querySelector(".accordion-chevron"); const startOpen = section.dataset.open !== "false"; + header.setAttribute("aria-expanded", String(startOpen)); if (!startOpen) { body.classList.add("collapsed"); @@ -211,6 +226,12 @@ function initAccordions() { header.addEventListener("click", () => { const nowCollapsed = body.classList.toggle("collapsed"); chevron.classList.toggle("rotated", !nowCollapsed); + section.dataset.open = String(!nowCollapsed); + header.setAttribute("aria-expanded", String(!nowCollapsed)); + animateElement(header, [ + { backgroundColor: "rgba(51, 65, 85, 0.62)" }, + { backgroundColor: "rgba(30, 41, 59, 0.5)" }, + ], { duration: 180 }); }); }); } @@ -226,6 +247,10 @@ function initBuilderTabs() { panels.forEach((p) => { if (p.dataset.builderPanel === tabName) { p.removeAttribute("hidden"); + animateElement(p, [ + { opacity: 0, transform: "translateY(7px)" }, + { opacity: 1, transform: "translateY(0)" }, + ], { duration: 200 }); } else { p.setAttribute("hidden", ""); } @@ -2020,7 +2045,7 @@ async function startBuild(config) { setSgnTxtButtonState("Building TXT...", "fa-solid fa-spinner fa-spin"); } - buildStatus.classList.remove("hidden"); + revealElement(buildStatus, { duration: 220, offset: 6 }); buildStatusText.textContent = "Starting build..."; buildStatus.querySelector("div").className = "flex items-center gap-2 p-3 rounded-lg bg-blue-900/40 border border-blue-700/60"; @@ -2136,7 +2161,7 @@ async function streamBuildOutput(buildId, config = {}) { files: info.files, }; saveBuildToStorage(buildData.id, buildData); - buildResults.classList.remove("hidden"); + revealBuildResults(); displayBuild(buildData); if (pendingUpdateAll) { @@ -2181,7 +2206,7 @@ async function streamBuildOutput(buildId, config = {}) { files: info.files, }; saveBuildToStorage(buildData.id, buildData); - buildResults.classList.remove("hidden"); + revealBuildResults(); displayBuild(buildData); } return; @@ -2250,7 +2275,7 @@ async function streamBuildOutput(buildId, config = {}) { }; saveBuildToStorage(data.buildId, buildData); - buildResults.classList.remove("hidden"); + revealBuildResults(); displayBuild(buildData); if (uploadedShareFiles.length > 0) { @@ -2304,7 +2329,7 @@ async function streamBuildOutput(buildId, config = {}) { files: info.files, }; saveBuildToStorage(buildData.id, buildData); - buildResults.classList.remove("hidden"); + revealBuildResults(); displayBuild(buildData); } } else { @@ -2394,7 +2419,7 @@ function renderShareLinksPanel(items) { } function showBuildFiles(files, buildId, expiresAt) { - buildResults.classList.remove("hidden"); + revealBuildResults(); buildFilesDiv.innerHTML = ""; const buildInfoDiv = document.createElement("div"); diff --git a/Overlord-Server/public/assets/deploy.js b/Overlord-Server/public/assets/deploy.js index 66bcd62..bddaa1a 100644 --- a/Overlord-Server/public/assets/deploy.js +++ b/Overlord-Server/public/assets/deploy.js @@ -1,4 +1,10 @@ import { escapeHtml, formatBytes } from "./format.js"; +import { + animateTextChange, + emphasizeElement, + staggerChildren, + swapPanels, +} from "./motion.js"; const clientList = document.getElementById("client-list"); const clientSearch = document.getElementById("client-search"); @@ -27,22 +33,22 @@ const fetchUrlBtn = document.getElementById("fetch-url-btn"); let activeTab = "upload"; function switchTab(tab) { + if (tab === activeTab) return; + const outgoing = activeTab === "upload" ? panelUpload : panelUrl; + const incoming = tab === "upload" ? panelUpload : panelUrl; activeTab = tab; if (tab === "upload") { tabUpload.classList.add("bg-slate-700", "text-slate-100"); tabUpload.classList.remove("text-slate-400"); tabUrl.classList.remove("bg-slate-700", "text-slate-100"); tabUrl.classList.add("text-slate-400"); - panelUpload.classList.remove("hidden"); - panelUrl.classList.add("hidden"); } else { tabUrl.classList.add("bg-slate-700", "text-slate-100"); tabUrl.classList.remove("text-slate-400"); tabUpload.classList.remove("bg-slate-700", "text-slate-100"); tabUpload.classList.add("text-slate-400"); - panelUrl.classList.remove("hidden"); - panelUpload.classList.add("hidden"); } + swapPanels(outgoing, incoming); } tabUpload.addEventListener("click", () => switchTab("upload")); @@ -228,6 +234,8 @@ function renderClients() { cb.addEventListener("change", handleClientToggle); }); + staggerChildren(clientList, { limit: 12, interval: 18, duration: 180, offset: 5 }); + updateSelectedCount(); } @@ -243,6 +251,7 @@ function handleClientToggle(e) { function updateSelectedCount() { selectedCountSpan.textContent = `${selectedClients.size} selected`; + animateTextChange(selectedCountSpan); executeBtn.disabled = selectedClients.size === 0 || !uploaded; if (updateBtn) { updateBtn.disabled = selectedClients.size === 0 || !uploaded; @@ -269,6 +278,7 @@ function matchesClientOs(clientOs, targetOs) { function setUploadStatus(text, tone = "text-slate-400") { uploadStatus.className = `mt-3 text-sm ${tone}`; uploadStatus.textContent = text; + animateTextChange(uploadStatus); } function setOsBadge(os) { @@ -513,6 +523,7 @@ executeBtn.addEventListener("click", async () => { `; }) .join(""); + staggerChildren(outputContainer, { limit: 16, interval: 30, duration: 220, offset: 7 }); executeBtn.disabled = false; }); @@ -747,6 +758,7 @@ function renderAutoDeployList(items) { `; }) .join(""); + staggerChildren(autoDeployList, { limit: 12, interval: 24, duration: 200, offset: 6 }); autoDeployList.querySelectorAll(".auto-deploy-toggle").forEach((btn) => { btn.addEventListener("click", async () => { @@ -759,6 +771,7 @@ function renderAutoDeployList(items) { body: JSON.stringify({ enabled: !currentlyEnabled }), }); if (res.ok) loadAutoDeploys(); + if (res.ok) emphasizeElement(btn, "rgba(52, 211, 153, 0.24)"); } catch (err) { console.error("Toggle failed:", err); } diff --git a/Overlord-Server/public/assets/filebrowser-transfer-panel.js b/Overlord-Server/public/assets/filebrowser-transfer-panel.js index d0cdc4d..bb889f4 100644 --- a/Overlord-Server/public/assets/filebrowser-transfer-panel.js +++ b/Overlord-Server/public/assets/filebrowser-transfer-panel.js @@ -1,4 +1,5 @@ import { formatBytes } from "./filebrowser-utils.js"; +import { concealElement, revealElement } from "./motion.js"; export function createTransferPanel({ onCancel }) { const transferPanel = document.getElementById("transfer-panel"); @@ -43,7 +44,10 @@ export function createTransferPanel({ onCancel }) { } transferList.appendChild(transferItem); - transferPanel.classList.remove("hidden"); + if (transferPanel.classList.contains("hidden")) { + revealElement(transferPanel, { duration: 180, offset: 6 }); + } + revealElement(transferItem, { duration: 190, offset: 7 }); } function updateTransferProgress(transferId, progress, current, total) { @@ -61,14 +65,15 @@ export function createTransferPanel({ onCancel }) { } } - function removeTransfer(transferId) { + async function removeTransfer(transferId) { const transferItem = document.getElementById(`transfer-${transferId}`); if (transferItem) { + await concealElement(transferItem, { duration: 130, offset: -5 }); transferItem.remove(); } if (transferList && transferList.children.length === 0) { - transferPanel?.classList.add("hidden"); + await concealElement(transferPanel, { duration: 120, offset: 5 }); } } diff --git a/Overlord-Server/public/assets/filebrowser.js b/Overlord-Server/public/assets/filebrowser.js index edb9a26..522e167 100644 --- a/Overlord-Server/public/assets/filebrowser.js +++ b/Overlord-Server/public/assets/filebrowser.js @@ -3,6 +3,12 @@ import { checkFeatureAccess } from "./feature-gate.js"; import { createFileHexHashManager } from "./filebrowser-hex-hash.js"; import { createFilePreviewModal } from "./filebrowser-preview-modal.js"; import { createTransferPanel } from "./filebrowser-transfer-panel.js"; +import { + animateElement, + concealElement, + revealElement, + staggerChildren, +} from "./motion.js"; import { KNOWN_BINARY_EXTS, PREVIEW_IMAGE_EXTS, @@ -1165,52 +1171,28 @@ function clearVirtualizedListMode() { function renderDirectoryStandard(entries, canGoUp, parentPath, disableAnimations) { clearVirtualizedListMode(); + fileListEl.style.transition = "none"; + fileListEl.style.opacity = "1"; + fileListEl.style.transform = "translateX(0)"; + fileListEl.innerHTML = ""; - if (!disableAnimations) { - fileListEl.style.opacity = "0"; - fileListEl.style.transform = "translateX(20px)"; + if (canGoUp) fileListEl.appendChild(createParentRow(parentPath)); + + if (entries.length === 0 && !canGoUp) { + fileListEl.innerHTML = + '
Empty directory
'; } else { - fileListEl.style.transition = "none"; - fileListEl.style.opacity = "1"; - fileListEl.style.transform = "translateX(0)"; + const fragment = document.createDocumentFragment(); + entries.forEach((entry) => fragment.appendChild(createFileRow(entry))); + fileListEl.appendChild(fragment); } - const renderList = () => { - fileListEl.innerHTML = ""; - - if (canGoUp) { - fileListEl.appendChild(createParentRow(parentPath)); - } - - if (entries.length === 0 && !canGoUp) { - fileListEl.innerHTML = - '
Empty directory
'; - fileListEl.style.opacity = "1"; - fileListEl.style.transform = "translateX(0)"; - return; - } - - entries.forEach((entry, index) => { - const row = createFileRow(entry); - if (!disableAnimations) { - row.style.animationDelay = `${index * 0.02}s`; - row.classList.add("card-animate"); - } - fileListEl.appendChild(row); - }); - - if (!disableAnimations) { - fileListEl.style.transition = - "opacity 0.3s ease-out, transform 0.3s ease-out"; - fileListEl.style.opacity = "1"; - fileListEl.style.transform = "translateX(0)"; - } - }; - - if (disableAnimations) { - renderList(); - } else { - setTimeout(renderList, 150); + if (!disableAnimations) { + animateElement(fileListEl, [ + { opacity: 0.35, transform: "translateX(8px)" }, + { opacity: 1, transform: "translateX(0)" }, + ], { duration: 190 }); + staggerChildren(fileListEl, { limit: 14, interval: 18, duration: 190, offset: 5 }); } } @@ -2384,6 +2366,7 @@ function handleCommandResult(msg) { function showContextMenu(x, y, entry) { contextMenu.style.left = `${x}px`; contextMenu.style.top = `${y}px`; + contextMenu.style.transformOrigin = `${x > window.innerWidth / 2 ? "right" : "left"} top`; contextMenu.classList.add("show"); contextMenu.dataset.path = entry.path; contextMenu.dataset.isDir = entry.isDir; @@ -3036,14 +3019,26 @@ const { requestFilePeek, } = fileHexHashManager; -function setPreviewPaneVisible(visible) { +function setPreviewPaneVisible(visible, animate = true) { if (!previewPaneHost) return; if (visible) { - previewPaneHost.classList.remove("hidden"); + previewPaneHost.classList.remove("preview-pane-user-hidden"); + if (animate) revealElement(previewPaneHost, { duration: 220, offset: 0, scale: 0.98 }); + else previewPaneHost.classList.remove("hidden"); previewPaneShowBtnHost?.classList.add("hidden"); } else { - previewPaneHost.classList.add("hidden"); - previewPaneShowBtnHost?.classList.remove("hidden"); + if (animate) { + concealElement(previewPaneHost, { duration: 140, offset: 0, scale: 0.98 }) + .then((concealed) => { + if (!concealed) return; + previewPaneHost.classList.add("preview-pane-user-hidden"); + revealElement(previewPaneShowBtnHost, { duration: 160, offset: 4 }); + }); + } else { + previewPaneHost.classList.add("hidden"); + previewPaneHost.classList.add("preview-pane-user-hidden"); + previewPaneShowBtnHost?.classList.remove("hidden"); + } } try { localStorage.setItem(PREVIEW_PANE_STATE_KEY, visible ? "1" : "0"); } catch {} } @@ -3053,7 +3048,7 @@ previewPaneShowBtn?.addEventListener("click", () => setPreviewPaneVisible(true)) (function initPreviewPaneVisibility() { let saved = "1"; try { saved = localStorage.getItem(PREVIEW_PANE_STATE_KEY) || "1"; } catch {} - setPreviewPaneVisible(saved !== "0"); + setPreviewPaneVisible(saved !== "0", false); })(); function clearPreviewPane() { diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css index 9b9ceb5..ef0c8b7 100644 --- a/Overlord-Server/public/assets/main.css +++ b/Overlord-Server/public/assets/main.css @@ -33,6 +33,29 @@ --color-primary-hover: #818cf8; --color-success: #22c55e; --color-danger: #ef4444; + + /* Shared interaction motion */ + --motion-fast: 140ms; + --motion-normal: 220ms; + --motion-slow: 360ms; + --motion-ease-out: cubic-bezier(0.2, 0.8, 0.2, 1); + --motion-ease-in: cubic-bezier(0.4, 0, 1, 1); +} + +.motion-surface { + transition: + border-color var(--motion-normal) var(--motion-ease-out), + background-color var(--motion-normal) var(--motion-ease-out), + box-shadow var(--motion-normal) var(--motion-ease-out), + opacity var(--motion-normal) var(--motion-ease-out), + transform var(--motion-normal) var(--motion-ease-out); +} + +.motion-status { + transition: + color var(--motion-fast) ease, + background-color var(--motion-fast) ease, + border-color var(--motion-fast) ease; } .monaco-editor, diff --git a/Overlord-Server/public/assets/motion.js b/Overlord-Server/public/assets/motion.js new file mode 100644 index 0000000..487f04f --- /dev/null +++ b/Overlord-Server/public/assets/motion.js @@ -0,0 +1,95 @@ +const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); +const activeAnimations = new WeakMap(); +const visibilityIntent = new WeakMap(); +let panelSwapSequence = 0; + +export function prefersReducedMotion() { + return reducedMotionQuery.matches; +} + +export function animateElement(element, keyframes, options = {}) { + if (!element || prefersReducedMotion() || typeof element.animate !== "function") { + return null; + } + + activeAnimations.get(element)?.cancel(); + const animation = element.animate(keyframes, { + duration: 220, + easing: "cubic-bezier(0.2, 0.8, 0.2, 1)", + fill: "none", + ...options, + }); + activeAnimations.set(element, animation); + animation.addEventListener("finish", () => { + if (activeAnimations.get(element) === animation) activeAnimations.delete(element); + }, { once: true }); + animation.addEventListener("cancel", () => { + if (activeAnimations.get(element) === animation) activeAnimations.delete(element); + }, { once: true }); + return animation; +} + +export function revealElement(element, options = {}) { + if (!element) return null; + visibilityIntent.set(element, true); + element.classList.remove("hidden"); + element.removeAttribute("hidden"); + return animateElement(element, [ + { opacity: 0, transform: `translateY(${options.offset ?? 8}px) scale(${options.scale ?? 0.99})` }, + { opacity: 1, transform: "translateY(0) scale(1)" }, + ], { duration: options.duration ?? 220 }); +} + +export async function concealElement(element, options = {}) { + if (!element || element.classList.contains("hidden") || element.hasAttribute("hidden")) return false; + visibilityIntent.set(element, false); + const animation = animateElement(element, [ + { opacity: 1, transform: "translateY(0) scale(1)" }, + { opacity: 0, transform: `translateY(${options.offset ?? -4}px) scale(${options.scale ?? 0.995})` }, + ], { duration: options.duration ?? 110, easing: "ease-in" }); + if (animation) { + try { await animation.finished; } catch {} + } + if (visibilityIntent.get(element) === false) { + element.classList.add("hidden"); + return true; + } + return false; +} + +export async function swapPanels(outgoing, incoming) { + if (!incoming || outgoing === incoming) return; + const sequence = ++panelSwapSequence; + if (outgoing) await concealElement(outgoing, { duration: 90, offset: -3 }); + if (sequence !== panelSwapSequence) return; + revealElement(incoming, { duration: 190, offset: 6 }); +} + +export function staggerChildren(container, options = {}) { + if (!container || prefersReducedMotion()) return; + const children = Array.from(container.children).slice(0, options.limit ?? 12); + children.forEach((child, index) => { + animateElement(child, [ + { opacity: 0, transform: `translateY(${options.offset ?? 7}px)` }, + { opacity: 1, transform: "translateY(0)" }, + ], { + duration: options.duration ?? 200, + delay: index * (options.interval ?? 24), + }); + }); +} + +export function emphasizeElement(element, tone = "rgba(56, 189, 248, 0.28)") { + return animateElement(element, [ + { transform: "scale(1)", boxShadow: "0 0 0 0 transparent" }, + { transform: "scale(1.012)", boxShadow: `0 0 0 3px ${tone}` }, + { transform: "scale(1)", boxShadow: "0 0 0 0 transparent" }, + ], { duration: 420, easing: "ease-out" }); +} + +export function animateTextChange(element) { + return animateElement(element, [ + { opacity: 0.45, transform: "translateY(3px)" }, + { opacity: 1, transform: "translateY(0)" }, + ], { duration: 160 }); +} diff --git a/Overlord-Server/public/assets/settings.js b/Overlord-Server/public/assets/settings.js index f5d5c59..5685e32 100644 --- a/Overlord-Server/public/assets/settings.js +++ b/Overlord-Server/public/assets/settings.js @@ -12,6 +12,13 @@ import { setEasterEggEnabled, showEnabledEasterEgg, } from "./easter-egg.js"; +import { + animateElement, + animateTextChange, + concealElement, + emphasizeElement, + revealElement, +} from "./motion.js"; const PREF_REFRESH_KEY = "overlord_refresh_interval_seconds"; const NAV_MODE_KEY = "sb_mode"; @@ -190,6 +197,7 @@ function showMessage(text, type = "ok") { } else { messageEl.classList.add("text-emerald-200", "border-emerald-700", "bg-emerald-900/30"); } + animateTextChange(messageEl); } function formatDate(timestamp) { @@ -470,7 +478,11 @@ function applyTlsForm() { } tlsTrustHelpBtn?.addEventListener("click", () => { - tlsTrustInstructions?.classList.toggle("hidden"); + if (tlsTrustInstructions?.classList.contains("hidden")) { + revealElement(tlsTrustInstructions, { duration: 200, offset: 5 }); + } else { + concealElement(tlsTrustInstructions, { duration: 120, offset: -3 }); + } }); async function loadActiveTlsCertificate() { @@ -792,7 +804,7 @@ async function startMfaSetup() { if (mfaQrCode) mfaQrCode.innerHTML = data.qrSvg || ""; if (mfaSecretText) mfaSecretText.textContent = data.secret || ""; if (mfaOtpauthLink) mfaOtpauthLink.href = data.otpauthUrl || "#"; - if (mfaSetupPanel) mfaSetupPanel.classList.remove("hidden"); + revealElement(mfaSetupPanel, { duration: 240, offset: 8 }); if (mfaEnableCodeInput) mfaEnableCodeInput.focus(); } catch { showMfaMessage("Network error while starting MFA setup.", "error"); @@ -813,7 +825,7 @@ async function enableMfa() { showMfaMessage(data.error || "Failed to enable MFA.", "error"); return; } - if (mfaSetupPanel) mfaSetupPanel.classList.add("hidden"); + concealElement(mfaSetupPanel, { duration: 130, offset: -4 }); if (mfaEnableCodeInput) mfaEnableCodeInput.value = ""; showMfaMessage("MFA enabled."); await loadMfaStatus(); @@ -1396,9 +1408,18 @@ function initSettingsSidebar() { } if (sectionMap.size === 0) return; + let activeLink = null; function setActive(link) { + if (link === activeLink) return; + activeLink = link; links.forEach((l) => l.classList.remove("active")); - if (link) link.classList.add("active"); + if (link) { + link.classList.add("active"); + animateElement(link, [ + { transform: "translateX(-2px)", opacity: 0.8 }, + { transform: "translateX(0)", opacity: 1 }, + ], { duration: 180 }); + } } function isAtPageBottom() { @@ -1421,6 +1442,7 @@ function initSettingsSidebar() { updateSettingsScrollOffset(); _clickLockUntil = Date.now() + 1200; setActive(link); + emphasizeElement(target, "rgba(56, 189, 248, 0.18)"); target.scrollIntoView({ behavior: "smooth", block: "start" }); history.replaceState(null, "", `#${id}`); }); diff --git a/Overlord-Server/public/build.html b/Overlord-Server/public/build.html index 72b5149..2eecc05 100644 --- a/Overlord-Server/public/build.html +++ b/Overlord-Server/public/build.html @@ -21,7 +21,7 @@ .accordion-body { display: grid; grid-template-rows: 1fr; - transition: grid-template-rows 0.2s ease, opacity 0.2s ease; + transition: grid-template-rows var(--motion-normal) var(--motion-ease-out), opacity var(--motion-fast) ease; opacity: 1; } .accordion-body.collapsed { @@ -32,7 +32,7 @@ overflow: hidden; } .accordion-chevron { - transition: transform 0.2s ease; + transition: transform var(--motion-normal) var(--motion-ease-out); } .accordion-chevron.rotated { transform: rotate(-180deg); @@ -67,11 +67,12 @@ color: rgb(148, 163, 184); font-size: 0.78rem; font-weight: 600; - transition: color 160ms ease, background 160ms ease, box-shadow 160ms ease; + transition: color var(--motion-fast) ease, background var(--motion-fast) ease, box-shadow var(--motion-normal) var(--motion-ease-out), transform var(--motion-fast) ease; } .builder-tab:hover { color: rgb(226, 232, 240); background: rgba(51, 65, 85, 0.45); + transform: translateY(-1px); } .builder-tab[aria-selected="true"] { color: white; diff --git a/Overlord-Server/public/filebrowser.html b/Overlord-Server/public/filebrowser.html index 54a3d85..335cbc4 100644 --- a/Overlord-Server/public/filebrowser.html +++ b/Overlord-Server/public/filebrowser.html @@ -23,6 +23,10 @@ } .file-item { position: relative; + transition: + background-color var(--motion-fast) ease, + box-shadow var(--motion-fast) ease, + border-color var(--motion-fast) ease; } .file-item.selected { background-color: rgba(14, 165, 233, 0.12); @@ -56,7 +60,7 @@ background-color: #2563eb; } .context-menu { - display: none; + display: block; position: fixed; z-index: 1000; background-color: #1e293b; @@ -64,9 +68,22 @@ border-radius: 0.5rem; padding: 0.5rem 0; box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.3); + opacity: 0; + visibility: hidden; + pointer-events: none; + transform: translateY(4px) scale(0.96); + transform-origin: top left; + transition: + opacity var(--motion-fast) ease, + transform var(--motion-fast) var(--motion-ease-out), + visibility 0s linear var(--motion-fast); } .context-menu.show { - display: block; + opacity: 1; + visibility: visible; + pointer-events: auto; + transform: translateY(0) scale(1); + transition-delay: 0s; } .context-menu-item { padding: 0.5rem 1rem; @@ -348,7 +365,9 @@ top: 5rem; max-height: calc(100vh - 6rem); overflow-y: auto; + transform-origin: top right; } + #preview-pane-host.preview-pane-user-hidden { display: none !important; } #preview-pane { /* host handles sticky + scroll; inner stays in normal flow */ } diff --git a/Overlord-Server/public/settings.html b/Overlord-Server/public/settings.html index 6c7d832..7afc2da 100644 --- a/Overlord-Server/public/settings.html +++ b/Overlord-Server/public/settings.html @@ -25,7 +25,7 @@ border-radius: 0.5rem; color: rgb(203 213 225); /* slate-300 */ cursor: pointer; - transition: background-color 120ms, color 120ms; + transition: background-color var(--motion-fast), color var(--motion-fast), box-shadow var(--motion-normal) var(--motion-ease-out), transform var(--motion-fast) ease; } .settings-nav-link span { min-width: 0; @@ -36,6 +36,7 @@ .settings-nav-link:hover { background-color: rgb(30 41 59 / 0.7); /* slate-800/70 */ color: rgb(241 245 249); /* slate-100 */ + transform: translateX(2px); } /* Active state pulls its color from --accent (set inline per link to match the link's icon). Falls back to sky-400 if not set. */ @@ -61,7 +62,10 @@ class applies scroll-margin-top, which both `scrollIntoView` and browser hash navigation honor. */ :root { --settings-scroll-mt: 1rem; } - .settings-section { scroll-margin-top: var(--settings-scroll-mt); } + .settings-section { + scroll-margin-top: var(--settings-scroll-mt); + transition: border-color var(--motion-normal) ease, box-shadow var(--motion-normal) ease; + } /* Also pin the sticky sidebar below the topbar so it doesn't slide under the nav when the page is scrolled. */ #settings-sidebar {