mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Chore: Restructure Repository for SvelteKit
This commit is contained in:
@@ -0,0 +1,813 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
X, Check, Trash, FloppyDisk,
|
||||
Square, Rows, BookOpen, MonitorPlay,
|
||||
ArrowsLeftRight, ArrowsIn, ArrowsOut, ArrowsVertical,
|
||||
ArrowsHorizontal,
|
||||
SidebarSimple,
|
||||
} from "phosphor-svelte";
|
||||
import type { ReaderSettings, ReaderPreset, FitMode } from "@store/state.svelte";
|
||||
import { store, updateSettings } from "@store/state.svelte";
|
||||
import { readerState, PAGE_STYLES, ZOOM_MIN, ZOOM_MAX } from "../store/readerState.svelte";
|
||||
import { fade, fly } from "svelte/transition";
|
||||
import { cubicOut } from "svelte/easing";
|
||||
|
||||
interface Props {
|
||||
fit: FitMode;
|
||||
style: string;
|
||||
rtl: boolean;
|
||||
zoom: number;
|
||||
zoomPct: number;
|
||||
perMangaEnabled: boolean;
|
||||
onTogglePerManga: () => void;
|
||||
onSavePreset: (name: string) => void;
|
||||
onApplyPreset: (settings: ReaderSettings) => void;
|
||||
onUpdatePreset: (id: string, patch: Partial<Pick<ReaderPreset, "name" | "settings">>) => void;
|
||||
onDeletePreset: (id: string) => void;
|
||||
onApplySettings: (patch: Partial<ReaderSettings>) => void;
|
||||
onCaptureZoomAnchor: () => void;
|
||||
onRestoreZoomAnchor: () => void;
|
||||
onClampZoom: (z: number) => number;
|
||||
barPosition: "top" | "left" | "right";
|
||||
onBarPositionChange: (pos: "top" | "left" | "right") => void;
|
||||
}
|
||||
|
||||
const {
|
||||
fit, style, rtl, zoom, zoomPct,
|
||||
perMangaEnabled, onTogglePerManga,
|
||||
onSavePreset, onApplyPreset, onUpdatePreset, onDeletePreset,
|
||||
onApplySettings,
|
||||
onCaptureZoomAnchor, onRestoreZoomAnchor, onClampZoom,
|
||||
barPosition, onBarPositionChange,
|
||||
}: Props = $props();
|
||||
|
||||
const presets = $derived(store.settings.readerPresets ?? []);
|
||||
const effectiveSettings = $derived.by(() => {
|
||||
const mangaId = store.activeManga?.id;
|
||||
const override = mangaId != null ? (store.settings.mangaReaderSettings ?? {})[mangaId] : undefined;
|
||||
return override ? { ...store.settings, ...override } : store.settings;
|
||||
});
|
||||
|
||||
let presetSaving = $state(false);
|
||||
let presetNameInput = $state("");
|
||||
let presetEditId = $state<string | null>(null);
|
||||
let presetEditName = $state("");
|
||||
|
||||
function close() {
|
||||
readerState.presetOpen = false;
|
||||
presetSaving = false;
|
||||
presetNameInput = "";
|
||||
presetEditId = null;
|
||||
}
|
||||
|
||||
function commitSavePreset() {
|
||||
if (!presetNameInput.trim()) return;
|
||||
onSavePreset(presetNameInput.trim());
|
||||
presetSaving = false;
|
||||
presetNameInput = "";
|
||||
}
|
||||
|
||||
function commitRenamePreset() {
|
||||
if (!presetEditId || !presetEditName.trim()) return;
|
||||
onUpdatePreset(presetEditId, { name: presetEditName.trim() });
|
||||
presetEditId = null;
|
||||
presetEditName = "";
|
||||
}
|
||||
|
||||
function describeSettings(s: ReaderSettings): string {
|
||||
const parts = [s.pageStyle ?? "single", s.fitMode ?? "width", (s.readingDirection ?? "ltr") === "rtl" ? "RTL" : "LTR"];
|
||||
if ((s.readerZoom ?? 1) !== 1.0) parts.push(`${Math.round((s.readerZoom ?? 1) * 100)}%`);
|
||||
if (!s.pageGap) parts.push("no gap");
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
function setZoom(v: number) {
|
||||
onCaptureZoomAnchor();
|
||||
onApplySettings({ readerZoom: onClampZoom(v) });
|
||||
onRestoreZoomAnchor();
|
||||
}
|
||||
|
||||
const fitOptions: { value: FitMode; label: string; icon: any }[] = [
|
||||
{ value: "width", label: "Fit Width", icon: ArrowsLeftRight },
|
||||
{ value: "height", label: "Fit Height", icon: ArrowsVertical },
|
||||
{ value: "screen", label: "Fit Screen", icon: ArrowsIn },
|
||||
{ value: "original", label: "Original", icon: ArrowsOut },
|
||||
];
|
||||
|
||||
const styleOptions: { value: string; label: string; icon: any }[] = [
|
||||
{ value: "single", label: "Single", icon: Square },
|
||||
{ value: "double", label: "Double", icon: BookOpen },
|
||||
{ value: "fade", label: "Fade", icon: MonitorPlay },
|
||||
{ value: "longstrip", label: "Long Strip", icon: Rows },
|
||||
];
|
||||
|
||||
const barOptions: { value: "top" | "left" | "right"; label: string }[] = [
|
||||
{ value: "left", label: "Left" },
|
||||
{ value: "top", label: "Top" },
|
||||
{ value: "right", label: "Right" },
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="backdrop" role="button" tabindex="-1" aria-label="Close settings" onclick={close} onkeydown={(e) => e.key === 'Escape' && close()} transition:fade={{ duration: 150 }}></div>
|
||||
|
||||
<div
|
||||
class="panel"
|
||||
role="dialog"
|
||||
aria-label="Reader settings & presets"
|
||||
transition:fly={{ x: 320, duration: 220, easing: cubicOut }}
|
||||
>
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">Reader Settings</span>
|
||||
{#if store.activeManga}
|
||||
<span class="panel-manga">{store.activeManga.title}</span>
|
||||
{/if}
|
||||
<button class="close-btn" onclick={close}><X size={14} weight="light" /></button>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
|
||||
<section class="section">
|
||||
<p class="section-label">Page Style</p>
|
||||
<div class="option-grid">
|
||||
{#each styleOptions as o}
|
||||
{@const Icon = o.icon}
|
||||
<button
|
||||
class="option-tile"
|
||||
class:active={style === o.value}
|
||||
onclick={() => onApplySettings({ pageStyle: o.value as typeof PAGE_STYLES[number] })}
|
||||
>
|
||||
<div class="tile-icon"><Icon size={18} weight={style === o.value ? "fill" : "light"} /></div>
|
||||
<span class="tile-label">{o.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if style === "double"}
|
||||
<label class="toggle-row">
|
||||
<span class="toggle-label">Offset double spreads</span>
|
||||
<button
|
||||
class="toggle"
|
||||
class:on={effectiveSettings.offsetDoubleSpreads}
|
||||
onclick={() => onApplySettings({ offsetDoubleSpreads: !effectiveSettings.offsetDoubleSpreads })}
|
||||
role="switch"
|
||||
aria-label="Offset double spreads"
|
||||
aria-checked={effectiveSettings.offsetDoubleSpreads}
|
||||
><span class="toggle-knob"></span></button>
|
||||
</label>
|
||||
{/if}
|
||||
{#if style === "longstrip"}
|
||||
<label class="toggle-row">
|
||||
<span class="toggle-label">Gap between pages</span>
|
||||
<button
|
||||
class="toggle"
|
||||
class:on={effectiveSettings.pageGap ?? true}
|
||||
onclick={() => onApplySettings({ pageGap: !(effectiveSettings.pageGap ?? true) })}
|
||||
role="switch"
|
||||
aria-label="Gap between pages"
|
||||
aria-checked={effectiveSettings.pageGap ?? true}
|
||||
><span class="toggle-knob"></span></button>
|
||||
</label>
|
||||
<label class="toggle-row">
|
||||
<span class="toggle-label">Auto next chapter</span>
|
||||
<button
|
||||
class="toggle"
|
||||
class:on={store.settings.autoNextChapter ?? false}
|
||||
onclick={() => updateSettings({ autoNextChapter: !(store.settings.autoNextChapter ?? false) })}
|
||||
role="switch"
|
||||
aria-label="Auto next chapter"
|
||||
aria-checked={store.settings.autoNextChapter ?? false}
|
||||
><span class="toggle-knob"></span></button>
|
||||
</label>
|
||||
<label class="toggle-row">
|
||||
<span class="toggle-label">Auto scroll</span>
|
||||
<button
|
||||
class="toggle"
|
||||
class:on={store.settings.autoScroll ?? false}
|
||||
onclick={() => updateSettings({ autoScroll: !(store.settings.autoScroll ?? false) })}
|
||||
role="switch"
|
||||
aria-label="Auto scroll"
|
||||
aria-checked={store.settings.autoScroll ?? false}
|
||||
><span class="toggle-knob"></span></button>
|
||||
</label>
|
||||
{#if store.settings.autoScroll}
|
||||
<div class="speed-row">
|
||||
<span class="speed-label">Speed</span>
|
||||
<input
|
||||
type="range"
|
||||
class="zoom-slider"
|
||||
min={1}
|
||||
max={30}
|
||||
step={1}
|
||||
value={store.settings.autoScrollSpeed ?? 5}
|
||||
oninput={(e) => updateSettings({ autoScrollSpeed: Number(e.currentTarget.value) })}
|
||||
/>
|
||||
<span class="speed-val">{store.settings.autoScrollSpeed ?? 5}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<p class="section-label">Fit Mode</p>
|
||||
<div class="option-grid">
|
||||
{#each fitOptions as o}
|
||||
{@const Icon = o.icon}
|
||||
<button
|
||||
class="option-tile"
|
||||
class:active={fit === o.value}
|
||||
onclick={() => onApplySettings({ fitMode: o.value })}
|
||||
>
|
||||
<div class="tile-icon"><Icon size={18} weight={fit === o.value ? "fill" : "light"} /></div>
|
||||
<span class="tile-label">{o.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<p class="section-label">Reading Direction</p>
|
||||
<div class="dir-row">
|
||||
<button
|
||||
class="dir-btn"
|
||||
class:active={!rtl}
|
||||
onclick={() => onApplySettings({ readingDirection: "ltr" })}
|
||||
>
|
||||
<ArrowsHorizontal size={14} weight="light" />
|
||||
<span>Left to Right</span>
|
||||
</button>
|
||||
<button
|
||||
class="dir-btn"
|
||||
class:active={rtl}
|
||||
onclick={() => onApplySettings({ readingDirection: "rtl" })}
|
||||
>
|
||||
<ArrowsHorizontal size={14} weight="light" style="transform:scaleX(-1)" />
|
||||
<span>Right to Left</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<p class="section-label">Bar Position</p>
|
||||
<div class="bar-grid">
|
||||
{#each barOptions as o}
|
||||
<button
|
||||
class="bar-tile"
|
||||
class:active={barPosition === o.value}
|
||||
onclick={() => onBarPositionChange(o.value)}
|
||||
>
|
||||
<div class="bar-tile-preview bar-preview-{o.value}">
|
||||
<div class="bar-preview-strip"></div>
|
||||
<div class="bar-preview-content"></div>
|
||||
</div>
|
||||
<span class="tile-label">{o.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-header-row">
|
||||
<p class="section-label" style="margin:0">Zoom</p>
|
||||
<span class="zoom-readout">{zoomPct}%</span>
|
||||
</div>
|
||||
<div class="zoom-row">
|
||||
<button class="zoom-step" aria-label="Zoom out" onclick={() => setZoom(zoom - 0.1)} disabled={zoom <= ZOOM_MIN}>−</button>
|
||||
<input
|
||||
type="range"
|
||||
class="zoom-slider"
|
||||
min={Math.round(ZOOM_MIN * 100)}
|
||||
max={Math.round(ZOOM_MAX * 100)}
|
||||
step={5}
|
||||
value={zoomPct}
|
||||
oninput={(e) => setZoom(Number(e.currentTarget.value) / 100)}
|
||||
/>
|
||||
<button class="zoom-step" aria-label="Zoom in" onclick={() => setZoom(zoom + 0.1)} disabled={zoom >= ZOOM_MAX}>+</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<p class="section-label">Image</p>
|
||||
<label class="toggle-row">
|
||||
<span class="toggle-label">Optimize contrast</span>
|
||||
<button
|
||||
class="toggle"
|
||||
class:on={effectiveSettings.optimizeContrast}
|
||||
onclick={() => onApplySettings({ optimizeContrast: !effectiveSettings.optimizeContrast })}
|
||||
role="switch"
|
||||
aria-label="Optimize contrast"
|
||||
aria-checked={effectiveSettings.optimizeContrast}
|
||||
><span class="toggle-knob"></span></button>
|
||||
</label>
|
||||
<label class="toggle-row">
|
||||
<span class="toggle-label">Pinch to zoom <span class="toggle-badge">experimental</span></span>
|
||||
<button
|
||||
class="toggle"
|
||||
class:on={store.settings.pinchZoom ?? false}
|
||||
onclick={() => updateSettings({ pinchZoom: !(store.settings.pinchZoom ?? false) })}
|
||||
role="switch"
|
||||
aria-label="Pinch to zoom"
|
||||
aria-checked={store.settings.pinchZoom ?? false}
|
||||
><span class="toggle-knob"></span></button>
|
||||
</label>
|
||||
<label class="toggle-row">
|
||||
<span class="toggle-label">Mark read on chapter advance</span>
|
||||
<button
|
||||
class="toggle"
|
||||
class:on={store.settings.markReadOnNext ?? true}
|
||||
onclick={() => updateSettings({ markReadOnNext: !(store.settings.markReadOnNext ?? true) })}
|
||||
role="switch"
|
||||
aria-label="Mark read on chapter advance"
|
||||
aria-checked={store.settings.markReadOnNext ?? true}
|
||||
><span class="toggle-knob"></span></button>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
{#if store.activeManga}
|
||||
<section class="section">
|
||||
<label class="toggle-row">
|
||||
<span class="toggle-label">Per-manga settings</span>
|
||||
<button
|
||||
class="toggle"
|
||||
class:on={perMangaEnabled}
|
||||
onclick={onTogglePerManga}
|
||||
role="switch"
|
||||
aria-label="Per-manga settings"
|
||||
aria-checked={perMangaEnabled}
|
||||
><span class="toggle-knob"></span></button>
|
||||
</label>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<section class="section">
|
||||
<div class="section-header-row">
|
||||
<p class="section-label" style="margin:0">Saved Presets</p>
|
||||
{#if !presetSaving}
|
||||
<button class="new-preset-btn" onclick={() => { presetSaving = true; presetNameInput = ""; }}>+ New</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if presetSaving}
|
||||
<div class="preset-name-row">
|
||||
<input
|
||||
class="preset-name-input"
|
||||
placeholder="Preset name…"
|
||||
bind:value={presetNameInput}
|
||||
onkeydown={(e) => { if (e.key === "Enter") commitSavePreset(); if (e.key === "Escape") presetSaving = false; }}
|
||||
/>
|
||||
<button class="small-btn" aria-label="Confirm" disabled={!presetNameInput.trim()} onclick={commitSavePreset}><Check size={12} weight="bold" /></button>
|
||||
<button class="small-btn" aria-label="Cancel" onclick={() => presetSaving = false}><X size={12} weight="light" /></button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if presets.length === 0 && !presetSaving}
|
||||
<p class="empty-hint">No presets saved yet. Save the current settings to create one.</p>
|
||||
{:else}
|
||||
<div class="preset-list">
|
||||
{#each presets as p (p.id)}
|
||||
{#if presetEditId === p.id}
|
||||
<div class="preset-name-row">
|
||||
<input
|
||||
class="preset-name-input"
|
||||
bind:value={presetEditName}
|
||||
onkeydown={(e) => { if (e.key === "Enter") commitRenamePreset(); if (e.key === "Escape") presetEditId = null; }}
|
||||
/>
|
||||
<button class="small-btn" aria-label="Confirm" disabled={!presetEditName.trim()} onclick={commitRenamePreset}><Check size={12} weight="bold" /></button>
|
||||
<button class="small-btn" aria-label="Cancel" onclick={() => presetEditId = null}><X size={12} weight="light" /></button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="preset-row">
|
||||
<button class="preset-apply" onclick={() => { onApplyPreset(p.settings); close(); }}>
|
||||
<span class="preset-name">{p.name}</span>
|
||||
<span class="preset-desc">{describeSettings(p.settings)}</span>
|
||||
</button>
|
||||
<button class="small-btn" title="Rename" onclick={() => { presetEditId = p.id; presetEditName = p.name; }}>
|
||||
<FloppyDisk size={12} weight="regular" />
|
||||
</button>
|
||||
<button class="small-btn danger" title="Delete" onclick={() => onDeletePreset(p.id)}>
|
||||
<Trash size={12} weight="regular" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: calc(var(--z-reader) + 20);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 320px;
|
||||
z-index: calc(var(--z-reader) + 21);
|
||||
background: var(--bg-surface);
|
||||
border-left: 1px solid var(--border-base);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: -12px 0 40px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
padding: 0 var(--sp-4);
|
||||
height: 48px;
|
||||
border-bottom: 1px solid var(--border-dim);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--weight-medium);
|
||||
color: var(--text-primary);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
}
|
||||
|
||||
.panel-manga {
|
||||
flex: 1;
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-faint);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
transition: color var(--t-base), background var(--t-base);
|
||||
}
|
||||
.close-btn:hover { color: var(--text-primary); background: var(--bg-overlay); }
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--sp-3) var(--sp-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-4);
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border-dim) transparent;
|
||||
}
|
||||
|
||||
.section { display: flex; flex-direction: column; gap: var(--sp-2); }
|
||||
|
||||
.section-label {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-2xs);
|
||||
color: var(--text-faint);
|
||||
letter-spacing: var(--tracking-wider);
|
||||
text-transform: uppercase;
|
||||
margin: 0 0 var(--sp-1);
|
||||
}
|
||||
|
||||
.section-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--sp-1);
|
||||
}
|
||||
|
||||
.option-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--sp-1);
|
||||
}
|
||||
|
||||
.option-tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: var(--sp-2) var(--sp-1);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-dim);
|
||||
background: var(--bg-overlay);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast);
|
||||
}
|
||||
.option-tile:hover { color: var(--text-primary); background: var(--bg-raised); border-color: var(--border-strong); }
|
||||
.option-tile.active { color: var(--accent-fg); background: var(--accent-muted); border-color: var(--accent-dim); }
|
||||
|
||||
.tile-icon { display: flex; align-items: center; justify-content: center; }
|
||||
.tile-label { font-family: var(--font-ui); font-size: 9px; letter-spacing: var(--tracking-wide); text-transform: capitalize; line-height: 1; }
|
||||
|
||||
.bar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--sp-1);
|
||||
}
|
||||
|
||||
.bar-tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: var(--sp-2) var(--sp-1);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-dim);
|
||||
background: var(--bg-overlay);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast);
|
||||
}
|
||||
.bar-tile:hover { color: var(--text-primary); background: var(--bg-raised); border-color: var(--border-strong); }
|
||||
.bar-tile.active { color: var(--accent-fg); background: var(--accent-muted); border-color: var(--accent-dim); }
|
||||
|
||||
.bar-tile-preview {
|
||||
width: 32px;
|
||||
height: 22px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid currentColor;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
opacity: 0.7;
|
||||
display: flex;
|
||||
}
|
||||
.bar-tile.active .bar-tile-preview { opacity: 1; }
|
||||
|
||||
.bar-preview-strip {
|
||||
background: currentColor;
|
||||
opacity: 0.5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.bar-preview-content {
|
||||
flex: 1;
|
||||
background: color-mix(in srgb, currentColor 8%, transparent);
|
||||
}
|
||||
|
||||
.bar-preview-top { flex-direction: column; }
|
||||
.bar-preview-left { flex-direction: row; }
|
||||
.bar-preview-right { flex-direction: row-reverse; }
|
||||
|
||||
.bar-preview-top .bar-preview-strip { height: 5px; width: 100%; }
|
||||
.bar-preview-left .bar-preview-strip { width: 5px; height: 100%; }
|
||||
.bar-preview-right .bar-preview-strip { width: 5px; height: 100%; }
|
||||
|
||||
.toggle-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--sp-1) 0;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.toggle-badge {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 9px;
|
||||
letter-spacing: var(--tracking-wide);
|
||||
text-transform: uppercase;
|
||||
color: var(--text-faint);
|
||||
border: 1px solid var(--border-dim);
|
||||
border-radius: 3px;
|
||||
padding: 1px 4px;
|
||||
margin-left: var(--sp-1);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
background: var(--border-strong);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: background var(--t-base);
|
||||
}
|
||||
.toggle.on { background: var(--accent-fg); }
|
||||
.toggle-knob {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
transition: left var(--t-base);
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
||||
}
|
||||
.toggle.on .toggle-knob { left: 16px; }
|
||||
|
||||
.dir-row { display: flex; gap: var(--sp-2); }
|
||||
|
||||
.dir-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--sp-2);
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-dim);
|
||||
background: var(--bg-overlay);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-xs);
|
||||
cursor: pointer;
|
||||
transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast);
|
||||
}
|
||||
.dir-btn:hover { color: var(--text-primary); background: var(--bg-raised); border-color: var(--border-strong); }
|
||||
.dir-btn.active { color: var(--accent-fg); background: var(--accent-muted); border-color: var(--accent-dim); }
|
||||
|
||||
.zoom-readout {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
}
|
||||
|
||||
.zoom-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
|
||||
.zoom-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-dim);
|
||||
background: var(--bg-overlay);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-base);
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
transition: color var(--t-fast), background var(--t-fast);
|
||||
}
|
||||
.zoom-step:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-raised); }
|
||||
.zoom-step:disabled { opacity: 0.25; cursor: default; }
|
||||
|
||||
.zoom-slider {
|
||||
flex: 1;
|
||||
height: 3px;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background: var(--border-strong);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.zoom-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-fg);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 0 2px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.new-preset-btn {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-2xs);
|
||||
color: var(--accent-fg);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 2px var(--sp-1);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background var(--t-fast);
|
||||
}
|
||||
.new-preset-btn:hover { background: var(--accent-muted); }
|
||||
|
||||
.preset-name-row { display: flex; align-items: center; gap: var(--sp-1); }
|
||||
|
||||
.preset-name-input {
|
||||
flex: 1;
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 5px 8px;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
transition: border-color var(--t-base);
|
||||
}
|
||||
.preset-name-input:focus { border-color: var(--accent-dim); }
|
||||
|
||||
.preset-list { display: flex; flex-direction: column; gap: 2px; }
|
||||
|
||||
.preset-row { display: flex; align-items: center; gap: var(--sp-1); }
|
||||
|
||||
.preset-apply {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
padding: 7px var(--sp-2);
|
||||
border-radius: var(--radius-md);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background var(--t-fast);
|
||||
min-width: 0;
|
||||
}
|
||||
.preset-apply:hover { background: var(--bg-overlay); }
|
||||
|
||||
.preset-name {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
font-weight: var(--weight-medium);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.preset-desc {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 10px;
|
||||
color: var(--text-faint);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.small-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-faint);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: color var(--t-fast), background var(--t-fast);
|
||||
}
|
||||
.small-btn:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-overlay); }
|
||||
.small-btn:disabled { opacity: 0.25; cursor: default; }
|
||||
.small-btn.danger:hover { color: var(--color-error); background: var(--color-error-bg); }
|
||||
|
||||
.empty-hint {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-faint);
|
||||
margin: 0;
|
||||
padding: var(--sp-2) 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.speed-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
padding: var(--sp-1) 0;
|
||||
}
|
||||
|
||||
.speed-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-faint);
|
||||
flex-shrink: 0;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.speed-val {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-secondary);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
min-width: 1.5ch;
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user