diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index 47a3da2..07638f8 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -2119,8 +2119,100 @@ body.layout-modern .global-sidebar-right { font-size: 1.5em; } -/* Mobile Stacking for Legacy Mode (max-width: 999px) */ +/* Mobile sidebar (max-width: 999px): sidebar fixed, capped to viewport width */ @media (max-width: 999px) { + /* Sidebar itself: cap width to viewport, always right-anchored */ + .item-sidebar-right, + .index-sidebar-right, + .global-sidebar-right { + width: min(280px, 80vw) !important; + right: 0 !important; + } + + body.sidebar-right-hidden .item-sidebar-right, + body.sidebar-right-hidden .index-sidebar-right, + body.sidebar-right-hidden .global-sidebar-right { + right: calc(-1 * min(280px, 80vw)) !important; + } + + /* Mobile: pagination centered in available space (left of sidebar) */ + .pagination-container-fluid { + right: min(280px, 80vw) !important; + transition: right 0.3s ease-in-out !important; + } + + /* Sidebar hidden: pagination expands to full viewport width */ + body.sidebar-right-hidden .pagination-container-fluid { + right: 0 !important; + } + + #sidebar-right-toggle { + top: 80% !important; + transform: none !important; + } +} + +/* 600–999px: sidebar is attached to the grid, same as desktop but with mobile width. + Grid reserves space for the sidebar so content is never hidden behind it. */ +@media (min-width: 600px) and (max-width: 999px) { + .index-layout-wrapper { + padding-right: min(280px, 80vw); + } + + body.sidebar-right-hidden .index-layout-wrapper { + padding-right: 0; + } + + /* Generic pagewrapper (non-index, non-item pages) also reserves space */ + .pagewrapper:not(:has(.index-layout-wrapper)):not(:has(.item-layout-container)) { + padding-right: min(280px, 80vw); + } + + body.sidebar-right-hidden + .pagewrapper:not(:has(.index-layout-wrapper)):not(:has(.item-layout-container)) { + padding-right: 0; + } + + /* Item page: extend the desktop grid layout to 600px so the fixed sidebar + aligns naturally — col3 width = sidebar width, so content never overlaps. */ + body.layout-modern .item-layout-container { + display: grid; + grid-template-columns: min(260px, 26vw) minmax(0, 1fr) min(280px, 80vw); + height: calc(100vh - var(--navbar-h, 50px)); + width: 100%; + overflow: hidden; + } + + body.layout-modern.sidebar-right-hidden .item-layout-container { + grid-template-columns: min(260px, 26vw) minmax(0, 1fr) 0px; + } + + body.layout-modern .item-layout-container .item-main-content { + grid-column: 2; + } + + body.layout-modern .item-sidebar-left { + grid-column: 1; + } + + /* Legacy layout: 2-column grid at this range */ + body.layout-legacy .item-layout-container { + display: grid; + grid-template-columns: minmax(0, 1fr) min(280px, 80vw); + height: auto !important; + overflow: visible !important; + } + + body.layout-legacy.sidebar-right-hidden .item-layout-container { + grid-template-columns: minmax(0, 1fr) 0px; + } +} + +/* Very small screens (≤599px): sidebar is always a pure overlay — never reserve space for it. + Wrappers stay at full width regardless of sidebar state; no transitions needed here. + Item view also switches to stacked layout here (not at 999px). */ +@media (max-width: 599px) { + /* Item view: stack content above comments/sidebar-left */ .item-layout-container { display: flex; flex-direction: column; @@ -2130,39 +2222,34 @@ body.layout-modern .global-sidebar-right { .item-layout-container .item-main-content { order: 1; - /* Main content (video) on top */ - /* padding: 10px; */ } .item-sidebar-left { order: 2; - /* Comments/Tags on bottom */ - /* padding: 10px; */ z-index: 1; } - /* Mobile Overrides if necessary — most styles now universal */ - .item-sidebar-right, - .index-sidebar-right, - .global-sidebar-right { - width: 280px !important; + .index-layout-wrapper, + .item-layout-container, + .pagewrapper, + body.sidebar-right-hidden .index-layout-wrapper, + body.sidebar-right-hidden .item-layout-container, + body.sidebar-right-hidden .pagewrapper { + padding-right: 5px !important; + transition: none !important; } - body.sidebar-right-hidden .item-sidebar-right, - body.sidebar-right-hidden .index-sidebar-right, - body.sidebar-right-hidden .global-sidebar-right { - right: -280px !important; - } - - /* Mobile: sidebar is an overlay, so pagination always spans full viewport */ .pagination-container-fluid, body.sidebar-right-hidden .pagination-container-fluid { right: 0 !important; + transition: none !important; } - #sidebar-right-toggle { - top: 80% !important; - transform: none !important; + /* All sidebars hide/show instantly at this breakpoint — no slide animation */ + .item-sidebar-right, + .index-sidebar-right, + .global-sidebar-right { + transition: none !important; } } @@ -2958,7 +3045,8 @@ body.layout-legacy .scroll-nav-wrapper { right: -45px; width: 40px; pointer-events: none; - z-index: 1000; + z-index: 0; + display: none; } body.layout-legacy .scroll-to-bottom { @@ -4720,11 +4808,12 @@ body.layout-modern .pagination-container-fluid { min-width: 0; } -@media (min-width: 1200px) { +@media (min-width: 1000px) { /* Reserve space for the fixed sidebar so content doesn't flow behind it */ .index-layout-wrapper { padding-right: 300px; + transition: padding-right 0.3s ease-in-out; } /* Modern layout: wider sidebar */ @@ -7703,6 +7792,7 @@ div.posts>a.thumb > p { bottom: 5px; left: 5px; gap: 2px; + align-items: center; } /* Make individual item entry subtler and faster for infinite scroll */ @@ -9748,6 +9838,20 @@ html[theme="f0ck95d"] .badge-dark { } } +/* At 600–999px: center the button in the free space left of the sidebar. + Shift left by half the sidebar width so transform:translateX(-50%) lands + at the center of the available area rather than the full viewport. */ +@media (min-width: 600px) and (max-width: 999px) { + .mobile-scroll-to-top { + left: calc(50% - min(140px, 40vw)); + transition: left 0.3s ease-in-out; + } + + body.sidebar-right-hidden .mobile-scroll-to-top { + left: 50%; + } +} + .steuerung.steuerung-icon .fa-solid { font-size: 0.85em; } @@ -9924,9 +10028,10 @@ html[theme="f0ck95"] .embed-responsive.embed-responsive-16by9 { position: relative; } -@media (min-width: 1200px) { +@media (min-width: 1000px) { .pagewrapper { padding-right: 300px; + transition: padding-right 0.3s ease-in-out; } body.layout-modern .pagewrapper { @@ -9942,7 +10047,7 @@ html[theme="f0ck95"] .embed-responsive.embed-responsive-16by9 { Since Index and Item pages have their own layout wrappers that handle space reservation, we disable the generic pagewrapper padding on those pages to avoid "double padding" or broken expansion. */ -@media (min-width: 1200px) { +@media (min-width: 1000px) { .pagewrapper:has(.index-layout-wrapper), .pagewrapper:has(.item-layout-container), @@ -11071,8 +11176,7 @@ div.posts>a::after { .pin-indicator { - position: absolute; - top: 5px; + position: relative; left: 5px; font-size: 13px; fill: var(--accent); diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js index 345f75e..da0a287 100644 --- a/public/s/js/f0ckm.js +++ b/public/s/js/f0ckm.js @@ -2307,9 +2307,49 @@ window.cancelAnimFrame = (function () { hidden = hiddenValue === 'true'; } document.body.classList.toggle('sidebar-right-hidden', hidden); + + // Always force-hide on very small screens regardless of saved preference + if (window.innerWidth <= 599) { + document.body.classList.add('sidebar-right-hidden'); + } }; window.initSidebarRightToggle(); + // Auto-hide sidebar on very small screens (≤599px), restore on wider + (() => { + const mq = window.matchMedia('(max-width: 599px)'); + const applyBreakpoint = (e) => { + // Freeze transitions so the class change is instant (no sliding during resize) + const sidebar = document.querySelector('.global-sidebar-right, .item-sidebar-right, .index-sidebar-right'); + const wrappers = document.querySelectorAll('.index-layout-wrapper, .item-layout-container, .pagination-container-fluid, .pagewrapper'); + if (sidebar) sidebar.style.setProperty('transition', 'none', 'important'); + wrappers.forEach(w => w.style.setProperty('transition', 'none', 'important')); + + if (e.matches) { + // Entering narrow: force-hide without touching localStorage + document.body.classList.add('sidebar-right-hidden'); + } else { + // Leaving narrow: restore from saved preference + const saved = localStorage.getItem('sidebarRightHidden'); + const hidden = saved === 'true'; + document.body.classList.toggle('sidebar-right-hidden', hidden); + } + + // Force reflow then restore transitions + if (sidebar) void sidebar.offsetWidth; + requestAnimationFrame(() => { + if (sidebar) sidebar.style.removeProperty('transition'); + wrappers.forEach(w => { + w.style.removeProperty('transition'); + w.style.removeProperty('padding-right'); + w.style.removeProperty('right'); + }); + }); + }; + mq.addEventListener('change', applyBreakpoint); + applyBreakpoint(mq); // run once on init + })(); + const loadPageAjax = async (url, replace = true, options = {}) => { if (isNavigating) return; @@ -6281,8 +6321,10 @@ window.cancelAnimFrame = (function () { if (settled) return; settled = true; - // Freeze all transitions (override CSS !important) so the class toggle - // doesn't trigger an additional right-property animation. + // Freeze sidebar AND wrapper transitions so the class toggle + // doesn't trigger a second padding-right animation. + const wrappers = getSidebarWrappers(); + wrappers.forEach(w => w.style.setProperty('transition', 'none', 'important')); sidebar.style.setProperty('transition', 'none', 'important'); if (stateChanging) { @@ -6300,6 +6342,11 @@ window.cancelAnimFrame = (function () { // Restore CSS-controlled transitions on the next frame requestAnimationFrame(() => { sidebar.style.removeProperty('transition'); + wrappers.forEach(w => { + w.style.removeProperty('transition'); + w.style.removeProperty('padding-right'); + w.style.removeProperty('right'); + }); swipeRT.isDraggingSidebar = false; }); }; @@ -6537,6 +6584,10 @@ window.cancelAnimFrame = (function () { const settle = () => { if (settled) return; settled = true; + // Freeze sidebar AND wrapper transitions so the class toggle + // doesn't trigger a second padding-right animation. + const wrappers = getSidebarWrappers(); + wrappers.forEach(w => w.style.setProperty('transition', 'none', 'important')); sidebar.style.setProperty('transition', 'none', 'important'); if (stateChanging) { @@ -6549,6 +6600,11 @@ window.cancelAnimFrame = (function () { sidebar.style.transform = ''; requestAnimationFrame(() => { sidebar.style.removeProperty('transition'); + wrappers.forEach(w => { + w.style.removeProperty('transition'); + w.style.removeProperty('padding-right'); + w.style.removeProperty('right'); + }); }); }; @@ -6616,15 +6672,53 @@ window.cancelAnimFrame = (function () { }); window.addEventListener('resize', syncEdgeZone, { passive: true }); - // Touchend on edgeZone: instant tap-to-toggle (no 300ms synthetic click delay) + // Returns true if (clientX, clientY) is within the pill's hit area. + // The pill is #sidebar-drag-zone::after — 4×40px, centered on pointer:fine, + // right-aligned with padding-right:6px on pointer:coarse. + const isWithinPillArea = (clientX, clientY) => { + const rect = edgeZone.getBoundingClientRect(); + const pillH = 40; + const hitPad = 12; // generous padding so it's easy to hit + // Vertical: pill is always centered in the drag zone + const pillCenterY = rect.top + rect.height / 2; + if (clientY < pillCenterY - pillH / 2 - hitPad || clientY > pillCenterY + pillH / 2 + hitPad) return false; + // Horizontal: right-aligned on touch, centered on mouse + const isCoarse = window.matchMedia('(pointer: coarse)').matches; + const pillCenterX = isCoarse ? rect.right - 6 - 2 : rect.left + rect.width / 2; + return clientX >= pillCenterX - 2 - hitPad && clientX <= pillCenterX + 2 + hitPad; + }; + + // Touchend: tap on the pill toggles sidebar; rest of drag zone only responds to swipe edgeZone.addEventListener('touchend', e => { - // Only if it was a clean tap (no significant swipe movement) if (Math.abs(swipeRT.xDiff || 0) < 15 && Math.abs(swipeRT.yDiff || 0) < 15) { - e.preventDefault(); - edgeZone.dispatchEvent(new Event('click')); + e.preventDefault(); // always prevent 300ms synthetic click + const touch = e.changedTouches[0]; + if (touch && isWithinPillArea(touch.clientX, touch.clientY)) { + window.toggleSidebarRight(); + } } }, { passive: false }); + // Helper: all layout wrappers that have padding-right animated by sidebar state + const getSidebarWrappers = () => { + // At ≤999px: sidebar is a pure overlay — only pagination tracks it. + if (window.innerWidth <= 599) { + return document.querySelectorAll('.pagination-container-fluid'); + } + // At ≥600px: exclude pagewrapper when a dedicated layout wrapper is present. + // CSS already zeroes pagewrapper.padding-right via :has on those pages, + // so including it here causes double-padding (pagewrapper + index-layout-wrapper both + // get 300px snapped, making the grid 600px too narrow, then jumping back on settle). + const hasDedicatedWrapper = !!( + document.querySelector('.index-layout-wrapper') || + document.querySelector('.item-layout-container') + ); + const sel = hasDedicatedWrapper + ? '.index-layout-wrapper, .pagination-container-fluid' + : '.pagination-container-fluid, .pagewrapper'; + return document.querySelectorAll(sel); + }; + // Smoothly toggle sidebar open/closed window.toggleSidebarRight = () => { const sidebar = document.querySelector('.global-sidebar-right, .item-sidebar-right, .index-sidebar-right'); @@ -6634,6 +6728,23 @@ window.cancelAnimFrame = (function () { const sidebarWidth = sidebar.offsetWidth || 300; const targetTranslate = isHidden ? -sidebarWidth : sidebarWidth; + // At ≤599px the sidebar is a pure overlay — never animate wrapper padding + const animateWrappers = window.innerWidth > 599; + + // Grid/pagewrapper: snap instantly in both directions (no animated reflow). + // Pagination always animates — it tracks sidebar position with no reflow cost. + const wrappers = getSidebarWrappers(); + if (animateWrappers) { + wrappers.forEach(w => { + const isPagination = w.classList.contains('pagination-container-fluid'); + const prop = isPagination ? 'right' : 'padding-right'; + const targetVal = isHidden ? sidebarWidth + 'px' : '0px'; + w.style.setProperty('transition', isPagination ? `${prop} 0.3s ease-in-out` : 'none', 'important'); + w.style.setProperty(prop, targetVal, 'important'); + }); + void sidebar.offsetWidth; // settle layout before sidebar transform begins + } + sidebar.style.setProperty('transition', 'transform 0.3s ease-in-out', 'important'); sidebar.style.transform = `translateX(${targetTranslate}px)`; @@ -6641,13 +6752,22 @@ window.cancelAnimFrame = (function () { const settle = () => { if (settled) return; settled = true; + // Freeze wrapper transitions so the class commit doesn't re-trigger them + wrappers.forEach(w => w.style.setProperty('transition', 'none', 'important')); sidebar.style.setProperty('transition', 'none', 'important'); const toHidden = !isHidden; document.body.classList.toggle('sidebar-right-hidden', toHidden); localStorage.setItem('sidebarRightHidden', toHidden); void sidebar.offsetWidth; sidebar.style.transform = ''; - requestAnimationFrame(() => sidebar.style.removeProperty('transition')); + requestAnimationFrame(() => { + sidebar.style.removeProperty('transition'); + wrappers.forEach(w => { + w.style.removeProperty('transition'); + w.style.removeProperty('padding-right'); + w.style.removeProperty('right'); + }); + }); }; const onEnd = ev => { @@ -6659,9 +6779,11 @@ window.cancelAnimFrame = (function () { setTimeout(settle, 350); }; - // Click on edgeZone: smoothly toggle sidebar open/closed - edgeZone.addEventListener('click', () => { - window.toggleSidebarRight(); + // Click on edgeZone: only toggle when the click lands on the pill + edgeZone.addEventListener('click', (e) => { + if (isWithinPillArea(e.clientX, e.clientY)) { + window.toggleSidebarRight(); + } }); //