From fa1c89af6332045aea19ed5d563ea823ea27fa7e Mon Sep 17 00:00:00 2001 From: PJ Date: Sun, 21 Jun 2026 14:37:02 -0400 Subject: [PATCH] changes --- index.html | 10 +++++++ src/components/EditorView.tsx | 34 +++++++++++++++++++++++- src/components/ResizeHandle.tsx | 36 +++++++++++++++++++++++++ src/panes.ts | 47 +++++++++++++++++++++++++++++++++ src/store/useWidth.ts | 15 +++++++++++ src/styles/app.css | 44 ++++++++++++++++++++++++++++++ src/width.ts | 28 ++++++++++++++++++++ 7 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 src/components/ResizeHandle.tsx create mode 100644 src/panes.ts create mode 100644 src/store/useWidth.ts create mode 100644 src/width.ts diff --git a/index.html b/index.html index b3f7687..3567db6 100644 --- a/index.html +++ b/index.html @@ -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) {} })(); diff --git a/src/components/EditorView.tsx b/src/components/EditorView.tsx index 59b7dac..907a077 100644 --- a/src/components/EditorView.tsx +++ b/src/components/EditorView.tsx @@ -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(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 && }
+
+ + {widthOpen && ( + <> +
setWidthOpen(false)} /> +
+ {WIDTH_OPTIONS.map((w) => ( + + ))} +
+ + )} +
{exportOpen && ( <> @@ -114,6 +144,7 @@ export function EditorView() {
+
{coverActive ? ( @@ -141,6 +172,7 @@ export function EditorView() { )}
+ {dock && } {dock && }
diff --git a/src/components/ResizeHandle.tsx b/src/components/ResizeHandle.tsx new file mode 100644 index 0000000..14e4551 --- /dev/null +++ b/src/components/ResizeHandle.tsx @@ -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) => { + 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 ( +
resetPaneWidth(pane)} + title="Drag to resize ยท double-click to reset" + /> + ); +} diff --git a/src/panes.ts b/src/panes.ts new file mode 100644 index 0000000..583728e --- /dev/null +++ b/src/panes.ts @@ -0,0 +1,47 @@ +export type Pane = "sidebar" | "dock"; + +const VAR: Record = { + sidebar: "--pane-sidebar", + dock: "--pane-dock", +}; + +const KEY: Record = { + sidebar: "margin-pane-sidebar", + dock: "margin-pane-dock", +}; + +const DEFAULT: Record = { + sidebar: 248, + dock: 384, +}; + +const MIN: Record = { + sidebar: 200, + dock: 280, +}; + +const MAX: Record = { + 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]); +} diff --git a/src/store/useWidth.ts b/src/store/useWidth.ts new file mode 100644 index 0000000..696fdeb --- /dev/null +++ b/src/store/useWidth.ts @@ -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((set) => ({ + width: initialWidth(), + setWidth: (width) => { + applyWidth(width); + set({ width }); + }, +})); diff --git a/src/styles/app.css b/src/styles/app.css index 9c48163..5da035b 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -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; diff --git a/src/width.ts b/src/width.ts new file mode 100644 index 0000000..af41959 --- /dev/null +++ b/src/width.ts @@ -0,0 +1,28 @@ +export type Width = "narrow" | "medium" | "wide" | "full"; + +const KEY = "margin-width"; + +const MEASURE: Record = { + 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); +}