This commit is contained in:
pj committed 2026-06-21 14:37:02 -04:00
1 parent 47710970c2
commit fa1c89af63
7 files changed
+213 -1

No files matched your search

+10
View File
@@ -11,6 +11,16 @@
if (t !== "light" && t !== "dark")
t = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
document.documentElement.setAttribute("data-theme", t);
var w = localStorage.getItem("margin-width");
var m = { narrow: "38em", medium: "46em", wide: "62em", full: "80em" }[w];
if (m) document.documentElement.style.setProperty("--measure", m);
[
["margin-pane-sidebar", "--pane-sidebar"],
["margin-pane-dock", "--pane-dock"],
].forEach(function (p) {
var v = parseInt(localStorage.getItem(p[0]), 10);
if (v > 0) document.documentElement.style.setProperty(p[1], v + "px");
});
} catch (e) {}
})();
</script>
+33 -1
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from "react";
import type { Editor as TiptapEditor } from "@tiptap/react";
import { Sidebar } from "./Sidebar";
import { Dock } from "./Dock";
import { ResizeHandle } from "./ResizeHandle";
import { Icon } from "./Icon";
import { Settings } from "./Settings";
import { CoverView } from "./CoverView";
@@ -9,6 +10,8 @@ import { Editor } from "../editor/Editor";
import { FloatingToolbar } from "../editor/FloatingToolbar";
import { COVER_ID, useBook } from "../store/useBook";
import { useTheme } from "../store/useTheme";
import { useWidth } from "../store/useWidth";
import { WIDTH_OPTIONS } from "../width";
import { bodyNumber, chapterKind } from "../model/book";
import { saveBook } from "../library";
import { isDesktop } from "../ipc";
@@ -26,11 +29,14 @@ export function EditorView() {
const markSaved = useBook((s) => s.markSaved);
const theme = useTheme((s) => s.theme);
const toggleTheme = useTheme((s) => s.toggle);
const width = useWidth((s) => s.width);
const setWidth = useWidth((s) => s.setWidth);
const [editor, setEditor] = useState<TiptapEditor | null>(null);
const [dock, setDock] = useState(true);
const [settingsOpen, setSettingsOpen] = useState(false);
const [exportOpen, setExportOpen] = useState(false);
const [widthOpen, setWidthOpen] = useState(false);
const saveNow = useCallback(() => {
const current = useBook.getState().book;
@@ -82,9 +88,33 @@ export function EditorView() {
{dirty && <span className="dirty-dot" />}
</button>
<div className="actions">
<div className="menu-wrap">
<button className="icon-btn" data-on={widthOpen} onClick={() => setWidthOpen((v) => !v)} title="Editor width">
<Icon d="M3 5v14M21 5v14M7 12h10M7 12l3-3M7 12l3 3M17 12l-3-3M17 12l-3 3" />
</button>
{widthOpen && (
<>
<div className="menu-backdrop" onClick={() => setWidthOpen(false)} />
<div className="menu">
{WIDTH_OPTIONS.map((w) => (
<button
key={w.id}
data-on={width === w.id}
onClick={() => {
setWidth(w.id);
setWidthOpen(false);
}}
>
{w.label}
</button>
))}
</div>
</>
)}
</div>
<div className="menu-wrap">
<button className="icon-btn" data-on={exportOpen} onClick={() => setExportOpen((v) => !v)} title="Export">
<Icon d="M12 4v10m0 0l-4-4m4 4l4-4M5 19h14" />
<Icon d="M5 13v6h14v-6M12 16V3M8 7l4-4 4 4" />
</button>
{exportOpen && (
<>
@@ -114,6 +144,7 @@ export function EditorView() {
<div className="body">
<Sidebar />
<ResizeHandle pane="sidebar" />
<main className="editor-pane">
{coverActive ? (
<CoverView />
@@ -141,6 +172,7 @@ export function EditorView() {
</>
)}
</main>
{dock && <ResizeHandle pane="dock" />}
{dock && <Dock />}
</div>
+36
View File
@@ -0,0 +1,36 @@
import type { PointerEvent } from "react";
import { applyPaneWidth, currentPaneWidth, resetPaneWidth, type Pane } from "../panes";
export function ResizeHandle({ pane }: { pane: Pane }) {
const onPointerDown = (e: PointerEvent<HTMLDivElement>) => {
e.preventDefault();
const handle = e.currentTarget;
const startX = e.clientX;
const startWidth = currentPaneWidth(pane);
handle.setPointerCapture(e.pointerId);
document.body.classList.add("resizing");
const onMove = (ev: globalThis.PointerEvent) => {
const delta = ev.clientX - startX;
applyPaneWidth(pane, pane === "sidebar" ? startWidth + delta : startWidth - delta);
};
const onUp = () => {
document.body.classList.remove("resizing");
handle.removeEventListener("pointermove", onMove);
handle.removeEventListener("pointerup", onUp);
};
handle.addEventListener("pointermove", onMove);
handle.addEventListener("pointerup", onUp);
};
return (
<div
className="pane-resizer"
role="separator"
aria-orientation="vertical"
onPointerDown={onPointerDown}
onDoubleClick={() => resetPaneWidth(pane)}
title="Drag to resize · double-click to reset"
/>
);
}
+47
View File
@@ -0,0 +1,47 @@
export type Pane = "sidebar" | "dock";
const VAR: Record<Pane, string> = {
sidebar: "--pane-sidebar",
dock: "--pane-dock",
};
const KEY: Record<Pane, string> = {
sidebar: "margin-pane-sidebar",
dock: "margin-pane-dock",
};
const DEFAULT: Record<Pane, number> = {
sidebar: 248,
dock: 384,
};
const MIN: Record<Pane, number> = {
sidebar: 200,
dock: 280,
};
const MAX: Record<Pane, number> = {
sidebar: 460,
dock: 720,
};
function clamp(pane: Pane, px: number): number {
return Math.round(Math.min(MAX[pane], Math.max(MIN[pane], px)));
}
export function currentPaneWidth(pane: Pane): number {
const raw = getComputedStyle(document.documentElement).getPropertyValue(VAR[pane]);
const n = parseInt(raw, 10);
return Number.isFinite(n) && n > 0 ? n : DEFAULT[pane];
}
export function applyPaneWidth(pane: Pane, px: number): void {
const w = clamp(pane, px);
document.documentElement.style.setProperty(VAR[pane], `${w}px`);
localStorage.setItem(KEY[pane], String(w));
}
export function resetPaneWidth(pane: Pane): void {
document.documentElement.style.setProperty(VAR[pane], `${DEFAULT[pane]}px`);
localStorage.removeItem(KEY[pane]);
}
+15
View File
@@ -0,0 +1,15 @@
import { create } from "zustand";
import { applyWidth, initialWidth, type Width } from "../width";
interface WidthState {
width: Width;
setWidth: (width: Width) => void;
}
export const useWidth = create<WidthState>((set) => ({
width: initialWidth(),
setWidth: (width) => {
applyWidth(width);
set({ width });
},
}));
+44
View File
@@ -98,6 +98,46 @@ button {
min-height: 0;
}
.pane-resizer {
flex: none;
width: 0;
position: relative;
z-index: 5;
cursor: col-resize;
}
.pane-resizer::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: -4px;
width: 8px;
}
.pane-resizer::after {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: -1px;
width: 2px;
background: var(--accent);
opacity: 0;
transition: opacity 120ms var(--ease);
}
.pane-resizer:hover::after,
body.resizing .pane-resizer::after {
opacity: 0.55;
}
body.resizing {
cursor: col-resize;
user-select: none;
-webkit-user-select: none;
}
.sidebar {
width: var(--pane-sidebar);
flex: none;
@@ -1030,6 +1070,10 @@ button {
color: var(--ink);
}
.menu button[data-on="true"] {
color: var(--accent);
}
.export-overlay {
position: fixed;
inset: 0;
+28
View File
@@ -0,0 +1,28 @@
export type Width = "narrow" | "medium" | "wide" | "full";
const KEY = "margin-width";
const MEASURE: Record<Width, string> = {
narrow: "38em",
medium: "46em",
wide: "62em",
full: "80em",
};
export const WIDTH_OPTIONS: { id: Width; label: string }[] = [
{ id: "narrow", label: "Narrow" },
{ id: "medium", label: "Medium" },
{ id: "wide", label: "Wide" },
{ id: "full", label: "Full" },
];
export function initialWidth(): Width {
const saved = localStorage.getItem(KEY);
if (saved && saved in MEASURE) return saved as Width;
return "medium";
}
export function applyWidth(width: Width) {
document.documentElement.style.setProperty("--measure", MEASURE[width]);
localStorage.setItem(KEY, width);
}