From 0b83db8fcea8f29c4ff25e06bd1bef2ee90f6477 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 4 Oct 2026 14:30:56 -0400 Subject: [PATCH] =?UTF-8?q?feat(web):=20book-detail=20annotations=20banner?= =?UTF-8?q?=20=E2=80=94=20=3Floc=3D=20reader=20jumps,=20single/zip=20markd?= =?UTF-8?q?own=20export?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Objective 7 (web half). The 'Annotations' button slides a right-hand banner (annotations_banner.templ + annotations-banner.ts) fed by the existing per-book GET endpoints: friendly labels (Section N parsed from the CFI spine / Page N from the PDF envelope / NN% + first-words fallbacks), capped highlight previews, rich x-html expansion through the objective-6 markdown pipeline, and per-row 'Open reader here' jumps riding /readers/:uuid?loc= (consumeLocJump: jump outranks stored progress for that open; URL stripped before sync can echo it). WEB-ONLY export: single .md or one-file-per-item zip via fflate — item format identical both modes ('# friendly label' / '# selection text' headers, raw markdown bodies, no raw CFIs anywhere). Verified live in Brave: 12 restored highlights + a markdown note labeled/expanded correctly; the jump navigated, landed on the right chapter and consumed ?loc=; export produced the 13-item .md (friendly headers only) and a valid 13-entry zip. Book-detail's existing recently-deleted modal stays untouched (banner is additive). --- package.json | 1 + templates/annotations_banner.templ | 138 ++++ templates/annotations_banner_templ.go | 61 ++ templates/book_detail.templ | 8 + templates/book_detail_templ.go | 983 +++++++++++++------------- templates/reader_templ.go | 10 +- web/src/annotations-banner.ts | 197 ++++++ web/src/book-detail.ts | 91 +++ web/src/reader/reader.ts | 36 +- web/static/style.css | 2 +- 10 files changed, 1044 insertions(+), 483 deletions(-) create mode 100644 templates/annotations_banner.templ create mode 100644 templates/annotations_banner_templ.go create mode 100644 web/src/annotations-banner.ts diff --git a/package.json b/package.json index 8d132c9..ffe56a8 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "alpinejs": "^3.15.8", "chart.js": "^4.5.1", "dompurify": "^3.4.16", + "fflate": "^0.8.3", "highlight.js": "^11.11.1", "htmx.org": "^2.0.8", "lunr": "^2.3.9", diff --git a/templates/annotations_banner.templ b/templates/annotations_banner.templ new file mode 100644 index 0000000..756c170 --- /dev/null +++ b/templates/annotations_banner.templ @@ -0,0 +1,138 @@ +package templates + +import "bookhoard/internal/handlers" + +// AnnotationsBanner is the book-detail slide-out listing every note and +// highlight on the book (objective 7). Rows show a friendly human location +// label (never a raw cfi:epubcfi(…) string), expand to the full rich markup +// (markdown pipeline identical to the reader's), and offer a per-row jump +// that opens the reader at the annotation's location via ?loc=. Web-only +// export lives at the top: single .md file, or one file per item zipped. +// Formatting is IDENTICAL in both modes: +// location note: "# " header + note source as body +// highlight: "# " header + note source as body +// (no note → header only) +templ AnnotationsBanner(book handlers.MediaDetail) { + + + +
+
+

Export annotations

+

Notes and highlights, as markdown files.

+
+ + +
+ +
+
+} diff --git a/templates/annotations_banner_templ.go b/templates/annotations_banner_templ.go new file mode 100644 index 0000000..c53a3d8 --- /dev/null +++ b/templates/annotations_banner_templ.go @@ -0,0 +1,61 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "bookhoard/internal/handlers" + +// AnnotationsBanner is the book-detail slide-out listing every note and +// highlight on the book (objective 7). Rows show a friendly human location +// label (never a raw cfi:epubcfi(…) string), expand to the full rich markup +// (markdown pipeline identical to the reader's), and offer a per-row jump +// that opens the reader at the annotation's location via ?loc=. Web-only +// export lives at the top: single .md file, or one file per item zipped. +// Formatting is IDENTICAL in both modes: +// +// location note: "# " header + note source as body +// highlight: "# " header + note source as body +// (no note → header only) +func AnnotationsBanner(book handlers.MediaDetail) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

Notes & Highlights

Loading…