mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-04 20:17:03 +00:00
some more fixes
This commit is contained in:
1 parent
7a3c04f170
commit
2b86a407ba
63 files changed
+12172
-44
No files matched your search
+16
-1
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
@@ -7,6 +7,7 @@ import { EditorView } from "./components/EditorView";
|
||||
import { BackupSettings } from "./components/BackupSettings";
|
||||
import { ExportPreview } from "./components/ExportPreview";
|
||||
import { UpdateDialog } from "./components/UpdateDialog";
|
||||
import { KeyboardShortcuts } from "./components/KeyboardShortcuts";
|
||||
import { useBook } from "./store/useBook";
|
||||
import { useBackup } from "./store/useBackup";
|
||||
import { useExportPreview } from "./store/useExportPreview";
|
||||
@@ -18,6 +19,18 @@ import { checkForUpdates } from "./updater";
|
||||
function App() {
|
||||
const book = useBook((s) => s.book);
|
||||
const openBook = useBook((s) => s.openBook);
|
||||
const [shortcutsOpen, setShortcutsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (event: KeyboardEvent) => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.shiftKey && !event.altKey && event.code === "Slash") {
|
||||
event.preventDefault();
|
||||
setShortcutsOpen(true);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onKey, true);
|
||||
return () => window.removeEventListener("keydown", onKey, true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDesktop) return;
|
||||
@@ -30,6 +43,7 @@ function App() {
|
||||
}
|
||||
else if (event.payload === "export-epub") runExport("epub");
|
||||
else if (event.payload === "check-updates") checkForUpdates(false);
|
||||
else if (event.payload === "keyboard-shortcuts") setShortcutsOpen(true);
|
||||
else if (event.payload === "report-issue")
|
||||
openUrl("https://github.com/priyanshujain/margin/issues").catch(() => {});
|
||||
});
|
||||
@@ -105,6 +119,7 @@ function App() {
|
||||
{isDesktop && <BackupSettings />}
|
||||
{isDesktop && <ExportPreview />}
|
||||
{isDesktop && <UpdateDialog />}
|
||||
<KeyboardShortcuts open={shortcutsOpen} onClose={() => setShortcutsOpen(false)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ export function BackupSettings() {
|
||||
title="Restore from Google Drive"
|
||||
message={
|
||||
<>
|
||||
This replaces your local books with the copies in Google Drive. Any local changes that haven't been backed
|
||||
This replaces your local projects with the copies in Google Drive. Any local changes that haven't been backed
|
||||
up will be lost.
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -473,8 +473,8 @@ export function EditorView() {
|
||||
coords={proofPopover.coords}
|
||||
onReplace={(suggestion) => {
|
||||
const { from, to } = proofPopover.issue;
|
||||
if (suggestion === "") editor.chain().focus().deleteRange({ from, to }).run();
|
||||
else editor.chain().focus().insertContentAt({ from, to }, suggestion).run();
|
||||
if (suggestion === "") editor.chain().focus(undefined, { scrollIntoView: false }).deleteRange({ from, to }).run();
|
||||
else editor.chain().focus(undefined, { scrollIntoView: false }).insertContentAt({ from, to }, suggestion).run();
|
||||
setProofPopover(null);
|
||||
}}
|
||||
onIgnore={() => {
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { useRef } from "react";
|
||||
import { useEscapeLayer } from "../escape";
|
||||
import { useFocusTrap } from "../focus";
|
||||
import { isDesktop } from "../ipc";
|
||||
import { Icon } from "./Icon";
|
||||
import "../styles/shortcuts.css";
|
||||
|
||||
const mac = navigator.platform.includes("Mac");
|
||||
const mod = mac ? "⌘" : "Ctrl";
|
||||
const alt = mac ? "⌥" : "Alt";
|
||||
const shift = mac ? "⇧" : "Shift";
|
||||
|
||||
const groups: { title: string; shortcuts: [string, string[]][] }[] = [
|
||||
{
|
||||
title: "Projects",
|
||||
shortcuts: [
|
||||
["New project", [mod, "N"]],
|
||||
["Save", [mod, "S"]],
|
||||
["Settings", [mod, ","]],
|
||||
["Export PDF", [mod, shift, "P"]],
|
||||
["Export EPUB", [mod, shift, "E"]],
|
||||
["Keyboard shortcuts", [mod, shift, "/"]],
|
||||
...(mac && isDesktop ? [["Open window", [mod, shift, "M"]] as [string, string[]]] : []),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Editing",
|
||||
shortcuts: [
|
||||
["Undo", [mod, "Z"]],
|
||||
["Redo", [mod, shift, "Z"]],
|
||||
["Cut", [mod, "X"]],
|
||||
["Copy", [mod, "C"]],
|
||||
["Paste", [mod, "V"]],
|
||||
["Paste without formatting", [mod, shift, "V"]],
|
||||
["Select all", [mod, "A"]],
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Formatting",
|
||||
shortcuts: [
|
||||
["Bold", [mod, "B"]],
|
||||
["Italic", [mod, "I"]],
|
||||
["Underline", [mod, "U"]],
|
||||
["Strikethrough", [mod, shift, "X"]],
|
||||
["Add or edit link", [mod, "K"]],
|
||||
["Heading", [mod, alt, "2 / 3"]],
|
||||
["Paragraph", [mod, alt, "0"]],
|
||||
["Bullet list", [mod, shift, "8"]],
|
||||
["Numbered list", [mod, shift, "7"]],
|
||||
["Task list", [mod, shift, "9"]],
|
||||
["Quote", [mod, shift, "B"]],
|
||||
["Align left", [mod, shift, "L"]],
|
||||
...(!isDesktop ? [["Align center", [mod, shift, "E"]] as [string, string[]]] : []),
|
||||
["Align right", [mod, shift, "R"]],
|
||||
["Indent / remove indent", ["Tab / Shift Tab"]],
|
||||
["Line break", [shift, "Enter"]],
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Navigation and search",
|
||||
shortcuts: [
|
||||
["Find", [mod, "F"]],
|
||||
...(mac ? [
|
||||
["Find and replace", [mod, alt, "F"]],
|
||||
["Toggle chapters", [mod, "\\"]],
|
||||
["Next / previous chapter", [mod, alt, "↓ / ↑"]],
|
||||
["Next / previous chapter", [mod, alt, "→ / ←"]],
|
||||
] as [string, string[]][] : []),
|
||||
["Next chapter", ["Ctrl", "Tab"]],
|
||||
["Previous chapter", ["Ctrl", shift, "Tab"]],
|
||||
["Next / previous search result", ["Enter / Shift Enter"]],
|
||||
["Close popup", ["Esc"]],
|
||||
["Return to all projects", ["Esc twice"]],
|
||||
],
|
||||
},
|
||||
...(mac && isDesktop ? [{
|
||||
title: "Apple Writing Tools",
|
||||
shortcuts: [
|
||||
["Proofread", [alt, shift, "F"]],
|
||||
["Rewrite", [alt, shift, "R"]],
|
||||
] as [string, string[]][],
|
||||
}] : []),
|
||||
];
|
||||
|
||||
export function KeyboardShortcuts({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEscapeLayer(open, onClose);
|
||||
useFocusTrap(ref, open);
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="overlay" onClick={onClose}>
|
||||
<div ref={ref} className="panel shortcuts-panel" role="dialog" aria-modal="true" aria-labelledby="shortcuts-title" onClick={(event) => event.stopPropagation()}>
|
||||
<div className="panel-head">
|
||||
<h2 id="shortcuts-title">Keyboard shortcuts</h2>
|
||||
<button className="icon-btn" onClick={onClose} title="Close" aria-label="Close keyboard shortcuts">
|
||||
<Icon d="M6 6l12 12M18 6L6 18" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="panel-body shortcuts-groups">
|
||||
{groups.map((group) => (
|
||||
<section key={group.title} className="shortcuts-group">
|
||||
<h3>{group.title}</h3>
|
||||
<dl>
|
||||
{group.shortcuts.map(([label, keys]) => (
|
||||
<div className="shortcut-row" key={label + keys.join()}>
|
||||
<dt>{label}</dt>
|
||||
<dd>{keys.map((key) => <kbd key={key}>{key}</kbd>)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -146,7 +146,7 @@ export function RowMenu({ label, onDuplicate, onMove, onDelete, onToggleTitle, t
|
||||
{onMove && (
|
||||
<button className="row-menu-item" onClick={moveOut}>
|
||||
<Icon d="M4 12h13M13 8l4 4-4 4M20 4v16" size={14} />
|
||||
Move to book…
|
||||
Move to project…
|
||||
</button>
|
||||
)}
|
||||
<button className="row-menu-item danger" onClick={remove}>
|
||||
|
||||
@@ -8,6 +8,20 @@ import { RowMenu } from "./RowMenu";
|
||||
import { MoveChapterDialog } from "./MoveChapterDialog";
|
||||
import { relativeTime } from "../time";
|
||||
import { isDesktop } from "../ipc";
|
||||
import type { JSONContent } from "@tiptap/core";
|
||||
|
||||
function countTasks(content: JSONContent): { completed: number; total: number } {
|
||||
const count = { completed: 0, total: 0 };
|
||||
const visit = (node: JSONContent) => {
|
||||
if (node.type === "taskItem") {
|
||||
count.total++;
|
||||
if (node.attrs?.checked) count.completed++;
|
||||
}
|
||||
node.content?.forEach(visit);
|
||||
};
|
||||
visit(content);
|
||||
return count;
|
||||
}
|
||||
|
||||
interface Row {
|
||||
chapter: Chapter;
|
||||
@@ -194,6 +208,7 @@ export function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
|
||||
<ul className="chapters">
|
||||
{group.rows.map((row, i) => {
|
||||
const isPart = chapterKind(row.chapter) === "part";
|
||||
const tasks = countTasks(row.chapter.content);
|
||||
const partLabel = `Part ${partRoman(row.part ?? 0)}`;
|
||||
const rowLabel = isPart
|
||||
? row.chapter.title
|
||||
@@ -241,6 +256,9 @@ export function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
|
||||
{row.chapter.id === activeChapterId && (
|
||||
<span className="meta">Edited {relativeTime(row.chapter.updatedAt, now)}</span>
|
||||
)}
|
||||
{tasks.total > 0 && (
|
||||
<span className="meta">{tasks.completed} of {tasks.total} tasks completed</span>
|
||||
)}
|
||||
</span>
|
||||
<RowMenu
|
||||
label="Page options"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useReducer, useRef, useState, type ReactNode } from "react";
|
||||
import { useEffect, useLayoutEffect, useReducer, useRef, useState, type ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import type { Editor } from "@tiptap/react";
|
||||
import { Icon } from "../components/Icon";
|
||||
import { useEscapeLayer } from "../escape";
|
||||
@@ -33,6 +34,7 @@ export function FloatingToolbar({ editor }: { editor: Editor | null }) {
|
||||
const alignPopRef = useRef<HTMLDivElement>(null);
|
||||
const [linkOpen, setLinkOpen] = useState(false);
|
||||
const [linkValue, setLinkValue] = useState("");
|
||||
const [linkPosition, setLinkPosition] = useState({ left: 0, top: 0 });
|
||||
const [alignOpen, setAlignOpen] = useState(false);
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
|
||||
@@ -45,6 +47,39 @@ export function FloatingToolbar({ editor }: { editor: Editor | null }) {
|
||||
useFocusTrap(linkPopRef, linkOpen);
|
||||
useFocusTrap(alignPopRef, alignOpen);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!editor || !linkOpen) return;
|
||||
const position = () => {
|
||||
const popover = linkPopRef.current;
|
||||
if (!popover) return;
|
||||
const { from, to } = editor.state.selection;
|
||||
const start = editor.view.coordsAtPos(from);
|
||||
const end = editor.view.coordsAtPos(to);
|
||||
const pane = editor.view.dom.closest(".editor-pane")?.getBoundingClientRect();
|
||||
const width = popover.offsetWidth;
|
||||
const height = popover.offsetHeight;
|
||||
const leftEdge = Math.max(12, pane?.left ?? 0);
|
||||
const rightEdge = Math.min(window.innerWidth - 12, pane?.right ?? window.innerWidth);
|
||||
const topEdge = Math.max(12, pane?.top ?? 0);
|
||||
const bottomEdge = Math.min(window.innerHeight - 12, pane?.bottom ?? window.innerHeight);
|
||||
const above = start.top - height - 8;
|
||||
const below = end.bottom + 8;
|
||||
setLinkPosition({
|
||||
left: Math.max(leftEdge, Math.min(start.left, rightEdge - width)),
|
||||
top: Math.max(topEdge, Math.min(above >= topEdge ? above : below, bottomEdge - height)),
|
||||
});
|
||||
};
|
||||
position();
|
||||
window.addEventListener("scroll", position, true);
|
||||
window.addEventListener("resize", position);
|
||||
editor.on("transaction", position);
|
||||
return () => {
|
||||
window.removeEventListener("scroll", position, true);
|
||||
window.removeEventListener("resize", position);
|
||||
editor.off("transaction", position);
|
||||
};
|
||||
}, [editor, linkOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
const update = () => forceUpdate();
|
||||
@@ -71,7 +106,7 @@ export function FloatingToolbar({ editor }: { editor: Editor | null }) {
|
||||
|
||||
useEscapeLayer(linkOpen, () => {
|
||||
setLinkOpen(false);
|
||||
editor?.commands.focus();
|
||||
editor?.commands.focus(undefined, { scrollIntoView: false });
|
||||
});
|
||||
useEscapeLayer(alignOpen, () => setAlignOpen(false));
|
||||
|
||||
@@ -85,17 +120,17 @@ export function FloatingToolbar({ editor }: { editor: Editor | null }) {
|
||||
const applyLink = () => {
|
||||
const href = normalizeUrl(linkValue);
|
||||
if (!href) {
|
||||
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
||||
editor.chain().focus(undefined, { scrollIntoView: false }).extendMarkRange("link").unsetLink().run();
|
||||
} else if (editor.state.selection.empty && !editor.isActive("link")) {
|
||||
editor.chain().focus().insertContent({ type: "text", text: href, marks: [{ type: "link", attrs: { href } }] }).run();
|
||||
editor.chain().focus(undefined, { scrollIntoView: false }).insertContent({ type: "text", text: href, marks: [{ type: "link", attrs: { href } }] }).run();
|
||||
} else {
|
||||
editor.chain().focus().extendMarkRange("link").setLink({ href }).run();
|
||||
editor.chain().focus(undefined, { scrollIntoView: false }).extendMarkRange("link").setLink({ href }).run();
|
||||
}
|
||||
setLinkOpen(false);
|
||||
};
|
||||
|
||||
const removeLink = () => {
|
||||
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
||||
editor.chain().focus(undefined, { scrollIntoView: false }).extendMarkRange("link").unsetLink().run();
|
||||
setLinkOpen(false);
|
||||
};
|
||||
|
||||
@@ -126,6 +161,7 @@ export function FloatingToolbar({ editor }: { editor: Editor | null }) {
|
||||
{tool(editor.isActive("heading", { level: 2 }), () => editor.chain().focus().toggleHeading({ level: 2 }).run(), "Heading", <Icon d="M5 5v14M5 12h8M13 5v14" />)}
|
||||
{tool(editor.isActive("blockquote"), () => editor.chain().focus().toggleBlockquote().run(), "Quote", <Icon d="M7 8h4v4a4 4 0 0 1-4 4M14 8h4v4a4 4 0 0 1-4 4" />)}
|
||||
{tool(editor.isActive("bulletList"), () => editor.chain().focus().toggleBulletList().run(), "Bulleted list", <Icon d="M8 6h12M8 12h12M8 18h12M3.5 6h.01M3.5 12h.01M3.5 18h.01" />)}
|
||||
{tool(editor.isActive("taskList"), () => editor.chain().focus().toggleTaskList().run(), "Task list (⌘⇧9)", <Icon d="M4 4h6v6H4zM5 7l1.5 1.5L9 5M14 7h6M4 15h6v6H4zM14 18h6" />)}
|
||||
<span className="tool-wrap">
|
||||
{tool(alignOpen || align !== "left", () => setAlignOpen((v) => !v), "Align", <Icon d={ALIGN_ICONS[align]} />)}
|
||||
{alignOpen && (
|
||||
@@ -152,11 +188,12 @@ export function FloatingToolbar({ editor }: { editor: Editor | null }) {
|
||||
<span className="tool-wrap">
|
||||
{tool(editor.isActive("link") || linkOpen, () => (linkOpen ? setLinkOpen(false) : openLink()), "Link (⌘K)", <Icon d="M10 13a5 5 0 0 0 7 0l2-2a5 5 0 0 0-7-7l-1 1M14 11a5 5 0 0 0-7 0l-2 2a5 5 0 0 0 7 7l1-1" />)}
|
||||
{linkOpen && (
|
||||
<>
|
||||
<div className="link-pop-backdrop" onMouseDown={() => setLinkOpen(false)} />
|
||||
<div ref={linkPopRef} className="link-pop" onMouseDown={(e) => e.stopPropagation()}>
|
||||
createPortal(<>
|
||||
<div className="link-pop-backdrop link-editor-backdrop" onMouseDown={() => setLinkOpen(false)} />
|
||||
<div ref={linkPopRef} className="link-pop" style={linkPosition} role="dialog" aria-label="Edit link" onMouseDown={(e) => e.stopPropagation()}>
|
||||
<input
|
||||
className="link-input"
|
||||
aria-label="Link URL"
|
||||
value={linkValue}
|
||||
placeholder="https://…"
|
||||
spellCheck={false}
|
||||
@@ -177,7 +214,7 @@ export function FloatingToolbar({ editor }: { editor: Editor | null }) {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</>, document.body)
|
||||
)}
|
||||
</span>
|
||||
{tool(false, () => editor.chain().focus().setHorizontalRule().run(), "Scene break", <Icon d="M5 12h5M14 12h5" />)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Extensions } from "@tiptap/core";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import { TaskList, TaskItem } from "@tiptap/extension-list";
|
||||
import { Figure } from "./figure";
|
||||
import { ParagraphIndent } from "./indent";
|
||||
import { TextAlign } from "./align";
|
||||
@@ -19,6 +20,8 @@ export const editorExtensions: Extensions = [
|
||||
placeholder: ({ node }) => (node.type.name === "heading" ? "" : "Begin your chapter…"),
|
||||
}),
|
||||
Figure,
|
||||
TaskList,
|
||||
TaskItem.configure({ nested: true }),
|
||||
ParagraphIndent,
|
||||
TextAlign,
|
||||
SearchHighlight,
|
||||
|
||||
@@ -22,12 +22,12 @@ export const ParagraphIndent = Extension.create({
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
Tab: () => {
|
||||
if (this.editor.isActive("listItem")) return false;
|
||||
if (this.editor.isActive("listItem") || this.editor.isActive("taskItem")) return false;
|
||||
this.editor.commands.updateAttributes("paragraph", { indent: true });
|
||||
return true;
|
||||
},
|
||||
"Shift-Tab": () => {
|
||||
if (this.editor.isActive("listItem")) return false;
|
||||
if (this.editor.isActive("listItem") || this.editor.isActive("taskItem")) return false;
|
||||
this.editor.commands.updateAttributes("paragraph", { indent: false });
|
||||
return true;
|
||||
},
|
||||
|
||||
+16
-7
@@ -1,6 +1,9 @@
|
||||
import { Extension } from "@tiptap/core";
|
||||
import type { JSONContent } from "@tiptap/core";
|
||||
import type { Editor, JSONContent } from "@tiptap/core";
|
||||
import { Plugin } from "@tiptap/pm/state";
|
||||
import { readText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { isDesktop } from "../ipc";
|
||||
import { useBook } from "../store/useBook";
|
||||
|
||||
function readImage(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -35,6 +38,17 @@ function plainContent(text: string): JSONContent[] {
|
||||
});
|
||||
}
|
||||
|
||||
async function pastePlain(editor: Editor): Promise<void> {
|
||||
try {
|
||||
const text = await (isDesktop ? readText() : navigator.clipboard.readText());
|
||||
if (text && !editor.isDestroyed) {
|
||||
editor.chain().focus(undefined, { scrollIntoView: false }).insertContent(plainContent(text)).run();
|
||||
}
|
||||
} catch (error) {
|
||||
useBook.getState().setNotice(`Could not paste without formatting: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const Paste = Extension.create({
|
||||
name: "pasteHandler",
|
||||
|
||||
@@ -42,12 +56,7 @@ export const Paste = Extension.create({
|
||||
const editor = this.editor;
|
||||
return {
|
||||
"Mod-Shift-v": () => {
|
||||
navigator.clipboard
|
||||
.readText()
|
||||
.then((text) => {
|
||||
if (text) editor.chain().focus().insertContent(plainContent(text)).run();
|
||||
})
|
||||
.catch(() => {});
|
||||
void pastePlain(editor);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -159,6 +159,10 @@ function block(node: JSONContent, paths: Map<string, string>): string {
|
||||
return `<ul>${(node.content ?? []).map((li) => listItem(li, paths)).join("")}</ul>`;
|
||||
case "orderedList":
|
||||
return `<ol>${(node.content ?? []).map((li) => listItem(li, paths)).join("")}</ol>`;
|
||||
case "taskList":
|
||||
return `<ul data-type="taskList">${(node.content ?? []).map((item) =>
|
||||
`<li data-type="taskItem" data-checked="${!!item.attrs?.checked}"><span aria-label="${item.attrs?.checked ? "Completed" : "Incomplete"}">${item.attrs?.checked ? "☑" : "☐"}</span><div>${(item.content ?? []).map((child) => block(child, paths)).join("")}</div></li>`,
|
||||
).join("")}</ul>`;
|
||||
case "horizontalRule":
|
||||
return `<hr class="scene-break"/>`;
|
||||
case "figure":
|
||||
@@ -500,6 +504,19 @@ li {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
li[data-type="taskItem"] > span {
|
||||
float: left;
|
||||
}
|
||||
|
||||
li[data-type="taskItem"] > div {
|
||||
margin-left: 1.6em;
|
||||
}
|
||||
|
||||
hr.scene-break {
|
||||
border: 0;
|
||||
margin: 1.6em 0;
|
||||
|
||||
@@ -111,6 +111,10 @@ function block(node: JSONContent, paths: Map<string, string>): string {
|
||||
return `#list(${(node.content ?? []).map((li) => listItem(li, paths)).join(", ")})`;
|
||||
case "orderedList":
|
||||
return `#enum(${(node.content ?? []).map((li) => listItem(li, paths)).join(", ")})`;
|
||||
case "taskList":
|
||||
return (node.content ?? []).map((item) =>
|
||||
`#list(marker: [${item.attrs?.checked ? "\\[x\\]" : "\\[ \\]"}], ${listItem(item, paths)})`,
|
||||
).join("\n\n");
|
||||
case "horizontalRule":
|
||||
return "#scenebreak";
|
||||
case "figure":
|
||||
|
||||
+5
-5
@@ -25,7 +25,7 @@ export function useFocusTrap(ref: RefObject<HTMLElement | null>, active = true):
|
||||
const root = ref.current;
|
||||
if (!active || !root) return;
|
||||
openTraps++;
|
||||
if (!root.contains(document.activeElement)) focusable(root)[0]?.focus();
|
||||
if (!root.contains(document.activeElement)) focusable(root)[0]?.focus({ preventScroll: true });
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key !== "Tab") return;
|
||||
@@ -36,13 +36,13 @@ export function useFocusTrap(ref: RefObject<HTMLElement | null>, active = true):
|
||||
const current = document.activeElement as HTMLElement | null;
|
||||
if (!current || !root.contains(current)) {
|
||||
e.preventDefault();
|
||||
(e.shiftKey ? last : first).focus();
|
||||
(e.shiftKey ? last : first).focus({ preventScroll: true });
|
||||
} else if (e.shiftKey && current === first) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
last.focus({ preventScroll: true });
|
||||
} else if (!e.shiftKey && current === last) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
first.focus({ preventScroll: true });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -50,7 +50,7 @@ export function useFocusTrap(ref: RefObject<HTMLElement | null>, active = true):
|
||||
return () => {
|
||||
openTraps--;
|
||||
root.removeEventListener("keydown", onKeyDown);
|
||||
if (opener.current?.isConnected) opener.current.focus();
|
||||
if (opener.current?.isConnected) opener.current.focus({ preventScroll: true });
|
||||
};
|
||||
}, [ref, active]);
|
||||
}
|
||||
+13
-1
@@ -233,6 +233,18 @@ function blockFromElement(el: Element, depth: number): JSONContent[] {
|
||||
return [{ type: "blockquote", content: inner.length ? inner : [{ type: "paragraph" }] }];
|
||||
}
|
||||
case "ul": {
|
||||
if (el.getAttribute("data-type") === "taskList") {
|
||||
const items = Array.from(el.children).filter((child) => child.localName === "li").map((item) => {
|
||||
const body = item.querySelector(":scope > div") ?? item;
|
||||
const content = blocksFrom(body, depth + 1);
|
||||
return {
|
||||
type: "taskItem",
|
||||
attrs: { checked: item.getAttribute("data-checked") === "true" },
|
||||
content: content.length ? content : [{ type: "paragraph" }],
|
||||
};
|
||||
});
|
||||
return items.length ? [{ type: "taskList", content: items }] : [];
|
||||
}
|
||||
const items = listItems(el, depth);
|
||||
return items.length ? [{ type: "bulletList", content: items }] : [];
|
||||
}
|
||||
@@ -467,7 +479,7 @@ function buildChapter(
|
||||
};
|
||||
}
|
||||
|
||||
export function filesToBook(files: RawFile[], fallbackName = "Imported book"): Book {
|
||||
export function filesToBook(files: RawFile[], fallbackName = "Imported project"): Book {
|
||||
const byPath = new Map<string, RawFile>();
|
||||
files.forEach((f) => byPath.set(normalize(f.path), f));
|
||||
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@ export async function createAndOpenBook(
|
||||
try {
|
||||
await saveBook(book);
|
||||
} catch (e) {
|
||||
onError?.(`Could not create book: ${e}`);
|
||||
onError?.(`Could not create project: ${e}`);
|
||||
}
|
||||
}
|
||||
open(book);
|
||||
|
||||
+56
-8
@@ -75,6 +75,8 @@ button {
|
||||
padding: 0 14px 0 84px;
|
||||
background: var(--shell);
|
||||
border-bottom: 1px solid var(--line);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.titlebar .lead {
|
||||
@@ -547,14 +549,20 @@ body.resizing .sidebar {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.prose a {
|
||||
color: var(--accent);
|
||||
.prose a,
|
||||
.page-body a,
|
||||
.device-body a {
|
||||
color: #1769c2;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--line-strong);
|
||||
text-decoration-color: currentColor;
|
||||
text-underline-offset: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .prose a {
|
||||
color: #79b5ff;
|
||||
}
|
||||
|
||||
.prose blockquote {
|
||||
margin: 1.7rem 0;
|
||||
padding-left: 1.2rem;
|
||||
@@ -930,11 +938,9 @@ body.resizing .sidebar {
|
||||
}
|
||||
|
||||
.link-pop {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 10px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 7;
|
||||
position: fixed;
|
||||
z-index: 19;
|
||||
max-width: calc(100vw - 24px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
@@ -966,6 +972,7 @@ body.resizing .sidebar {
|
||||
}
|
||||
|
||||
.link-input {
|
||||
min-width: 0;
|
||||
width: 220px;
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
@@ -976,6 +983,40 @@ body.resizing .sidebar {
|
||||
font-size: var(--t-2);
|
||||
}
|
||||
|
||||
.link-editor-backdrop {
|
||||
z-index: 18;
|
||||
}
|
||||
|
||||
:is(.prose, .page-body, .device-body) ul[data-type="taskList"] {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
:is(.prose, .page-body, .device-body) li[data-type="taskItem"] {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.65em;
|
||||
}
|
||||
|
||||
:is(.prose, .page-body, .device-body) li[data-type="taskItem"] > label {
|
||||
flex: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.prose li[data-type="taskItem"] input {
|
||||
cursor: pointer;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
:is(.prose, .page-body, .device-body) li[data-type="taskItem"] > div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:is(.prose, .page-body, .device-body) li[data-type="taskItem"] p {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.link-input::placeholder {
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
@@ -1501,6 +1542,8 @@ body.resizing .sidebar {
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 14px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.shelf {
|
||||
@@ -1514,6 +1557,7 @@ body.resizing .sidebar {
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
aspect-ratio: 5 / 7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1554,6 +1598,8 @@ body.resizing .sidebar {
|
||||
}
|
||||
|
||||
.card-title {
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
font-family: var(--font-book);
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
@@ -1562,6 +1608,8 @@ body.resizing .sidebar {
|
||||
}
|
||||
|
||||
.card-author {
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--t-2);
|
||||
color: var(--ink-soft);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
.panel.shortcuts-panel {
|
||||
width: min(900px, calc(100vw - 32px));
|
||||
}
|
||||
|
||||
.panel-body.shortcuts-groups {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 28px 36px;
|
||||
}
|
||||
|
||||
.shortcuts-group h3 {
|
||||
margin: 0 0 12px;
|
||||
font-size: var(--t-3);
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.shortcuts-group dl {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.shortcut-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 34px;
|
||||
font-size: var(--t-2);
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.shortcut-row dd {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.shortcut-row kbd {
|
||||
min-width: 22px;
|
||||
padding: 3px 5px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--r-sm);
|
||||
background: var(--raised);
|
||||
color: var(--ink);
|
||||
font-family: var(--font-ui);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.panel-body.shortcuts-groups {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user