Group the two visibility switches, and put the drawing tools back where they belong
"Zeichnung" and "Randnotizen" are two halves of one question — how much of your own work do you want to see right now — and they sat at opposite ends of the toolbar, each beside the tool that made them. They are now one pair in one shape at the right-hand end. Each still appears only once there is something of its kind to hide, so the pair can be lopsided, which is honest. The drawing tools had drifted past the pin controls as those were added, so switching to Zeichnen made them appear somewhere other than where the eye already was. They follow the button that turns them on again. A separator no longer carries a ×. It sat a few pixels from the label you click to rename, putting the gesture that tidies the list next to the one that destroys part of it — so removal is a right-click behind a confirmation that says the notes are not affected, which is how everything else in that list is acted on. 218 unit tests, 237 browser checks across eleven suites. The toolbar checks compare measured positions rather than presence, since the complaint was about where things were, not whether they existed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4c1b632029
commit
0a7cf26b28
6 files changed
+208
-104
No files matched your search
+26
-1
@@ -202,6 +202,31 @@ export default function App() {
|
||||
writeSeparators(separators.map((s) => (s.id === id ? { ...s, label } : s)));
|
||||
const removeSeparator = (id) => writeSeparators(separators.filter((s) => s.id !== id));
|
||||
|
||||
const separatorContextMenu = (e, separator) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setCtxMenu({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
items: [
|
||||
{
|
||||
label: "Trenner entfernen",
|
||||
icon: I.trash,
|
||||
danger: true,
|
||||
action: () =>
|
||||
confirm({
|
||||
title: "Trenner entfernen",
|
||||
message: separator.label
|
||||
? `Trennlinie "${separator.label}" entfernen? Die Notizen bleiben.`
|
||||
: "Diese Trennlinie entfernen? Die Notizen bleiben.",
|
||||
confirmLabel: "Entfernen",
|
||||
onConfirm: () => removeSeparator(separator.id),
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const isSearching = searchQuery.trim().length > 0;
|
||||
|
||||
const filteredNotes = useMemo(() => {
|
||||
@@ -614,7 +639,7 @@ export default function App() {
|
||||
separators={separators}
|
||||
onAddSeparator={addSeparator}
|
||||
onRenameSeparator={renameSeparator}
|
||||
onRemoveSeparator={removeSeparator}
|
||||
onSeparatorContextMenu={separatorContextMenu}
|
||||
dnd={noteDnd}
|
||||
onToggleSidebar={() => setSidebarOpen(!sidebarOpen)}
|
||||
onSelectNote={openNote}
|
||||
|
||||
@@ -43,8 +43,13 @@ const MIDDLE_W = 132;
|
||||
*
|
||||
* Draggable like a note, because a divider that cannot move with what it
|
||||
* divides is worse than none.
|
||||
*
|
||||
* Removed by right-click rather than by a × on the row. The × sat a few pixels
|
||||
* from the label you click to rename, so the gesture that tidies the list and
|
||||
* the gesture that destroys part of it were neighbours — and right-click is
|
||||
* already how everything else in this list is acted on.
|
||||
*/
|
||||
function SeparatorRow({ T, separator, onRename, onRemove }) {
|
||||
function SeparatorRow({ T, separator, onRename, onContextMenu }) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [draft, setDraft] = useState(separator.label);
|
||||
|
||||
@@ -56,6 +61,7 @@ function SeparatorRow({ T, separator, onRename, onRemove }) {
|
||||
return (
|
||||
<div
|
||||
data-separator={separator.id}
|
||||
onContextMenu={(e) => onContextMenu(e, separator)}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
@@ -131,23 +137,6 @@ function SeparatorRow({ T, separator, onRename, onRemove }) {
|
||||
)}
|
||||
</div>
|
||||
<div style={{ height: 1, background: T.border, flex: 1, minWidth: 8 }} />
|
||||
<button
|
||||
onClick={() => onRemove(separator.id)}
|
||||
title="Trenner entfernen"
|
||||
aria-label="Remove separator"
|
||||
style={{
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
color: T.faint,
|
||||
cursor: "pointer",
|
||||
fontSize: 13,
|
||||
lineHeight: 1,
|
||||
padding: "0 2px",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -249,7 +238,7 @@ export function NoteList({
|
||||
separators,
|
||||
onAddSeparator,
|
||||
onRenameSeparator,
|
||||
onRemoveSeparator,
|
||||
onSeparatorContextMenu,
|
||||
dnd,
|
||||
onToggleSidebar,
|
||||
onSelectNote,
|
||||
@@ -489,7 +478,7 @@ export function NoteList({
|
||||
T={T}
|
||||
separator={entry.separator}
|
||||
onRename={onRenameSeparator}
|
||||
onRemove={onRemoveSeparator}
|
||||
onContextMenu={onSeparatorContextMenu}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -25,6 +25,29 @@ import { I } from "../../ui/icons.jsx";
|
||||
import { PEN_SIZES, DEFAULT_PEN_SIZE, newSaveToken } from "../drawing/strokes.js";
|
||||
import { FONT_UI } from "../../ui/theme";
|
||||
|
||||
/**
|
||||
* The pair of switches for what is drawn on top of the document.
|
||||
*
|
||||
* One shape for both, because they are two halves of one question — how much of
|
||||
* your own work do you want to see right now — and a pair that looked different
|
||||
* would not read as a pair.
|
||||
*/
|
||||
const visibilityButton = (T, shown) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 6,
|
||||
height: 28,
|
||||
padding: "0 10px",
|
||||
borderRadius: 8,
|
||||
cursor: "pointer",
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
border: `1px solid ${shown ? T.border : T.accent + "66"}`,
|
||||
background: shown ? "transparent" : T.accent + "1a",
|
||||
color: shown ? T.muted : T.accent,
|
||||
});
|
||||
|
||||
/** Pages this far outside the viewport are drawn ahead of being scrolled to. */
|
||||
const RENDER_MARGIN_PX = 1200;
|
||||
const ZOOM_STEPS = [0.5, 0.75, 1, 1.25, 1.5, 2, 3];
|
||||
@@ -589,53 +612,6 @@ export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle, foc
|
||||
{I.pen} {drawing ? "Zeichnen" : "Lesen"}
|
||||
</button>
|
||||
|
||||
{/* Reading the document as it was printed is a normal thing to want, and
|
||||
so is having every remark in view. Both are one click away. */}
|
||||
{totalPinnedNotes(ink) > 0 && (
|
||||
<button
|
||||
data-pdf-show-pins={showPins ? "on" : "off"}
|
||||
onClick={() => {
|
||||
setShowPins((v) => !v);
|
||||
setOpenPin(null);
|
||||
}}
|
||||
title={showPins ? "Randnotizen ausblenden" : "Randnotizen einblenden"}
|
||||
style={{
|
||||
...barButton,
|
||||
width: "auto",
|
||||
padding: "0 10px",
|
||||
gap: 6,
|
||||
border: `1px solid ${showPins ? T.border : T.accent + "66"}`,
|
||||
background: showPins ? "transparent" : T.accent + "1a",
|
||||
color: showPins ? T.muted : T.accent,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{showPins ? I.eye : I.eyeOff} Randnotizen
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* A note about a passage belongs on the passage. */}
|
||||
<button
|
||||
data-pdf-pin-mode={placing ? "on" : "off"}
|
||||
onClick={() => {
|
||||
setPlacing((v) => !v);
|
||||
setDrawing(false);
|
||||
setOpenPin(null);
|
||||
}}
|
||||
title={placing ? "Stelle anklicken — oder abbrechen" : "Notiz an eine Stelle heften"}
|
||||
style={{
|
||||
...barButton,
|
||||
width: "auto",
|
||||
padding: "0 10px",
|
||||
gap: 6,
|
||||
background: placing ? T.accent + "22" : "transparent",
|
||||
color: placing ? T.accent : T.muted,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{I.pin} {placing ? "Stelle wählen" : "Randnotiz"}
|
||||
</button>
|
||||
|
||||
{drawing && (
|
||||
<>
|
||||
{["pen", "highlighter", "eraser"].map((t) => (
|
||||
@@ -723,32 +699,27 @@ export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle, foc
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* A labelled control, not a dot. Hiding the ink changes what the page
|
||||
shows, so it has to be findable without being clicked to find out —
|
||||
and its state has to be readable at a glance rather than from the
|
||||
difference between a filled and a hollow circle.
|
||||
Only shown once there is a drawing: until then it would offer to hide
|
||||
nothing. */}
|
||||
{totalStrokes(ink) > 0 && (
|
||||
<button
|
||||
data-pdf-hide-ink={hideInk ? "on" : "off"}
|
||||
onClick={() => setHideInk((v) => !v)}
|
||||
title={hideInk ? "Zeichnung wieder einblenden" : "Zeichnung ausblenden"}
|
||||
style={{
|
||||
...barButton,
|
||||
width: "auto",
|
||||
padding: "0 10px",
|
||||
gap: 6,
|
||||
border: `1px solid ${hideInk ? T.accent + "66" : T.border}`,
|
||||
background: hideInk ? T.accent + "1a" : "transparent",
|
||||
color: hideInk ? T.accent : T.muted,
|
||||
fontWeight: 600,
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{hideInk ? I.eyeOff : I.eye} {hideInk ? "Zeichnung aus" : "Zeichnung an"}
|
||||
</button>
|
||||
)}
|
||||
{/* A note about a passage belongs on the passage. */}
|
||||
<button
|
||||
data-pdf-pin-mode={placing ? "on" : "off"}
|
||||
onClick={() => {
|
||||
setPlacing((v) => !v);
|
||||
setDrawing(false);
|
||||
setOpenPin(null);
|
||||
}}
|
||||
title={placing ? "Stelle anklicken — oder abbrechen" : "Notiz an eine Stelle heften"}
|
||||
style={{
|
||||
...barButton,
|
||||
width: "auto",
|
||||
padding: "0 10px",
|
||||
gap: 6,
|
||||
background: placing ? T.accent + "22" : "transparent",
|
||||
color: placing ? T.accent : T.muted,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{I.pin} {placing ? "Stelle wählen" : "Randnotiz"}
|
||||
</button>
|
||||
|
||||
{notesToggle}
|
||||
|
||||
@@ -760,6 +731,39 @@ export function PdfViewer({ note, dark, onUpdate, onPagesReady, notesToggle, foc
|
||||
<button onClick={() => stepZoom(1)} title="Größer" style={barButton}>
|
||||
+
|
||||
</button>
|
||||
|
||||
{/* What is shown on top of the document, kept together at the end of
|
||||
the bar. Drawings and margin notes are two halves of one question —
|
||||
"how much of my own work do I want to see right now" — and they used
|
||||
to sit at opposite ends of the toolbar, each next to the tool that
|
||||
made them. Each still appears only once there is something of that
|
||||
kind to hide. */}
|
||||
{(totalStrokes(ink) > 0 || totalPinnedNotes(ink) > 0) && (
|
||||
<div style={{ width: 1, height: 18, background: T.border, margin: "0 2px" }} />
|
||||
)}
|
||||
{totalStrokes(ink) > 0 && (
|
||||
<button
|
||||
data-pdf-hide-ink={hideInk ? "on" : "off"}
|
||||
onClick={() => setHideInk((v) => !v)}
|
||||
title={hideInk ? "Zeichnung einblenden" : "Zeichnung ausblenden"}
|
||||
style={visibilityButton(T, !hideInk)}
|
||||
>
|
||||
{hideInk ? I.eyeOff : I.eye} Zeichnung
|
||||
</button>
|
||||
)}
|
||||
{totalPinnedNotes(ink) > 0 && (
|
||||
<button
|
||||
data-pdf-show-pins={showPins ? "on" : "off"}
|
||||
onClick={() => {
|
||||
setShowPins((v) => !v);
|
||||
setOpenPin(null);
|
||||
}}
|
||||
title={showPins ? "Randnotizen ausblenden" : "Randnotizen einblenden"}
|
||||
style={visibilityButton(T, showPins)}
|
||||
>
|
||||
{showPins ? I.eye : I.eyeOff} Randnotizen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user