feat(bookdrop): compact Grimmory-style toolbar, reclaim vertical space (bookshelf-bp34) #621
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-bp34"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
What is preserved verbatim
bookdrop-review-target,bookdrop-extract-pattern-target,bookdrop-bulk-edit-targetdata-action="..."on every button/select_csrfhidden inputs and all hidden ID fields#extract-pattern-open-btnand#bulk-edit-open-btn(referenced by browser e2e tests)Out of scope (possible follow-up)
Moving Accept/Reject to a bottom action bar (Grimmory-style finalize row) — deferred to a follow-up bead.
Test plan
make testpasses (Go unit tests)npm testpasses (904 Vitest JS tests)style=attributes (CSP-safe).btn,.btn-small,.btn-ghostcanonical classes#extract-pattern-open-btnand#bulk-edit-open-btnby ID — both preservedCloses bead bookshelf-bp34 on merge.
Security Review — PR #621 (bookshelf-bp34)
BookDrop toolbar UI/CSS rework. Scope:
templates/pages/bookdrop_index.html+static/css/main.css. Behavior unchanged per description.CSRF preserved: Both POST forms (
/bookdrop/accept,/bookdrop/reject) carry<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">in the new layout — identical to the removed code. No regression.XSS: All template expressions in added lines are standard auto-escaped Go
html/templateinterpolations ({{$.CSRFToken}}). No new{{template.HTML(...)}},template.JS,template.URL, or other raw-output functions introduced. The pre-existing{{.LibraryPathsJSON}}in a<script type="application/json">tag is unchanged context (not new). No user-controlled data (filenames, paths) is interpolated into HTML attributes without escaping.CSP / inline handlers: No
onclick=,onerror=,style=attribute values, or<script>tags added. The existing{{/* No inline JS — strict CSP */}}comment is preserved. Stimulusdata-action=attributes are unchanged declarative wiring — no new JS surface.Authz: Pure presentation restructuring. No server-side gating code touched; no route registrations, middleware, or handler logic in this diff.
CSS-only change note:
.bookdrop-no-librariesrenamed to.bookdrop-no-libraries-hintwith style changes — no security impact. All other CSS deltas are layout/spacing only.REVIEW VERDICT: 0 blocker, 0 major, 0 minor
Code Review — bookshelf-bp34 (PR #621)
Phase 0: DEMO Verification
No DEMO block in the bead description — this is a UI/CSS-only rework with no runnable command output to verify. Proceeding under the understanding that CI green + diff review is the appropriate verification path for pure template/CSS changes.
Phase 1 & 2: Spec Compliance + Code Quality
Behavior preservation (Stimulus targets/actions)
All targets declared in
bookdrop_review_controller.js(lines 31–49) are present in the new template. Thehas*guards that protect optional targets are correctly relied upon:hasAcceptBtnTarget/hasRejectBtnTargetguard_updateUI()— correct; both buttons are inside{{if $.Libraries}}.submitAccept/submitRejectaccessacceptIdsTarget,acceptFormTargetetc. withouthas*guards (lines 151–154, 173–174) — but those methods are only reachable by clickingacceptBtn/rejectBtn, which are themselves only rendered when$.Librariesis truthy. No reachability issue.openBtnfor extract-pattern and bulk-edit are now unconditionally rendered (outside the{{if $.Libraries}}block) — both controllers guard withhasOpenBtnTarget(lines 153–154 of each controller). Correct.CSP — no inline style= or on handlers introduced.* Confirmed clean.
Canonical components.
btn,btn-small,btn-ghost,btn-primary,btn-danger-ghostall resolve to existing CSS rules. No bespoke parallel class system introduced.Dead CSS cleanup. Removed classes
.bookdrop-toolbar-bulk,.bookdrop-no-librariesare gone from bothmain.cssand the template. Orphaned.bookdrop-metadata-actions(used only in the old no-libraries fallback div) is also fully removed. New.bookdrop-no-libraries-hintclass is defined in CSS and used in the template — no orphans.Template correctness. No unclosed
{{actions. No broken Go template syntax.{{$.CSRFToken}}and{{$.Libraries}}scoping is correct (using$to access top-level data inside range blocks is not needed here since the forms are not inside a range, but$.Librariesis correct where used).One behavioral change worth noting (not a blocker): The
toolbardiv is now always rendered when{{if .Proposals}}is true, regardless of whether libraries are configured. Previously the toolbar was wrapped in{{if $.Libraries}}and was entirely absent when no libraries existed — instead showing a<p class="bookdrop-no-libraries">and a separate<div class="bookdrop-metadata-actions">for the two modal buttons. The new design always shows the compact toolbar row with the Extract Pattern and Bulk Edit buttons, plus an inline hint span when no libraries are configured. This is intentional per the bead description (reclaim vertical space, consolidate into one row) and is not a regression — the Extract Pattern / Bulk Edit modals remain reachable in both cases.No findings.
REVIEW VERDICT: 0 blocker, 0 major, 0 minor
The bottom action bar's Accept/Reject forms are guarded by {{if $.Libraries}}, so the test needs a library row to make those buttons appear in the DOM. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>Rework #2 — Grimmory-matched BookDrop layout screenshot
Three zones visible:
DOM verified: acceptInBar=true, acceptInToolbar=false, barExists=true, pageHeaderExists=true
Security Review — PR #621 (bookshelf-bp34)
Scope: BookDrop review page three-zone layout rework; Accept/Reject/Refresh POST forms relocated from top toolbar to new bottom action bar and page-header respectively.
CSRF preservation
Refresh form (
templates/pages/bookdrop_index.htmlline ~746):Present. ✓
Reject form (bottom action bar, line ~889):
Present. ✓
Accept form (bottom action bar, line ~898):
Present. ✓
All three POST forms retain their
_csrfhidden input after relocation. No CSRF regression.XSS / template escaping
All data interpolations in the diff use
{{.CSRFToken}},{{.Name}},{{.ID}},{{$.CSRFToken}},{{.LibraryPathsJSON}}— all passed throughhtml/template's auto-escaping. No newtemplate.HTML,template.JS,template.URL,safeHTML, or raw pipe operators introduced. No unescaped filename or path rendered into an attribute. No XSS surface change.CSP / inline scripts
No
onclick,onXevent handlers, or<script>tags with executable content introduced. All interactivity wired via Stimulusdata-actionattributes. The controller comment on line ~380 explicitly notes "strict CSP forbids inline scripts." No CSP weakening.Authorization
No server-side handler, service, or middleware changes in the diff. Accept/Reject/Refresh routes remain gated by whatever auth middleware existed before. Presentation-only change — no authz surface removed or bypassed.
Committed files / .gitignore
.gitignoreaddstmp/to exclude ephemeral go-rod screenshots saved during e2e runs. No secrets, credentials, or build artifacts committed. No sensitive paths accidentally exposed.JS test fixture note
The Vitest unit test fixture (
bookdrop_review_controller.test.js) omits_csrfhidden inputs from its<form>elements — correct behavior for a jsdom unit test that never exercises CSRF middleware. Not a security concern.REVIEW VERDICT: 0 blocker, 0 major, 0 minor
CODE REVIEW — bookshelf-bp34 (PR #621)
Phase 0: DEMO verification
Bead comment confirms CI green at
d6bdb52a, mergeable=True, DOM verified (acceptInBar=true,acceptInToolbar=false). Diff confirms controller root is<section class="bookdrop-index" data-controller="bookdrop-review …">and the bottom bar is a direct child of that section — all targets are contained within the root. DEMO passes.Findings
[MAJOR] templates/pages/bookdrop_index.html:9 — Duplicate h1 on the page
internal/bookdrop/review_handler.go:208still setsdata.PageTitle = "BookDrop", which causesbase.html(line 430) to render<h1 class="top-bar-title">BookDrop</h1>in the top bar. The PR simultaneously adds<h1>BookDrop Review</h1>inside the{{define "content"}}block. The page now has two<h1>elements — invalid HTML (accessibility, SEO, screen-reader landmark confusion). Fix: either cleardata.PageTitlein the handler so the top-bar h1 is suppressed, or remove the<h1>from the page-header div and keep the top-bar title.[MAJOR] templates/pages/bookdrop_index.html:15 — Duplicate Refresh button
internal/bookdrop/review_handler.go:212–216rendersbookdrop_index_controls.htmlintoPageControls, which emits a "Refresh" button in the top bar (base.html:432). The PR adds a second Refresh button (↻ Refresh) inside the new<div class="page-header-actions">. Users see two Refresh controls on the same page. Fix: remove the new Refresh form frombookdrop_index.html(the existing top-bar controls one is sufficient) OR delete thebookdrop_index_controls.htmlrender path and keep only the new inline one — but do not ship both.[MINOR] e2e/browser/bookdrop_bottom_bar_test.go:244 — Hardcoded /tmp path for screenshot
os.WriteFile("/tmp/bookdrop_bottom_bar.png", …)is hardcoded. On macOS/tmpis a symlink to/private/tmp; in CI (Linux) it works fine. Preferos.TempDir()+ a named file (filepath.Join(os.TempDir(), "bookdrop_bottom_bar.png")) to be portable and self-documenting.[MINOR] e2e/browser/bookdrop_bottom_bar_test.go:147–199 — Single It step has multiple Expect calls
The first
Itblock contains 6Expectcalls asserting multiple distinct properties (button presence, location, disabled state, selectionCount location). Per unit-test conventions only the multi-Expect relaxation applies here since this is an e2e It — this is explicitly permitted by the E2E Testing Policy in CLAUDE.md ("Multi-Expect relaxation: journey It steps may have multiple Expect calls"). Not a violation; noting it for clarity.Stimulus correctness
Controller root (
<section class="bookdrop-index">) contains: toolbar, bottom bar, table, modals. TheacceptForm/rejectForm/acceptBtn/rejectBtn/clearBtn/bottomSelectAllCheckbox/selectionCounttargets are all inside the section root — no target is orphaned outside the controller element. TheonRowCheckboxChangerefactor (removing the direct_updateSelectAllState()call and having_updateUI()call it instead at line 254) is correct and non-regressive. Both select-all checkboxes are synced via_updateSelectAllState(). CSP: no inlinestyle=, noon*handlers. Canonical.btn,.btn-small,.btn-primary,.btn-danger-ghost,.btn-ghostclasses used throughout.--space-*tokens used in all new CSS rules.REVIEW VERDICT: 0 blocker, 2 major, 2 minor
BookDrop top-bar layout fix — screenshot
After this fix: single top bar = title + subtitle + one ↻ Refresh button. No duplicate h1, no duplicate Refresh.
BookDrop top-bar layout fix — screenshot
After this fix: single top bar = title + subtitle + one Refresh button. No duplicate h1, no duplicate Refresh.
Security Re-Review — PR #621 (bd-bookshelf-bp34)
Focus areas per request: PageSubtitle XSS safety, CSRF preservation, no inline scripts/handlers, no authz removal, no secrets.
No security findings.
PageSubtitle — XSS safety confirmed
base_data.godeclaresPageSubtitle string(plainstring, nottemplate.HTML). The template renders it as{{.PageSubtitle}}insidehtml/template— Go'shtml/templatepackage HTML-escapes allstringvalues automatically. A future caller passing user-controlled data through this field (e.g. a book title) would have it safely escaped before insertion into the DOM. Notemplate.HTMLcast, no| safeHTML, noJS/URLcontext misuse is present. Safe.CSRF — all POST forms preserved
Both the old toolbar forms (removed) and the new bottom-action-bar forms (added) carry
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">. Count is identical: 2 forms (accept + reject). The Refresh form inbookdrop_index_controls.htmlretains its_csrfinput unchanged. CSRF protection is unbroken.No inline scripts or event handlers
The JS controller uses only
data-action="..."Stimulus declarative bindings. Noonclick=,onchange=,<script>blocks, orjavascript:URIs appear anywhere in the diff. Consistent with the project's strict CSP (script-src 'self').No authz or server-gating removed
The
{{if $.Libraries}}guard around accept/reject forms is preserved in the new bottom action bar. The toolbar restructure is presentation-only; no handler-level auth checks were touched. The page subtitle is a static string set by the handler — it is not user-controlled.No secrets or temp artifacts committed
.gitignoreaddstmp/(go-rod screenshot output). No credentials, tokens, or sensitive files appear in the diff.REVIEW VERDICT: 0 blocker, 0 major, 0 minor
CODE REVIEW: APPROVED
Branch: bd-bookshelf-bp34
Reviewed: current diff (fresh pass after fix round)
Phase 0: Prior MAJOR Resolution
MAJOR 1 (duplicate
<h1>) — RESOLVED.The
bookdrop_index.htmlpage template contains zero<h1>tags. The single<h1 class="top-bar-title">lives intemplates/layouts/base.html:431inside{{if .PageTitle}}, rendered fromBaseData.PageTitle. The in-content "BookDrop Review" heading has been removed entirely.MAJOR 2 (duplicate Refresh button) — RESOLVED.
The page template
bookdrop_index.htmlcontains no Refresh button. The only Refresh button is intemplates/pages/bookdrop_index_controls.html:7(the top-bar controls slot), now decorated with ↻ (↻). No second Refresh button exists anywhere in the template.Phase 1: Spec Compliance
All checklist items verified:
<h1>— confirmed:base.html:431only, zero in page template.bookdrop_index_controls.html:7only.PageSubtitlebackward-compatible —BaseData.PageSubtitleis typestring(internal/tmpl/base_data.go:210), defaults to zero value (empty string). Template atbase.html:432is{{if .PageSubtitle}}<p class="top-bar-subtitle">{{.PageSubtitle}}</p>{{end}}— conditional on non-empty, so all other pages that do not set it are visually unchanged. The handler sets it as a plain Go string literal inreview_handler.go:259; notemplate.HTMLcast — auto-escaped correctly./tmpminor —bookdrop_bottom_bar_test.go:245usesfilepath.Join(os.TempDir(), "bookdrop_bottom_bar.png")(portable). The prior [MINOR] is resolved. Note: pre-existing tests (bookdrop_extract_pattern_test.go:205,230,metadata_fetch_cover_apply_test.go:166) still use literal"/tmp/"— those are out of scope for this PR.style=attributes, noon*event handlers, no inline<script>in templates. All new CSS classes (.top-bar-head,.top-bar-subtitle,.bookdrop-action-bar, sub-classes) usevar(--fg-muted),var(--border),var(--space-*)canonical tokens. Removed dead classes:.bookdrop-toolbar-bulk,.bookdrop-bulk-label,.bookdrop-no-libraries. New class.bookdrop-no-libraries-hintreplaces the old block-level.bookdrop-no-librariescleanly.<section class="bookdrop-index" data-controller="bookdrop-review ...">which encloses every target:toolbar,selectAllCheckbox,bottomSelectAllCheckbox,clearBtn,row,rowCheckbox,rowLibrary,rowPath,selectionCount,bulkLibrary,bulkPath,acceptForm,acceptIds,acceptLibraryId,acceptPathId,rejectForm,rejectIds,acceptBtn,rejectBtn. All targets are within the controller root. CSRF inputs are present on both accept and reject forms._updateSelectAllStaterefactor —onRowCheckboxChangeno longer calls_updateSelectAllStatedirectly; instead_updateUI(called from all event handlers) calls_updateSelectAllStateat line 257. Logic is correct and bothselectAllCheckboxTargetandbottomSelectAllCheckboxTargetare synced in one place.bookdrop_bottom_bar_test.gois anOrderedjourney withBeforeAll(satisfies policy). TwoItsteps share the setup. Multi-Expectper step is acceptable under the e2e relaxation rule.Phase 2: Code Quality
No findings.
REVIEW VERDICT: 0 blocker, 0 major, 0 minor
3d256e99745eefe0a38d