mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-02 19:17:03 +00:00
changes
This commit is contained in:
1 parent
47710970c2
commit
fa1c89af63
7 files changed
+213
-1
No files matched your search
+10
@@ -11,6 +11,16 @@
|
|||||||
if (t !== "light" && t !== "dark")
|
if (t !== "light" && t !== "dark")
|
||||||
t = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
t = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
document.documentElement.setAttribute("data-theme", t);
|
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) {}
|
} catch (e) {}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from "react";
|
|||||||
import type { Editor as TiptapEditor } from "@tiptap/react";
|
import type { Editor as TiptapEditor } from "@tiptap/react";
|
||||||
import { Sidebar } from "./Sidebar";
|
import { Sidebar } from "./Sidebar";
|
||||||
import { Dock } from "./Dock";
|
import { Dock } from "./Dock";
|
||||||
|
import { ResizeHandle } from "./ResizeHandle";
|
||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
import { Settings } from "./Settings";
|
import { Settings } from "./Settings";
|
||||||
import { CoverView } from "./CoverView";
|
import { CoverView } from "./CoverView";
|
||||||
@@ -9,6 +10,8 @@ import { Editor } from "../editor/Editor";
|
|||||||
import { FloatingToolbar } from "../editor/FloatingToolbar";
|
import { FloatingToolbar } from "../editor/FloatingToolbar";
|
||||||
import { COVER_ID, useBook } from "../store/useBook";
|
import { COVER_ID, useBook } from "../store/useBook";
|
||||||
import { useTheme } from "../store/useTheme";
|
import { useTheme } from "../store/useTheme";
|
||||||
|
import { useWidth } from "../store/useWidth";
|
||||||
|
import { WIDTH_OPTIONS } from "../width";
|
||||||
import { bodyNumber, chapterKind } from "../model/book";
|
import { bodyNumber, chapterKind } from "../model/book";
|
||||||
import { saveBook } from "../library";
|
import { saveBook } from "../library";
|
||||||
import { isDesktop } from "../ipc";
|
import { isDesktop } from "../ipc";
|
||||||
@@ -26,11 +29,14 @@ export function EditorView() {
|
|||||||
const markSaved = useBook((s) => s.markSaved);
|
const markSaved = useBook((s) => s.markSaved);
|
||||||
const theme = useTheme((s) => s.theme);
|
const theme = useTheme((s) => s.theme);
|
||||||
const toggleTheme = useTheme((s) => s.toggle);
|
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 [editor, setEditor] = useState<TiptapEditor | null>(null);
|
||||||
const [dock, setDock] = useState(true);
|
const [dock, setDock] = useState(true);
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
const [exportOpen, setExportOpen] = useState(false);
|
const [exportOpen, setExportOpen] = useState(false);
|
||||||
|
const [widthOpen, setWidthOpen] = useState(false);
|
||||||
|
|
||||||
const saveNow = useCallback(() => {
|
const saveNow = useCallback(() => {
|
||||||
const current = useBook.getState().book;
|
const current = useBook.getState().book;
|
||||||
@@ -82,9 +88,33 @@ export function EditorView() {
|
|||||||
{dirty && <span className="dirty-dot" />}
|
{dirty && <span className="dirty-dot" />}
|
||||||
</button>
|
</button>
|
||||||
<div className="actions">
|
<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">
|
<div className="menu-wrap">
|
||||||
<button className="icon-btn" data-on={exportOpen} onClick={() => setExportOpen((v) => !v)} title="Export">
|
<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>
|
</button>
|
||||||
{exportOpen && (
|
{exportOpen && (
|
||||||
<>
|
<>
|
||||||
@@ -114,6 +144,7 @@ export function EditorView() {
|
|||||||
|
|
||||||
<div className="body">
|
<div className="body">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
<ResizeHandle pane="sidebar" />
|
||||||
<main className="editor-pane">
|
<main className="editor-pane">
|
||||||
{coverActive ? (
|
{coverActive ? (
|
||||||
<CoverView />
|
<CoverView />
|
||||||
@@ -141,6 +172,7 @@ export function EditorView() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
{dock && <ResizeHandle pane="dock" />}
|
||||||
{dock && <Dock />}
|
{dock && <Dock />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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]);
|
||||||
|
}
|
||||||
@@ -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 });
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -98,6 +98,46 @@ button {
|
|||||||
min-height: 0;
|
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 {
|
.sidebar {
|
||||||
width: var(--pane-sidebar);
|
width: var(--pane-sidebar);
|
||||||
flex: none;
|
flex: none;
|
||||||
@@ -1030,6 +1070,10 @@ button {
|
|||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu button[data-on="true"] {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.export-overlay {
|
.export-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user