Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

#36

Celebrate a landed change request with a confetti burst

closed · opened by keenan 13d ago

keenan opened this 13d ago

Landing a change request is the payoff for the whole review loop, but the page just reloads and looks exactly like any other view. A short confetti burst on the page you land on is a small, self-contained piece of feedback that costs nothing when nothing is being landed.

This is a pleasant-to-have, not a correctness issue.

Cost, measured

canvas-confetti 1.9.3 (dist/confetti.browser.min.js from the npm tarball):

Asset Raw Gzipped
canvas-confetti 1.9.3 (min) 10.8 KB 4.5 KB
static/htmx.min.js (already vendored) 52 KB ~16 KB
static/vole.css 68 KB ~12 KB

Zero dependencies, ISC license (same family as the vendored Lucide). It draws to one fixed z-index:100 canvas on requestAnimationFrame, supports prefers-reduced-motion through its disableForReducedMotion option, and falls back to an OffscreenCanvas worker where available.

Constraints that shape the change

  • server/web/auth.go sets script-src 'self', so there is no CDN option. The library must be vendored into server/web/static/ with a canvas-confetti.LICENSE beside htmx.LICENSE and lucide.LICENSE, per the rule in server/web/templates.go that scripts are static assets and never inlined into a template.
  • Static() serves http.FileServerFS with no gzip middleware, so the honest wire cost is the full 10.8 KB. Putting the tag in chrome.html would pay that on every page load for everyone. It should therefore be lazy-loaded: the ~11 KB is paid once, by the person who just landed the request.
  • Nothing on the merge path currently marks a successful landing to the browser. MergeRequest redirects 303 to the request page, which is also the URL of a normal visit. A ?merged=1 query parameter, mirroring the existing ?saved=1 idiom in settings.go, keeps the burst tied to the write. A refresh or a shared link then does not replay it.

Scope / checklist

  • Vendor canvas-confetti 1.9.3 as static/confetti.browser.min.js with its ISC canvas-confetti.LICENSE.
  • Redirect a successful landing to /r/<name>/requests/<id>?merged=1; leave every refusal path unchanged, including the HTMX fragment reply.
  • In static/app.js, when ?merged is present, append the script tag, fire one burst from the merge control, and stop. Do not load the script otherwise.
  • Pass disableForReducedMotion: true and call confetti.reset() before firing so a back/forward cache restore cannot stack canvases.
  • Guard the DOM work so the enhancement stays optional: no console error and no extra request on any page without the parameter.
  • Cover the redirect in a test: a successful landing redirects with ?merged=1 and the request page renders normally with the parameter present.

Acceptance criteria

  • A successful merge lands on the request page with ?merged=1 in the URL, and the merged banner it shows today is unchanged.
  • /static/confetti.browser.min.js is served from the embedded assets and is requested only on a page carrying ?merged=1; a plain request page load issues no request for it.
  • A user with prefers-reduced-motion: reduce sees the same merged page and no animation.
  • The static asset set stays embed-only: no build step, no npm, no CDN, and script-src 'self' is not loosened.
  • go test ./... passes, and vole workflow check is clean.

Comments (1)

keenan 13d ago

Implemented in #01M5XZDTV4R3EE44RA8P9BGRG4.

One correction to the numbers above: the 10.8 KB minified file is a jsDelivr artifact, not something upstream publishes. The 1.9.3 npm tarball ships only dist/confetti.browser.js at 24.9 KB raw / 6.9 KB gzipped, and that is what is vendored (sha512 verified against the registry’s dist.integrity).

That is why the lazy load is load-bearing rather than a nicety: Static() has no gzip middleware, so anything in the page shell costs every page load its full 24.9 KB. Fetched only on the ?merged=1 redirect, it costs one reader once.

useWorker is left off, so no blob: worker is created and default-src needs no relaxation; disableForReducedMotion is on.