Chapters as rows, in two views, in their own colours

Three things Marc asked for, and one of them turned out to be the hinge
the other two hang from.

**A chapter is a row like any other.** Same grip, same drag, same place
in a hand-made arrangement as a note or a separator. That needed notes
and chapters numbered from *one* counter: numbered separately they are
two sequences that cannot be interleaved again, and a chapter dragged
between two notes springs back to the top. Positions therefore have to be
named rather than read off an array index, so POST /notes/reorder now
takes `positions: [{id, order}]` beside the older `ids`, and chapters
have a reorder route of their own.

**Two ways for a chapter to open**, chosen per account beside the
typeface, for the same reason that one lives there: it is a choice about
how your own notes are laid out and it should follow you to the phone.
Beside the list, its notes in a further column the way OneNote lays out
pages — the notebook's own list carrying on to the left, chapter rows and
all. Or unfolded under the chapter's own row, indented against a rule in
its colour. A phone has room for neither and fills the list instead, with
a way back, which is what it did before there was a choice.

**Its own colour**, from eight named ones rather than the full palette:
the point of chapters is that they are different places, and twenty
near-identical blues do not help anyone tell them apart.

The colour is validated on the way in — it is written by a browser, so
only a plain #rrggbb gets through and anything else becomes no colour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-12 14:20:14 +02:00
1 parent 6cad632a0c
commit d74fea4905
12 files changed
+640 -88

No files matched your search

+23 -3
View File
@@ -54,6 +54,7 @@ export interface NoteData {
chapters: Chapter[];
createChapter: (notebookId: string, name: string) => Promise<Chapter>;
renameChapter: (id: string, name: string) => Promise<void>;
recolourChapter: (id: string, color: string | null) => Promise<void>;
deleteChapter: (id: string) => Promise<void>;
deleteNotebook: (id: string) => Promise<void>;
reorderNotebooks: (dragId: string, targetId: string) => void;
@@ -79,6 +80,8 @@ export function useNoteData(user: User | null, activeNote: string | null): NoteD
notesRef.current = notes;
const notebooksRef = useRef(notebooks);
notebooksRef.current = notebooks;
const chaptersRef = useRef(chapters);
chaptersRef.current = chapters;
const contentsRef = useRef(noteContents);
contentsRef.current = noteContents;
@@ -300,14 +303,25 @@ export function useNoteData(user: User | null, activeNote: string | null): NoteD
const all = notebook.separators || [];
const mine = all.filter((sep) => (sep.chapterId ?? null) === chapterId);
const others = all.filter((sep) => (sep.chapterId ?? null) !== chapterId);
/* Chapters are rows in this list too, and only in the notebook itself — a
chapter has no chapters of its own. */
const inList = chapterId
? []
: chaptersRef.current.filter((c) => c.notebookId === notebookId);
const next = reindexEntries(inNotebook, mine, dragId, targetId);
const next = reindexEntries(inNotebook, mine, dragId, targetId, inList);
if (!next) return;
const merged = [...others, ...next.separators.map((sep) => ({ ...sep, chapterId }))];
const position = new Map(next.noteIds.map((id, i) => [id, i]));
const position = new Map(next.notes.map((n) => [n.id, n.order]));
setNotes((p) => p.map((n) => (position.has(n.id) ? { ...n, sortOrder: position.get(n.id)! } : n)));
void sync.reorderNotes(next.noteIds);
void sync.reorderNotes(next.notes);
if (next.chapters.length) {
const chapterAt = new Map(next.chapters.map((c) => [c.id, c.order]));
setChapters((p) => p.map((c) => (chapterAt.has(c.id) ? { ...c, sortOrder: chapterAt.get(c.id)! } : c)));
void sync.reorderChapters(next.chapters);
}
/* Only written when it actually changed.
*
@@ -353,6 +367,11 @@ export function useNoteData(user: User | null, activeNote: string | null): NoteD
await sync.updateChapter(id, { name });
}, []);
const recolourChapter = useCallback(async (id: string, color: string | null) => {
setChapters((p) => p.map((c) => (c.id === id ? { ...c, color } : c)));
await sync.updateChapter(id, { color });
}, []);
const deleteChapter = useCallback(
async (id: string) => {
await sync.deleteChapter(id);
@@ -416,6 +435,7 @@ export function useNoteData(user: User | null, activeNote: string | null): NoteD
chapters,
createChapter,
renameChapter,
recolourChapter,
deleteChapter,
deleteNotebook,
reorderNotebooks,