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
@@ -214,10 +214,25 @@ export function EditorPane({
|
||||
<button onClick={onOpenHistory} title="Version history" style={toolbarButton(T, T.faint)}>
|
||||
{I.history}
|
||||
</button>
|
||||
<button onClick={onToggleStar} style={toolbarButton(T, note.starred ? "#f0b429" : T.faint)}>
|
||||
<button
|
||||
onClick={onToggleStar}
|
||||
title={note.starred ? "Unstar" : "Star"}
|
||||
style={toolbarButton(T, note.starred ? "#f0b429" : T.faint)}
|
||||
>
|
||||
{note.starred ? I.starFill : I.star}
|
||||
</button>
|
||||
<button onClick={onTrash} style={toolbarButton(T, T.danger)}>
|
||||
{/* Set apart, and no longer painted red while doing nothing. Standing
|
||||
permanently in the danger colour next to the star made the most
|
||||
destructive control the loudest thing on the screen; it turns red
|
||||
under the pointer, when it is about to be used. */}
|
||||
<div style={{ width: 1, height: 18, background: T.borderLight, margin: "0 4px" }} />
|
||||
<button
|
||||
onClick={onTrash}
|
||||
title="Move to Trash"
|
||||
onMouseEnter={(e) => (e.currentTarget.style.color = T.danger)}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.color = T.faint)}
|
||||
style={toolbarButton(T, T.faint)}
|
||||
>
|
||||
{I.trash}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
sizeCanvas,
|
||||
} from "../drawing/strokes.js";
|
||||
import { COLORS } from "../../ui/constants.jsx";
|
||||
|
||||
import { I } from "../../ui/icons.jsx";
|
||||
import { BacklinksPanel } from "../../ui/BacklinksPanel.jsx";
|
||||
|
||||
@@ -67,6 +68,15 @@ export function NoteEditor({
|
||||
// new vector strokes so migrating a note never loses what was already drawn.
|
||||
const legacyRaster = note.drawingData && !hideDrawing;
|
||||
|
||||
/* Whether this note needs a page at all.
|
||||
*
|
||||
* The fixed 3:4 stage exists for the canvas: a stroke has to land in the same
|
||||
* place on every screen, which needs one agreed page. Writing needs no such
|
||||
* thing, and paid for it with a bordered box and an empty page below two lines
|
||||
* of text. So the page appears exactly when something is on it — or about to
|
||||
* be — and plain text is simply text. */
|
||||
const needsPage = mode === "draw" || hasStrokes || !!legacyRaster;
|
||||
|
||||
const commitStrokes = (next) => {
|
||||
strokesRef.current = next;
|
||||
setStrokes(next);
|
||||
@@ -75,15 +85,6 @@ export function NoteEditor({
|
||||
const T = useMemo(
|
||||
() => ({
|
||||
bg: dark ? "#0d1117" : "#ffffff",
|
||||
/* The sheet.
|
||||
*
|
||||
* In daylight it is warm off-white — plain white next to a white toolbar
|
||||
* is not a sheet, it is an absence of one. At night it is a raised surface
|
||||
* rather than a white rectangle: a page that glows brighter than the room
|
||||
* is what makes an app unusable in the evening, and "paper" in a dark room
|
||||
* still means "the surface I write on", not "white". */
|
||||
paper: dark ? "#1b2430" : "#fbfaf7",
|
||||
paperEdge: dark ? "#333f4e" : "#e4dfd5",
|
||||
toolbar: dark ? "#161b22" : "#f6f8fa",
|
||||
border: dark ? "#30363d" : "#d0d7de",
|
||||
text: dark ? "#e6edf3" : "#1f2328",
|
||||
@@ -163,13 +164,7 @@ export function NoteEditor({
|
||||
const mobile = window.innerWidth < 768;
|
||||
setIsMobileViewport(mobile);
|
||||
const sidePad = mobile ? 10 : 28;
|
||||
/* How wide a sheet gets.
|
||||
*
|
||||
* With 56px of inner margin this leaves a text column of about 68
|
||||
* characters at the body size — measured, not guessed. The old 760 ran to
|
||||
* nearly 90, past the width at which the eye reliably finds the start of
|
||||
* the next line, and the single biggest reason the surface read badly. */
|
||||
const desktopMax = 620;
|
||||
const desktopMax = 760;
|
||||
const minW = mobile ? 220 : 320;
|
||||
const maxAllowed = Math.max(220, W - sidePad * 2);
|
||||
const targetW = mobile ? maxAllowed : Math.min(desktopMax, maxAllowed);
|
||||
@@ -460,9 +455,11 @@ export function NoteEditor({
|
||||
* width a property of the window, which is what it always looked like. */
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
// The desk the sheet lies on, a touch darker than the sheet so the
|
||||
// shadow has something to fall onto.
|
||||
background: isMobileViewport ? T.paper : dark ? "#0b0f14" : "#eceae5",
|
||||
background: isMobileViewport
|
||||
? T.bg
|
||||
: dark
|
||||
? "linear-gradient(180deg,#0f141b 0%,#0d1117 60%)"
|
||||
: "linear-gradient(180deg,#f8fafd 0%,#eef3f8 70%)",
|
||||
}}
|
||||
>
|
||||
{/* Main toolbar */}
|
||||
@@ -671,25 +668,22 @@ export function NoteEditor({
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width: stageSize.w,
|
||||
minHeight: stageSize.h,
|
||||
// Centred. It used to sit 28px from the left and stop short of the
|
||||
// right, so the margins were visibly unequal.
|
||||
// Without a page the column is as wide as it needs to be to read
|
||||
// comfortably, and centred; with one it is the canvas's page.
|
||||
// The text caps its own width in the stylesheet, so this is only
|
||||
// the page when there is one.
|
||||
width: needsPage ? stageSize.w : "100%",
|
||||
minHeight: needsPage ? stageSize.h : undefined,
|
||||
margin: "0 auto",
|
||||
background: T.paper,
|
||||
border: isMobileViewport ? "none" : `1px solid ${T.paperEdge}`,
|
||||
borderRadius: isMobileViewport ? 0 : 3,
|
||||
// Paper lies on something; a sheet with no shadow is a panel.
|
||||
boxShadow: isMobileViewport
|
||||
? "none"
|
||||
: dark
|
||||
? "0 10px 30px rgba(0,0,0,0.45)"
|
||||
: "0 10px 28px rgba(60,50,30,0.13)",
|
||||
background: needsPage ? T.bg : "transparent",
|
||||
border: needsPage && !isMobileViewport ? `1px solid ${T.border}` : "none",
|
||||
borderRadius: needsPage && !isMobileViewport ? 14 : 0,
|
||||
boxShadow: needsPage && !isMobileViewport ? "0 16px 36px rgba(0,0,0,0.14)" : "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
minHeight: stageSize.h,
|
||||
minHeight: needsPage ? stageSize.h : undefined,
|
||||
width: "100%",
|
||||
pointerEvents: mode === "text" ? "auto" : "none",
|
||||
}}
|
||||
@@ -697,7 +691,7 @@ export function NoteEditor({
|
||||
<EditorSurface
|
||||
editor={textEditor}
|
||||
dark={dark}
|
||||
minHeight={stageSize.h}
|
||||
minHeight={needsPage ? stageSize.h : 0}
|
||||
onOpenNote={onOpenNote}
|
||||
resolveLinkLabel={resolveLinkLabel}
|
||||
/>
|
||||
|
||||
@@ -524,7 +524,9 @@ export function NoteList({
|
||||
padding: 30,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 36 }}>{isTrashView ? "🗑️" : "📝"}</div>
|
||||
<div style={{ display: "flex", opacity: 0.5, transform: "scale(2.2)", marginBottom: 12 }}>
|
||||
{isTrashView ? I.trash : I.note}
|
||||
</div>
|
||||
<div style={{ fontSize: 13, textAlign: "center" }}>
|
||||
{isTrashView ? "Trash is empty" : "No notes yet"}
|
||||
</div>
|
||||
@@ -603,7 +605,7 @@ export function NoteList({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
padding: "12px 14px",
|
||||
padding: "9px 12px",
|
||||
borderRadius: 10,
|
||||
border: `1px solid ${activeNote === note.id ? T.accent + "55" : "transparent"}`,
|
||||
background: activeNote === note.id ? T.accentSoft : "transparent",
|
||||
@@ -621,10 +623,11 @@ export function NoteList({
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 13 }}>
|
||||
{note.trashed ? "🗑️" : note.type === "pdf" ? "📄" : "📝"}
|
||||
<span style={{ display: "flex", color: T.faint, flexShrink: 0 }}>
|
||||
{note.trashed ? I.trash : note.type === "pdf" ? I.pdf : I.note}
|
||||
</span>
|
||||
<span
|
||||
data-note-title
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
@@ -638,7 +641,7 @@ export function NoteList({
|
||||
{note.title}
|
||||
</span>
|
||||
{note.starred && !note.trashed && (
|
||||
<span style={{ color: "#f0b429", fontSize: 11 }}>⭐</span>
|
||||
<span style={{ display: "flex", color: "#f0b429" }}>{I.starFill}</span>
|
||||
)}
|
||||
</div>
|
||||
{snippet && (
|
||||
@@ -657,7 +660,7 @@ export function NoteList({
|
||||
{snippet}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ fontSize: 10, color: T.faint, marginTop: 3 }}>
|
||||
<div style={{ fontSize: 10.5, color: T.faint, marginTop: 3 }}>
|
||||
{fmtDate(note.updatedAt)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
Reference in new issue
Block a user