Put the interface into English, and give the notebook one way in
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>
This commit is contained in:
1 parent
0a7cf26b28
commit
52bd2b8f3d
27 files changed
+469
-336
No files matched your search
@@ -44,17 +44,20 @@ 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.
|
||||
* Both renaming and removing are right-click, which is how everything else in
|
||||
* this list is acted on. A left-click on the label started renaming, so the row
|
||||
* could not be clicked — to select it, to drag from it — without falling into an
|
||||
* edit; and the × for removal sat a few pixels from that same target.
|
||||
*/
|
||||
function SeparatorRow({ T, separator, onRename, onContextMenu }) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
function SeparatorRow({ T, separator, editing, onStartRename, onRename, onContextMenu }) {
|
||||
const [draft, setDraft] = useState(separator.label);
|
||||
|
||||
useEffect(() => {
|
||||
if (editing) setDraft(separator.label);
|
||||
}, [editing, separator.label]);
|
||||
|
||||
const commit = () => {
|
||||
setEditing(false);
|
||||
onStartRename(null);
|
||||
if (draft !== separator.label) onRename(separator.id, draft);
|
||||
};
|
||||
|
||||
@@ -86,7 +89,7 @@ function SeparatorRow({ T, separator, onRename, onContextMenu }) {
|
||||
if (e.key === "Enter") commit();
|
||||
if (e.key === "Escape") {
|
||||
setDraft(separator.label);
|
||||
setEditing(false);
|
||||
onStartRename(null);
|
||||
}
|
||||
}}
|
||||
aria-label="Separator label"
|
||||
@@ -109,12 +112,8 @@ function SeparatorRow({ T, separator, onRename, onContextMenu }) {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
setDraft(separator.label);
|
||||
setEditing(true);
|
||||
}}
|
||||
title="Umbenennen"
|
||||
<div
|
||||
title="Right-click for options"
|
||||
style={{
|
||||
width: "100%",
|
||||
border: "1px solid transparent",
|
||||
@@ -125,15 +124,14 @@ function SeparatorRow({ T, separator, onRename, onContextMenu }) {
|
||||
letterSpacing: 0.6,
|
||||
textTransform: "uppercase",
|
||||
textAlign: "center",
|
||||
cursor: "text",
|
||||
padding: "1px 4px",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{separator.label || "Benennen"}
|
||||
</button>
|
||||
{separator.label || "Unnamed"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ height: 1, background: T.border, flex: 1, minWidth: 8 }} />
|
||||
@@ -221,6 +219,100 @@ function SortMenu({ T, mode, modes, onChange, disabled }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* One way into the notebook.
|
||||
*
|
||||
* Making a note, importing a PDF and importing Markdown are the same act —
|
||||
* putting something new in here — and they had grown into three buttons side by
|
||||
* side, two of which were icons you had to hover to identify.
|
||||
*/
|
||||
function AddMenu({ T, onCreateNote, onImportPdf, onImportMarkdown }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const box = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const close = (e) => {
|
||||
if (!box.current?.contains(e.target)) setOpen(false);
|
||||
};
|
||||
document.addEventListener("pointerdown", close);
|
||||
return () => document.removeEventListener("pointerdown", close);
|
||||
}, [open]);
|
||||
|
||||
const item = (label, icon, action) => (
|
||||
<button
|
||||
data-add-option={label}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
action();
|
||||
}}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 9,
|
||||
width: "100%",
|
||||
padding: "8px 10px",
|
||||
borderRadius: 8,
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
color: T.text,
|
||||
cursor: "pointer",
|
||||
fontSize: 12.5,
|
||||
textAlign: "left",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
<span style={{ display: "flex", color: T.muted }}>{icon}</span>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative" }} ref={box}>
|
||||
<button
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
title="New Note"
|
||||
data-add-menu={open ? "on" : "off"}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 10,
|
||||
border: "none",
|
||||
background: T.accent,
|
||||
cursor: "pointer",
|
||||
color: "#fff",
|
||||
boxShadow: `0 2px 8px ${T.accent}44`,
|
||||
}}
|
||||
>
|
||||
{I.plus}
|
||||
</button>
|
||||
{open && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 38,
|
||||
right: 0,
|
||||
minWidth: 190,
|
||||
background: T.card,
|
||||
border: `1px solid ${T.border}`,
|
||||
borderRadius: 12,
|
||||
padding: 6,
|
||||
zIndex: 300,
|
||||
boxShadow: "0 10px 26px rgba(0,0,0,.28)",
|
||||
}}
|
||||
>
|
||||
{item("New note", I.plus, onCreateNote)}
|
||||
{item("Upload PDF", I.pdf, onImportPdf)}
|
||||
{item("Import Markdown", I.import, onImportMarkdown)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** The middle column: every note in the selected folder, in the chosen order. */
|
||||
export function NoteList({
|
||||
T,
|
||||
@@ -234,9 +326,13 @@ export function NoteList({
|
||||
sortMode,
|
||||
sortModes,
|
||||
onSortModeChange,
|
||||
query,
|
||||
onQueryChange,
|
||||
reorderable,
|
||||
separators,
|
||||
onAddSeparator,
|
||||
renamingSeparator,
|
||||
onStartRenameSeparator,
|
||||
onRenameSeparator,
|
||||
onSeparatorContextMenu,
|
||||
dnd,
|
||||
@@ -248,6 +344,10 @@ export function NoteList({
|
||||
onImportMarkdown,
|
||||
onEmptyTrash,
|
||||
}) {
|
||||
// Opened by the button and kept open while there is something typed in it, so
|
||||
// a search never disappears because the box collapsed under it.
|
||||
const [searching, setSearching] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -307,39 +407,22 @@ export function NoteList({
|
||||
{!isTrashView && <SortMenu T={T} mode={sortMode} modes={sortModes} onChange={onSortModeChange} />}
|
||||
{!isTrashView && (
|
||||
<button
|
||||
onClick={onImportMarkdown}
|
||||
title="Notizen importieren (Markdown oder ZIP)"
|
||||
data-import-button
|
||||
style={iconButton(T)}
|
||||
onClick={() => setSearching((v) => !v)}
|
||||
data-search-toggle={searching || query ? "on" : "off"}
|
||||
title="Search this notebook"
|
||||
aria-label="Search notes"
|
||||
style={{ ...iconButton(T), color: searching || query ? T.accent : T.muted }}
|
||||
>
|
||||
{I.import}
|
||||
{I.search}
|
||||
</button>
|
||||
)}
|
||||
{!isTrashView && (
|
||||
<button onClick={onImportPdf} title="Import PDF" style={iconButton(T)}>
|
||||
{I.pdf}
|
||||
</button>
|
||||
)}
|
||||
{!isTrashView && (
|
||||
<button
|
||||
onClick={onCreateNote}
|
||||
title="New Note"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 10,
|
||||
border: "none",
|
||||
background: T.accent,
|
||||
cursor: "pointer",
|
||||
color: "#fff",
|
||||
boxShadow: `0 2px 8px ${T.accent}44`,
|
||||
}}
|
||||
>
|
||||
{I.plus}
|
||||
</button>
|
||||
<AddMenu
|
||||
T={T}
|
||||
onCreateNote={onCreateNote}
|
||||
onImportPdf={onImportPdf}
|
||||
onImportMarkdown={onImportMarkdown}
|
||||
/>
|
||||
)}
|
||||
{isTrashView && trashedCount > 0 && (
|
||||
<button
|
||||
@@ -365,44 +448,65 @@ export function NoteList({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Said once, where it matters: the arrangement is being edited, and the
|
||||
search box would otherwise silently take that ability away. */}
|
||||
{sortMode === "manual" && !isTrashView && (
|
||||
{/* The search box lives with the list it narrows, not in the sidebar.
|
||||
Searching has always been scoped to the notebook you are in; from the
|
||||
sidebar it read as searching everything, which it never did. */}
|
||||
{(searching || query) && !isTrashView && (
|
||||
<div style={{ padding: "8px 12px", borderBottom: `1px solid ${T.borderLight}` }}>
|
||||
<input
|
||||
autoFocus
|
||||
value={query}
|
||||
onChange={(e) => onQueryChange(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
onQueryChange("");
|
||||
setSearching(false);
|
||||
}
|
||||
}}
|
||||
placeholder={`Search in ${title}...`}
|
||||
aria-label="Search in this notebook"
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "7px 10px",
|
||||
borderRadius: 9,
|
||||
border: `1px solid ${T.border}`,
|
||||
background: T.inputBg,
|
||||
color: T.text,
|
||||
fontSize: 12.5,
|
||||
outline: "none",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Just the one control. The sentence that used to sit here explained the
|
||||
order the sort menu already names, on every visit, forever. */}
|
||||
{reorderable && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
padding: "5px 10px 5px 16px",
|
||||
fontSize: 10.5,
|
||||
color: T.faint,
|
||||
justifyContent: "flex-end",
|
||||
padding: "5px 10px",
|
||||
borderBottom: `1px solid ${T.borderLight}`,
|
||||
}}
|
||||
>
|
||||
<span style={{ flex: 1 }}>
|
||||
{reorderable
|
||||
? "Eigene Reihenfolge — zum Umsortieren ziehen"
|
||||
: "Eigene Reihenfolge — Suche aktiv, Ziehen pausiert"}
|
||||
</span>
|
||||
{reorderable && (
|
||||
<button
|
||||
onClick={onAddSeparator}
|
||||
data-add-separator
|
||||
title="Trennlinie einfügen"
|
||||
style={{
|
||||
border: `1px solid ${T.border}`,
|
||||
background: "transparent",
|
||||
color: T.muted,
|
||||
borderRadius: 8,
|
||||
padding: "2px 8px",
|
||||
fontSize: 10.5,
|
||||
cursor: "pointer",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
+ Trenner
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onAddSeparator}
|
||||
data-add-separator
|
||||
title="Insert a separator line"
|
||||
style={{
|
||||
border: `1px solid ${T.border}`,
|
||||
background: "transparent",
|
||||
color: T.muted,
|
||||
borderRadius: 8,
|
||||
padding: "2px 8px",
|
||||
fontSize: 10.5,
|
||||
cursor: "pointer",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
+ Separator
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -477,6 +581,8 @@ export function NoteList({
|
||||
<SeparatorRow
|
||||
T={T}
|
||||
separator={entry.separator}
|
||||
editing={renamingSeparator === entry.id}
|
||||
onStartRename={onStartRenameSeparator}
|
||||
onRename={onRenameSeparator}
|
||||
onContextMenu={onSeparatorContextMenu}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user