fix(web): banner highlight-note dialog prefilled; explicit note boundary; pencil on every note row
- BUG: startBannerEdit extracted the note by splitting row.rich — the RENDERED HTML — on \n\n, which the concatenated markup never contains, so highlight-note dialogs always opened empty. Both row kinds carry the raw note on bannerSources; use that lookup. - Expanded body now splits at the highlight→note boundary: the selection paragraph (left border in the row's highlight color), a divider + pencil NOTE label, then the note markdown — instead of the two texts flowing together. - The note-indicator pencil (corner badge) now shows on ALL note rows, not just highlights with attached notes. Verified in headless Chromium: badge on note and highlight rows; expanded highlight shows the bordered selection, NOTE label, and note markdown; the highlight edit dialog prefills with the note text.
This commit is contained in:
1 parent
e7ccd7b222
commit
44c0e5640f
4 files changed
+49
-22
No files matched your search
@@ -61,11 +61,11 @@ templ AnnotationsBanner(book handlers.MediaDetail) {
|
|||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<template x-for="row in bannerRows" :key="row.key">
|
<template x-for="row in bannerRows" :key="row.key">
|
||||||
<div class="card p-0 overflow-hidden relative">
|
<div class="card p-0 overflow-hidden relative">
|
||||||
<!-- Note-attached indicator: quiet stroke glyph in the
|
<!-- Note indicator: a quiet stroke glyph in the card's
|
||||||
card's upper-right corner (never left of text) —
|
upper-right corner (never left of text) — every
|
||||||
highlights with an attached note only. -->
|
note row, and highlights with an attached note. -->
|
||||||
<svg
|
<svg
|
||||||
x-show="row.kind === 'highlight' && row.hasNote"
|
x-show="row.kind === 'note' || (row.kind === 'highlight' && row.hasNote)"
|
||||||
class="absolute top-2 right-2 pointer-events-none reader-icon"
|
class="absolute top-2 right-2 pointer-events-none reader-icon"
|
||||||
style="color: var(--text-secondary);"
|
style="color: var(--text-secondary);"
|
||||||
width="13" height="13" viewBox="0 0 20 20" aria-hidden="true"
|
width="13" height="13" viewBox="0 0 20 20" aria-hidden="true"
|
||||||
@@ -73,7 +73,7 @@ templ AnnotationsBanner(book handlers.MediaDetail) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-full text-left px-4 py-3 flex items-start gap-3 hover:bg-surface-hover"
|
class="w-full text-left px-4 py-3 flex items-start gap-3 hover:bg-surface-hover"
|
||||||
:class="row.kind === 'highlight' && row.hasNote ? 'pr-8' : ''"
|
:class="(row.kind === 'note' || (row.kind === 'highlight' && row.hasNote)) ? 'pr-8' : ''"
|
||||||
@click="row.expanded = !row.expanded"
|
@click="row.expanded = !row.expanded"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -115,8 +115,28 @@ templ AnnotationsBanner(book handlers.MediaDetail) {
|
|||||||
>✕ Delete</button>
|
>✕ Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Expanded rich body (markdown pipeline) -->
|
<!-- Expanded rich body (markdown pipeline). Highlight
|
||||||
<div x-show="row.expanded" class="note-rich px-4 pb-4" x-html="row.rich"></div>
|
rows split at the highlight→note boundary: the
|
||||||
|
selection (bordered in the row color), then a
|
||||||
|
labeled divider, then the note markdown. -->
|
||||||
|
<div x-show="row.expanded" class="px-4 pb-4">
|
||||||
|
<p
|
||||||
|
x-show="row.kind === 'highlight'"
|
||||||
|
class="text-sm"
|
||||||
|
:style="row.kind === 'highlight' ? 'border-left: 3px solid ' + row.color + '; padding-left: .75rem; color: var(--text-secondary); margin: 0 0 ' + (row.hasNote ? '.75rem' : '0') + ';' : 'color: var(--text-secondary);'"
|
||||||
|
x-text="row.richSelection"
|
||||||
|
></p>
|
||||||
|
<template x-if="row.kind === 'highlight' && row.hasNote">
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-1 pt-2 pb-1 mt-2 border-t"
|
||||||
|
style="border-color: var(--border); color: var(--text-secondary);"
|
||||||
|
>
|
||||||
|
<svg class="reader-icon" width="12" height="12" viewBox="0 0 20 20" aria-hidden="true"><path d="M 4 13 L 4 16 L 7 16 L 14.5 8.5 L 11.5 5.5 L 4 13 M 12.5 4.5 L 15.5 7.5"></path></svg>
|
||||||
|
<span class="text-xs font-medium uppercase tracking-wide">Note</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="note-rich text-sm" x-html="row.richNote"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func AnnotationsBanner(book handlers.MediaDetail) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</button></div><div class=\"px-4 py-3\"><button type=\"button\" id=\"export-annotations-btn\" :disabled=\"annotationsBannerLoading || bannerRows.length === 0\" @click=\"showExportDialog()\" class=\"w-full py-2 mb-3 text-sm font-medium rounded-lg border transition-opacity hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed\" style=\"border-color: var(--border); color: var(--text-primary);\">Export all notes and highlights</button><p x-show=\"annotationsBannerLoading\" class=\"text-sm py-4\" style=\"color: var(--text-secondary);\">Loading…</p><template x-if=\"!annotationsBannerLoading && bannerRows.length === 0\"><p class=\"text-sm py-4\" style=\"color: var(--text-secondary);\">No notes or highlights on this book yet.</p></template><!-- Notes first, then highlights — stable ledger order --><div class=\"space-y-2\"><template x-for=\"row in bannerRows\" :key=\"row.key\"><div class=\"card p-0 overflow-hidden relative\"><!-- Note-attached indicator: quiet stroke glyph in the\n\t\t\t\t\t\t card's upper-right corner (never left of text) —\n\t\t\t\t\t\t highlights with an attached note only. --><svg x-show=\"row.kind === 'highlight' && row.hasNote\" class=\"absolute top-2 right-2 pointer-events-none reader-icon\" style=\"color: var(--text-secondary);\" width=\"13\" height=\"13\" viewBox=\"0 0 20 20\" aria-hidden=\"true\"><path d=\"M 4 13 L 4 16 L 7 16 L 14.5 8.5 L 11.5 5.5 L 4 13 M 12.5 4.5 L 15.5 7.5\"></path></svg> <button type=\"button\" class=\"w-full text-left px-4 py-3 flex items-start gap-3 hover:bg-surface-hover\" :class=\"row.kind === 'highlight' && row.hasNote ? 'pr-8' : ''\" @click=\"row.expanded = !row.expanded\"><span class=\"w-1.5 self-stretch rounded-full shrink-0\" :style=\"{ backgroundColor: row.color }\"></span> <span class=\"flex-1 min-w-0\"><span class=\"block text-sm\" x-text=\"row.preview\"></span> <span class=\"block text-xs mt-0.5\" style=\"color: var(--text-secondary);\" x-text=\"row.label\"></span></span></button><!-- Jump · edit (notes + attached highlight notes) · delete\n\t\t\t\t\t\t (location notes) — objective 8a --><div class=\"flex items-center justify-between px-4 pb-3 pt-1\"><a x-show=\"row.loc\" :href=\"'/readers/' + bannerMediaId + '?loc=' + encodeURIComponent(row.loc)\" class=\"text-xs px-2 py-1 rounded-lg border hover:bg-surface-hover transition-colors\" style=\"border-color: var(--border); color: var(--accent);\">Open reader here ↗</a><div class=\"flex items-center gap-1\"><button type=\"button\" @click=\"startBannerEdit(row)\" class=\"text-xs px-2 py-1 rounded-lg hover:bg-surface-hover\" style=\"color: var(--text-secondary);\" x-text=\"row.kind === 'highlight' ? (row.hasNote ? 'Edit note' : 'Add note') : 'Edit'\"></button> <button type=\"button\" @click=\"row.kind === 'note' ? deleteBannerNote(row) : deleteBannerHighlight(row)\" class=\"text-xs px-2 py-1 rounded-lg hover:bg-surface-hover\" style=\"color: var(--status-danger);\">✕ Delete</button></div></div><!-- Expanded rich body (markdown pipeline) --><div x-show=\"row.expanded\" class=\"note-rich px-4 pb-4\" x-html=\"row.rich\"></div></div></template></div></div></div><!-- Note edit dialog (objective 8a): raw markdown source; for\n\t highlight rows the dialog edits the note ATTACHED to the\n\t highlight (never the highlight itself) --><div id=\"banner-note-dialog\" x-cloak x-show=\"bannerEditOpen\" class=\"fixed inset-0 z-[60] flex items-center justify-center p-4\" style=\"background: rgba(0,0,0,.6);\"><div class=\"max-w-md w-full card p-6\"><h4 class=\"text-lg font-semibold mb-1\" style=\"color: var(--text-primary);\" x-text=\"bannerEditRow && bannerEditRow.kind === 'highlight' ? (bannerEditRow.hasNote ? 'Edit highlight note' : 'Add note to highlight') : 'Edit note'\"></h4><p class=\"text-xs mb-3\" style=\"color: var(--text-secondary);\">Markdown supported</p><textarea x-model=\"bannerEditText\" rows=\"6\" class=\"input w-full text-sm\"></textarea><div class=\"flex gap-2 mt-4\"><button type=\"button\" @click=\"saveBannerNote()\" :disabled=\"!String(bannerEditText || '').trim()\" class=\"flex-1 py-2 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed\">Save</button> <button type=\"button\" @click=\"bannerEditOpen = false\" class=\"flex-1 py-2 text-sm border rounded hover:bg-surface-hover\" style=\"border-color: var(--border); color: var(--text-secondary);\">Cancel</button></div></div></div><!-- Export format dialog --><div id=\"annotations-export-dialog\" x-cloak x-show=\"annotationsExportOpen\" class=\"fixed inset-0 z-[60] flex items-center justify-center p-4\" style=\"background: rgba(0,0,0,.6);\"><div class=\"max-w-sm w-full card p-6\"><h4 class=\"text-lg font-semibold mb-1\" style=\"color: var(--text-primary);\">Export annotations</h4><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary);\">Notes and highlights, as markdown files.</p><divLine truncated
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</button></div><div class=\"px-4 py-3\"><button type=\"button\" id=\"export-annotations-btn\" :disabled=\"annotationsBannerLoading || bannerRows.length === 0\" @click=\"showExportDialog()\" class=\"w-full py-2 mb-3 text-sm font-medium rounded-lg border transition-opacity hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed\" style=\"border-color: var(--border); color: var(--text-primary);\">Export all notes and highlights</button><p x-show=\"annotationsBannerLoading\" class=\"text-sm py-4\" style=\"color: var(--text-secondary);\">Loading…</p><template x-if=\"!annotationsBannerLoading && bannerRows.length === 0\"><p class=\"text-sm py-4\" style=\"color: var(--text-secondary);\">No notes or highlights on this book yet.</p></template><!-- Notes first, then highlights — stable ledger order --><div class=\"space-y-2\"><template x-for=\"row in bannerRows\" :key=\"row.key\"><div class=\"card p-0 overflow-hidden relative\"><!-- Note indicator: a quiet stroke glyph in the card's\n\t\t\t\t\t\t upper-right corner (never left of text) — every\n\t\t\t\t\t\t note row, and highlights with an attached note. --><svg x-show=\"row.kind === 'note' || (row.kind === 'highlight' && row.hasNote)\" class=\"absolute top-2 right-2 pointer-events-none reader-icon\" style=\"color: var(--text-secondary);\" width=\"13\" height=\"13\" viewBox=\"0 0 20 20\" aria-hidden=\"true\"><path d=\"M 4 13 L 4 16 L 7 16 L 14.5 8.5 L 11.5 5.5 L 4 13 M 12.5 4.5 L 15.5 7.5\"></path></svg> <button type=\"button\" class=\"w-full text-left px-4 py-3 flex items-start gap-3 hover:bg-surface-hover\" :class=\"(row.kind === 'note' || (row.kind === 'highlight' && row.hasNote)) ? 'pr-8' : ''\" @click=\"row.expanded = !row.expanded\"><span class=\"w-1.5 self-stretch rounded-full shrink-0\" :style=\"{ backgroundColor: row.color }\"></span> <span class=\"flex-1 min-w-0\"><span class=\"block text-sm\" x-text=\"row.preview\"></span> <span class=\"block text-xs mt-0.5\" style=\"color: var(--text-secondary);\" x-text=\"row.label\"></span></span></button><!-- Jump · edit (notes + attached highlight notes) · delete\n\t\t\t\t\t\t (location notes) — objective 8a --><div class=\"flex items-center justify-between px-4 pb-3 pt-1\"><a x-show=\"row.loc\" :href=\"'/readers/' + bannerMediaId + '?loc=' + encodeURIComponent(row.loc)\" class=\"text-xs px-2 py-1 rounded-lg border hover:bg-surface-hover transition-colors\" style=\"border-color: var(--border); color: var(--accent);\">Open reader here ↗</a><div class=\"flex items-center gap-1\"><button type=\"button\" @click=\"startBannerEdit(row)\" class=\"text-xs px-2 py-1 rounded-lg hover:bg-surface-hover\" style=\"color: var(--text-secondary);\" x-text=\"row.kind === 'highlight' ? (row.hasNote ? 'Edit note' : 'Add note') : 'Edit'\"></button> <button type=\"button\" @click=\"row.kind === 'note' ? deleteBannerNote(row) : deleteBannerHighlight(row)\" class=\"text-xs px-2 py-1 rounded-lg hover:bg-surface-hover\" style=\"color: var(--status-danger);\">✕ Delete</button></div></div><!-- Expanded rich body (markdown pipeline). Highlight\n\t\t\t\t\t\t rows split at the highlight→note boundary: the\n\t\t\t\t\t\t selection (bordered in the row color), then a\n\t\t\t\t\t\t labeled divider, then the note markdown. --><div x-show=\"row.expanded\" class=\"px-4 pb-4\"><p x-show=\"row.kind === 'highlight'\" class=\"text-sm\" :style=\"row.kind === 'highlight' ? 'border-left: 3px solid ' + row.color + '; padding-left: .75rem; color: var(--text-secondary); margin: 0 0 ' + (row.hasNote ? '.75rem' : '0') + ';' : 'color: var(--text-secondary);'\" x-text=\"row.richSelection\"></p><template x-if=\"row.kind === 'highlight' && row.hasNote\"><div class=\"flex items-center gap-1 pt-2 pb-1 mt-2 border-t\" style=\"border-color: var(--border); color: var(--text-secondary);\"><svg class=\"reader-icon\" width=\"12\" height=\"12\" viewBox=\"0 0 20 20\" aria-hidden=\"true\"><path d=\"M 4 13 L 4 16 L 7 16 L 14.5 8.5 L 11.5 5.5 L 4 13 M 12.5 4.5 L 15.5 7.5\"></path></svg> <span class=\"text-xs font-medium uppercase tracking-wide\">Note</span></div></template><div class=\"note-rich text-sm\" x-html=\"row.richNote\"></div></div></div></template></div></div></div><!-- Note edit dialog (objective 8a): raw markdown source; for\n\t highlight rows the dialog edits the note ATTACHED to the\n\t highlight (never the highlight itself) --><div id=\"banner-note-dialog\" x-cloak x-show=\"bannerEditOpen\" class=\"fixed inset-0 z-[60] flex items-center justify-center p-4\" style=\"background: rgba(0,0,0,.6);\"><div class=\"max-w-md w-full card p-6\"><h4 class=\"text-lg font-semibold mb-1\" style=\"color: var(--text-primary);\" x-text=\"bannerEditRow && bannerEditRow.kind === 'highlight' ? (bannerEditRow.hasNote ? 'Edit highlight note' : 'Add note to highlight') : 'Edit note'\"></h4><p class=\"text-xs mb-3\" style=\"color: var(--text-secondary);\">MarkdowLine truncated
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,11 @@ interface AnnotationRow {
|
|||||||
label: string;
|
label: string;
|
||||||
/** compact preview line: stripped markdown (note) or capped selection */
|
/** compact preview line: stripped markdown (note) or capped selection */
|
||||||
preview: string;
|
preview: string;
|
||||||
/** rich body for the expanded view (pre-sanitized) */
|
/** expanded view, split so the highlight→note boundary is explicit:
|
||||||
rich: string;
|
* the escaped selection text, then the note's markdown-rendered HTML
|
||||||
|
* (empty when the row has no note body). */
|
||||||
|
richSelection: string;
|
||||||
|
richNote: string;
|
||||||
/** the ?loc= handoff value for the jump */
|
/** the ?loc= handoff value for the jump */
|
||||||
loc: string;
|
loc: string;
|
||||||
/** row accent color (swatch) */
|
/** row accent color (swatch) */
|
||||||
@@ -96,7 +99,8 @@ function buildAnnotationRows(
|
|||||||
kind: "note",
|
kind: "note",
|
||||||
label,
|
label,
|
||||||
preview: stripNote(content) || "(empty note)",
|
preview: stripNote(content) || "(empty note)",
|
||||||
rich: renderNoteHtml(content),
|
richSelection: "",
|
||||||
|
richNote: renderNoteHtml(content),
|
||||||
loc,
|
loc,
|
||||||
color: "var(--accent)",
|
color: "var(--accent)",
|
||||||
hasNote: true,
|
hasNote: true,
|
||||||
@@ -135,17 +139,19 @@ function buildAnnotationRows(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const capped = selection.length > 80 ? selection.slice(0, 80) + "…" : selection;
|
const capped = selection.length > 80 ? selection.slice(0, 80) + "…" : selection;
|
||||||
// Expanded body: the full highlight text followed by its rich note.
|
// Expanded body, split at the highlight→note boundary: the escaped
|
||||||
const rich = note
|
// selection, then the note's rendered markdown under a labeled divider
|
||||||
? `<p style="color: var(--text-secondary); margin: 0 0 .5rem;">${escapeHtmlForRich(selection)}</p>` + renderNoteHtml(note)
|
// (rendered by the banner template).
|
||||||
: `<p style="color: var(--text-secondary);">${escapeHtmlForRich(selection) || "…"}</p>`;
|
const richSelection = escapeHtmlForRich(selection);
|
||||||
|
const richNote = note ? renderNoteHtml(note) : "";
|
||||||
|
|
||||||
const row: AnnotationRow = {
|
const row: AnnotationRow = {
|
||||||
key: `hl-${h.id}`,
|
key: `hl-${h.id}`,
|
||||||
kind: "highlight",
|
kind: "highlight",
|
||||||
label,
|
label,
|
||||||
preview: capped || "(empty highlight)",
|
preview: capped || "(empty highlight)",
|
||||||
rich,
|
richSelection,
|
||||||
|
richNote,
|
||||||
loc,
|
loc,
|
||||||
color,
|
color,
|
||||||
hasNote: !!note,
|
hasNote: !!note,
|
||||||
|
|||||||
@@ -225,13 +225,14 @@ Alpine.data("bookDetail", () => {
|
|||||||
},
|
},
|
||||||
// ----- note edit/delete at the banner (objective 8a) -----
|
// ----- note edit/delete at the banner (objective 8a) -----
|
||||||
startBannerEdit(row: AnnotationRow): void {
|
startBannerEdit(row: AnnotationRow): void {
|
||||||
// Highlight rows edit the note ATTACHED to the highlight (never the
|
// Both kinds carry the raw note body on their source: highlights
|
||||||
// highlight itself): rich = "<selection head>\n\n<note source>".
|
// hold the ATTACHED note (never the highlight itself), location
|
||||||
|
// notes hold their content. The old code parsed row.rich (rendered
|
||||||
|
// HTML — no \n\n separators in it) and came up empty every time.
|
||||||
this.bannerEditRow = row;
|
this.bannerEditRow = row;
|
||||||
this.bannerEditText =
|
this.bannerEditText = String(
|
||||||
row.kind === "highlight"
|
this.bannerSources.find(s => s.row.key === row.key)?.source ?? "",
|
||||||
? String((row.rich.split("\n\n").slice(1).join("\n\n") || "")).trim()
|
);
|
||||||
: String(this.bannerSources.find(s => s.row.key === row.key)?.source ?? "");
|
|
||||||
this.bannerEditOpen = true;
|
this.bannerEditOpen = true;
|
||||||
},
|
},
|
||||||
async saveBannerNote() {
|
async saveBannerNote() {
|
||||||
|
|||||||
Reference in new issue
Block a user