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. +
+ )} +
+ +
+ +
+
+
+ ); +}