The interface had drifted into two languages — the frame in English, everything added later in German — which is worse than either, since the same idea appeared under two names depending on when that part was written. Sorting by title still uses the German collation, so Übung 2 comes before Übung 10: that is about the notes, not the chrome. Two archive folders were renamed with it (bilder/ → images/, zeichnungen/ → drawings/). The importer reads both spellings, and both the old and new marker line for a drawing — archives exported before this are on disk, and an import that quietly dropped their drawings would be the worst kind of tidy-up. Making a note, uploading a PDF and importing Markdown were three buttons side by side, two of them icons you had to hover to identify. They are the same act — putting something new in this notebook — so they are now one + menu. The search box moves out of the sidebar and into the note list, labelled with the notebook it narrows. Nothing changed about what it does: it has always searched only the notebook you are in, but from the sidebar it read as searching everything. A separator no longer carries a sentence explaining the order the sort menu already names. Renaming joins removing on the right-click menu — a left-click on the label started an edit, so the row could not be touched without falling into one. 218 unit tests, 238 browser checks across eleven suites. Two of those caught real breakage from this change rather than needing a translation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
132 lines
3.5 KiB
React
132 lines
3.5 KiB
React
import { useEffect, useRef, useState } from "react";
|
|
|
|
/**
|
|
* A pinned note on a phone.
|
|
*
|
|
* There is no margin to write in: a page narrow enough to read on a phone leaves
|
|
* no white space beside it, and stealing 200px for a column would make the
|
|
* document itself unreadable. So the note comes up from the bottom instead —
|
|
* over the page rather than beside it, but only while it is being read or
|
|
* written, and within reach of a thumb rather than at the top of the screen.
|
|
*
|
|
* The page stays visible above it, so the passage the note is about is still
|
|
* there while writing about it.
|
|
*/
|
|
export function PdfNoteSheet({ T, note, number, onChange, onClose, onRemove }) {
|
|
const [draft, setDraft] = useState(note.text);
|
|
const areaRef = useRef(null);
|
|
|
|
useEffect(() => setDraft(note.text), [note.id]);
|
|
|
|
const commit = () => {
|
|
if (draft !== note.text) onChange(draft);
|
|
};
|
|
const close = () => {
|
|
commit();
|
|
onClose();
|
|
};
|
|
|
|
useEffect(() => {
|
|
const onKey = (e) => {
|
|
if (e.key === "Escape") close();
|
|
};
|
|
window.addEventListener("keydown", onKey);
|
|
return () => window.removeEventListener("keydown", onKey);
|
|
});
|
|
|
|
return (
|
|
<div
|
|
data-pin-sheet={note.id}
|
|
style={{
|
|
position: "absolute",
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
zIndex: 60,
|
|
background: T.bar,
|
|
borderTop: `1px solid ${T.border}`,
|
|
borderRadius: "14px 14px 0 0",
|
|
boxShadow: "0 -8px 26px rgba(0,0,0,.3)",
|
|
padding: "10px 12px calc(10px + env(safe-area-inset-bottom))",
|
|
}}
|
|
>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }}>
|
|
<span
|
|
style={{
|
|
width: 20,
|
|
height: 20,
|
|
borderRadius: "50%",
|
|
background: T.accent,
|
|
color: "#fff",
|
|
fontSize: 11,
|
|
fontWeight: 700,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{number}
|
|
</span>
|
|
<span style={{ fontSize: 11.5, color: T.muted, flex: 1 }}>Note on this passage</span>
|
|
<button
|
|
onClick={onRemove}
|
|
data-pin-remove
|
|
style={{
|
|
border: "none",
|
|
background: "transparent",
|
|
color: T.danger,
|
|
cursor: "pointer",
|
|
fontSize: 11.5,
|
|
padding: 0,
|
|
}}
|
|
>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
|
|
<textarea
|
|
ref={areaRef}
|
|
autoFocus
|
|
value={draft}
|
|
onChange={(e) => setDraft(e.target.value)}
|
|
onKeyDown={(e) => e.stopPropagation()}
|
|
aria-label="Pinned note"
|
|
placeholder="Note on this passage…"
|
|
rows={3}
|
|
style={{
|
|
width: "100%",
|
|
resize: "none",
|
|
border: `1px solid ${T.border}`,
|
|
borderRadius: 8,
|
|
background: "transparent",
|
|
color: T.text,
|
|
fontSize: 13,
|
|
lineHeight: 1.5,
|
|
padding: "8px 10px",
|
|
outline: "none",
|
|
fontFamily: "inherit",
|
|
}}
|
|
/>
|
|
|
|
<button
|
|
onClick={close}
|
|
style={{
|
|
width: "100%",
|
|
marginTop: 8,
|
|
border: "none",
|
|
background: T.accent,
|
|
color: "#fff",
|
|
borderRadius: 10,
|
|
cursor: "pointer",
|
|
fontSize: 13,
|
|
fontWeight: 600,
|
|
padding: "9px 0",
|
|
}}
|
|
>
|
|
Done
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|