refactor: clean up chapter reader UI — nav, audio, lang switcher, bottom CTA

- Consolidate top nav into a single row: ← back | ← → chapter arrows | settings gear
  Removes the duplicate prev/next buttons that appeared at both top and bottom
- Language switcher moved inline into chapter meta line (after word count);
  lock icons made smaller and less distracting for free users; removes the
  separate "Upgrade to Pro" text link
- Audio player wrapped in a collapsible "Listen to this chapter" panel;
  auto-expands if audio is already playing for the chapter, collapsed otherwise
  so content is immediately visible on page load
- Bottom nav redesigned: next chapter is a full-width card CTA with hover
  accent, previous is a small secondary text link — clear visual hierarchy
- Remove floating gear button (settings now triggered from top nav settings icon)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Admin
2026-04-05 17:57:44 +05:00
parent 0ae71c62f9
commit 222627a18c

View File

@@ -286,23 +286,36 @@
const wordCount = $derived( const wordCount = $derived(
html ? (html.replace(/<[^>]*>/g, '').match(/\S+/g)?.length ?? 0) : 0 html ? (html.replace(/<[^>]*>/g, '').match(/\S+/g)?.length ?? 0) : 0
); );
// Audio panel: auto-open if this chapter is already loaded/playing in the store
// svelte-ignore state_referenced_locally
let audioExpanded = $state(
audioStore.slug === data.book.slug && audioStore.chapter === data.chapter.number
);
$effect(() => {
// Expand automatically when the store starts playing this chapter
if (audioStore.slug === data.book.slug && audioStore.chapter === data.chapter.number && audioStore.isPlaying) {
audioExpanded = true;
}
});
</script> </script>
<svelte:head> <svelte:head>
<title>{data.chapter.title || m.reader_chapter_n({ n: String(data.chapter.number) })}{data.book.title} — libnovel</title> <title>{data.chapter.title || m.reader_chapter_n({ n: String(data.chapter.number) })}{data.book.title} — libnovel</title>
</svelte:head> </svelte:head>
<!-- Reading progress bar (scroll mode) --> <!-- Reading progress bar (scroll mode, fixed at top of viewport) -->
{#if layout.readMode === 'scroll'} {#if layout.readMode === 'scroll'}
<div class="reading-progress" style="width: {scrollProgress * 100}%"></div> <div class="reading-progress" style="width: {scrollProgress * 100}%"></div>
{/if} {/if}
<!-- Top nav (hidden in focus mode) --> <!-- ── Top navigation (hidden in focus mode) ─────────────────────────────── -->
{#if !layout.focusMode} {#if !layout.focusMode}
<div class="flex items-center justify-between mb-6 gap-4"> <div class="flex items-center justify-between mb-8 gap-2">
<!-- Left: back to chapters list -->
<a <a
href="/books/{data.book.slug}/chapters" href="/books/{data.book.slug}/chapters"
class="text-(--color-muted) hover:text-(--color-text) text-sm flex items-center gap-1 transition-colors" class="flex items-center gap-1 text-(--color-muted) hover:text-(--color-text) text-sm transition-colors shrink-0"
> >
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
@@ -310,148 +323,177 @@
{m.reader_back_to_chapters()} {m.reader_back_to_chapters()}
</a> </a>
<div class="flex gap-2"> <!-- Right: prev/next chapter arrows + settings -->
<div class="flex items-center gap-1">
{#if data.prev} {#if data.prev}
<a <a
href="/books/{data.book.slug}/chapters/{data.prev}" href="/books/{data.book.slug}/chapters/{data.prev}"
class="px-3 py-1.5 rounded bg-(--color-surface-3) text-(--color-text) text-sm hover:bg-(--color-surface-2) transition-colors" title="{m.reader_chapter_n({ n: String(data.prev) })}"
class="p-2 rounded-lg text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors"
> >
&larr; {m.reader_chapter_n({ n: String(data.prev) })} <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</a> </a>
{/if} {/if}
{#if data.next} {#if data.next}
<a <a
href="/books/{data.book.slug}/chapters/{data.next}" href="/books/{data.book.slug}/chapters/{data.next}"
class="px-3 py-1.5 rounded bg-(--color-surface-3) text-(--color-text) text-sm hover:bg-(--color-surface-2) transition-colors" title="{m.reader_chapter_n({ n: String(data.next) })}"
class="p-2 rounded-lg text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors"
> >
{m.reader_chapter_n({ n: String(data.next) })} &rarr; <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a> </a>
{/if} {/if}
{#if settingsCtx}
<button
type="button"
onclick={() => { settingsPanelOpen = !settingsPanelOpen; settingsTab = 'reading'; }}
aria-label="Reader settings"
class="p-2 rounded-lg transition-colors hover:bg-(--color-surface-2) {settingsPanelOpen ? 'text-(--color-brand)' : 'text-(--color-muted) hover:text-(--color-text)'}"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
</svg>
</button>
{/if}
</div> </div>
</div> </div>
<!-- Chapter heading --> <!-- Chapter heading + meta + language switcher -->
<div class="mb-4"> <div class="mb-6">
<h1 class="text-xl font-bold text-(--color-text)"> <h1 class="text-xl font-bold text-(--color-text) leading-snug">
{data.chapter.title || m.reader_chapter_n({ n: String(data.chapter.number) })} {data.chapter.title || m.reader_chapter_n({ n: String(data.chapter.number) })}
</h1> </h1>
{#if wordCount > 0} <div class="flex items-center flex-wrap gap-x-3 gap-y-1.5 mt-2">
<p class="text-(--color-muted) text-xs mt-1"> {#if wordCount > 0}
{m.reader_words({ n: wordCount.toLocaleString() })} <p class="text-(--color-muted) text-xs">
<span class="opacity-50 mx-1">·</span> {m.reader_words({ n: wordCount.toLocaleString() })}
~{Math.max(1, Math.round(wordCount / 200))} min read <span class="opacity-40 mx-0.5">·</span>
</p> ~{Math.max(1, Math.round(wordCount / 200))} min
{/if} </p>
</div>
{/if}
<!-- Language switcher (not shown for preview chapters or focus mode) -->
{#if !data.isPreview && !layout.focusMode}
<div class="flex items-center gap-2 mb-6 flex-wrap">
<span class="text-(--color-muted) text-xs">Read in:</span>
<!-- English (original) -->
<a
href={langUrl('')}
class="px-2 py-0.5 rounded text-xs font-medium transition-colors {currentLang() === '' ? 'bg-(--color-brand) text-(--color-surface)' : 'bg-(--color-surface-2) text-(--color-muted) hover:text-(--color-text)'}"
>
EN
</a>
{#each SUPPORTED_LANGS as { code, label }}
{#if !data.isPro}
<!-- Locked for free users -->
<a
href="/profile"
title="Upgrade to Pro to read in {label}"
class="flex items-center gap-0.5 px-2 py-0.5 rounded text-xs font-medium bg-(--color-surface-2) text-(--color-muted) opacity-60 cursor-pointer hover:opacity-80 transition-opacity"
>
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 1a4.5 4.5 0 00-4.5 4.5V9H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-.5V5.5A4.5 4.5 0 0010 1zm3 8V5.5a3 3 0 10-6 0V9h6z" clip-rule="evenodd" />
</svg>
{label}
</a>
{:else if currentLang() === code && (translationStatus === 'pending' || translationStatus === 'running')}
<!-- Spinning indicator while translating -->
<span class="flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium bg-(--color-surface-2) text-(--color-muted)">
<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
{label}
</span>
{:else if currentLang() === code}
<!-- Active translated lang -->
<a
href={langUrl(code)}
class="px-2 py-0.5 rounded text-xs font-medium bg-(--color-brand) text-(--color-surface)"
>{label}</a>
{:else}
<!-- Inactive lang: click to request/navigate -->
<button
onclick={() => requestTranslation(code)}
disabled={translatingLang !== '' && translatingLang !== code && (translationStatus === 'pending' || translationStatus === 'running')}
class="px-2 py-0.5 rounded text-xs font-medium bg-(--color-surface-2) text-(--color-muted) hover:text-(--color-text) transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
>{label}</button>
{/if} {/if}
{/each}
{#if !data.isPro} <!-- Language switcher (inline, compact) -->
<a href="/profile" class="text-xs text-(--color-brand) hover:underline ml-1">Upgrade to Pro</a> {#if !data.isPreview}
{/if} <div class="flex items-center gap-1">
<a
href={langUrl('')}
class="px-2 py-0.5 rounded text-xs font-medium transition-colors
{currentLang() === '' ? 'bg-(--color-brand) text-(--color-surface)' : 'bg-(--color-surface-2) text-(--color-muted) hover:text-(--color-text)'}"
>EN</a>
{#each SUPPORTED_LANGS as { code, label }}
{#if !data.isPro}
<a
href="/profile"
title="Upgrade to Pro to read in {label}"
class="flex items-center gap-0.5 px-2 py-0.5 rounded text-xs font-medium bg-(--color-surface-2) text-(--color-muted)/50 cursor-pointer"
>
<svg class="w-2.5 h-2.5 shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 1a4.5 4.5 0 00-4.5 4.5V9H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-.5V5.5A4.5 4.5 0 0010 1zm3 8V5.5a3 3 0 10-6 0V9h6z" clip-rule="evenodd" />
</svg>
{label}
</a>
{:else if currentLang() === code && (translationStatus === 'pending' || translationStatus === 'running')}
<span class="flex items-center gap-0.5 px-2 py-0.5 rounded text-xs font-medium bg-(--color-surface-2) text-(--color-muted)">
<svg class="w-2.5 h-2.5 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
{label}
</span>
{:else if currentLang() === code}
<a href={langUrl(code)} class="px-2 py-0.5 rounded text-xs font-medium bg-(--color-brand) text-(--color-surface)">{label}</a>
{:else}
<button
type="button"
onclick={() => requestTranslation(code)}
disabled={translatingLang !== '' && translatingLang !== code && (translationStatus === 'pending' || translationStatus === 'running')}
class="px-2 py-0.5 rounded text-xs font-medium bg-(--color-surface-2) text-(--color-muted) hover:text-(--color-text) transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
>{label}</button>
{/if}
{/each}
{#if !data.isPro}
<a href="/profile" class="text-xs text-(--color-brand) hover:underline ml-0.5">Pro</a>
{/if}
</div>
{/if}
</div>
</div> </div>
{/if} {/if}
<!-- Audio player (hidden in focus mode) --> <!-- ── Audio section (collapsible, hidden in focus/preview mode) ──────────── -->
{#if !data.isPreview && !layout.focusMode} {#if !data.isPreview && !layout.focusMode}
{#if !page.data.user} {#if !page.data.user}
<!-- Unauthenticated: sign-in prompt --> <!-- Unauthenticated -->
<div class="mb-6 px-4 py-3 rounded-lg bg-(--color-surface-2) border border-(--color-border) flex items-center justify-between gap-4"> <div class="mb-6 px-4 py-3 rounded-lg bg-(--color-surface-2) border border-(--color-border) flex items-center justify-between gap-4">
<div> <div>
<p class="text-(--color-text) text-sm font-medium">{m.reader_signin_for_audio()}</p> <p class="text-(--color-text) text-sm font-medium">{m.reader_signin_for_audio()}</p>
<p class="text-(--color-muted) text-xs mt-0.5">{m.reader_signin_audio_desc()}</p> <p class="text-(--color-muted) text-xs mt-0.5">{m.reader_signin_audio_desc()}</p>
</div>
<a href="/login" class="shrink-0 px-4 py-2 rounded bg-(--color-brand) text-(--color-surface) text-sm font-semibold hover:bg-(--color-brand-dim) transition-colors">
{m.nav_sign_in()}
</a>
</div> </div>
<a {:else if audioProRequired}
href="/login" <div class="mb-6 px-4 py-2.5 rounded-lg bg-(--color-surface-2) border border-(--color-brand)/30 flex items-center justify-between gap-4">
class="shrink-0 px-4 py-2 rounded bg-(--color-brand) text-(--color-surface) text-sm font-semibold hover:bg-(--color-brand-dim) transition-colors" <div>
> <p class="text-(--color-text) text-sm font-medium">Daily audio limit reached</p>
{m.nav_sign_in()} <p class="text-(--color-muted) text-xs mt-0.5">Upgrade to Pro for unlimited audio.</p>
</a> </div>
</div> <a href="/profile" class="shrink-0 px-3 py-1.5 rounded bg-(--color-brand) text-(--color-surface) text-sm font-semibold hover:bg-(--color-brand-dim) transition-colors">
{:else if audioProRequired} Upgrade
<div class="mb-6 px-4 py-3 rounded-lg bg-(--color-surface-2) border border-(--color-brand)/30 flex items-center justify-between gap-4"> </a>
<div>
<p class="text-(--color-text) text-sm font-medium">Daily audio limit reached</p>
<p class="text-(--color-muted) text-xs mt-0.5">Free users can listen to 3 chapters per day. Upgrade to Pro for unlimited audio.</p>
</div> </div>
<a {:else}
href="/profile" <!-- Collapsible audio panel -->
class="shrink-0 px-4 py-2 rounded bg-(--color-brand) text-(--color-surface) text-sm font-semibold hover:bg-(--color-brand-dim) transition-colors" <div class="mb-6">
> <button
Upgrade type="button"
</a> onclick={() => { audioExpanded = !audioExpanded; }}
class="w-full flex items-center justify-between px-4 py-2.5 bg-(--color-surface-2) border border-(--color-border) text-sm text-(--color-text) transition-colors hover:border-(--color-brand)/40
{audioExpanded ? 'rounded-t-lg' : 'rounded-lg'}"
>
<span class="flex items-center gap-2">
<svg class="w-4 h-4 text-(--color-brand)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072M12 18.364a8 8 0 010-12.728M8.464 15.536a5 5 0 010-7.072" />
</svg>
<span class="font-medium">Listen to this chapter</span>
{#if audioStore.slug === data.book.slug && audioStore.chapter === data.chapter.number && audioStore.isPlaying}
<span class="text-xs text-(--color-brand)">● Playing</span>
{/if}
</span>
<svg class="w-4 h-4 text-(--color-muted) transition-transform duration-200 {audioExpanded ? 'rotate-180' : ''}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
{#if audioExpanded}
<div class="border border-t-0 border-(--color-border) rounded-b-lg overflow-hidden">
<AudioPlayer
slug={data.book.slug}
chapter={data.chapter.number}
chapterTitle={data.chapter.title || m.reader_chapter_n({ n: String(data.chapter.number) })}
bookTitle={data.book.title}
cover={data.book.cover}
nextChapter={data.next}
chapters={data.chapters}
voices={data.voices}
playerStyle={layout.playerStyle}
onProRequired={() => { audioProRequired = true; }}
/>
</div>
{/if}
</div>
{/if}
{:else if data.isPreview}
<div class="mb-6 px-4 py-2 rounded bg-(--color-surface-2)/60 border border-(--color-border) text-(--color-muted) text-sm">
{m.reader_preview_audio_notice()}
</div> </div>
{:else}
<AudioPlayer
slug={data.book.slug}
chapter={data.chapter.number}
chapterTitle={data.chapter.title || m.reader_chapter_n({ n: String(data.chapter.number) })}
bookTitle={data.book.title}
cover={data.book.cover}
nextChapter={data.next}
chapters={data.chapters}
voices={data.voices}
playerStyle={layout.playerStyle}
onProRequired={() => { audioProRequired = true; }}
/>
{/if}
{:else}
<div class="mb-6 px-4 py-3 rounded bg-(--color-surface-2)/60 border border-(--color-border) text-(--color-muted) text-sm">
{m.reader_preview_audio_notice()}
</div>
{/if} {/if}
<!-- Chapter content --> <!-- ── Chapter content ───────────────────────────────────────────────────── -->
{#if fetchingContent} {#if fetchingContent}
<div class="flex flex-col items-center gap-3 py-16 text-(--color-muted) text-sm"> <div class="flex flex-col items-center gap-3 py-16 text-(--color-muted) text-sm">
<svg class="w-6 h-6 animate-spin" fill="none" viewBox="0 0 24 24"> <svg class="w-6 h-6 animate-spin" fill="none" viewBox="0 0 24 24">
@@ -465,7 +507,7 @@
<p>{fetchError || m.reader_audio_error()}</p> <p>{fetchError || m.reader_audio_error()}</p>
</div> </div>
{:else if layout.readMode === 'paginated'} {:else if layout.readMode === 'paginated'}
<!-- ── Paginated reader ─────────────────────────────────────────────── --> <!-- ── Paginated reader ───────────────────────────────────────────── -->
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div <div
role="none" role="none"
@@ -496,11 +538,9 @@
</svg> </svg>
Prev Prev
</button> </button>
<span class="text-sm text-(--color-muted) tabular-nums"> <span class="text-sm text-(--color-muted) tabular-nums">
{pageIndex + 1} <span class="opacity-40">/</span> {totalPages} {pageIndex + 1} <span class="opacity-40">/</span> {totalPages}
</span> </span>
<button <button
type="button" type="button"
onclick={() => { if (pageIndex < totalPages - 1) pageIndex++; }} onclick={() => { if (pageIndex < totalPages - 1) pageIndex++; }}
@@ -513,50 +553,59 @@
</svg> </svg>
</button> </button>
</div> </div>
<!-- Tap hint -->
<p class="text-center text-xs text-(--color-muted)/40 mt-2">Tap left/right · Arrow keys · Space</p> <p class="text-center text-xs text-(--color-muted)/40 mt-2">Tap left/right · Arrow keys · Space</p>
{:else} {:else}
<!-- ── Scroll reader ────────────────────────────────────────────────── --> <!-- ── Scroll reader ──────────────────────────────────────────────── -->
<div class="prose-chapter mt-8 {layout.paraStyle === 'indented' ? 'para-indented' : ''}"> <div class="prose-chapter mt-8 {layout.paraStyle === 'indented' ? 'para-indented' : ''}">
{@html html} {@html html}
</div> </div>
{/if} {/if}
<!-- Bottom nav + comments (hidden in focus mode) --> <!-- ── Bottom navigation + comments (hidden in focus mode) ───────────────── -->
{#if !layout.focusMode} {#if !layout.focusMode}
<div class="flex justify-between mt-12 pt-6 border-t border-(--color-border) gap-4"> <div class="mt-14 pt-6 border-t border-(--color-border)">
{#if data.prev} <!-- Next chapter: prominent full-width CTA -->
<a {#if data.next}
href="/books/{data.book.slug}/chapters/{data.prev}" <a
class="px-4 py-2 rounded bg-(--color-surface-3) text-(--color-text) text-sm hover:bg-(--color-surface-2) transition-colors" href="/books/{data.book.slug}/chapters/{data.next}"
> class="group flex items-center justify-between px-5 py-4 rounded-xl bg-(--color-surface-2) border border-(--color-border) hover:border-(--color-brand)/50 transition-colors"
&larr; {m.reader_prev_chapter()} >
</a> <div>
{:else} <p class="text-xs text-(--color-muted) mb-0.5">{m.reader_next_chapter()}</p>
<span></span> <p class="text-(--color-text) font-semibold group-hover:text-(--color-brand) transition-colors">
{/if} {m.reader_chapter_n({ n: String(data.next) })}
{#if data.next} </p>
<a </div>
href="/books/{data.book.slug}/chapters/{data.next}" <svg class="w-5 h-5 text-(--color-muted) group-hover:text-(--color-brand) group-hover:translate-x-0.5 transition-all" fill="none" stroke="currentColor" viewBox="0 0 24 24">
class="px-4 py-2 rounded bg-(--color-surface-3) text-(--color-text) text-sm hover:bg-(--color-surface-2) transition-colors" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
> </svg>
{m.reader_next_chapter()} &rarr; </a>
</a> {/if}
{/if} <!-- Previous chapter: small secondary link -->
</div> {#if data.prev}
<a
href="/books/{data.book.slug}/chapters/{data.prev}"
class="mt-3 inline-flex items-center gap-1 text-sm text-(--color-muted) hover:text-(--color-text) transition-colors"
>
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
{m.reader_prev_chapter()}
</a>
{/if}
</div>
<div class="mt-12"> <div class="mt-12">
<CommentsSection <CommentsSection
slug={data.book.slug} slug={data.book.slug}
chapter={data.chapter.number} chapter={data.chapter.number}
isLoggedIn={!!page.data.user} isLoggedIn={!!page.data.user}
currentUserId={page.data.user?.id ?? ''} currentUserId={page.data.user?.id ?? ''}
/> />
</div> </div>
{/if} {/if}
<!-- Focus mode floating nav (shown only in focus mode) --> <!-- ── Focus mode floating nav ───────────────────────────────────────────── -->
{#if layout.focusMode} {#if layout.focusMode}
<div class="fixed bottom-[4.5rem] left-1/2 -translate-x-1/2 z-50 flex items-center gap-2"> <div class="fixed bottom-[4.5rem] left-1/2 -translate-x-1/2 z-50 flex items-center gap-2">
{#if data.prev} {#if data.prev}
@@ -595,22 +644,8 @@
</div> </div>
{/if} {/if}
<!-- ── Reader settings bottom sheet ─────────────────────────────────────── --> <!-- ── Reader settings bottom sheet ─────────────────────────────────────── -->
{#if settingsCtx} {#if settingsCtx}
<!-- Gear button — sits just above the mini-player (bottom-[4.5rem]) -->
<button
onclick={() => { settingsPanelOpen = !settingsPanelOpen; settingsTab = 'reading'; }}
aria-label="Reader settings"
class="fixed bottom-[4.5rem] right-4 z-50 w-11 h-11 rounded-full bg-(--color-surface-2) border border-(--color-border) text-(--color-muted) hover:text-(--color-text) hover:border-zinc-500 transition-colors flex items-center justify-center shadow-lg"
>
<svg class="w-5 h-5 {settingsPanelOpen ? 'text-(--color-brand)' : ''}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
</button>
<!-- Bottom sheet -->
{#if settingsPanelOpen} {#if settingsPanelOpen}
<!-- Backdrop --> <!-- Backdrop -->
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
@@ -648,12 +683,11 @@
{#if settingsTab === 'reading'} {#if settingsTab === 'reading'}
<!-- ── Typography group ──────────────────────────────────────── --> <!-- ── Typography ──────────────────────────────────────────────── -->
<div class="mb-1"> <div class="mb-1">
<p class="text-[10px] font-semibold text-(--color-muted) uppercase tracking-wider mb-2">Typography</p> <p class="text-[10px] font-semibold text-(--color-muted) uppercase tracking-wider mb-2">Typography</p>
<div class="bg-(--color-surface-3) rounded-xl overflow-hidden divide-y divide-(--color-border)"> <div class="bg-(--color-surface-3) rounded-xl overflow-hidden divide-y divide-(--color-border)">
<!-- Font -->
<div class="flex items-center gap-3 px-3 py-2.5"> <div class="flex items-center gap-3 px-3 py-2.5">
<span class="text-xs text-(--color-muted) w-10 shrink-0">Font</span> <span class="text-xs text-(--color-muted) w-10 shrink-0">Font</span>
<div class="flex gap-1.5 flex-1"> <div class="flex gap-1.5 flex-1">
@@ -670,7 +704,6 @@
</div> </div>
</div> </div>
<!-- Size -->
<div class="flex items-center gap-3 px-3 py-2.5"> <div class="flex items-center gap-3 px-3 py-2.5">
<span class="text-xs text-(--color-muted) w-10 shrink-0">Size</span> <span class="text-xs text-(--color-muted) w-10 shrink-0">Size</span>
<div class="flex gap-1.5 flex-1"> <div class="flex gap-1.5 flex-1">
@@ -690,12 +723,11 @@
</div> </div>
</div> </div>
<!-- ── Layout group ──────────────────────────────────────────── --> <!-- ── Layout ──────────────────────────────────────────────────── -->
<div class="mt-4 mb-1"> <div class="mt-4 mb-1">
<p class="text-[10px] font-semibold text-(--color-muted) uppercase tracking-wider mb-2">Layout</p> <p class="text-[10px] font-semibold text-(--color-muted) uppercase tracking-wider mb-2">Layout</p>
<div class="bg-(--color-surface-3) rounded-xl overflow-hidden divide-y divide-(--color-border)"> <div class="bg-(--color-surface-3) rounded-xl overflow-hidden divide-y divide-(--color-border)">
<!-- Read mode -->
<div class="flex items-center gap-3 px-3 py-2.5"> <div class="flex items-center gap-3 px-3 py-2.5">
<span class="text-xs text-(--color-muted) w-16 shrink-0">Mode</span> <span class="text-xs text-(--color-muted) w-16 shrink-0">Mode</span>
<div class="flex gap-1.5 flex-1"> <div class="flex gap-1.5 flex-1">
@@ -713,7 +745,6 @@
</div> </div>
</div> </div>
<!-- Line spacing -->
<div class="flex items-center gap-3 px-3 py-2.5"> <div class="flex items-center gap-3 px-3 py-2.5">
<span class="text-xs text-(--color-muted) w-16 shrink-0">Spacing</span> <span class="text-xs text-(--color-muted) w-16 shrink-0">Spacing</span>
<div class="flex gap-1.5 flex-1"> <div class="flex gap-1.5 flex-1">
@@ -731,7 +762,6 @@
</div> </div>
</div> </div>
<!-- Width -->
<div class="flex items-center gap-3 px-3 py-2.5"> <div class="flex items-center gap-3 px-3 py-2.5">
<span class="text-xs text-(--color-muted) w-16 shrink-0">Width</span> <span class="text-xs text-(--color-muted) w-16 shrink-0">Width</span>
<div class="flex gap-1.5 flex-1"> <div class="flex gap-1.5 flex-1">
@@ -749,7 +779,6 @@
</div> </div>
</div> </div>
<!-- Paragraphs -->
<div class="flex items-center gap-3 px-3 py-2.5"> <div class="flex items-center gap-3 px-3 py-2.5">
<span class="text-xs text-(--color-muted) w-16 shrink-0">Paragraphs</span> <span class="text-xs text-(--color-muted) w-16 shrink-0">Paragraphs</span>
<div class="flex gap-1.5 flex-1"> <div class="flex gap-1.5 flex-1">
@@ -767,7 +796,6 @@
</div> </div>
</div> </div>
<!-- Focus mode -->
<button <button
type="button" type="button"
onclick={() => setLayout('focusMode', !layout.focusMode)} onclick={() => setLayout('focusMode', !layout.focusMode)}
@@ -784,12 +812,11 @@
{:else} {:else}
<!-- ── Listening tab ──────────────────────────────────────────── --> <!-- ── Listening tab ──────────────────────────────────────────── -->
<div class="mb-1"> <div class="mb-1">
<p class="text-[10px] font-semibold text-(--color-muted) uppercase tracking-wider mb-2">Player</p> <p class="text-[10px] font-semibold text-(--color-muted) uppercase tracking-wider mb-2">Player</p>
<div class="bg-(--color-surface-3) rounded-xl overflow-hidden divide-y divide-(--color-border)"> <div class="bg-(--color-surface-3) rounded-xl overflow-hidden divide-y divide-(--color-border)">
<!-- Player style -->
<div class="flex items-center gap-3 px-3 py-2.5"> <div class="flex items-center gap-3 px-3 py-2.5">
<span class="text-xs text-(--color-muted) w-14 shrink-0">Style</span> <span class="text-xs text-(--color-muted) w-14 shrink-0">Style</span>
<div class="flex gap-1.5 flex-1"> <div class="flex gap-1.5 flex-1">