feat: reading view modes — progress bar, paginated, spacing, width, focus
Some checks failed
Release / Test backend (push) Successful in 38s
Release / Check ui (push) Failing after 27s
Release / Docker / ui (push) Has been skipped
Release / Docker / caddy (push) Successful in 43s
Release / Docker / backend (push) Successful in 2m50s
Release / Docker / runner (push) Successful in 3m3s
Release / Gitea Release (push) Has been skipped
Some checks failed
Release / Test backend (push) Successful in 38s
Release / Check ui (push) Failing after 27s
Release / Docker / ui (push) Has been skipped
Release / Docker / caddy (push) Successful in 43s
Release / Docker / backend (push) Successful in 2m50s
Release / Docker / runner (push) Successful in 3m3s
Release / Gitea Release (push) Has been skipped
Five new reader settings (persisted in localStorage as reader_layout_v1): - Read mode: Scroll (default) vs Pages — paginated mode splits content into viewport-height pages, navigate with tap left/right, arrow keys, or Prev/Next buttons. Recalculates on content change. - Line spacing: Tight (1.55) / Normal (1.85) / Loose (2.2) via --reading-line-height CSS var on :root. - Reading width: Narrow (58ch) / Normal (72ch) / Wide (90ch) via --reading-max-width CSS var on :root. - Paragraph style: Spaced (default) vs Indented (text-indent: 2em, tight margin — book-like feel). - Focus mode: hides audio player, language switcher, bottom nav and comments so only the text remains. Scroll progress bar: thin 2px brand-colored bar fixed at top of viewport in scroll mode, fills as you read through the chapter. All options added to the floating settings gear panel. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -106,12 +106,14 @@ html {
|
|||||||
:root {
|
:root {
|
||||||
--reading-font: system-ui, -apple-system, sans-serif;
|
--reading-font: system-ui, -apple-system, sans-serif;
|
||||||
--reading-size: 1.05rem;
|
--reading-size: 1.05rem;
|
||||||
|
--reading-line-height: 1.85;
|
||||||
|
--reading-max-width: 72ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Chapter prose ─────────────────────────────────────────────────── */
|
/* ── Chapter prose ─────────────────────────────────────────────────── */
|
||||||
.prose-chapter {
|
.prose-chapter {
|
||||||
max-width: 72ch;
|
max-width: var(--reading-max-width, 72ch);
|
||||||
line-height: 1.85;
|
line-height: var(--reading-line-height, 1.85);
|
||||||
font-family: var(--reading-font);
|
font-family: var(--reading-font);
|
||||||
font-size: var(--reading-size);
|
font-size: var(--reading-size);
|
||||||
color: var(--color-muted);
|
color: var(--color-muted);
|
||||||
@@ -134,6 +136,12 @@ html {
|
|||||||
margin-bottom: 1.2em;
|
margin-bottom: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Indented paragraph style — book-like, no gap, indent instead */
|
||||||
|
.prose-chapter.para-indented p {
|
||||||
|
text-indent: 2em;
|
||||||
|
margin-bottom: 0.35em;
|
||||||
|
}
|
||||||
|
|
||||||
.prose-chapter em {
|
.prose-chapter em {
|
||||||
color: var(--color-muted);
|
color: var(--color-muted);
|
||||||
}
|
}
|
||||||
@@ -147,6 +155,31 @@ html {
|
|||||||
margin: 2em 0;
|
margin: 2em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Reading progress bar ───────────────────────────────────────────── */
|
||||||
|
.reading-progress {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 2px;
|
||||||
|
z-index: 100;
|
||||||
|
background: var(--color-brand);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: width 0.1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Paginated reader ───────────────────────────────────────────────── */
|
||||||
|
.paginated-container {
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginated-container .prose-chapter {
|
||||||
|
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Hide scrollbars (used on horizontal carousels) ────────────────── */
|
/* ── Hide scrollbars (used on horizontal carousels) ────────────────── */
|
||||||
.scrollbar-none {
|
.scrollbar-none {
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, untrack, getContext } from 'svelte';
|
import { onMount, untrack, getContext } from 'svelte';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import AudioPlayer from '$lib/components/AudioPlayer.svelte';
|
import AudioPlayer from '$lib/components/AudioPlayer.svelte';
|
||||||
@@ -56,7 +57,114 @@
|
|||||||
if (settingsCtx) settingsCtx.fontSize = v;
|
if (settingsCtx) settingsCtx.fontSize = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translation state
|
// ── Layout / reading-view prefs (localStorage) ──────────────────────────────
|
||||||
|
type ReadMode = 'scroll' | 'paginated';
|
||||||
|
type LineSpacing = 'compact' | 'normal' | 'relaxed';
|
||||||
|
type ReadWidth = 'narrow' | 'normal' | 'wide';
|
||||||
|
type ParaStyle = 'spaced' | 'indented';
|
||||||
|
|
||||||
|
interface LayoutPrefs {
|
||||||
|
readMode: ReadMode;
|
||||||
|
lineSpacing: LineSpacing;
|
||||||
|
readWidth: ReadWidth;
|
||||||
|
paraStyle: ParaStyle;
|
||||||
|
focusMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LAYOUT_KEY = 'reader_layout_v1';
|
||||||
|
const LINE_HEIGHTS: Record<LineSpacing, number> = { compact: 1.55, normal: 1.85, relaxed: 2.2 };
|
||||||
|
const READ_WIDTHS: Record<ReadWidth, string> = { narrow: '58ch', normal: '72ch', wide: 'min(90ch, 100%)' };
|
||||||
|
const DEFAULT_LAYOUT: LayoutPrefs = { readMode: 'scroll', lineSpacing: 'normal', readWidth: 'normal', paraStyle: 'spaced', focusMode: false };
|
||||||
|
|
||||||
|
function loadLayout(): LayoutPrefs {
|
||||||
|
if (!browser) return DEFAULT_LAYOUT;
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(LAYOUT_KEY);
|
||||||
|
if (raw) return { ...DEFAULT_LAYOUT, ...JSON.parse(raw) as Partial<LayoutPrefs> };
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
return DEFAULT_LAYOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout = $state<LayoutPrefs>(loadLayout());
|
||||||
|
|
||||||
|
function setLayout<K extends keyof LayoutPrefs>(key: K, value: LayoutPrefs[K]) {
|
||||||
|
layout = { ...layout, [key]: value };
|
||||||
|
if (browser) localStorage.setItem(LAYOUT_KEY, JSON.stringify(layout));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply reading CSS vars whenever layout changes
|
||||||
|
$effect(() => {
|
||||||
|
if (!browser) return;
|
||||||
|
document.documentElement.style.setProperty('--reading-line-height', String(LINE_HEIGHTS[layout.lineSpacing]));
|
||||||
|
document.documentElement.style.setProperty('--reading-max-width', READ_WIDTHS[layout.readWidth]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Scroll progress bar ──────────────────────────────────────────────────────
|
||||||
|
let scrollProgress = $state(0);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!browser || layout.readMode === 'paginated') { scrollProgress = 0; return; }
|
||||||
|
function onScroll() {
|
||||||
|
const el = document.documentElement;
|
||||||
|
const max = el.scrollHeight - el.clientHeight;
|
||||||
|
scrollProgress = max > 0 ? el.scrollTop / max : 0;
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', onScroll, { passive: true });
|
||||||
|
return () => window.removeEventListener('scroll', onScroll);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Paginated mode ───────────────────────────────────────────────────────────
|
||||||
|
let pageIndex = $state(0);
|
||||||
|
let totalPages = $state(1);
|
||||||
|
let paginatedContainerEl = $state<HTMLDivElement | null>(null);
|
||||||
|
let paginatedContentEl = $state<HTMLDivElement | null>(null);
|
||||||
|
let containerH = $state(0);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (layout.readMode !== 'paginated') { pageIndex = 0; totalPages = 1; return; }
|
||||||
|
// Re-run when html changes or container is bound
|
||||||
|
void html; void paginatedContainerEl; void paginatedContentEl;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (!paginatedContainerEl || !paginatedContentEl) return;
|
||||||
|
const h = paginatedContainerEl.clientHeight;
|
||||||
|
if (h > 0) {
|
||||||
|
containerH = h;
|
||||||
|
totalPages = Math.max(1, Math.ceil(paginatedContentEl.scrollHeight / h));
|
||||||
|
pageIndex = Math.min(pageIndex, totalPages - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset page index when chapter changes
|
||||||
|
$effect(() => { void data.chapter.number; pageIndex = 0; });
|
||||||
|
|
||||||
|
function handlePaginatedClick(e: MouseEvent) {
|
||||||
|
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||||
|
if (e.clientX - rect.left > rect.width / 2) {
|
||||||
|
if (pageIndex < totalPages - 1) pageIndex++;
|
||||||
|
} else {
|
||||||
|
if (pageIndex > 0) pageIndex--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard nav for paginated mode
|
||||||
|
$effect(() => {
|
||||||
|
if (!browser) return;
|
||||||
|
function onKey(e: KeyboardEvent) {
|
||||||
|
if (layout.readMode !== 'paginated') return;
|
||||||
|
if (e.key === 'ArrowRight' || e.key === 'PageDown' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (pageIndex < totalPages - 1) pageIndex++;
|
||||||
|
} else if (e.key === 'ArrowLeft' || e.key === 'PageUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (pageIndex > 0) pageIndex--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', onKey);
|
||||||
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Translation state
|
||||||
const SUPPORTED_LANGS = [
|
const SUPPORTED_LANGS = [
|
||||||
{ code: 'ru', label: 'RU' },
|
{ code: 'ru', label: 'RU' },
|
||||||
{ code: 'id', label: 'ID' },
|
{ code: 'id', label: 'ID' },
|
||||||
@@ -189,6 +297,11 @@
|
|||||||
<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) -->
|
||||||
|
{#if layout.readMode === 'scroll'}
|
||||||
|
<div class="reading-progress" style="width: {scrollProgress * 100}%"></div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Top nav -->
|
<!-- Top nav -->
|
||||||
<div class="flex items-center justify-between mb-6 gap-4">
|
<div class="flex items-center justify-between mb-6 gap-4">
|
||||||
<a
|
<a
|
||||||
@@ -235,8 +348,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Language switcher (not shown for preview chapters) -->
|
<!-- Language switcher (not shown for preview chapters or focus mode) -->
|
||||||
{#if !data.isPreview}
|
{#if !data.isPreview && !layout.focusMode}
|
||||||
<div class="flex items-center gap-2 mb-6 flex-wrap">
|
<div class="flex items-center gap-2 mb-6 flex-wrap">
|
||||||
<span class="text-(--color-muted) text-xs">Read in:</span>
|
<span class="text-(--color-muted) text-xs">Read in:</span>
|
||||||
|
|
||||||
@@ -292,8 +405,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Audio player -->
|
<!-- Audio player (hidden in focus mode) -->
|
||||||
{#if !data.isPreview}
|
{#if !data.isPreview && !layout.focusMode}
|
||||||
{#if !page.data.user}
|
{#if !page.data.user}
|
||||||
<!-- Unauthenticated: sign-in prompt -->
|
<!-- Unauthenticated: sign-in prompt -->
|
||||||
<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">
|
||||||
@@ -353,13 +466,66 @@
|
|||||||
<div class="text-(--color-muted) text-center py-16">
|
<div class="text-(--color-muted) text-center py-16">
|
||||||
<p>{fetchError || m.reader_audio_error()}</p>
|
<p>{fetchError || m.reader_audio_error()}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if layout.readMode === 'paginated'}
|
||||||
|
<!-- ── Paginated reader ─────────────────────────────────────────────── -->
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
bind:this={paginatedContainerEl}
|
||||||
|
class="paginated-container mt-8"
|
||||||
|
style="height: {layout.focusMode ? 'calc(100svh - 8rem)' : 'calc(100svh - 26rem)'};"
|
||||||
|
onclick={handlePaginatedClick}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
bind:this={paginatedContentEl}
|
||||||
|
class="prose-chapter {layout.paraStyle === 'indented' ? 'para-indented' : ''}"
|
||||||
|
style="transform: translateY({containerH > 0 ? -(pageIndex * containerH) : 0}px);"
|
||||||
|
>
|
||||||
|
{@html html}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page indicator + nav -->
|
||||||
|
<div class="flex items-center justify-between mt-4 select-none">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => { if (pageIndex > 0) pageIndex--; }}
|
||||||
|
disabled={pageIndex === 0}
|
||||||
|
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-(--color-surface-2) border border-(--color-border) text-sm text-(--color-muted) hover:text-(--color-text) disabled:opacity-30 transition-colors"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
Prev
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span class="text-sm text-(--color-muted) tabular-nums">
|
||||||
|
{pageIndex + 1} <span class="opacity-40">/</span> {totalPages}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => { if (pageIndex < totalPages - 1) pageIndex++; }}
|
||||||
|
disabled={pageIndex === totalPages - 1}
|
||||||
|
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-(--color-surface-2) border border-(--color-border) text-sm text-(--color-muted) hover:text-(--color-text) disabled:opacity-30 transition-colors"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
<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>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tap hint -->
|
||||||
|
<p class="text-center text-xs text-(--color-muted)/40 mt-2">Tap left/right · Arrow keys · Space</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="prose-chapter mt-8">
|
<!-- ── Scroll reader ────────────────────────────────────────────────── -->
|
||||||
|
<div class="prose-chapter mt-8 {layout.paraStyle === 'indented' ? 'para-indented' : ''}">
|
||||||
{@html html}
|
{@html html}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Bottom nav -->
|
<!-- Bottom nav + comments (hidden in paginated focus mode) -->
|
||||||
|
{#if !(layout.focusMode && layout.readMode === 'paginated')}
|
||||||
<div class="flex justify-between mt-12 pt-6 border-t border-(--color-border) gap-4">
|
<div class="flex justify-between mt-12 pt-6 border-t border-(--color-border) gap-4">
|
||||||
{#if data.prev}
|
{#if data.prev}
|
||||||
<a
|
<a
|
||||||
@@ -381,7 +547,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chapter comments -->
|
|
||||||
<div class="mt-12">
|
<div class="mt-12">
|
||||||
<CommentsSection
|
<CommentsSection
|
||||||
slug={data.book.slug}
|
slug={data.book.slug}
|
||||||
@@ -390,6 +555,7 @@
|
|||||||
currentUserId={page.data.user?.id ?? ''}
|
currentUserId={page.data.user?.id ?? ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- ── Floating reader settings ─────────────────────────────────────────── -->
|
<!-- ── Floating reader settings ─────────────────────────────────────────── -->
|
||||||
{#if settingsCtx}
|
{#if settingsCtx}
|
||||||
@@ -480,6 +646,95 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<div class="border-t border-(--color-border)"></div>
|
||||||
|
|
||||||
|
<!-- Read mode -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p class="text-xs text-(--color-muted)">Read mode</p>
|
||||||
|
<div class="flex gap-1.5">
|
||||||
|
{#each ([['scroll', 'Scroll'], ['paginated', 'Pages']] as const) as [mode, label]}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => setLayout('readMode', mode)}
|
||||||
|
class="flex-1 py-1.5 rounded-lg border text-xs font-medium transition-colors
|
||||||
|
{layout.readMode === mode
|
||||||
|
? 'border-(--color-brand) bg-(--color-brand)/10 text-(--color-brand)'
|
||||||
|
: 'border-(--color-border) bg-(--color-surface-3) text-(--color-muted) hover:border-(--color-brand)/50 hover:text-(--color-text)'}"
|
||||||
|
aria-pressed={layout.readMode === mode}
|
||||||
|
>{label}</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Line spacing -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p class="text-xs text-(--color-muted)">Line spacing</p>
|
||||||
|
<div class="flex gap-1.5">
|
||||||
|
{#each ([['compact', 'Tight'], ['normal', 'Normal'], ['relaxed', 'Loose']] as const) as [s, label]}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => setLayout('lineSpacing', s)}
|
||||||
|
class="flex-1 py-1.5 rounded-lg border text-xs font-medium transition-colors
|
||||||
|
{layout.lineSpacing === s
|
||||||
|
? 'border-(--color-brand) bg-(--color-brand)/10 text-(--color-brand)'
|
||||||
|
: 'border-(--color-border) bg-(--color-surface-3) text-(--color-muted) hover:border-(--color-brand)/50 hover:text-(--color-text)'}"
|
||||||
|
aria-pressed={layout.lineSpacing === s}
|
||||||
|
>{label}</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reading width -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p class="text-xs text-(--color-muted)">Width</p>
|
||||||
|
<div class="flex gap-1.5">
|
||||||
|
{#each ([['narrow', 'Narrow'], ['normal', 'Normal'], ['wide', 'Wide']] as const) as [w, label]}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => setLayout('readWidth', w)}
|
||||||
|
class="flex-1 py-1.5 rounded-lg border text-xs font-medium transition-colors
|
||||||
|
{layout.readWidth === w
|
||||||
|
? 'border-(--color-brand) bg-(--color-brand)/10 text-(--color-brand)'
|
||||||
|
: 'border-(--color-border) bg-(--color-surface-3) text-(--color-muted) hover:border-(--color-brand)/50 hover:text-(--color-text)'}"
|
||||||
|
aria-pressed={layout.readWidth === w}
|
||||||
|
>{label}</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Paragraph style -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p class="text-xs text-(--color-muted)">Paragraphs</p>
|
||||||
|
<div class="flex gap-1.5">
|
||||||
|
{#each ([['spaced', 'Spaced'], ['indented', 'Indented']] as const) as [s, label]}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => setLayout('paraStyle', s)}
|
||||||
|
class="flex-1 py-1.5 rounded-lg border text-xs font-medium transition-colors
|
||||||
|
{layout.paraStyle === s
|
||||||
|
? 'border-(--color-brand) bg-(--color-brand)/10 text-(--color-brand)'
|
||||||
|
: 'border-(--color-border) bg-(--color-surface-3) text-(--color-muted) hover:border-(--color-brand)/50 hover:text-(--color-text)'}"
|
||||||
|
aria-pressed={layout.paraStyle === s}
|
||||||
|
>{label}</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Focus mode -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => setLayout('focusMode', !layout.focusMode)}
|
||||||
|
class="w-full flex items-center justify-between py-2 px-3 rounded-lg border text-xs font-medium transition-colors
|
||||||
|
{layout.focusMode
|
||||||
|
? 'border-(--color-brand) bg-(--color-brand)/10 text-(--color-brand)'
|
||||||
|
: 'border-(--color-border) bg-(--color-surface-3) text-(--color-muted) hover:border-(--color-brand)/50 hover:text-(--color-text)'}"
|
||||||
|
aria-pressed={layout.focusMode}
|
||||||
|
>
|
||||||
|
<span>Focus mode</span>
|
||||||
|
<span class="opacity-60 text-xs">{layout.focusMode ? 'On — audio & nav hidden' : 'Off'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<p class="text-xs text-(--color-muted)/60 text-center">Changes save automatically</p>
|
<p class="text-xs text-(--color-muted)/60 text-center">Changes save automatically</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user