Write pinned notes in the margin, and give the phone a sheet instead
A note about a passage belongs beside the passage, not on top of it. Only a numbered marker stays on the page now, with a hairline out to a card in the margin, level with its anchor. Cards keep one height so two notes close together can be laid out by pushing the second down; the one being written in grows and rises above the rest. A phone has no margin to write in — a page narrow enough to read leaves no white space beside it, and taking a column would make the document unreadable. There the note comes up from the bottom instead: over the page, only while open, within reach of a thumb, with the passage still visible above it. Reserving the column exposed a loop worth remembering. A flex item is as wide as its content unless told otherwise, so the column made the viewer wider, which made there be room for the column: the page ended up at 254px inside a 420px phone while the layout insisted it had 611px. The viewer and the editor pane now say minWidth: 0, and the toolbar scrolls rather than cutting its last controls off on a narrow screen. Splitting each row into page and margin also broke which pages counted as visible — the check read data-pdf-page off the row, found nothing, and cancelled every text layer before it rendered. It reads the row's own attribute now. 215 unit tests, 219 browser checks across eleven suites, including the phone layout at 420px. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6438648313
commit
011ba82a94
7 files changed
+653
-208
No files matched your search
@@ -0,0 +1,131 @@
|
||||
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 }}>Notiz zu dieser Stelle</span>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
data-pin-remove
|
||||
style={{
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
color: T.danger,
|
||||
cursor: "pointer",
|
||||
fontSize: 11.5,
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
Entfernen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
ref={areaRef}
|
||||
autoFocus
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
aria-label="Pinned note"
|
||||
placeholder="Notiz zu dieser Stelle…"
|
||||
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",
|
||||
}}
|
||||
>
|
||||
Fertig
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user