From c12ffa250ea2a58f181f6799d44eac48d3925d45 Mon Sep 17 00:00:00 2001 From: marc Date: Tue, 11 Aug 2026 12:08:54 +0200 Subject: [PATCH] Make the writing surface a sheet of paper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It was a page-shaped box that was not page-coloured: the same fill as everything around it with a hairline border, so it read as an empty panel. Now it is warm off-white in daylight and a raised surface at night — deliberately not a white rectangle, since a page that glows brighter than the room is what makes an app unusable in the evening. The line ran to 84 characters, past the width at which the eye reliably finds the start of the next line, and that was the single biggest reason the surface read badly. Measured, not guessed: 620px of sheet with a 56px margin gives about 73. It is centred now — it used to sit 28px from the left and stop short of the right — with almost square corners instead of a dialog's 14px radius. Blocks on the sheet are tinted from paper rather than from the app background, or they read as sitting underneath it. Five German strings survived the earlier pass, "Alle Notizen exportieren" among them: that sweep filtered on umlauts, so sentences without one went straight through. Swept again by word. 218 unit tests, 244 browser checks across eleven suites. Co-Authored-By: Claude Opus 5 --- frontend/src/editor/editor.css | 24 ++++++++----- frontend/src/export/print.ts | 2 +- frontend/src/features/notes/NoteEditor.jsx | 42 ++++++++++++++++------ frontend/src/features/notes/NoteList.jsx | 2 +- frontend/src/features/pdf/PdfPins.jsx | 2 +- frontend/src/import/zipRead.test.ts | 2 +- frontend/src/import/zipRead.ts | 4 +-- frontend/src/ui/AccountMenu.jsx | 2 +- frontend/src/ui/ExportDialog.jsx | 2 +- readme.md | 27 ++++++++++++++ 10 files changed, 82 insertions(+), 27 deletions(-) diff --git a/frontend/src/editor/editor.css b/frontend/src/editor/editor.css index 2f8d224..57965f9 100644 --- a/frontend/src/editor/editor.css +++ b/frontend/src/editor/editor.css @@ -8,10 +8,10 @@ --ne-text: #1f2328; --ne-muted: #656d76; --ne-faint: #afb8c1; - --ne-border: #d0d7de; + --ne-border: #ded8cd; --ne-accent: #0969da; - --ne-surface: #ffffff; - --ne-subtle: #f6f8fa; + --ne-surface: #fbfaf7; + --ne-subtle: #f2efe8; } .note-editor[data-theme="dark"] { --ne-text: #e6edf3; @@ -19,20 +19,28 @@ --ne-faint: #484f58; --ne-border: #30363d; --ne-accent: #388bfd; - --ne-surface: #0d1117; - --ne-subtle: #161b22; + --ne-surface: #161c24; + --ne-subtle: #1d242e; } .note-editor .ProseMirror { outline: none; color: var(--ne-text); font-family: "Newsreader", Georgia, serif; - font-size: 15px; - line-height: 1.7; - padding: 20px 24px; + 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; 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; } diff --git a/frontend/src/export/print.ts b/frontend/src/export/print.ts index 77780bf..9bfc113 100644 --- a/frontend/src/export/print.ts +++ b/frontend/src/export/print.ts @@ -158,7 +158,7 @@ function articleFor(note: PrintableNote): string {

${esc(note.meta.title || "Untitled")}

${when ? `
${esc(when)}
` : ""} ${body} - ${note.drawing ? `
Zeichnung
` : ""} + ${note.drawing ? `
Drawing
` : ""} `; } diff --git a/frontend/src/features/notes/NoteEditor.jsx b/frontend/src/features/notes/NoteEditor.jsx index ccca1fe..4647696 100644 --- a/frontend/src/features/notes/NoteEditor.jsx +++ b/frontend/src/features/notes/NoteEditor.jsx @@ -75,6 +75,15 @@ 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", @@ -154,7 +163,13 @@ export function NoteEditor({ const mobile = window.innerWidth < 768; setIsMobileViewport(mobile); const sidePad = mobile ? 10 : 28; - const desktopMax = 760; + /* 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 minW = mobile ? 220 : 320; const maxAllowed = Math.max(220, W - sidePad * 2); const targetW = mobile ? maxAllowed : Math.min(desktopMax, maxAllowed); @@ -445,11 +460,9 @@ export function NoteEditor({ * width a property of the window, which is what it always looked like. */ flex: 1, minWidth: 0, - background: isMobileViewport - ? T.bg - : dark - ? "linear-gradient(180deg,#0f141b 0%,#0d1117 60%)" - : "linear-gradient(180deg,#f8fafd 0%,#eef3f8 70%)", + // 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", }} > {/* Main toolbar */} @@ -660,11 +673,18 @@ export function NoteEditor({ position: "relative", width: stageSize.w, minHeight: stageSize.h, - margin: isMobileViewport ? "0 auto" : "0 0 0 28px", - background: T.bg, - border: isMobileViewport ? "none" : `1px solid ${T.border}`, - borderRadius: isMobileViewport ? 0 : 14, - boxShadow: isMobileViewport ? "none" : "0 16px 36px rgba(0,0,0,0.14)", + // Centred. It used to sit 28px from the left and stop short of the + // right, so the margins were visibly unequal. + 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)", }} >
startDrag(e, pin)} onPointerMove={onPointerMove} onPointerUp={(e) => endDrag(e, pin)} diff --git a/frontend/src/import/zipRead.test.ts b/frontend/src/import/zipRead.test.ts index 4f3bfc3..7ca0279 100644 --- a/frontend/src/import/zipRead.test.ts +++ b/frontend/src/import/zipRead.test.ts @@ -40,7 +40,7 @@ describe("readZip", () => { }); it("refuses something that is not a ZIP", async () => { - await expect(readZip(new TextEncoder().encode("nur Text").buffer)).rejects.toThrow(/keine ZIP/); + await expect(readZip(new TextEncoder().encode("nur Text").buffer)).rejects.toThrow(/not a ZIP/); }); it("returns an empty list for an archive with no entries", async () => { diff --git a/frontend/src/import/zipRead.ts b/frontend/src/import/zipRead.ts index a1e5411..21c733a 100644 --- a/frontend/src/import/zipRead.ts +++ b/frontend/src/import/zipRead.ts @@ -35,7 +35,7 @@ function findEocd(view: DataView): number { async function inflateRaw(bytes: Uint8Array): Promise { if (typeof DecompressionStream === "undefined") { - throw new Error("Dieses Archiv ist komprimiert; der Browser kann es nicht entpacken."); + throw new Error("This archive is compressed and the browser cannot unpack it."); } const copy = new Uint8Array(bytes.byteLength); copy.set(bytes); @@ -53,7 +53,7 @@ export async function readZip(buffer: ArrayBuffer): Promise { const all = new Uint8Array(buffer); const eocd = findEocd(view); - if (eocd < 0) throw new Error("Das ist keine ZIP-Datei."); + if (eocd < 0) throw new Error("That is not a ZIP file."); const count = view.getUint16(eocd + 10, true); let at = view.getUint32(eocd + 16, true); diff --git a/frontend/src/ui/AccountMenu.jsx b/frontend/src/ui/AccountMenu.jsx index 3fccaa7..e386e19 100644 --- a/frontend/src/ui/AccountMenu.jsx +++ b/frontend/src/ui/AccountMenu.jsx @@ -386,7 +386,7 @@ export function AccountMenu({ user, onClose, onLogout, onUserUpdate, onOpenAdmin marginBottom: 8, }} > - {I.export} Alle Notizen exportieren + {I.export} Export all notes {user?.isAdmin && (
+
Reading the note…
) : parts === null ? ( stepParts() ) : ( diff --git a/readme.md b/readme.md index af3024a..406e992 100644 --- a/readme.md +++ b/readme.md @@ -368,6 +368,33 @@ state of their own, so they can be read on their own. --- +## The writing surface + +A real sheet, lying on a desk. It had been a page-*shaped* box that was not +page-*coloured* — the same fill as everything around it with a hairline border, +so it read as an empty panel rather than as paper. + +- **Warm off-white in daylight** (`#fbfaf7`). Plain white next to a white toolbar + is not a sheet, it is the absence of one. +- **A raised surface at night** (`#1b2430`), lighter than the desk, with an edge + and a shadow — deliberately not a white rectangle. A page that glows brighter + than the room is what makes an app unusable in the evening; in a dark theme + "paper" means *the surface I write on*, not *white*. +- **About 73 characters to the line**, measured rather than guessed: 620px of + sheet with a 56px margin. It ran to 84 before, past the width at which the eye + reliably finds the start of the next line, and that was the single biggest + reason the surface read badly. +- **Centred**, with almost square corners. It used to sit 28px from the left and + stop short of the right, and carried a 14px radius that belongs to a dialog. + +Blocks *on* the sheet — quotes, code, table headers — are tinted from paper +rather than from the app background, or they would read as sitting underneath it. + +The sheet keeps the drawing's page proportion, so a short note leaves an empty +page below it. That is what a sheet of paper does, and it is the price of having +one: the canvas needs a fixed page for a stroke to land in the same place on +every screen. + ## Interface language English throughout. It had drifted into two languages — the frame in English,