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
@@ -7,12 +7,15 @@ import { extractAndCache, indexPdfText } from "./pdfText";
|
||||
import "./textLayer.css";
|
||||
import { PdfDrawLayer } from "./PdfDrawLayer.jsx";
|
||||
import { PdfPins } from "./PdfPins.jsx";
|
||||
import { MARGIN_W, PdfMargin } from "./PdfMargin.jsx";
|
||||
import { PdfNoteSheet } from "./PdfNoteSheet.jsx";
|
||||
import {
|
||||
allPinnedNotes,
|
||||
asPdfStrokeDoc,
|
||||
hasOverlay,
|
||||
notesForPage,
|
||||
strokesForPage,
|
||||
totalPinnedNotes,
|
||||
totalStrokes,
|
||||
withPage,
|
||||
withPageNotes,
|
||||
@@ -68,48 +71,68 @@ function PdfPage({ doc, index, width, height, size, visible, ink, pins, selectab
|
||||
}, [doc, index, width, visible]);
|
||||
|
||||
return (
|
||||
/* The page and its margin are one row, so a note card can be placed at the
|
||||
height of its anchor without knowing where the page sits in the scroll. */
|
||||
<div
|
||||
data-pdf-page={index + 1}
|
||||
style={{
|
||||
position: "relative",
|
||||
width,
|
||||
height,
|
||||
margin: "0 auto 16px",
|
||||
background: "#fff",
|
||||
boxShadow: "0 2px 10px rgba(0,0,0,0.25)",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
data-pdf-row={index + 1}
|
||||
style={{ display: "flex", justifyContent: "center", gap: 12, marginBottom: 16 }}
|
||||
>
|
||||
<canvas ref={canvasRef} style={{ display: "block", width: "100%" }} />
|
||||
<div ref={textRef} className="pdf-text-layer" data-selectable={selectable ? "true" : "false"} />
|
||||
<PdfDrawLayer
|
||||
pageNo={index + 1}
|
||||
pageSize={size}
|
||||
cssWidth={width}
|
||||
cssHeight={height}
|
||||
strokes={ink.strokes}
|
||||
drawing={ink.drawing}
|
||||
tool={ink.tool}
|
||||
color={ink.color}
|
||||
sizeIdx={ink.sizeIdx}
|
||||
hidden={ink.hidden}
|
||||
onCommit={ink.onCommit}
|
||||
/>
|
||||
{!pins.hidden && (
|
||||
<PdfPins
|
||||
T={pins.T}
|
||||
<div
|
||||
data-pdf-page={index + 1}
|
||||
style={{
|
||||
position: "relative",
|
||||
width,
|
||||
height,
|
||||
flexShrink: 0,
|
||||
background: "#fff",
|
||||
boxShadow: "0 2px 10px rgba(0,0,0,0.25)",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<canvas ref={canvasRef} style={{ display: "block", width: "100%" }} />
|
||||
<div ref={textRef} className="pdf-text-layer" data-selectable={selectable ? "true" : "false"} />
|
||||
<PdfDrawLayer
|
||||
pageNo={index + 1}
|
||||
pageSize={size}
|
||||
cssWidth={width}
|
||||
cssHeight={height}
|
||||
strokes={ink.strokes}
|
||||
drawing={ink.drawing}
|
||||
tool={ink.tool}
|
||||
color={ink.color}
|
||||
sizeIdx={ink.sizeIdx}
|
||||
hidden={ink.hidden}
|
||||
onCommit={ink.onCommit}
|
||||
/>
|
||||
{!pins.hidden && (
|
||||
<PdfPins
|
||||
T={pins.T}
|
||||
pageNo={index + 1}
|
||||
pageSize={size}
|
||||
cssWidth={width}
|
||||
cssHeight={height}
|
||||
notes={pins.notes}
|
||||
placing={pins.placing}
|
||||
openId={pins.openId}
|
||||
withMargin={pins.withMargin}
|
||||
onOpen={pins.onOpen}
|
||||
onPlace={(at) => pins.onPlace(index + 1, size, at)}
|
||||
onMove={(id, x, y) => pins.onMove(index + 1, size, id, x, y)}
|
||||
numberOf={pins.numberOf}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{pins.withMargin && !pins.hidden && (
|
||||
<PdfMargin
|
||||
T={pins.T}
|
||||
notes={pins.notes}
|
||||
placing={pins.placing}
|
||||
pageHeight={size.height}
|
||||
cssHeight={height}
|
||||
openId={pins.openId}
|
||||
onOpen={pins.onOpen}
|
||||
onPlace={(at) => pins.onPlace(index + 1, size, at)}
|
||||
onChange={(id, text) => pins.onChange(index + 1, size, id, text)}
|
||||
onMove={(id, x, y) => pins.onMove(index + 1, size, id, x, y)}
|
||||
onRemove={(id) => pins.onRemove(index + 1, size, id)}
|
||||
numberOf={pins.numberOf}
|
||||
/>
|
||||
@@ -343,7 +366,30 @@ export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle }) {
|
||||
// The widest page decides the scale, so a document mixing portrait and
|
||||
// landscape does not make the narrow pages jump about as you scroll.
|
||||
const widestPt = sizes.reduce((w, s) => Math.max(w, s.width), 0) || 1;
|
||||
const fitWidth = Math.max(240, Math.min(containerWidth - 32, 1100));
|
||||
|
||||
/* Whether there is room to write beside the page.
|
||||
*
|
||||
* On a phone there is not: a page narrow enough to read leaves no white space
|
||||
* next to it, and taking 200px for a column would make the document itself
|
||||
* unreadable. There the notes come up from the bottom instead.
|
||||
*
|
||||
* The column is kept as soon as the document has any pinned note, or while one
|
||||
* is being placed — so it is already there before the first note exists and
|
||||
* the page does not jump sideways at the moment of writing it. */
|
||||
const roomForMargin = containerWidth >= MARGIN_W + 380;
|
||||
const withMargin = roomForMargin && (placing || totalPinnedNotes(ink) > 0);
|
||||
|
||||
/* The open note, with the page it belongs to — the sheet needs the page size
|
||||
to write a change back into the right page's overlay. */
|
||||
const openSheet = (() => {
|
||||
if (!openPin) return null;
|
||||
const found = allPinnedNotes(ink).find((n) => n.id === openPin);
|
||||
const size = found ? sizes[found.page - 1] : null;
|
||||
return found && size ? { note: found, page: found.page, size } : null;
|
||||
})();
|
||||
|
||||
const reserved = withMargin ? MARGIN_W + 12 : 0;
|
||||
const fitWidth = Math.max(240, Math.min(containerWidth - 32 - reserved, 1100));
|
||||
const pageWidth = Math.round(fitWidth * zoom);
|
||||
const scale = pageWidth / widestPt;
|
||||
|
||||
@@ -359,7 +405,10 @@ export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle }) {
|
||||
let current = 1;
|
||||
|
||||
for (const child of inner.children) {
|
||||
const n = Number(child.dataset.pdfPage);
|
||||
// The row, not the page: the page sits inside it next to its margin, and
|
||||
// reading `dataset.pdfPage` off the row found nothing — so every page was
|
||||
// "out of view", and the text layers were cancelled before they rendered.
|
||||
const n = Number(child.dataset.pdfRow);
|
||||
if (!n) continue;
|
||||
const start = child.offsetTop;
|
||||
const end = start + child.offsetHeight;
|
||||
@@ -428,7 +477,21 @@ export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", minHeight: 0, background: T.bg }}>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: 0,
|
||||
/* Without this a flex item is at least as wide as its content, so the
|
||||
margin column made the viewer wider, which made there be room for the
|
||||
margin column — the page shrank to 254px inside a 420px phone while
|
||||
the layout insisted it was 611px wide. */
|
||||
minWidth: 0,
|
||||
background: T.bg,
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@@ -441,6 +504,10 @@ export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle }) {
|
||||
fontSize: 12,
|
||||
color: T.muted,
|
||||
flexShrink: 0,
|
||||
/* On a phone the controls are wider than the screen. Scrolling the bar
|
||||
keeps every one of them reachable; without it the ones at the end
|
||||
were simply cut off. */
|
||||
overflowX: "auto",
|
||||
}}
|
||||
>
|
||||
<button onClick={() => goToPage(Math.max(1, pageNo - 1))} title="Vorherige Seite" style={barButton}>
|
||||
@@ -629,7 +696,7 @@ export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref={scrollRef} style={{ flex: 1, overflow: "auto", padding: 16, minHeight: 0 }}>
|
||||
<div ref={scrollRef} style={{ flex: 1, overflow: "auto", padding: 16, minHeight: 0, minWidth: 0 }}>
|
||||
{state.status === "loading" && (
|
||||
<div
|
||||
style={{ color: T.muted, fontSize: 13, fontFamily: FONT_UI, textAlign: "center", padding: 40 }}
|
||||
@@ -667,6 +734,7 @@ export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle }) {
|
||||
placing,
|
||||
openId: openPin,
|
||||
hidden: hideInk,
|
||||
withMargin,
|
||||
onOpen: setOpenPin,
|
||||
onPlace: placeNote,
|
||||
onChange: changeNote,
|
||||
@@ -679,6 +747,19 @@ export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle }) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* No margin to write in — the note comes up from the bottom instead, over
|
||||
the page but within reach of a thumb, and only while it is open. */}
|
||||
{!withMargin && openSheet && (
|
||||
<PdfNoteSheet
|
||||
T={T}
|
||||
note={openSheet.note}
|
||||
number={pinNumbers.get(openSheet.note.id) ?? ""}
|
||||
onChange={(text) => changeNote(openSheet.page, openSheet.size, openSheet.note.id, text)}
|
||||
onRemove={() => removeNote(openSheet.page, openSheet.size, openSheet.note.id)}
|
||||
onClose={() => setOpenPin(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user