Reader: Notiz-Eingabe direkt im Rand statt Sprung zur Sidebar (SW v25)
All checks were successful
Build and push Docker image / build (push) Successful in 14s
Test / test (push) Successful in 14s

Notizen setzen/bearbeiten passiert jetzt inline an der Stelle im Notizen-Rand,
wo die Notiz auch angezeigt wird — kein Wechsel mehr zur rechten Sidebar.
Textfeld wird beim Öffnen automatisch fokussiert und ausgewählt. Enter
speichert, Escape verwirft die Änderung.

Stift-Button (Marker-Modus) zieht in die Titelzeile des Notizen-Rands um,
neben "Notes" — vorher musste man dafür ständig zur Hauptleiste zurück, die
im Immersive-Modus eingeklappt ist.

Nebenbei zwei Bugs beim Testen gefunden und behoben: ein Klick auf leere
Randfläche während ein Editor bereits offen war, legte durchs Blur-Event
gleichzeitig eine zweite, ungewollte Notiz an; und Escape im Notizfeld
bubbled zum globalen Escape-Handler durch, der dann den ganzen Reader
schloss (und damit die gerade bearbeiteten Highlights aus dem Speicher warf).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
marwin 2026-08-04 11:23:16 +02:00
parent a2f4be8e1e
commit 3f40a4078a
4 changed files with 75 additions and 6 deletions

View file

@ -1820,6 +1820,7 @@ body.reader-immersive.reader-show-bottom .reader-overlay { bottom: var(--bar-h)
} }
.reader-margin.open .reader-margin-header { display:flex; } .reader-margin.open .reader-margin-header { display:flex; }
.reader-margin-title { font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--muted,#888); } .reader-margin-title { font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--muted,#888); }
.reader-margin-header-actions { display:flex; align-items:center; gap:6px; }
.reader-margin-markers { position:relative; flex:1; overflow-y:auto; overflow-x:hidden; cursor:crosshair; } .reader-margin-markers { position:relative; flex:1; overflow-y:auto; overflow-x:hidden; cursor:crosshair; }
.margin-note { position:absolute; left:8px; right:8px; cursor:pointer; } .margin-note { position:absolute; left:8px; right:8px; cursor:pointer; }
@ -1845,6 +1846,16 @@ body.reader-immersive.reader-show-bottom .reader-overlay { bottom: var(--bar-h)
border-left-color:#e6c229; background:rgba(241,196,15,.13); border-left-color:#e6c229; background:rgba(241,196,15,.13);
} }
/* Inline note editor appears right where the note lives in the margin, so
setting/editing a note never means jumping to a separate panel */
.margin-note-editor { position:absolute; left:8px; right:8px; z-index:1; }
.margin-note-textarea {
width:100%; min-height:52px; font-size:11px; line-height:1.4; padding:5px 7px;
border-radius:3px; border:1px solid var(--accent,#e63946);
background:var(--bg-card,#1a1a1a); color:var(--fg,#eee);
box-shadow:1px 2px 5px rgba(0,0,0,.4); resize:vertical;
}
@media (max-width: 600px) { @media (max-width: 600px) {
.reader-margin.open { width:150px; } .reader-margin.open { width:150px; }
.reader-margin-title { display:none; } .reader-margin-title { display:none; }

View file

@ -5356,6 +5356,7 @@ function toggleAnnotationsMargin() {
function _repositionMarginMarkers() { function _repositionMarginMarkers() {
const markersEl = $('reader-margin-markers'); const markersEl = $('reader-margin-markers');
if (!markersEl) return; if (!markersEl) return;
if (markersEl.querySelector('.margin-note-editor')) return; // don't blow away an active inline edit
markersEl.innerHTML = ''; markersEl.innerHTML = '';
if (!readerAnnotationsMarginOpen || currentPdfDoc) return; if (!readerAnnotationsMarginOpen || currentPdfDoc) return;
@ -5401,7 +5402,7 @@ function handleMarginClick(e) {
const markerBtn = e.target.closest('.margin-note'); const markerBtn = e.target.closest('.margin-note');
if (markerBtn) { if (markerBtn) {
const h = currentHighlights.find(x => x.id === markerBtn.dataset.highlightId); const h = currentHighlights.find(x => x.id === markerBtn.dataset.highlightId);
if (h) showHighlightTooltip(markerBtn, h); if (h) editNoteInlineInMargin(h);
return; return;
} }
@ -5410,6 +5411,9 @@ function handleMarginClick(e) {
if (!readerAnnotationsMarginOpen || currentPdfDoc) return; if (!readerAnnotationsMarginOpen || currentPdfDoc) return;
const markersEl = $('reader-margin-markers'); const markersEl = $('reader-margin-markers');
if (!markersEl || !markersEl.contains(e.target)) return; if (!markersEl || !markersEl.contains(e.target)) return;
// If an editor is already open, this click just blurs/commits it (see its
// own blur handler) — it shouldn't also create a second, unrelated note.
if (markersEl.querySelector('.margin-note-editor')) return;
const contentEl = $('reader-content'); const contentEl = $('reader-content');
if (!contentEl) return; if (!contentEl) return;
@ -5478,7 +5482,59 @@ function createFreeformNote(range) {
renderHighlight(h); renderHighlight(h);
_repositionMarginMarkers(); _repositionMarginMarkers();
debounceSaveHighlights(); debounceSaveHighlights();
openNoteEditor(h); editNoteInlineInMargin(h);
}
// Edit a note's text right where it lives in the margin, instead of jumping
// to the separate right-hand sidebar — the whole point of setting a note in
// the margin is that you shouldn't have to look away from it to write it.
function editNoteInlineInMargin(h) {
const markersEl = $('reader-margin-markers');
if (!markersEl) return;
markersEl.querySelector('.margin-note-editor')?.remove();
const existingCard = markersEl.querySelector(`[data-highlight-id="${h.id}"]`);
const top = existingCard ? (parseFloat(existingCard.style.top) || 0) : 0;
if (existingCard) existingCard.style.display = 'none';
const editor = document.createElement('div');
editor.className = 'margin-note-editor';
editor.style.top = top + 'px';
editor.innerHTML = '<textarea class="margin-note-textarea"></textarea>';
markersEl.appendChild(editor);
const textarea = editor.querySelector('textarea');
textarea.value = h.note || '';
function commit() {
const text = textarea.value.trim();
if (h.note !== text) {
h.note = text;
highlightsDirty = true;
debounceSaveHighlights();
}
editor.remove();
_repositionMarginMarkers();
}
textarea.addEventListener('blur', commit);
textarea.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
e.stopPropagation();
textarea.blur(); // triggers commit()
} else if (e.key === 'Escape') {
// Stop this from bubbling to the document-level Escape handler, which
// would otherwise close the whole reader (it doesn't know we just want
// to cancel this one edit) — see the global keydown listener.
e.stopPropagation();
textarea.removeEventListener('blur', commit); // discard, don't save
editor.remove();
_repositionMarginMarkers();
}
});
textarea.focus();
textarea.select();
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -2,7 +2,7 @@
* diora service worker caches the app shell for offline use. * diora service worker caches the app shell for offline use.
*/ */
const CACHE = 'diora-v24'; const CACHE = 'diora-v25';
const PODCAST_CACHE = 'diora-podcast-v1'; const PODCAST_CACHE = 'diora-podcast-v1';
const SHELL = [ const SHELL = [
'/static/css/app.css', '/static/css/app.css',

View file

@ -343,7 +343,6 @@
<button class="btn-icon" id="reader-bookmark-btn" onclick="addBookmark()" title="Bookmark"></button> <button class="btn-icon" id="reader-bookmark-btn" onclick="addBookmark()" title="Bookmark"></button>
<button class="btn-icon" id="reader-bm-list-btn" onclick="openBookmarksSidebar()" title="Bookmarks"></button> <button class="btn-icon" id="reader-bm-list-btn" onclick="openBookmarksSidebar()" title="Bookmarks"></button>
<button class="btn-icon" id="reader-toc-btn" onclick="openTocSidebar()" title="Table of contents"></button> <button class="btn-icon" id="reader-toc-btn" onclick="openTocSidebar()" title="Table of contents"></button>
<button class="btn-icon" id="reader-marker-btn" onclick="toggleMarkerMode()" title="Mark text"></button>
<button class="btn-icon" id="reader-margin-btn" onclick="toggleAnnotationsMargin()" title="Notes &amp; highlights"></button> <button class="btn-icon" id="reader-margin-btn" onclick="toggleAnnotationsMargin()" title="Notes &amp; highlights"></button>
<button class="btn-icon" id="reader-reset-pos-btn" onclick="saveReaderProgress(true)" title="Diese Position als Lesefortschritt setzen (überschreibt gespeicherten Fortschritt)"></button> <button class="btn-icon" id="reader-reset-pos-btn" onclick="saveReaderProgress(true)" title="Diese Position als Lesefortschritt setzen (überschreibt gespeicherten Fortschritt)"></button>
<button class="btn-icon" onclick="closeReader()" title="Close (Esc)"></button> <button class="btn-icon" onclick="closeReader()" title="Close (Esc)"></button>
@ -353,8 +352,11 @@
<aside id="reader-margin" class="reader-margin"> <aside id="reader-margin" class="reader-margin">
<div class="reader-margin-header"> <div class="reader-margin-header">
<span class="reader-margin-title">Notes</span> <span class="reader-margin-title">Notes</span>
<button class="btn-icon" id="reader-margin-list-btn" onclick="openAnnotationsSidebar()" title="List all highlights &amp; notes, jump to any of them"></button> <span class="reader-margin-header-actions">
<button class="btn-icon" id="reader-margin-export-btn" onclick="exportAnnotations()" title="Export as text file"></button> <button class="btn-icon" id="reader-marker-btn" onclick="toggleMarkerMode()" title="Mark text"></button>
<button class="btn-icon" id="reader-margin-list-btn" onclick="openAnnotationsSidebar()" title="List all highlights &amp; notes, jump to any of them"></button>
<button class="btn-icon" id="reader-margin-export-btn" onclick="exportAnnotations()" title="Export as text file"></button>
</span>
</div> </div>
<div id="reader-margin-markers" class="reader-margin-markers"></div> <div id="reader-margin-markers" class="reader-margin-markers"></div>
</aside> </aside>