From 632ea5dcd9297d3a57e086f600943564f9abdddc Mon Sep 17 00:00:00 2001 From: marc Date: Wed, 12 Aug 2026 13:43:51 +0200 Subject: [PATCH] Move a note in one press, and stop Draw inventing a page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moving lived under a right-click, behind a submenu: two steps before the list of places appeared at all, and on a phone a long press followed by a flyout. There is a button on the row now, and it opens one screen with the chapters of this notebook and the other notebooks together, marking where the note already is. The spare sheet in Draw mode was drawn as solid paper, so pressing Draw on a one-page note made a second page appear from nowhere while the count still said "of 1". It exists for a good reason — pages otherwise come only from how far the writing runs, so drawing on page two would mean writing two pages first — but it should look like the offer it is. Dashed outline, half opacity, and it says what it is for. It counts once something is on it. And the two buttons above the list did not match: the one I added last round was larger and bolder than the one it sat beside, and the older one's target was nineteen pixels tall, which a finger misses. Both are one shape now. A separator's label matches a chapter's name in size for the same reason — two type treatments in one list is noise, and the difference was not saying anything. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 21 +++ e2e/chapters.mjs | 17 +- e2e/drawing.mjs | 13 ++ frontend/src/App.jsx | 14 ++ frontend/src/features/notes/NoteEditor.jsx | 68 +++++--- frontend/src/features/notes/NoteList.jsx | 45 +++++- frontend/src/ui/MoveNoteDialog.jsx | 180 +++++++++++++++++++++ 7 files changed, 329 insertions(+), 29 deletions(-) create mode 100644 frontend/src/ui/MoveNoteDialog.jsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d9fde0..6e68257 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,27 @@ not from the output of `docker push`. --- +## Unreleased + +**Added** +- **A move button on every note row.** Moving used to be a right-click into a + submenu — two steps before the list of places appeared, and on a phone a long + press followed by a flyout. One press now opens a dialog with the chapters of + this notebook and the other notebooks on one screen, marking where the note + is now. + +**Changed** +- **The spare page in Draw mode reads as an offer**, not as a page. A second + sheet appeared the moment Draw was pressed, drawn as solid paper, while the + count still said "of 1" — which looks like the app inventing a page. It is a + dashed outline saying "Draw here to add a page", and it counts only once + something is on it. +- *+ Chapter* and *+ Separator* are the same shape. The new one had been built + without matching the old, and the old one's 19px target was one a finger + misses. Separator labels also match a chapter's name in size now. + +--- + ## 2026-08-12 — Chapters **Images** — web `sha256:790ff73a…`, server `sha256:181d77be…` diff --git a/e2e/chapters.mjs b/e2e/chapters.mjs index 9604b4a..a8af018 100644 --- a/e2e/chapters.mjs +++ b/e2e/chapters.mjs @@ -31,12 +31,14 @@ try { await p.getByRole("button", { name: "Create", exact: true }).click(); await p.waitForTimeout(2000); }; + /* One press of the button on the row, then choose. It used to be a + right-click into a submenu — two steps before the places even appeared, and + on a phone a long press followed by a flyout. */ const moveTo = async (noteTitle, target) => { - await p.locator("[data-note-row]").filter({ hasText: noteTitle }).first().click({ button: "right", force: true }); - await p.waitForTimeout(600); - await p.locator("[data-submenu-toggle]").first().click(); - await p.waitForTimeout(500); - await p.locator("[data-submenu] button").filter({ hasText: target }).first().click(); + const row = p.locator("[data-note-row]").filter({ hasText: noteTitle }).first(); + await row.locator("xpath=preceding-sibling::button[@data-move-note]").first().click(); + await p.waitForTimeout(700); + await p.locator("[data-move-target]").filter({ hasText: target }).first().click(); await p.waitForTimeout(2500); }; @@ -59,6 +61,11 @@ try { check(server.length === 1 && server[0].name === "Rechnungen", "the server has it", JSON.stringify(server)); /* ── filing a note ── */ + check( + (await p.locator("[data-move-note]").count()) === 2, + "every note has a move button, without a right-click", + String(await p.locator("[data-move-note]").count()) + ); await moveTo("Stromrechnung", "Rechnungen"); check( (await titles()).join(",") === "Zählerstand", diff --git a/e2e/drawing.mjs b/e2e/drawing.mjs index 9ac5119..4053e31 100644 --- a/e2e/drawing.mjs +++ b/e2e/drawing.mjs @@ -149,6 +149,19 @@ try { (await p.locator('canvas[data-draw-page="2"]').count()) === 1, "there is a blank page past the end to draw on, before anything is written" ); + /* It has to read as an offer, not as a page that appeared by itself. Drawn + solid, a second sheet turned up the moment Draw was pressed while the count + still said "of 1", which looks like the app inventing a page. */ + check( + (await p.locator("[data-spare-page]").count()) === 1, + "and it is marked as the spare rather than drawn as paper", + String(await p.locator("[data-spare-page]").count()) + ); + check( + (await p.locator("[data-note-position]").innerText()).includes("of 1"), + "so the count still says one page until something is on it", + await p.locator("[data-note-position]").innerText() + ); await draw(p, 1); const first = await inkGrid(p, 1); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e192758..96e605e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -13,6 +13,7 @@ import { I } from "./ui/icons.jsx"; import { makeTheme, FONT_UI } from "./ui/theme"; import { ConfirmDialog } from "./ui/ConfirmDialog.jsx"; import { NameDialog } from "./ui/NameDialog.jsx"; +import { MoveNoteDialog } from "./ui/MoveNoteDialog.jsx"; import { ContextMenu } from "./ui/ContextMenu.jsx"; import { EditNotebookModal, CreateNotebookModal } from "./ui/NotebookModals.jsx"; import { AccountMenu } from "./ui/AccountMenu.jsx"; @@ -90,6 +91,8 @@ export default function App() { const [ctxMenu, setCtxMenu] = useState(null); // { title, value, onSave } while a chapter is being named or renamed. const [namingChapter, setNamingChapter] = useState(null); + // The note whose destination is being chosen, if any. + const [movingNote, setMovingNote] = useState(null); const [confirmDialog, setConfirmDialog] = useState(null); // Bumped on a version restore to force a fresh editor on the restored body. const [restoreEpoch, setRestoreEpoch] = useState(0); @@ -776,6 +779,7 @@ export default function App() { onOpenChapter={setActiveChapter} onAddChapter={activeNotebook ? addChapter : null} onChapterContextMenu={chapterContextMenu} + onMoveNote={setMovingNote} onReorder={rearrangeNotes} onToggleSidebar={() => setSidebarOpen(!sidebarOpen)} onSelectNote={openNote} @@ -900,6 +904,16 @@ export default function App() { )} /> )} + {movingNote && ( + data.moveNote(id, folderId, chapterId)} + onClose={() => setMovingNote(null)} + dark={dark} + /> + )} {namingChapter && ( ( -
- ))} + {Array.from({ length: canvasPages }, (_, i) => { + /* The last sheet in Draw mode is the spare one, not a page. + Drawn as an outline rather than as paper: it appeared as a full + second page the moment Draw was pressed, while the count still + said "of 1" — which reads as the app inventing a page. It is an + offer, and should look like one until something is on it. */ + const spare = mode === "draw" && i + 1 > pages.count; + return ( +
+ {spare && ( +
+ Draw here to add a page +
+ )} +
+ ); + })}
(
); +/** + * Moving a note, in one press. + * + * It used to be a right-click into a submenu — two steps before the list of + * places appeared at all, and on a phone a long press followed by a flyout. + * Sitting on the row beside the grip, it says what it does and takes one tap. + */ +const MoveButton = ({ T, onClick }) => ( + +); + /** Width the separator's label keeps in both states, so renaming moves nothing. */ const MIDDLE_W = 132; @@ -136,7 +166,7 @@ function SeparatorRow({ T, separator, editing, onStartRename, onRename, onContex borderRadius: 6, background: "transparent", color: T.text, - fontSize: 11.5, + fontSize: 12.5, fontWeight: 700, letterSpacing: 0.8, textAlign: "center", @@ -154,7 +184,7 @@ function SeparatorRow({ T, separator, editing, onStartRename, onRename, onContex border: "1px solid transparent", background: "transparent", color: separator.label ? T.text : T.muted, - fontSize: 11.5, + fontSize: 12.5, fontWeight: 700, letterSpacing: 0.8, textTransform: "uppercase", @@ -423,6 +453,7 @@ export function NoteList({ onOpenChapter, onAddChapter, onChapterContextMenu, + onMoveNote, onReorder, onToggleSidebar, onSelectNote, @@ -592,7 +623,7 @@ export function NoteList({ display: "flex", alignItems: "center", gap: 5, - padding: "4px 10px", + padding: "5px 10px", borderRadius: 8, border: `1px solid ${T.border}`, background: "transparent", @@ -614,8 +645,9 @@ export function NoteList({ background: "transparent", color: T.muted, borderRadius: 8, - padding: "2px 8px", - fontSize: 10.5, + padding: "5px 10px", + fontSize: 11.5, + fontWeight: 600, cursor: "pointer", whiteSpace: "nowrap", }} @@ -720,6 +752,9 @@ export function NoteList({ {showDropLine && }
{reorderable && } + {onMoveNote && !note.trashed && ( + onMoveNote(note)} /> + )} + ); + + const heading = (text) => ( +
+ {text} +
+ ); + + return ( +
+
e.stopPropagation()} + style={{ + width: "100%", + maxWidth: 400, + maxHeight: "80vh", + display: "flex", + flexDirection: "column", + background: T.bg, + border: `1px solid ${T.border}`, + borderRadius: 16, + padding: "20px 10px 12px", + fontFamily: "'DM Sans',sans-serif", + }} + > +

Move note

+
+ {note.title || "Untitled"} +
+ +
+ {here.length > 0 && heading("In this notebook")} + {here.length > 0 && ( + go(note.folderId, null)}> + The notebook itself + + )} + {here.map((c) => ( + go(note.folderId, c.id)} + > + {c.name} + + ))} + + {others.length > 0 && heading("Another notebook")} + {others.map((nb) => ( + go(nb.id, null)}> + {nb.name} + + ))} + + {here.length === 0 && others.length === 0 && ( +
+ There is nowhere else to put it yet — make another notebook, or a chapter in this one. +
+ )} +
+ +
+ +
+
+
+ ); +}