feat(theme): opt-in lightbox with native dialog for image and text-media grids #105

Merged
dominik.polakovics merged 2 commits from afk/93 into main 2026-07-18 03:55:31 +02:00

An opt-in lightbox bool param (default false) on the image and text-media element sidecars. When enabled, every linkless grid image is wrapped in an anchor to the full-size asset and the grid carries a bare data-lightbox marker; a dependency-free theme script upgrades those anchors into a native <dialog> overlay. A per-image link always wins over the lightbox, and without JS the anchor simply opens the full-size image.

What changed

  • Sidecarslightbox bool (default false) appended to image.yaml and text-media.yaml; purely additive, existing content untouched (bool is an existing engine field type, so no Go changes and no AGENTS.md vocabulary change).
  • Templates — the two shortcodes thread the flag into ce/figure.html, mark the grid wrapper with data-lightbox, and set a Page.Store flag; baseof.html emits <script src="/js/lightbox.js" defer> only on pages whose main block rendered a lightbox grid. The figure partial wraps linkless images in <a class="ce-lightbox" href="<full asset>">; a set img.link keeps its plain anchor (link wins). Image-reference resolution is factored into a new ce/img-res.html partial so ce/img.html and the anchor href resolve identically.
  • Theme scriptassets/js/lightbox.js, a dependency-free IIFE in the consent.js mold (document-delegated, survives preview DOM swaps): one shared native <dialog>, Esc + backdrop close, prev/next buttons and ArrowLeft/ArrowRight cycling within the grid (wrapping, linked images excluded), focus returned to the trigger on close, alt text carried into the enlarged view. Modified/secondary clicks fall through to normal navigation.
  • CSS.ce-lightbox* section in theme.css using the existing tokens; control focus rings kept visible.
  • TestsTestLightboxInProduction (internal/theme) pins the built-site contract on new kitchen-sink fixtures: data-lightbox wrappers, resolved lightbox anchors for a library ref and a bundle-local ref, link-wins inside a lightbox grid, page-scoped script emission, and a lightbox-free home page staying byte-clean of all markers.

Verification

  • Full CI mirror green: web vitest (792), go vet, go test ./... (all packages incl. the new test), go build.
  • Real Hugo production build of the fixture site, plus real-browser (headless Chromium via playwright-core) end-to-end checks, all passing: keyboard-only open (Tab+Enter), arrow-key prev/next with wrap-around that skips the linked image, keyboard-reachable Close/Prev/Next buttons, Esc and backdrop-click close with focus restored to the trigger, per-image link navigating instead of opening the overlay, no-JS anchor fallback to the image file, and the script absent from pages without a lightbox grid.

Closes #93

🤖 Generated with Claude Code

An opt-in `lightbox` bool param (default false) on the `image` and `text-media` element sidecars. When enabled, every linkless grid image is wrapped in an anchor to the full-size asset and the grid carries a bare `data-lightbox` marker; a dependency-free theme script upgrades those anchors into a native `<dialog>` overlay. A per-image `link` always wins over the lightbox, and without JS the anchor simply opens the full-size image. ## What changed - **Sidecars** — `lightbox` bool (default false) appended to `image.yaml` and `text-media.yaml`; purely additive, existing content untouched (`bool` is an existing engine field type, so no Go changes and no AGENTS.md vocabulary change). - **Templates** — the two shortcodes thread the flag into `ce/figure.html`, mark the grid wrapper with `data-lightbox`, and set a `Page.Store` flag; `baseof.html` emits `<script src="/js/lightbox.js" defer>` only on pages whose main block rendered a lightbox grid. The figure partial wraps linkless images in `<a class="ce-lightbox" href="<full asset>">`; a set `img.link` keeps its plain anchor (link wins). Image-reference resolution is factored into a new `ce/img-res.html` partial so `ce/img.html` and the anchor href resolve identically. - **Theme script** — `assets/js/lightbox.js`, a dependency-free IIFE in the consent.js mold (document-delegated, survives preview DOM swaps): one shared native `<dialog>`, Esc + backdrop close, prev/next buttons and ArrowLeft/ArrowRight cycling within the grid (wrapping, linked images excluded), focus returned to the trigger on close, alt text carried into the enlarged view. Modified/secondary clicks fall through to normal navigation. - **CSS** — `.ce-lightbox*` section in `theme.css` using the existing tokens; control focus rings kept visible. - **Tests** — `TestLightboxInProduction` (internal/theme) pins the built-site contract on new kitchen-sink fixtures: `data-lightbox` wrappers, resolved lightbox anchors for a library ref and a bundle-local ref, link-wins inside a lightbox grid, page-scoped script emission, and a lightbox-free home page staying byte-clean of all markers. ## Verification - Full CI mirror green: web vitest (792), `go vet`, `go test ./...` (all packages incl. the new test), `go build`. - Real Hugo production build of the fixture site, plus real-browser (headless Chromium via playwright-core) end-to-end checks, all passing: keyboard-only open (Tab+Enter), arrow-key prev/next with wrap-around that skips the linked image, keyboard-reachable Close/Prev/Next buttons, Esc and backdrop-click close with focus restored to the trigger, per-image link navigating instead of opening the overlay, no-JS anchor fallback to the image file, and the script absent from pages without a lightbox grid. Closes #93 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(theme): opt-in lightbox with native dialog for image and text-media grids
All checks were successful
ci / build-and-test (pull_request) Successful in 6m51s
11e3331031
An opt-in `lightbox` bool param (default false) on the image and
text-media sidecars. When enabled, every linkless grid image is wrapped
in an anchor to the full-size asset and the grid carries a bare
data-lightbox marker; a dependency-free theme script (lightbox.js)
upgrades those anchors into a native <dialog> overlay — Esc and
backdrop close, prev/next within the grid (buttons and arrow keys,
wrapping), focus returned to the trigger on close, alt text carried
into the enlarged view. baseof.html ships the script only on pages
whose Store was flagged by a rendered lightbox grid. A per-image
`link` always wins over the lightbox, and without JS the anchor simply
opens the full-size image.

Image-reference resolution is factored out of ce/img.html into
ce/img-res.html so the figure partial builds the anchor href through
the exact same rules.

Closes #93

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into afk/93
All checks were successful
ci / build-and-test (pull_request) Successful in 7m11s
ec23e5385b
# Conflicts:
#	internal/theme/theme_test.go
Author
Owner

This was generated by AI while landing a PR.

Validation verdict: PASS (with a merge-conflict resolution applied — see below).

Verification signal relied on: the green Forgejo Actions run ci / build-and-test on the resolution merge commit ec23e53 (run 94, success, 7m11s) — full Go + web suites. This ran on the resolved tree, not just the pre-conflict PR.

Merge conflict resolved:

  • internal/theme/theme_test.go conflicted because main advanced (cta #101, cards #99) after CI last ran. Both sides only ADDED independent test functions (TestLightboxInProduction on the PR; TestCtaRenderingInProduction + TestCardsInProduction on main) — zero deletions, no import changes. Resolved as the deterministic union (all three functions present). Re-verified locally: the three theme tests pass and go vet ./internal/theme is clean; then confirmed by the fresh green CI above. Pushed to afk/93 (11e3331..ec23e53).

Substantive review:

  • Sidecars: lightbox bool (default false) on image.yaml + text-media.yaml; bool is an existing field type, so no engine/AGENTS.md vocabulary change (AGENTS.md correctly untouched).
  • Precedence in ce/figure.html: per-image link wins → else lightbox anchor → else plain img. Full-size href resolved via the new ce/img-res.html (factored out of ce/img.html, behaviour-preserving), with raw-ref fallback.
  • Script is page-scoped: shortcodes set a deckleLightbox Store flag only when a grid actually renders; baseof.html emits js/lightbox.js only then, outside the hugo.IsServer block.
  • lightbox.js is a dependency-free, document-delegated IIFE: native <dialog>, Esc/backdrop/button close, wrap-around prev/next (buttons + arrows) scoped to a.ce-lightbox within [data-lightbox] so linked images are skipped, focus restored to the trigger, alt carried into the enlarged view; modified/secondary clicks fall through to native navigation (no-JS fallback preserved).
  • Verified empirically with a real Hugo build that an engine-emitted unquoted lightbox=false param types as a genuine bool (falsy), so {{ if $lightbox }} does not spuriously activate on default images — the case the hand-authored fixtures do not cover.

No outstanding blockers. Awaiting a free-text merge go-ahead before merging.

> *This was generated by AI while landing a PR.* **Validation verdict: PASS** (with a merge-conflict resolution applied — see below). Verification signal relied on: the green Forgejo Actions run `ci / build-and-test` on the resolution merge commit `ec23e53` (run 94, success, 7m11s) — full Go + web suites. This ran on the resolved tree, not just the pre-conflict PR. Merge conflict resolved: - `internal/theme/theme_test.go` conflicted because `main` advanced (cta #101, cards #99) after CI last ran. Both sides only ADDED independent test functions (`TestLightboxInProduction` on the PR; `TestCtaRenderingInProduction` + `TestCardsInProduction` on main) — zero deletions, no import changes. Resolved as the deterministic union (all three functions present). Re-verified locally: the three theme tests pass and `go vet ./internal/theme` is clean; then confirmed by the fresh green CI above. Pushed to `afk/93` (`11e3331..ec23e53`). Substantive review: - Sidecars: `lightbox` bool (default false) on `image.yaml` + `text-media.yaml`; `bool` is an existing field type, so no engine/AGENTS.md vocabulary change (AGENTS.md correctly untouched). - Precedence in `ce/figure.html`: per-image `link` wins → else `lightbox` anchor → else plain img. Full-size href resolved via the new `ce/img-res.html` (factored out of `ce/img.html`, behaviour-preserving), with raw-ref fallback. - Script is page-scoped: shortcodes set a `deckleLightbox` Store flag only when a grid actually renders; `baseof.html` emits `js/lightbox.js` only then, outside the `hugo.IsServer` block. - `lightbox.js` is a dependency-free, document-delegated IIFE: native `<dialog>`, Esc/backdrop/button close, wrap-around prev/next (buttons + arrows) scoped to `a.ce-lightbox` within `[data-lightbox]` so linked images are skipped, focus restored to the trigger, alt carried into the enlarged view; modified/secondary clicks fall through to native navigation (no-JS fallback preserved). - Verified empirically with a real Hugo build that an engine-emitted unquoted `lightbox=false` param types as a genuine bool (falsy), so `{{ if $lightbox }}` does not spuriously activate on default images — the case the hand-authored fixtures do not cover. No outstanding blockers. Awaiting a free-text merge go-ahead before merging.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle!105
No description provided.