diff --git a/src/core/actions/index.ts b/src/core/actions/index.ts new file mode 100644 index 0000000..5d37ceb --- /dev/null +++ b/src/core/actions/index.ts @@ -0,0 +1 @@ +export * from "./selectPortal"; \ No newline at end of file diff --git a/src/core/actions/selectPortal.ts b/src/core/actions/selectPortal.ts new file mode 100644 index 0000000..78dd499 --- /dev/null +++ b/src/core/actions/selectPortal.ts @@ -0,0 +1,40 @@ +import type { Attachment } from "svelte/attachments"; + +/** + * {@attach selectPortal(triggerEl)} + * + * Moves the decorated element to and positions it below `triggerEl`. + * The element stays reactive — Svelte still owns its DOM, we just re-parent it. + * + * The portalled menu element is stored on `triggerEl.__selectMenuEl` so that + * the outside-click guard in Settings.svelte can exclude it from dismissal. + */ +export function selectPortal(triggerEl: HTMLElement & { __selectMenuEl?: HTMLElement | null }): Attachment { + return (menuEl: HTMLElement) => { + // Position & move to body + function position() { + const r = triggerEl.getBoundingClientRect(); + menuEl.style.position = "fixed"; + menuEl.style.top = `${r.bottom + 4}px`; + menuEl.style.left = `${r.right - menuEl.offsetWidth}px`; + // clamp to viewport left edge + const left = parseFloat(menuEl.style.left); + if (left < 8) menuEl.style.left = "8px"; + } + + document.body.appendChild(menuEl); + triggerEl.__selectMenuEl = menuEl; + position(); + + // Reposition on scroll / resize while open + window.addEventListener("scroll", position, true); + window.addEventListener("resize", position); + + return () => { + window.removeEventListener("scroll", position, true); + window.removeEventListener("resize", position); + triggerEl.__selectMenuEl = null; + menuEl.remove(); + }; + }; +} \ No newline at end of file diff --git a/src/design/base/scrollbars.css b/src/design/base/scrollbars.css index 9bbf59e..c512765 100644 --- a/src/design/base/scrollbars.css +++ b/src/design/base/scrollbars.css @@ -1,9 +1,9 @@ * { scrollbar-width: thin; - scrollbar-color: var(--border-strong) transparent; + scrollbar-color: transparent transparent; } *::-webkit-scrollbar { width: 4px; height: 4px; } *::-webkit-scrollbar-track { background: transparent; } -*::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 99px; } -*::-webkit-scrollbar-thumb:hover { background: var(--text-faint); } \ No newline at end of file +*::-webkit-scrollbar-thumb { background: transparent; border-radius: 99px; } +*::-webkit-scrollbar-thumb:hover { background: transparent; } \ No newline at end of file diff --git a/src/features/settings/components/Settings.css b/src/features/settings/components/Settings.css index 09962a3..5366646 100644 --- a/src/features/settings/components/Settings.css +++ b/src/features/settings/components/Settings.css @@ -1,9 +1,10 @@ /* ── Animations ───────────────────────────────────────────────────── */ -@keyframes s-fade-in { from { opacity: 0 } to { opacity: 1 } } -@keyframes s-scale-in { from { transform: scale(0.97); opacity: 0 } to { transform: scale(1); opacity: 1 } } -@keyframes s-pulse { 0%,100% { opacity: 1 } 50% { opacity: 0.55 } } -@keyframes s-icon-down { from { transform: translateY(-5px); opacity: 0 } to { transform: translateY(0); opacity: 1 } } -@keyframes s-icon-up { from { transform: translateY( 5px); opacity: 0 } to { transform: translateY(0); opacity: 1 } } +@keyframes s-fade-in { from { opacity: 0 } to { opacity: 1 } } +@keyframes s-scale-in { from { transform: scale(0.97); opacity: 0 } to { transform: scale(1); opacity: 1 } } +@keyframes s-pulse { 0%,100% { opacity: 1 } 50% { opacity: 0.55 } } +@keyframes s-icon-down { from { transform: translateY(-5px); opacity: 0 } to { transform: translateY(0); opacity: 1 } } +@keyframes s-icon-up { from { transform: translateY( 5px); opacity: 0 } to { transform: translateY(0); opacity: 1 } } +@keyframes s-dropdown-in { from { transform: translateY(-6px) scale(0.98); opacity: 0 } to { transform: translateY(0) scale(1); opacity: 1 } } /* ── Backdrop & Modal Shell ───────────────────────────────────────── */ @@ -139,12 +140,7 @@ .s-content-body { flex: 1; overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: var(--border-base) transparent; } -.s-content-body::-webkit-scrollbar { width: 4px; } -.s-content-body::-webkit-scrollbar-track { background: transparent; } -.s-content-body::-webkit-scrollbar-thumb { background: var(--border-base); border-radius: 2px; } /* ── Panel & Section ──────────────────────────────────────────────── */ @@ -319,19 +315,17 @@ .s-select-caret.open { transform: rotate(180deg); } .s-select-menu { - position: absolute; - top: calc(100% + 4px); - right: 0; - min-width: 100%; + position: fixed; /* portal sets top/left via inline style */ + min-width: 140px; background: var(--bg-raised); border: 1px solid var(--border-base); border-radius: var(--radius-md); padding: var(--sp-1); z-index: 9999; box-shadow: 0 8px 28px rgba(0,0,0,0.45); - animation: s-scale-in 0.1s ease both; transform-origin: top right; } +.s-select-menu.anims { animation: s-dropdown-in 0.15s cubic-bezier(0.22,1,0.36,1) both; } .s-select-option { display: block; @@ -972,12 +966,7 @@ max-height: 300px; overflow-y: auto; padding: 0 var(--sp-2); - scrollbar-width: thin; - scrollbar-color: var(--border-base) transparent; } -.s-release-scroll::-webkit-scrollbar { width: 4px; } -.s-release-scroll::-webkit-scrollbar-track { background: transparent; } -.s-release-scroll::-webkit-scrollbar-thumb { background: var(--border-base); border-radius: 2px; } .s-release-row { border-radius: var(--radius-md); diff --git a/src/features/settings/components/Settings.svelte b/src/features/settings/components/Settings.svelte index cc9e88b..ada94f0 100644 --- a/src/features/settings/components/Settings.svelte +++ b/src/features/settings/components/Settings.svelte @@ -85,11 +85,31 @@ // Shared select dropdown state (passed to sections that need it) let selectOpen: string | null = $state(null); - function toggleSelect(id: string) { selectOpen = selectOpen === id ? null : id; } + let closingSelect: string | null = $state(null); + + const CLOSE_ANIM_MS = 120; + + function closeSelect() { + if (!selectOpen) return; + closingSelect = selectOpen; + selectOpen = null; + setTimeout(() => { closingSelect = null; }, CLOSE_ANIM_MS); + } + + function toggleSelect(id: string) { + if (selectOpen === id) { closeSelect(); } + else { closingSelect = null; selectOpen = id; } + } $effect(() => { const handler = (e: MouseEvent) => { - if (selectOpen && !(e.target as HTMLElement).closest(".s-select")) selectOpen = null; + if (!selectOpen) return; + const t = e.target as HTMLElement; + // Keep open if click is inside the trigger wrapper (.s-select) + if (t.closest(".s-select")) return; + // Keep open if click landed inside the portalled menu (appended to ) + if (t.closest(".s-select-menu")) return; + closeSelect(); }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); @@ -140,25 +160,25 @@
{#if tab === "general"} - + {:else if tab === "appearance"} {:else if tab === "reader"} - + {:else if tab === "library"} - + {:else if tab === "performance"} {:else if tab === "keybinds"} {:else if tab === "storage"} - + {:else if tab === "folders"} {:else if tab === "tracking"} {:else if tab === "security"} - + {:else if tab === "content"} {:else if tab === "about"} diff --git a/src/features/settings/components/ThemeEditor.svelte b/src/features/settings/components/ThemeEditor.svelte index 5a63e73..112562f 100644 --- a/src/features/settings/components/ThemeEditor.svelte +++ b/src/features/settings/components/ThemeEditor.svelte @@ -444,9 +444,6 @@ padding: var(--sp-4) var(--sp-5); display: flex; flex-direction: column; gap: var(--sp-6); } - .editor-pane::-webkit-scrollbar { width: 4px; } - .editor-pane::-webkit-scrollbar-track { background: transparent; } - .editor-pane::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 9999px; } .group { display: flex; flex-direction: column; gap: var(--sp-1); } .group-label { diff --git a/src/features/settings/sections/GeneralSettings.svelte b/src/features/settings/sections/GeneralSettings.svelte index f0715f9..4b18687 100644 --- a/src/features/settings/sections/GeneralSettings.svelte +++ b/src/features/settings/sections/GeneralSettings.svelte @@ -1,12 +1,17 @@
@@ -54,15 +59,15 @@
Idle screen timeoutShow the Moku idle splash after this much inactivity
-
- - {#if selectOpen === "idle-timeout"} -
+ {#if selectOpen === "idle-timeout" || closingSelect === "idle-timeout"} +
{#each [["0","Never"],["1","1 minute"],["2","2 minutes"],["5","5 minutes"],["10","10 minutes"],["15","15 minutes"],["30","30 minutes"]] as [v, l]} - + {/each}
{/if} diff --git a/src/features/settings/sections/LibrarySettings.svelte b/src/features/settings/sections/LibrarySettings.svelte index 88853c2..bfd5ad1 100644 --- a/src/features/settings/sections/LibrarySettings.svelte +++ b/src/features/settings/sections/LibrarySettings.svelte @@ -1,13 +1,17 @@
@@ -31,15 +35,15 @@
Default sort directionInitial chapter list order when opening a manga
-
- {#if selectOpen === "sort-dir"} -
+
{#each [["desc","Newest first"],["asc","Oldest first"]] as [v, l]} - + {/each}
{/if} diff --git a/src/features/settings/sections/ReaderSettings.svelte b/src/features/settings/sections/ReaderSettings.svelte index d3bddd5..13e09d0 100644 --- a/src/features/settings/sections/ReaderSettings.svelte +++ b/src/features/settings/sections/ReaderSettings.svelte @@ -1,13 +1,19 @@
@@ -17,15 +23,15 @@
Default layoutHow chapters open by default
-
- {#if selectOpen === "page-style"} -
+
{#each [["single","Single page"],["longstrip","Long strip"]] as [v, l]} - + {/each}
{/if} @@ -33,15 +39,15 @@
Reading directionLeft-to-right for most manga, right-to-left for Japanese
-
- {#if selectOpen === "reading-dir"} -
+
{#each [["ltr","Left to right"],["rtl","Right to left"]] as [v, l]} - + {/each}
{/if} @@ -67,15 +73,15 @@
Default fit modeHow pages are scaled to fill the reader on open
-
- {#if selectOpen === "fit-mode"} -
+
{#each [["width","Fit width"],["height","Fit height"],["screen","Fit screen"],["original","Original (1:1)"]] as [v, l]} - + {/each}
{/if}