Take the paper back out, and quiet the surfaces around it
The sheet was not what it wanted to be, so writing is plain text again: about 73 characters to the line, centred, no border and no shadow. The fixed page belongs to the canvas — a stroke has to land in the same place on every screen, which needs one agreed page — so it appears exactly when something is on it or about to be: in Draw, or when the note already has a drawing. The cap lives in the stylesheet rather than on a container, so it holds on a page as well as off one. Emoji are gone from the note rows and the sidebar. They are drawn by the operating system, so they looked different on every machine and were loud enough that a row's picture caught the eye before its title did. Rows are tighter by three pixels of padding. The trash button no longer stands permanently in the danger colour beside the star, which made the most destructive control the loudest thing on screen. It sits behind a divider and turns red under the pointer, when it is about to be used. On a phone the toolbar's fourteen controls scrolled sideways, so the ones at the end could only be reached by dragging the bar and nothing said they were there. Nine remain, plus a menu holding the headings, quote, code and undo. A new `sync` suite proves what was assumed: a PDF written on one device — the document, the drawing and the pinned notes — reaches a second one. Two browser contexts are two devices. Both search suites read a row's title as the second line of its text, which only worked while the icon was itself a text emoji; they read it by name now. 218 unit tests, 249 browser checks across twelve suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c12ffa250e
commit
830af6532a
13 files changed
+396
-155
No files matched your search
@@ -56,6 +56,7 @@ const AtIcon = () => (
|
||||
export function EditorToolbar({ editor, T, icons, compact, dark }) {
|
||||
const [, force] = useState(0);
|
||||
const [showTable, setShowTable] = useState(false);
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
const [linkDialog, setLinkDialog] = useState(null);
|
||||
|
||||
// ProseMirror state changes do not go through React; subscribe so the active
|
||||
@@ -80,6 +81,15 @@ export function EditorToolbar({ editor, T, icons, compact, dark }) {
|
||||
return () => document.removeEventListener("pointerdown", close);
|
||||
}, [showTable]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showMore) return;
|
||||
const close = (e) => {
|
||||
if (!e.target.closest?.("[data-more-menu]")) setShowMore(false);
|
||||
};
|
||||
document.addEventListener("pointerdown", close);
|
||||
return () => document.removeEventListener("pointerdown", close);
|
||||
}, [showMore]);
|
||||
|
||||
if (!editor) return null;
|
||||
|
||||
const btn = (active) => ({
|
||||
@@ -274,7 +284,91 @@ export function EditorToolbar({ editor, T, icons, compact, dark }) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{!compact && (
|
||||
{/* On a phone the rest goes behind one button.
|
||||
*
|
||||
* There were fourteen controls in a row that scrolled sideways, so the
|
||||
* ones at the end could only be reached by dragging the bar — and nothing
|
||||
* said they were there. A menu is one tap and shows what it holds. */}
|
||||
{compact ? (
|
||||
<div style={{ position: "relative" }} data-more-menu>
|
||||
<button
|
||||
style={btn(showMore)}
|
||||
title="More"
|
||||
data-toolbar-more={showMore ? "on" : "off"}
|
||||
onClick={() => setShowMore((v) => !v)}
|
||||
>
|
||||
{icons.dots}
|
||||
</button>
|
||||
{showMore && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 36,
|
||||
right: 0,
|
||||
minWidth: 180,
|
||||
background: T.toolbar,
|
||||
border: `1px solid ${T.border}`,
|
||||
borderRadius: 12,
|
||||
padding: 6,
|
||||
zIndex: 200,
|
||||
boxShadow: "0 8px 20px rgba(0,0,0,.25)",
|
||||
}}
|
||||
>
|
||||
{[
|
||||
[
|
||||
"Body text",
|
||||
() => editor.chain().focus().setParagraph().run(),
|
||||
editor.isActive("paragraph"),
|
||||
],
|
||||
[
|
||||
"Heading 1",
|
||||
() => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
||||
editor.isActive("heading", { level: 1 }),
|
||||
],
|
||||
[
|
||||
"Heading 2",
|
||||
() => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
||||
editor.isActive("heading", { level: 2 }),
|
||||
],
|
||||
[
|
||||
"Heading 3",
|
||||
() => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
||||
editor.isActive("heading", { level: 3 }),
|
||||
],
|
||||
[null],
|
||||
[
|
||||
"Quote",
|
||||
() => editor.chain().focus().toggleBlockquote().run(),
|
||||
editor.isActive("blockquote"),
|
||||
],
|
||||
[
|
||||
"Code block",
|
||||
() => editor.chain().focus().toggleCodeBlock().run(),
|
||||
editor.isActive("codeBlock"),
|
||||
],
|
||||
[null],
|
||||
["Undo", () => editor.chain().focus().undo().run(), false],
|
||||
["Redo", () => editor.chain().focus().redo().run(), false],
|
||||
].map(([label, action, active], i) =>
|
||||
label === null ? (
|
||||
<div key={`d${i}`} style={{ height: 1, background: T.border, margin: "5px 2px" }} />
|
||||
) : (
|
||||
<button
|
||||
key={label}
|
||||
style={{ ...menuItem, color: active ? T.active : T.text }}
|
||||
onClick={() => {
|
||||
action();
|
||||
setShowMore(false);
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{divider}
|
||||
<button
|
||||
@@ -291,63 +385,61 @@ export function EditorToolbar({ editor, T, icons, compact, dark }) {
|
||||
>
|
||||
<CodeIcon />
|
||||
</button>
|
||||
|
||||
{divider}
|
||||
<select
|
||||
value={
|
||||
editor.isActive("heading", { level: 1 })
|
||||
? "h1"
|
||||
: editor.isActive("heading", { level: 2 })
|
||||
? "h2"
|
||||
: editor.isActive("heading", { level: 3 })
|
||||
? "h3"
|
||||
: "p"
|
||||
}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
if (v === "p") editor.chain().focus().setParagraph().run();
|
||||
else
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.toggleHeading({ level: Number(v[1]) })
|
||||
.run();
|
||||
}}
|
||||
style={{
|
||||
border: `1px solid ${T.border}`,
|
||||
borderRadius: 6,
|
||||
padding: "3px 6px",
|
||||
background: T.toolbar,
|
||||
color: T.text,
|
||||
fontSize: 11,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<option value="p">Body</option>
|
||||
<option value="h1">H1</option>
|
||||
<option value="h2">H2</option>
|
||||
<option value="h3">H3</option>
|
||||
</select>
|
||||
|
||||
{divider}
|
||||
<button
|
||||
style={{ ...btn(false), opacity: editor.can().undo() ? 1 : 0.35 }}
|
||||
title="Undo"
|
||||
onClick={() => editor.chain().focus().undo().run()}
|
||||
>
|
||||
{icons.undo}
|
||||
</button>
|
||||
<button
|
||||
style={{ ...btn(false), opacity: editor.can().redo() ? 1 : 0.35 }}
|
||||
title="Redo"
|
||||
onClick={() => editor.chain().focus().redo().run()}
|
||||
>
|
||||
{icons.redo}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{divider}
|
||||
<select
|
||||
value={
|
||||
editor.isActive("heading", { level: 1 })
|
||||
? "h1"
|
||||
: editor.isActive("heading", { level: 2 })
|
||||
? "h2"
|
||||
: editor.isActive("heading", { level: 3 })
|
||||
? "h3"
|
||||
: "p"
|
||||
}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
if (v === "p") editor.chain().focus().setParagraph().run();
|
||||
else
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.toggleHeading({ level: Number(v[1]) })
|
||||
.run();
|
||||
}}
|
||||
style={{
|
||||
border: `1px solid ${T.border}`,
|
||||
borderRadius: 6,
|
||||
padding: "3px 6px",
|
||||
background: T.toolbar,
|
||||
color: T.text,
|
||||
fontSize: 11,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<option value="p">Body</option>
|
||||
<option value="h1">H1</option>
|
||||
<option value="h2">H2</option>
|
||||
<option value="h3">H3</option>
|
||||
</select>
|
||||
|
||||
{divider}
|
||||
<button
|
||||
style={{ ...btn(false), opacity: editor.can().undo() ? 1 : 0.35 }}
|
||||
title="Undo"
|
||||
onClick={() => editor.chain().focus().undo().run()}
|
||||
>
|
||||
{icons.undo}
|
||||
</button>
|
||||
<button
|
||||
style={{ ...btn(false), opacity: editor.can().redo() ? 1 : 0.35 }}
|
||||
title="Redo"
|
||||
onClick={() => editor.chain().focus().redo().run()}
|
||||
>
|
||||
{icons.redo}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditorToolbar;
|
||||
@@ -8,10 +8,10 @@
|
||||
--ne-text: #1f2328;
|
||||
--ne-muted: #656d76;
|
||||
--ne-faint: #afb8c1;
|
||||
--ne-border: #ded8cd;
|
||||
--ne-border: #d0d7de;
|
||||
--ne-accent: #0969da;
|
||||
--ne-surface: #fbfaf7;
|
||||
--ne-subtle: #f2efe8;
|
||||
--ne-surface: #ffffff;
|
||||
--ne-subtle: #f6f8fa;
|
||||
}
|
||||
.note-editor[data-theme="dark"] {
|
||||
--ne-text: #e6edf3;
|
||||
@@ -19,28 +19,27 @@
|
||||
--ne-faint: #484f58;
|
||||
--ne-border: #30363d;
|
||||
--ne-accent: #388bfd;
|
||||
--ne-surface: #161c24;
|
||||
--ne-subtle: #1d242e;
|
||||
--ne-surface: #0d1117;
|
||||
--ne-subtle: #161b22;
|
||||
}
|
||||
|
||||
.note-editor .ProseMirror {
|
||||
/* About 73 characters to the line at this size — measured, not guessed. The
|
||||
text used to run the full width of the pane, past 84, which is beyond the
|
||||
point where the eye reliably finds the start of the next line. Capped here
|
||||
rather than on the container so it holds on a page as well as off one. */
|
||||
max-width: 527px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
outline: none;
|
||||
color: var(--ne-text);
|
||||
font-family: "Newsreader", Georgia, serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.75;
|
||||
/* A margin, the way a page has one. The old 24px let the text run to the very
|
||||
edge of the sheet, which is the one thing paper never does. */
|
||||
padding: 44px 56px 64px;
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
padding: 20px 24px;
|
||||
min-height: 100%;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.note-editor .ProseMirror {
|
||||
padding: 22px 20px 40px;
|
||||
}
|
||||
}
|
||||
.note-editor .ProseMirror > * + * {
|
||||
margin-top: 0.6em;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user