feat: settings panel becomes native-style bottom sheet on mobile with drag-to-expand/dismiss
This commit is contained in:
@@ -2161,6 +2161,56 @@ const chapterTmpl = `
|
|||||||
/* ── Safe-area inset ────────────────────────────────────────────────────── */
|
/* ── Safe-area inset ────────────────────────────────────────────────────── */
|
||||||
#mini-player { padding-bottom: env(safe-area-inset-bottom, 0); }
|
#mini-player { padding-bottom: env(safe-area-inset-bottom, 0); }
|
||||||
|
|
||||||
|
/* ── Settings bottom sheet (mobile) ────────────────────────────────────── */
|
||||||
|
#settings-backdrop {
|
||||||
|
display: none;
|
||||||
|
position: fixed; inset: 0; z-index: 90;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
#settings-panel {
|
||||||
|
/* mobile: bottom sheet */
|
||||||
|
position: fixed;
|
||||||
|
left: 0; right: 0; bottom: 0;
|
||||||
|
z-index: 100;
|
||||||
|
max-height: 92dvh;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
background: #18181b;
|
||||||
|
border-top: 1px solid #27272a;
|
||||||
|
border-radius: 1rem 1rem 0 0;
|
||||||
|
box-shadow: 0 -4px 32px rgba(0,0,0,.5);
|
||||||
|
transform: translateY(100%);
|
||||||
|
transition: transform 0.32s cubic-bezier(0.32,0.72,0,1);
|
||||||
|
will-change: transform;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||||
|
}
|
||||||
|
#settings-panel.sheet-open {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
.sheet-handle {
|
||||||
|
width: 2.5rem; height: 4px;
|
||||||
|
background: #3f3f46; border-radius: 9999px;
|
||||||
|
margin: 0.75rem auto 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
#settings-backdrop { display: none !important; }
|
||||||
|
#settings-panel {
|
||||||
|
position: absolute;
|
||||||
|
left: auto; right: max(0.5rem, calc(50% - 32rem + 0.5rem));
|
||||||
|
bottom: calc(100% + 0.25rem);
|
||||||
|
top: auto;
|
||||||
|
width: 20rem;
|
||||||
|
max-height: 80vh;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
border: 1px solid #27272a;
|
||||||
|
transform: none !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
#settings-panel.sheet-open { transform: none !important; }
|
||||||
|
.sheet-handle { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Toggle switch ──────────────────────────────────────────────────────── */
|
/* ── Toggle switch ──────────────────────────────────────────────────────── */
|
||||||
.toggle-switch { position: relative; display: inline-block; width: 2.25rem; height: 1.25rem; flex-shrink: 0; }
|
.toggle-switch { position: relative; display: inline-block; width: 2.25rem; height: 1.25rem; flex-shrink: 0; }
|
||||||
.toggle-switch input { opacity: 0; width: 0; height: 0; position: absolute; }
|
.toggle-switch input { opacity: 0; width: 0; height: 0; position: absolute; }
|
||||||
@@ -2326,12 +2376,17 @@ const chapterTmpl = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings panel (opens upward) -->
|
<!-- Backdrop (mobile only) -->
|
||||||
|
<div id="settings-backdrop" onclick="closeSettings()"></div>
|
||||||
|
|
||||||
|
<!-- Settings panel — bottom sheet on mobile, popover on sm+ -->
|
||||||
<div id="settings-panel"
|
<div id="settings-panel"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-label="Reader settings"
|
aria-label="Reader settings"
|
||||||
hidden
|
aria-modal="true">
|
||||||
class="fixed inset-x-4 top-1/2 -translate-y-1/2 max-h-[75vh] overflow-y-auto sm:absolute sm:inset-x-auto sm:top-auto sm:translate-y-0 sm:bottom-[calc(100%+0.25rem)] sm:right-[max(0.5rem,calc(50%-32rem+0.5rem))] sm:w-80 sm:max-h-[80vh] bg-zinc-900 border border-zinc-800 rounded-xl shadow-2xl z-[100]">
|
|
||||||
|
<!-- Drag handle -->
|
||||||
|
<div class="sheet-handle" id="sheet-handle"></div>
|
||||||
|
|
||||||
<div class="p-4 space-y-4">
|
<div class="p-4 space-y-4">
|
||||||
|
|
||||||
@@ -2496,7 +2551,7 @@ const chapterTmpl = `
|
|||||||
window.closeChapterList = function () { chapterListPanel.style.display = 'none'; };
|
window.closeChapterList = function () { chapterListPanel.style.display = 'none'; };
|
||||||
window.toggleChapterList = function () {
|
window.toggleChapterList = function () {
|
||||||
var open = chapterListPanel.style.display !== 'none';
|
var open = chapterListPanel.style.display !== 'none';
|
||||||
settingsPanel.style.display = 'none';
|
closeSettings();
|
||||||
if (open) { closeChapterList(); }
|
if (open) { closeChapterList(); }
|
||||||
else {
|
else {
|
||||||
chapterListPanel.style.display = 'block';
|
chapterListPanel.style.display = 'block';
|
||||||
@@ -2504,19 +2559,82 @@ const chapterTmpl = `
|
|||||||
if (cur) cur.scrollIntoView({ block: 'center' });
|
if (cur) cur.scrollIntoView({ block: 'center' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.toggleSettings = function () {
|
var settingsBackdrop = document.getElementById('settings-backdrop');
|
||||||
var open = settingsPanel.style.display !== 'none';
|
|
||||||
|
function openSettings() {
|
||||||
closeChapterList();
|
closeChapterList();
|
||||||
settingsPanel.style.display = open ? 'none' : 'block';
|
settingsPanel.classList.add('sheet-open');
|
||||||
|
settingsBackdrop.style.display = 'block';
|
||||||
|
}
|
||||||
|
function closeSettings() {
|
||||||
|
settingsPanel.classList.remove('sheet-open');
|
||||||
|
settingsBackdrop.style.display = 'none';
|
||||||
|
}
|
||||||
|
window.closeSettings = closeSettings;
|
||||||
|
window.toggleSettings = function () {
|
||||||
|
if (settingsPanel.classList.contains('sheet-open')) closeSettings();
|
||||||
|
else openSettings();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Bottom sheet drag-to-dismiss / drag-to-expand ─────────────────────────
|
||||||
|
(function sheetDrag() {
|
||||||
|
var handle = document.getElementById('sheet-handle');
|
||||||
|
if (!handle) return;
|
||||||
|
var startY = 0, startH = 0, dragging = false;
|
||||||
|
var DISMISS_THRESHOLD = 80; // px swipe down to dismiss
|
||||||
|
|
||||||
|
function onStart(clientY) {
|
||||||
|
if (window.innerWidth >= 640) return; // desktop: no drag
|
||||||
|
startY = clientY;
|
||||||
|
startH = settingsPanel.getBoundingClientRect().height;
|
||||||
|
dragging = true;
|
||||||
|
settingsPanel.style.transition = 'none';
|
||||||
|
}
|
||||||
|
function onMove(clientY) {
|
||||||
|
if (!dragging) return;
|
||||||
|
var dy = clientY - startY; // positive = dragging down
|
||||||
|
if (dy < 0) {
|
||||||
|
// stretching up — increase height, clamped to 92dvh
|
||||||
|
var newH = Math.min(startH - dy, window.innerHeight * 0.92);
|
||||||
|
settingsPanel.style.maxHeight = newH + 'px';
|
||||||
|
settingsPanel.style.transform = 'translateY(0)';
|
||||||
|
} else {
|
||||||
|
// dragging down — translate panel down
|
||||||
|
settingsPanel.style.transform = 'translateY(' + dy + 'px)';
|
||||||
|
settingsPanel.style.maxHeight = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onEnd(clientY) {
|
||||||
|
if (!dragging) return;
|
||||||
|
dragging = false;
|
||||||
|
settingsPanel.style.transition = '';
|
||||||
|
var dy = clientY - startY;
|
||||||
|
if (dy > DISMISS_THRESHOLD) {
|
||||||
|
closeSettings();
|
||||||
|
settingsPanel.style.maxHeight = '';
|
||||||
|
} else {
|
||||||
|
settingsPanel.style.transform = '';
|
||||||
|
// snap: if we dragged up, keep the new height
|
||||||
|
if (dy < 0) {
|
||||||
|
var snapped = Math.min(startH - dy, window.innerHeight * 0.92);
|
||||||
|
settingsPanel.style.maxHeight = snapped + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.addEventListener('touchstart', function (e) { onStart(e.touches[0].clientY); }, { passive: true });
|
||||||
|
handle.addEventListener('touchmove', function (e) { onMove(e.touches[0].clientY); }, { passive: true });
|
||||||
|
handle.addEventListener('touchend', function (e) { onEnd(e.changedTouches[0].clientY); }, { passive: true });
|
||||||
|
}());
|
||||||
|
|
||||||
if (window.__ttsClickOutside) document.removeEventListener('click', window.__ttsClickOutside);
|
if (window.__ttsClickOutside) document.removeEventListener('click', window.__ttsClickOutside);
|
||||||
window.__ttsClickOutside = function (e) {
|
window.__ttsClickOutside = function (e) {
|
||||||
if (stale()) { document.removeEventListener('click', window.__ttsClickOutside); return; }
|
if (stale()) { document.removeEventListener('click', window.__ttsClickOutside); return; }
|
||||||
var sb = document.getElementById('settings-btn');
|
var sb = document.getElementById('settings-btn');
|
||||||
if (settingsPanel.style.display !== 'none' &&
|
if (settingsPanel.classList.contains('sheet-open') &&
|
||||||
!settingsPanel.contains(e.target) && e.target !== sb && !sb.contains(e.target)) {
|
!settingsPanel.contains(e.target) && e.target !== sb && !sb.contains(e.target) &&
|
||||||
settingsPanel.style.display = 'none';
|
e.target !== settingsBackdrop) {
|
||||||
|
closeSettings();
|
||||||
}
|
}
|
||||||
if (chapterListPanel.style.display !== 'none' &&
|
if (chapterListPanel.style.display !== 'none' &&
|
||||||
!chapterListPanel.contains(e.target) &&
|
!chapterListPanel.contains(e.target) &&
|
||||||
|
|||||||
Reference in New Issue
Block a user