mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 17:29:55 -05:00
@@ -1,81 +1,77 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
X, CaretLeft, CaretRight,
|
||||
Square, Rows, BookOpen, MonitorPlay,
|
||||
ArrowsLeftRight, ArrowsIn, ArrowsOut, ArrowsVertical,
|
||||
X, CaretLeft, CaretRight, CaretUp, CaretDown,
|
||||
MagnifyingGlassMinus, MagnifyingGlassPlus,
|
||||
Bookmark, MapPin, Download, Check, GearSix,
|
||||
Bookmark, MapPin, Download, Check, GearSix, Sliders,
|
||||
} from "phosphor-svelte";
|
||||
import { store, updateSettings } from "@store/state.svelte";
|
||||
import { openReader, closeReader } from "@store/state.svelte";
|
||||
import { readerState, MARKER_COLORS, MARKER_COLOR_HEX, ZOOM_STEP, ZOOM_MIN, ZOOM_MAX, PAGE_STYLES } from "../store/readerState.svelte";
|
||||
import { store, updateSettings } from "@store/state.svelte";
|
||||
import { openReader, closeReader } from "@store/state.svelte";
|
||||
import { readerState, MARKER_COLORS, MARKER_COLOR_HEX, ZOOM_STEP, ZOOM_MIN, ZOOM_MAX } from "../store/readerState.svelte";
|
||||
import { fly } from "svelte/transition";
|
||||
import { cubicOut, cubicIn } from "svelte/easing";
|
||||
import type { FitMode } from "@store/state.svelte";
|
||||
import type { Chapter } from "@types";
|
||||
import type { Chapter } from "@types";
|
||||
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
displayChapter: Chapter | null;
|
||||
adjacent: { prev: Chapter | null; next: Chapter | null };
|
||||
displayChapter: Chapter | null;
|
||||
adjacent: { prev: Chapter | null; next: Chapter | null };
|
||||
visibleChunkLastPage: number;
|
||||
fit: FitMode;
|
||||
fitLabel: string;
|
||||
style: string;
|
||||
rtl: boolean;
|
||||
zoom: number;
|
||||
zoomPct: number;
|
||||
isFullscreen: boolean;
|
||||
isBookmarked: boolean;
|
||||
hasMarkerOnPage: boolean;
|
||||
currentPageMarkers: { id: string; color: import("@store/state.svelte").MarkerColor; note: string }[];
|
||||
autoNext: boolean;
|
||||
markOnNext: boolean;
|
||||
uiVisible: boolean;
|
||||
hideTimer: ReturnType<typeof setTimeout> | null;
|
||||
zoom: number;
|
||||
zoomPct: number;
|
||||
isFullscreen: boolean;
|
||||
isBookmarked: boolean;
|
||||
hasMarkerOnPage: boolean;
|
||||
currentPageMarkers: { id: string; color: import("@store/state.svelte").MarkerColor; note: string }[];
|
||||
uiVisible: boolean;
|
||||
hideTimer: ReturnType<typeof setTimeout> | null;
|
||||
barPosition: "top" | "left" | "right";
|
||||
progressBar?: Snippet;
|
||||
onCaptureZoomAnchor: () => void;
|
||||
onRestoreZoomAnchor: () => void;
|
||||
onMaybeMarkRead: () => void;
|
||||
onToggleBookmark: () => void;
|
||||
onCommitMarker: () => void;
|
||||
onDeleteMarker: () => void;
|
||||
onClampZoom: (z: number) => number;
|
||||
onDlOpen: () => void;
|
||||
onSettingsOpen: () => void;
|
||||
win: import("@tauri-apps/api/window").Window;
|
||||
onMaybeMarkRead: () => void;
|
||||
onToggleBookmark: () => void;
|
||||
onCommitMarker: () => void;
|
||||
onDeleteMarker: () => void;
|
||||
onClampZoom: (z: number) => number;
|
||||
onApplySettings: (patch: Parameters<typeof updateSettings>[0]) => void;
|
||||
onDlOpen: () => void;
|
||||
onSettingsOpen: () => void;
|
||||
hasMangaOverride: boolean;
|
||||
win: import("@tauri-apps/api/window").Window;
|
||||
}
|
||||
|
||||
const {
|
||||
displayChapter, adjacent, visibleChunkLastPage,
|
||||
fit, fitLabel, style, rtl, zoom, zoomPct,
|
||||
isFullscreen, isBookmarked, hasMarkerOnPage, currentPageMarkers,
|
||||
autoNext, markOnNext, uiVisible, hideTimer,
|
||||
zoom, zoomPct, isFullscreen,
|
||||
isBookmarked, hasMarkerOnPage, currentPageMarkers,
|
||||
uiVisible, hideTimer,
|
||||
barPosition, progressBar,
|
||||
onCaptureZoomAnchor, onRestoreZoomAnchor,
|
||||
onMaybeMarkRead, onToggleBookmark, onCommitMarker, onDeleteMarker,
|
||||
onClampZoom, onDlOpen, onSettingsOpen, win,
|
||||
onClampZoom, onApplySettings, onDlOpen, onSettingsOpen,
|
||||
hasMangaOverride, win,
|
||||
}: Props = $props();
|
||||
|
||||
const isVertical = $derived(barPosition === "left" || barPosition === "right");
|
||||
const popoverSide = $derived(
|
||||
barPosition === "left" ? "right" :
|
||||
barPosition === "right" ? "left" :
|
||||
"bottom"
|
||||
);
|
||||
|
||||
function adjustZoom(delta: number) {
|
||||
onCaptureZoomAnchor();
|
||||
updateSettings({ readerZoom: onClampZoom(zoom + delta) });
|
||||
onApplySettings({ readerZoom: onClampZoom(zoom + delta) });
|
||||
onRestoreZoomAnchor();
|
||||
}
|
||||
|
||||
function resetZoom() {
|
||||
onCaptureZoomAnchor();
|
||||
updateSettings({ readerZoom: 1.0 });
|
||||
onApplySettings({ readerZoom: 1.0 });
|
||||
onRestoreZoomAnchor();
|
||||
}
|
||||
|
||||
function cycleStyle() {
|
||||
const idx = PAGE_STYLES.indexOf(style as typeof PAGE_STYLES[number]);
|
||||
updateSettings({ pageStyle: PAGE_STYLES[(idx + 1) % PAGE_STYLES.length] });
|
||||
}
|
||||
|
||||
function cycleFit() {
|
||||
const opts: FitMode[] = ["width", "height", "screen", "original"];
|
||||
updateSettings({ fitMode: opts[(opts.indexOf(fit) + 1) % opts.length] });
|
||||
}
|
||||
|
||||
function keepUiAlive() {
|
||||
readerState.uiVisible = true;
|
||||
if (hideTimer) clearTimeout(hideTimer);
|
||||
@@ -102,105 +98,115 @@
|
||||
readerState.openMarker("", "", "yellow");
|
||||
}
|
||||
}
|
||||
|
||||
let chapterHover = $state(false);
|
||||
let chapterHoverTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function showChapterPopover() {
|
||||
if (chapterHoverTimer) clearTimeout(chapterHoverTimer);
|
||||
chapterHover = true;
|
||||
}
|
||||
|
||||
function hideChapterPopover() {
|
||||
chapterHoverTimer = setTimeout(() => { chapterHover = false; }, 120);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="topbar" class:hidden={!uiVisible}>
|
||||
|
||||
<div class="topbar-left">
|
||||
<div
|
||||
class="bar"
|
||||
class:bar-top={barPosition === "top"}
|
||||
class:bar-left={barPosition === "left"}
|
||||
class:bar-right={barPosition === "right"}
|
||||
class:hidden={!uiVisible}
|
||||
>
|
||||
<div class="bar-start">
|
||||
<button class="icon-btn" onclick={closeReader} title="Close reader"><X size={15} weight="light" /></button>
|
||||
|
||||
<button class="icon-btn"
|
||||
onclick={() => { if (adjacent.prev) { onMaybeMarkRead(); openReader(adjacent.prev, store.activeChapterList); } }}
|
||||
disabled={!adjacent.prev}>
|
||||
<CaretLeft size={14} weight="light" />
|
||||
{#if isVertical}
|
||||
<CaretUp size={14} weight="light" />
|
||||
{:else}
|
||||
<CaretLeft size={14} weight="light" />
|
||||
{/if}
|
||||
</button>
|
||||
<span class="ch-label">
|
||||
<span class="ch-title">{store.activeManga?.title}</span>
|
||||
<span class="ch-sep">/</span>
|
||||
<span>{displayChapter?.name}</span>
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="ch-hover-wrap"
|
||||
onmouseenter={showChapterPopover}
|
||||
onmouseleave={hideChapterPopover}
|
||||
role="presentation"
|
||||
>
|
||||
<button class="ch-pill" title="{store.activeManga?.title} / {displayChapter?.name}">
|
||||
{#if isVertical}
|
||||
<span class="ch-info"></span>
|
||||
{:else}
|
||||
<span class="ch-title">{store.activeManga?.title}</span>
|
||||
<span class="ch-sep">/</span>
|
||||
<span class="ch-name">{displayChapter?.name}</span>
|
||||
<span class="ch-page">{store.pageNumber} / {visibleChunkLastPage || "…"}</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if chapterHover && isVertical}
|
||||
<div class="ch-popover ch-popover-{popoverSide}">
|
||||
<span class="ch-pop-title">{store.activeManga?.title}</span>
|
||||
<span class="ch-pop-sep">/</span>
|
||||
<span class="ch-pop-name">{displayChapter?.name}</span>
|
||||
<span class="ch-pop-page">{store.pageNumber} / {visibleChunkLastPage || "…"}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button class="icon-btn"
|
||||
onclick={() => { if (adjacent.next) { onMaybeMarkRead(); openReader(adjacent.next, store.activeChapterList); } }}
|
||||
disabled={!adjacent.next}>
|
||||
<CaretRight size={14} weight="light" />
|
||||
{#if isVertical}
|
||||
<CaretDown size={14} weight="light" />
|
||||
{:else}
|
||||
<CaretRight size={14} weight="light" />
|
||||
{/if}
|
||||
</button>
|
||||
<span class="page-label">{store.pageNumber} / {visibleChunkLastPage || "…"}</span>
|
||||
|
||||
{#if !isVertical}
|
||||
<span class="bar-sep"></span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="topbar-right">
|
||||
<div class="top-sep"></div>
|
||||
|
||||
<button class="mode-btn" onclick={cycleFit}>
|
||||
{#if fit === "width"}<ArrowsLeftRight size={14} weight="light" />
|
||||
{:else if fit === "height"}<ArrowsVertical size={14} weight="light" />
|
||||
{:else if fit === "screen"}<ArrowsIn size={14} weight="light" />
|
||||
{:else}<ArrowsOut size={14} weight="light" />{/if}
|
||||
<span class="mode-label">{fitLabel}</span>
|
||||
</button>
|
||||
{#if isVertical && progressBar}
|
||||
<div class="bar-middle">
|
||||
{@render progressBar()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="bar-end">
|
||||
<div class="zoom-wrap">
|
||||
<div class="zoom-inline">
|
||||
<button class="zoom-step-btn" onclick={() => adjustZoom(-ZOOM_STEP)} title="Zoom out" disabled={zoom <= ZOOM_MIN}>
|
||||
<button class="icon-btn zoom-icon-btn" onclick={() => adjustZoom(-ZOOM_STEP)} title="Zoom out" disabled={zoom <= ZOOM_MIN}>
|
||||
<MagnifyingGlassMinus size={13} weight="light" />
|
||||
</button>
|
||||
<div class="zoom-divider"></div>
|
||||
<button class="zoom-pct-btn" onclick={() => readerState.zoomOpen = !readerState.zoomOpen} title="Click to adjust zoom">
|
||||
{zoomPct}%
|
||||
</button>
|
||||
<button class="zoom-step-btn" onclick={() => adjustZoom(ZOOM_STEP)} title="Zoom in" disabled={zoom >= ZOOM_MAX}>
|
||||
<div class="zoom-divider"></div>
|
||||
<button class="icon-btn zoom-icon-btn" onclick={() => adjustZoom(ZOOM_STEP)} title="Zoom in" disabled={zoom >= ZOOM_MAX}>
|
||||
<MagnifyingGlassPlus size={13} weight="light" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if readerState.zoomOpen}
|
||||
<div class="zoom-popover">
|
||||
<div class="popover zoom-popover popover-{popoverSide}">
|
||||
<div class="zoom-slider-row">
|
||||
<input type="range" class="zoom-slider" min={10} max={100} step={5} value={zoomPct}
|
||||
oninput={(e) => { onCaptureZoomAnchor(); updateSettings({ readerZoom: onClampZoom(Number(e.currentTarget.value) / 100) }); onRestoreZoomAnchor(); }} />
|
||||
oninput={(e) => { onCaptureZoomAnchor(); onApplySettings({ readerZoom: onClampZoom(Number(e.currentTarget.value) / 100) }); onRestoreZoomAnchor(); }} />
|
||||
</div>
|
||||
<button class="zoom-reset" onclick={resetZoom} disabled={zoom === 1.0}>Reset</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button class="mode-btn" class:active={rtl} onclick={() => updateSettings({ readingDirection: rtl ? "ltr" : "rtl" })}>
|
||||
<ArrowsLeftRight size={14} weight="light" /><span class="mode-label">{rtl ? "RTL" : "LTR"}</span>
|
||||
</button>
|
||||
|
||||
<button class="mode-btn" onclick={cycleStyle} title="Cycle view mode">
|
||||
{#if style === "single"}<Square size={14} weight="light" />
|
||||
{:else if style === "fade"}<MonitorPlay size={14} weight="light" />
|
||||
{:else if style === "double"}<BookOpen size={14} weight="light" />
|
||||
{:else}<Rows size={14} weight="light" />{/if}
|
||||
<span class="mode-label">{style}</span>
|
||||
</button>
|
||||
|
||||
<div class="mode-extras">
|
||||
{#if style === "double"}
|
||||
<button class="mode-btn" class:active={store.settings.offsetDoubleSpreads}
|
||||
onclick={() => updateSettings({ offsetDoubleSpreads: !store.settings.offsetDoubleSpreads })}>
|
||||
<span class="mode-label">Offset</span>
|
||||
</button>
|
||||
{/if}
|
||||
{#if style === "longstrip"}
|
||||
<button class="mode-btn" class:active={store.settings.pageGap}
|
||||
onclick={() => updateSettings({ pageGap: !store.settings.pageGap })}>
|
||||
<span class="mode-label">Gap</span>
|
||||
</button>
|
||||
<button class="mode-btn" class:active={autoNext}
|
||||
onclick={() => updateSettings({ autoNextChapter: !autoNext })}>
|
||||
<span class="mode-label">Auto</span>
|
||||
</button>
|
||||
{/if}
|
||||
{#if !autoNext}
|
||||
<button class="mode-btn" class:active={markOnNext}
|
||||
onclick={() => updateSettings({ markReadOnNext: !markOnNext })}>
|
||||
<span class="mode-label">Mk.Read</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button class="mode-btn" onclick={onDlOpen}>
|
||||
<Download size={14} weight="light" />
|
||||
</button>
|
||||
|
||||
<div class="marker-wrap">
|
||||
<button
|
||||
class="icon-btn"
|
||||
@@ -214,7 +220,7 @@
|
||||
</button>
|
||||
|
||||
{#if readerState.markerOpen}
|
||||
<div class="marker-popover" role="presentation"
|
||||
<div class="popover marker-popover popover-{popoverSide}" role="presentation"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onmouseenter={keepUiAlive}
|
||||
>
|
||||
@@ -270,6 +276,20 @@
|
||||
<Bookmark size={15} weight={isBookmarked ? "fill" : "regular"} />
|
||||
</button>
|
||||
|
||||
<button class="icon-btn" onclick={onDlOpen}>
|
||||
<Download size={14} weight="light" />
|
||||
</button>
|
||||
|
||||
<button class="icon-btn" class:active={hasMangaOverride}
|
||||
onclick={() => { readerState.presetOpen = true; readerState.markerOpen = false; readerState.zoomOpen = false; readerState.dlOpen = false; }}
|
||||
title="Reader settings">
|
||||
<Sliders size={13} weight="regular" />
|
||||
</button>
|
||||
|
||||
<button class="icon-btn" onclick={onSettingsOpen} title="Settings">
|
||||
<GearSix size={13} weight="regular" />
|
||||
</button>
|
||||
|
||||
<div class="wc-wrap">
|
||||
<button
|
||||
class="icon-btn"
|
||||
@@ -284,54 +304,98 @@
|
||||
</svg>
|
||||
</button>
|
||||
{#if readerState.winOpen}
|
||||
<div class="wc-clip" onmouseenter={wcResetTimer} onmousemove={wcResetTimer}>
|
||||
<div
|
||||
class="wc-clip wc-clip-{popoverSide}"
|
||||
onmouseenter={wcResetTimer}
|
||||
onmousemove={wcResetTimer}
|
||||
>
|
||||
<div
|
||||
class="wc-bar"
|
||||
role="presentation"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
in:fly={{ y: '-100%', duration: 200, easing: cubicOut }}
|
||||
out:fly={{ y: '-100%', duration: 150, easing: cubicIn }}
|
||||
in:fly={isVertical
|
||||
? (barPosition === "left" ? { x: '-100%', duration: 200, easing: cubicOut } : { x: '100%', duration: 200, easing: cubicOut })
|
||||
: { y: '-100%', duration: 200, easing: cubicOut }}
|
||||
out:fly={isVertical
|
||||
? (barPosition === "left" ? { x: '-100%', duration: 150, easing: cubicIn } : { x: '100%', duration: 150, easing: cubicIn })
|
||||
: { y: '-100%', duration: 150, easing: cubicIn }}
|
||||
>
|
||||
<button class="wc-icon-btn" onclick={() => { readerState.winOpen = false; onSettingsOpen(); }} title="Settings">
|
||||
<GearSix size={13} weight="regular" />
|
||||
</button>
|
||||
<div class="wc-bar-sep"></div>
|
||||
<button class="wc-icon-btn" onclick={() => { readerState.winOpen = false; win.minimize(); }} title="Minimize">
|
||||
<svg width="10" height="2" viewBox="0 0 10 2"><line x1="0" y1="1" x2="10" y2="1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
<button class="wc-icon-btn" onclick={() => { readerState.winOpen = false; win.toggleMaximize(); }} title={isFullscreen ? "Exit Fullscreen" : "Fullscreen"}>
|
||||
{#if isFullscreen}
|
||||
<svg width="11" height="11" viewBox="0 0 11 11">
|
||||
<polyline points="1,4 1,1 4,1" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="7,1 10,1 10,4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="10,7 10,10 7,10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="4,10 1,10 1,7" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<button class="wc-icon-btn" onclick={() => { readerState.winOpen = false; win.minimize(); }} title="Minimize">
|
||||
<svg width="10" height="2" viewBox="0 0 10 2"><line x1="0" y1="1" x2="10" y2="1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
<button class="wc-icon-btn" onclick={() => { readerState.winOpen = false; win.toggleMaximize(); }} title={isFullscreen ? "Exit Fullscreen" : "Fullscreen"}>
|
||||
{#if isFullscreen}
|
||||
<svg width="11" height="11" viewBox="0 0 11 11">
|
||||
<polyline points="1,4 1,1 4,1" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="7,1 10,1 10,4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="10,7 10,10 7,10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="4,10 1,10 1,7" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg width="10" height="10" viewBox="0 0 10 10"><rect x="0.75" y="0.75" width="8.5" height="8.5" rx="1" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
|
||||
{/if}
|
||||
</button>
|
||||
<button class="wc-icon-btn wc-icon-close" onclick={() => { readerState.winOpen = false; win.close(); }} title="Close">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<line x1="1" y1="1" x2="9" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="9" y1="1" x2="1" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg width="10" height="10" viewBox="0 0 10 10"><rect x="0.75" y="0.75" width="8.5" height="8.5" rx="1" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
|
||||
{/if}
|
||||
</button>
|
||||
<button class="wc-icon-btn wc-icon-close" onclick={() => { readerState.winOpen = false; win.close(); }} title="Close">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<line x1="1" y1="1" x2="9" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="9" y1="1" x2="1" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.topbar { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-1); padding: 0 var(--sp-3); height: 40px; background: var(--bg-void); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; position: relative; z-index: 2; transition: opacity 0.25s ease; overflow: visible; }
|
||||
.topbar.hidden { opacity: 0; pointer-events: none; }
|
||||
.bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-1);
|
||||
background: var(--bg-void);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
transition: opacity 0.25s ease;
|
||||
overflow: visible;
|
||||
}
|
||||
.bar.hidden { opacity: 0; pointer-events: none; }
|
||||
|
||||
.topbar-left { display: flex; align-items: center; gap: var(--sp-1); min-width: 0; flex: 1; overflow: hidden; }
|
||||
.topbar-right { display: flex; align-items: center; gap: var(--sp-1); flex-shrink: 0; }
|
||||
.mode-extras { display: flex; align-items: center; gap: var(--sp-1); min-width: 0; }
|
||||
.bar-top {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0 var(--sp-3);
|
||||
height: 40px;
|
||||
border-bottom: 1px solid var(--border-dim);
|
||||
}
|
||||
|
||||
.bar-left, .bar-right {
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: var(--sp-3) 0;
|
||||
width: 40px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
border-bottom: none;
|
||||
}
|
||||
.bar-left { left: 0; border-right: 1px solid var(--border-dim); }
|
||||
.bar-right { right: 0; border-left: 1px solid var(--border-dim); }
|
||||
|
||||
.bar-start, .bar-end {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-1);
|
||||
}
|
||||
.bar-top .bar-start { flex: 1; overflow: hidden; }
|
||||
.bar-left .bar-start,
|
||||
.bar-left .bar-end,
|
||||
.bar-right .bar-start,
|
||||
.bar-right .bar-end {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.icon-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); }
|
||||
.icon-btn:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-raised); }
|
||||
@@ -339,25 +403,104 @@
|
||||
.icon-btn.active { color: var(--accent-fg); }
|
||||
.marker-btn-has { color: var(--marker-color, var(--accent-fg)) !important; }
|
||||
|
||||
.ch-label { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--text-sm); color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
||||
.ch-hover-wrap { position: relative; min-width: 0; }
|
||||
|
||||
.ch-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
padding: 2px 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: none;
|
||||
cursor: default;
|
||||
transition: background var(--t-fast);
|
||||
}
|
||||
.bar-left .ch-pill, .bar-right .ch-pill {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
.ch-info { font-size: 15px; line-height: 1; color: var(--text-faint); flex-shrink: 0; }
|
||||
.ch-title { color: var(--text-secondary); font-weight: var(--weight-medium); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ch-sep { color: var(--text-faint); flex-shrink: 0; }
|
||||
.page-label { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted); letter-spacing: var(--tracking-wide); flex-shrink: 0; }
|
||||
.top-sep { width: 1px; height: 16px; background: var(--border-dim); flex-shrink: 0; margin: 0 var(--sp-1); }
|
||||
.ch-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ch-page { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted); letter-spacing: var(--tracking-wide); flex-shrink: 0; }
|
||||
|
||||
.mode-btn { display: flex; align-items: center; gap: 4px; padding: 4px var(--sp-2); border-radius: var(--radius-sm); color: var(--text-muted); flex-shrink: 0; font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); transition: color var(--t-base), background var(--t-base); }
|
||||
.mode-btn:hover { color: var(--text-primary); background: var(--bg-raised); }
|
||||
.mode-btn.active { color: var(--accent-fg); background: var(--accent-muted); }
|
||||
.mode-label { text-transform: capitalize; }
|
||||
.ch-popover {
|
||||
position: absolute;
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--border-base);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
white-space: nowrap;
|
||||
z-index: 100;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
||||
font-size: var(--text-sm);
|
||||
pointer-events: none;
|
||||
animation: scaleIn 0.1s ease both;
|
||||
}
|
||||
.ch-popover-right { left: calc(100% + 8px); top: 50%; translate: 0 -50%; transform-origin: left center; }
|
||||
.ch-popover-left { right: calc(100% + 8px); top: 50%; translate: 0 -50%; transform-origin: right center; }
|
||||
.ch-pop-title { color: var(--text-secondary); font-weight: var(--weight-medium); }
|
||||
.ch-pop-sep { color: var(--text-faint); }
|
||||
.ch-pop-name { color: var(--text-muted); }
|
||||
.ch-pop-page { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
|
||||
|
||||
.zoom-wrap { position: relative; flex-shrink: 0; }
|
||||
.zoom-inline { display: flex; align-items: center; gap: 1px; background: var(--bg-overlay); border: 1px solid var(--border-base); border-radius: var(--radius-sm); overflow: hidden; }
|
||||
.zoom-step-btn { display: flex; align-items: center; justify-content: center; width: 22px; height: 24px; color: var(--text-muted); transition: color var(--t-base), background var(--t-base); }
|
||||
.zoom-step-btn:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-raised); }
|
||||
.zoom-step-btn:disabled { opacity: 0.25; cursor: default; }
|
||||
.zoom-pct-btn { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); color: var(--text-secondary); padding: 0 var(--sp-2); height: 24px; min-width: 38px; text-align: center; transition: color var(--t-base), background var(--t-base); border-left: 1px solid var(--border-dim); border-right: 1px solid var(--border-dim); }
|
||||
.bar-sep { width: 1px; height: 16px; background: var(--border-dim); flex-shrink: 0; margin: 0 var(--sp-1); }
|
||||
|
||||
.zoom-wrap { position: relative; flex-shrink: 0; }
|
||||
.zoom-inline { display: flex; align-items: center; }
|
||||
.bar-left .zoom-inline, .bar-right .zoom-inline { flex-direction: column; }
|
||||
|
||||
.zoom-icon-btn { width: 28px; height: 28px; }
|
||||
.zoom-divider {
|
||||
background: var(--border-dim);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.bar-top .zoom-divider { width: 1px; height: 16px; }
|
||||
.bar-left .zoom-divider,
|
||||
.bar-right .zoom-divider { height: 1px; width: 16px; }
|
||||
|
||||
.zoom-pct-btn {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-2xs);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
color: var(--text-secondary);
|
||||
height: 28px;
|
||||
min-width: 38px;
|
||||
text-align: center;
|
||||
transition: color var(--t-base), background var(--t-base);
|
||||
padding: 0 var(--sp-1);
|
||||
border-radius: 0;
|
||||
}
|
||||
.bar-left .zoom-pct-btn,
|
||||
.bar-right .zoom-pct-btn { height: 24px; min-width: unset; width: 28px; writing-mode: vertical-rl; font-size: 9px; padding: var(--sp-1) 0; }
|
||||
.zoom-pct-btn:hover { color: var(--text-primary); background: var(--bg-raised); }
|
||||
.zoom-popover { position: absolute; top: calc(100% + 6px); left: 50%; translate: -50% 0; background: var(--bg-raised); border: 1px solid var(--border-base); border-radius: var(--radius-lg); padding: var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-2); box-shadow: 0 8px 24px rgba(0,0,0,0.5); z-index: 100; min-width: 180px; animation: scaleIn 0.1s ease both; transform-origin: top center; }
|
||||
|
||||
.popover {
|
||||
position: absolute;
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--border-base);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
||||
z-index: 100;
|
||||
animation: scaleIn 0.1s ease both;
|
||||
}
|
||||
.popover-bottom { top: calc(100% + 6px); left: 50%; translate: -50% 0; transform-origin: top center; }
|
||||
.popover-right { left: calc(100% + 8px); top: 50%; translate: 0 -50%; transform-origin: left center; }
|
||||
.popover-left { right: calc(100% + 8px); top: 50%; translate: 0 -50%; transform-origin: right center; }
|
||||
|
||||
.zoom-popover { padding: var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-2); min-width: 180px; }
|
||||
.zoom-slider-row { display: flex; align-items: center; gap: var(--sp-2); }
|
||||
.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: 12px; height: 12px; border-radius: 50%; background: var(--accent-fg); cursor: pointer; }
|
||||
@@ -366,7 +509,7 @@
|
||||
.zoom-reset:disabled { opacity: 0.3; cursor: default; }
|
||||
|
||||
.marker-wrap { position: relative; flex-shrink: 0; }
|
||||
.marker-popover { position: absolute; top: calc(100% + 8px); right: 0; width: 240px; background: var(--bg-surface); border: 1px solid var(--border-base); border-radius: var(--radius-lg); padding: var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-3); box-shadow: 0 12px 32px rgba(0,0,0,0.6), 0 2px 8px rgba(0,0,0,0.4); z-index: 100; animation: scaleIn 0.1s ease both; transform-origin: top right; }
|
||||
.marker-popover { width: 240px; padding: var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-3); }
|
||||
.marker-pop-header { display: flex; align-items: center; justify-content: space-between; }
|
||||
.marker-pop-title { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; }
|
||||
.marker-delete-btn { display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: var(--radius-sm); border: none; background: none; color: var(--text-faint); cursor: pointer; transition: color var(--t-fast), background var(--t-fast); }
|
||||
@@ -390,11 +533,25 @@
|
||||
.wc-wrap { position: static; flex-shrink: 0; }
|
||||
.wc-clip {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
}
|
||||
.wc-clip-bottom {
|
||||
top: 100%;
|
||||
right: var(--sp-3);
|
||||
z-index: 100;
|
||||
clip-path: inset(0 -20px -20px -20px);
|
||||
}
|
||||
.wc-clip-right {
|
||||
left: calc(100% + 1px);
|
||||
top: auto;
|
||||
bottom: var(--sp-3);
|
||||
clip-path: inset(-20px -20px -20px 0);
|
||||
}
|
||||
.wc-clip-left {
|
||||
right: calc(100% + 1px);
|
||||
top: auto;
|
||||
bottom: var(--sp-3);
|
||||
clip-path: inset(-20px 0 -20px -20px);
|
||||
}
|
||||
.wc-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -402,10 +559,12 @@
|
||||
padding: 3px 10px 4px;
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--border-base);
|
||||
border-top: none;
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.45);
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
.wc-clip-bottom .wc-bar { border-top: none; border-radius: 0 0 8px 8px; flex-direction: row; }
|
||||
.wc-clip-right .wc-bar { border-left: none; border-radius: 0 8px 8px 0; flex-direction: column; padding: 10px 4px; }
|
||||
.wc-clip-left .wc-bar { border-right: none; border-radius: 8px 0 0 8px; flex-direction: column; padding: 10px 4px; }
|
||||
|
||||
.wc-icon-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -422,7 +581,17 @@
|
||||
}
|
||||
.wc-icon-btn:hover { color: var(--text-primary); background: var(--bg-overlay); }
|
||||
.wc-icon-close:hover { color: #fff; background: #c0392b; }
|
||||
.wc-bar-sep { width: 1px; height: 12px; background: var(--border-dim); margin: 0 2px; flex-shrink: 0; }
|
||||
|
||||
@keyframes scaleIn { from { opacity: 0; transform: scale(0.97) } to { opacity: 1; transform: scale(1) } }
|
||||
|
||||
.bar-middle {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
padding: var(--sp-1) 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user