mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-02 11:07:04 +00:00
changes
This commit is contained in:
1 parent
47710970c2
commit
fa1c89af63
7 files changed
+213
-1
No files matched your search
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
@@ -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