Fix: Settings Drop-down Fix V1 (WIP) (#102)

This commit is contained in:
Youwes09
2026-06-14 04:35:32 -05:00
parent ab61e12153
commit df9755ddf2
4 changed files with 53 additions and 51 deletions
+7 -7
View File
@@ -4,7 +4,7 @@
import { settingsState, updateSettings } from '$lib/state/settings.svelte' import { settingsState, updateSettings } from '$lib/state/settings.svelte'
import { eventToKeybind } from '$lib/core/keybinds/keybindEngine' import { eventToKeybind } from '$lib/core/keybinds/keybindEngine'
import type { Keybinds } from '$lib/core/keybinds/defaultBinds' import type { Keybinds } from '$lib/core/keybinds/defaultBinds'
import { anchorToModal } from '$lib/core/ui/selectPortal' import { selectPortal } from '$lib/core/ui/selectPortal'
import GeneralSettings from './sections/GeneralSettings.svelte' import GeneralSettings from './sections/GeneralSettings.svelte'
import AppearanceSettings from './sections/AppearanceSettings.svelte' import AppearanceSettings from './sections/AppearanceSettings.svelte'
@@ -175,13 +175,13 @@
<div class="s-content-body" bind:this={contentBodyEl}> <div class="s-content-body" bind:this={contentBodyEl}>
{#if tab === 'general'} {#if tab === 'general'}
<GeneralSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {anchorToModal} {modalEl} {anims} /> <GeneralSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {selectPortal} {modalEl} {anims} />
{:else if tab === 'appearance'} {:else if tab === 'appearance'}
<AppearanceSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {anchorToModal} {modalEl} {anims} {onOpenThemeEditor} /> <AppearanceSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {selectPortal} {modalEl} {anims} {onOpenThemeEditor} />
{:else if tab === 'reader'} {:else if tab === 'reader'}
<ReaderSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {anchorToModal} {modalEl} {anims} /> <ReaderSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {selectPortal} {modalEl} {anims} />
{:else if tab === 'library'} {:else if tab === 'library'}
<LibrarySettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {anchorToModal} {modalEl} {anims} /> <LibrarySettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {selectPortal} {modalEl} {anims} />
{:else if tab === 'automation'} {:else if tab === 'automation'}
<AutomationSettings /> <AutomationSettings />
{:else if tab === 'performance'} {:else if tab === 'performance'}
@@ -189,13 +189,13 @@
{:else if tab === 'keybinds'} {:else if tab === 'keybinds'}
<KeybindsSettings bind:listeningKey /> <KeybindsSettings bind:listeningKey />
{:else if tab === 'storage'} {:else if tab === 'storage'}
<StorageSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {anchorToModal} {modalEl} /> <StorageSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {selectPortal} {modalEl} />
{:else if tab === 'folders'} {:else if tab === 'folders'}
<FoldersSettings /> <FoldersSettings />
{:else if tab === 'tracking'} {:else if tab === 'tracking'}
<TrackingSettings /> <TrackingSettings />
{:else if tab === 'security'} {:else if tab === 'security'}
<SecuritySettings {selectOpen} {toggleSelect} {registerTrigger} {getTrigger} {anchorToModal} {modalEl} /> <SecuritySettings {selectOpen} {toggleSelect} {registerTrigger} {getTrigger} {selectPortal} {modalEl} />
{:else if tab === 'content'} {:else if tab === 'content'}
<ContentSettings /> <ContentSettings />
{:else if tab === 'about'} {:else if tab === 'about'}
@@ -2,17 +2,23 @@
import { settingsState, updateSettings } from '$lib/state/settings.svelte' import { settingsState, updateSettings } from '$lib/state/settings.svelte'
import { homeState } from '$lib/state/home.svelte' import { homeState } from '$lib/state/home.svelte'
import type { Settings } from '$lib/types/settings' import type { Settings } from '$lib/types/settings'
import type { Action } from 'svelte/action'
interface Props { interface Props {
selectOpen: string | null selectOpen: string | null
closingSelect?: string | null closingSelect?: string | null
toggleSelect: (id: string) => void toggleSelect: (id: string) => void
anims: boolean registerTrigger: (id: string, el: HTMLElement) => void
getTrigger: (id: string) => HTMLElement | undefined
selectPortal: Action<HTMLElement, HTMLElement | undefined>
anims: boolean
} }
let { selectOpen, toggleSelect, anims }: Props = $props() let { selectOpen, closingSelect, toggleSelect, registerTrigger, getTrigger, selectPortal, anims }: Props = $props()
let triggerSortDir = $state<HTMLButtonElement>(null!) let triggerSortDir = $state<HTMLButtonElement>(null!)
$effect(() => { if (triggerSortDir) registerTrigger('sort-dir', triggerSortDir) })
function clearHistory() { function clearHistory() {
homeState.history = [] homeState.history = []
} }
@@ -54,8 +60,8 @@
<span>{{ 'desc':'Newest first','asc':'Oldest first' }[settingsState.settings.chapterSortDir]}</span> <span>{{ 'desc':'Newest first','asc':'Oldest first' }[settingsState.settings.chapterSortDir]}</span>
<svg class="s-select-caret" class:open={selectOpen === 'sort-dir'} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg> <svg class="s-select-caret" class:open={selectOpen === 'sort-dir'} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
</button> </button>
{#if selectOpen === 'sort-dir'} {#if selectOpen === 'sort-dir' || closingSelect === 'sort-dir'}
<div class="s-select-menu" class:anims> <div use:selectPortal={getTrigger('sort-dir')} class="s-select-menu" class:anims class:closing={closingSelect === 'sort-dir'}>
{#each [['desc','Newest first'],['asc','Oldest first']] as [v, l]} {#each [['desc','Newest first'],['asc','Oldest first']] as [v, l]}
<button class="s-select-option" class:active={settingsState.settings.chapterSortDir === v} onclick={() => { updateSettings({ chapterSortDir: v as Settings['chapterSortDir'] }); toggleSelect('sort-dir') }}>{l}</button> <button class="s-select-option" class:active={settingsState.settings.chapterSortDir === v} onclick={() => { updateSettings({ chapterSortDir: v as Settings['chapterSortDir'] }); toggleSelect('sort-dir') }}>{l}</button>
{/each} {/each}
@@ -1,18 +1,26 @@
<script lang="ts"> <script lang="ts">
import { settingsState, updateSettings } from '$lib/state/settings.svelte' import { settingsState, updateSettings } from '$lib/state/settings.svelte'
import type { Settings, FitMode } from '$lib/types/settings' import type { Settings, FitMode } from '$lib/types/settings'
import type { Action } from 'svelte/action'
interface Props { interface Props {
selectOpen: string | null selectOpen: string | null
closingSelect?: string | null closingSelect?: string | null
toggleSelect: (id: string) => void toggleSelect: (id: string) => void
anims: boolean registerTrigger: (id: string, el: HTMLElement) => void
getTrigger: (id: string) => HTMLElement | undefined
selectPortal: Action<HTMLElement, HTMLElement | undefined>
anims: boolean
} }
let { selectOpen, toggleSelect, anims }: Props = $props() let { selectOpen, closingSelect, toggleSelect, registerTrigger, getTrigger, selectPortal, anims }: Props = $props()
let triggerPageStyle = $state<HTMLButtonElement>(null!) let triggerPageStyle = $state<HTMLButtonElement>(null!)
let triggerReadingDir = $state<HTMLButtonElement>(null!) let triggerReadingDir = $state<HTMLButtonElement>(null!)
let triggerFitMode = $state<HTMLButtonElement>(null!) let triggerFitMode = $state<HTMLButtonElement>(null!)
$effect(() => { if (triggerPageStyle) registerTrigger('page-style', triggerPageStyle) })
$effect(() => { if (triggerReadingDir) registerTrigger('reading-dir', triggerReadingDir) })
$effect(() => { if (triggerFitMode) registerTrigger('fit-mode', triggerFitMode) })
</script> </script>
<div class="s-panel"> <div class="s-panel">
@@ -27,8 +35,8 @@
<span>{{ 'single':'Single page','longstrip':'Long strip' }[settingsState.settings.pageStyle === 'double' ? 'single' : settingsState.settings.pageStyle]}</span> <span>{{ 'single':'Single page','longstrip':'Long strip' }[settingsState.settings.pageStyle === 'double' ? 'single' : settingsState.settings.pageStyle]}</span>
<svg class="s-select-caret" class:open={selectOpen === 'page-style'} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg> <svg class="s-select-caret" class:open={selectOpen === 'page-style'} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
</button> </button>
{#if selectOpen === 'page-style'} {#if selectOpen === 'page-style' || closingSelect === 'page-style'}
<div class="s-select-menu" class:anims> <div use:selectPortal={getTrigger('page-style')} class="s-select-menu" class:anims class:closing={closingSelect === 'page-style'}>
{#each [['single','Single page'],['longstrip','Long strip']] as [v, l]} {#each [['single','Single page'],['longstrip','Long strip']] as [v, l]}
<button class="s-select-option" class:active={(settingsState.settings.pageStyle === 'double' ? 'single' : settingsState.settings.pageStyle) === v} onclick={() => { updateSettings({ pageStyle: v as Settings['pageStyle'] }); toggleSelect('page-style') }}>{l}</button> <button class="s-select-option" class:active={(settingsState.settings.pageStyle === 'double' ? 'single' : settingsState.settings.pageStyle) === v} onclick={() => { updateSettings({ pageStyle: v as Settings['pageStyle'] }); toggleSelect('page-style') }}>{l}</button>
{/each} {/each}
@@ -43,8 +51,8 @@
<span>{{ 'ltr':'Left to right','rtl':'Right to left' }[settingsState.settings.readingDirection]}</span> <span>{{ 'ltr':'Left to right','rtl':'Right to left' }[settingsState.settings.readingDirection]}</span>
<svg class="s-select-caret" class:open={selectOpen === 'reading-dir'} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg> <svg class="s-select-caret" class:open={selectOpen === 'reading-dir'} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
</button> </button>
{#if selectOpen === 'reading-dir'} {#if selectOpen === 'reading-dir' || closingSelect === 'reading-dir'}
<div class="s-select-menu" class:anims> <div use:selectPortal={getTrigger('reading-dir')} class="s-select-menu" class:anims class:closing={closingSelect === 'reading-dir'}>
{#each [['ltr','Left to right'],['rtl','Right to left']] as [v, l]} {#each [['ltr','Left to right'],['rtl','Right to left']] as [v, l]}
<button class="s-select-option" class:active={settingsState.settings.readingDirection === v} onclick={() => { updateSettings({ readingDirection: v as Settings['readingDirection'] }); toggleSelect('reading-dir') }}>{l}</button> <button class="s-select-option" class:active={settingsState.settings.readingDirection === v} onclick={() => { updateSettings({ readingDirection: v as Settings['readingDirection'] }); toggleSelect('reading-dir') }}>{l}</button>
{/each} {/each}
@@ -81,8 +89,8 @@
<span>{{ 'width':'Fit width','height':'Fit height','screen':'Fit screen','original':'Original (1:1)' }[settingsState.settings.fitMode ?? 'width']}</span> <span>{{ 'width':'Fit width','height':'Fit height','screen':'Fit screen','original':'Original (1:1)' }[settingsState.settings.fitMode ?? 'width']}</span>
<svg class="s-select-caret" class:open={selectOpen === 'fit-mode'} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg> <svg class="s-select-caret" class:open={selectOpen === 'fit-mode'} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
</button> </button>
{#if selectOpen === 'fit-mode'} {#if selectOpen === 'fit-mode' || closingSelect === 'fit-mode'}
<div class="s-select-menu" class:anims> <div use:selectPortal={getTrigger('fit-mode')} class="s-select-menu" class:anims class:closing={closingSelect === 'fit-mode'}>
{#each [['width','Fit width'],['height','Fit height'],['screen','Fit screen'],['original','Original (1:1)']] as [v, l]} {#each [['width','Fit width'],['height','Fit height'],['screen','Fit screen'],['original','Original (1:1)']] as [v, l]}
<button class="s-select-option" class:active={(settingsState.settings.fitMode ?? 'width') === v} onclick={() => { updateSettings({ fitMode: v as FitMode }); toggleSelect('fit-mode') }}>{l}</button> <button class="s-select-option" class:active={(settingsState.settings.fitMode ?? 'width') === v} onclick={() => { updateSettings({ fitMode: v as FitMode }); toggleSelect('fit-mode') }}>{l}</button>
{/each} {/each}
+14 -26
View File
@@ -1,19 +1,13 @@
/**
* position:fixed dropdown anchored to a trigger element.
*
* getBoundingClientRect() returns full viewport coords.
* position:fixed is also relative to the viewport.
* So we just divide by zoom — no sidebar/titlebar subtraction needed
* (those subtractions are only needed in ContextMenu because its x/y come
* from a MouseEvent which is relative to the zoomed content area, not the viewport).
*/
export function selectPortal( export function selectPortal(
node: HTMLElement, node: HTMLElement,
trigger: HTMLElement | undefined, trigger: HTMLElement | undefined,
): { update(t: HTMLElement | undefined): void; destroy(): void } { ): { update(t: HTMLElement | undefined): void; destroy(): void } {
let currentTrigger = trigger let currentTrigger = trigger
node.style.position = 'fixed'
node.style.visibility = 'hidden' node.style.visibility = 'hidden'
node.style.zIndex = '99999'
document.body.appendChild(node)
function getZoom(): number { function getZoom(): number {
const raw = parseFloat(document.documentElement.style.zoom || '1') || 1 const raw = parseFloat(document.documentElement.style.zoom || '1') || 1
@@ -23,33 +17,26 @@ export function selectPortal(
function position() { function position() {
if (!currentTrigger) return if (!currentTrigger) return
const zoom = getZoom() const zoom = getZoom()
const r = currentTrigger.getBoundingClientRect() const r = currentTrigger.getBoundingClientRect()
const vw = window.innerWidth
// Convert viewport px → CSS px by dividing by zoom const vh = window.innerHeight
const left = r.left / zoom
const top = r.top / zoom
const bottom = r.bottom / zoom
const width = r.width / zoom
const vw = window.innerWidth / zoom
const vh = window.innerHeight / zoom
const menuH = node.offsetHeight const menuH = node.offsetHeight
const menuW = node.offsetWidth const menuW = node.offsetWidth
const above = menuH > 0 && (vh - bottom) < menuH + 8 && top > menuH + 8 const above = menuH > 0 && (vh - r.bottom) < menuH + 8 && r.top > menuH + 8
const cssLeft = Math.max(4, r.left + menuW > vw ? r.right - menuW : r.left) / zoom
const cssTop = (above ? r.top - menuH - 4 : r.bottom + 4) / zoom
const cssLeft = Math.min(left, vw - menuW - 4) node.style.left = `${cssLeft}px`
const cssTop = above ? top - menuH - 4 : bottom + 4
node.style.left = `${Math.max(4, cssLeft)}px`
node.style.top = `${cssTop}px` node.style.top = `${cssTop}px`
node.style.minWidth = `${width}px` node.style.minWidth = `${r.width / zoom}px`
node.style.visibility = 'visible' node.style.visibility = 'visible'
} }
requestAnimationFrame(() => position()) requestAnimationFrame(() => position())
window.addEventListener('scroll', position, { capture: true, passive: true }) window.addEventListener('scroll', position, { capture: true, passive: true })
window.addEventListener('resize', position, { passive: true }) window.addEventListener('resize', position, { passive: true })
@@ -62,6 +49,7 @@ export function selectPortal(
destroy() { destroy() {
window.removeEventListener('scroll', position, true) window.removeEventListener('scroll', position, true)
window.removeEventListener('resize', position) window.removeEventListener('resize', position)
if (document.body.contains(node)) node.remove()
}, },
} }
} }