- Shipped
- September 7, 2026 at 7:41 AM UTC
- Author
- Kamo
- Commit
- e9f05e7
The six animations shipped last change were hard-wired to the six contact events. They are now a catalogue any of which binds to any event, and the member picks, from a new pane on **************** beside the org's sounds. Twenty-three effects that paint, plus No effect, in six groups by what they DO rather than by what they were designed for: Quiet halo, pinpoint, crosshair, reticle Drawn inward converge, aperture, pinch, magnet Pushed outward bloom, burst, ripple, pool, echo, chime, shockwave Lively nova, confetti, orbit, spiral, comet, droplet Technical hexlock, scan No effect is the first option in every dropdown and is never the default: silence has to be one click away, but shipping it as the default means nobody discovers the feature exists. The defaults stay the six from last change, which are a set rather than six picks — a press pulls energy in and a release lets it out, so an ordinary click still reads as one gesture. Three modules, each knowing only what it must. effects.ts is the catalogue: a draw function plus how long it lives and how far it paints, and nothing about mice or members. binding.ts maps the six events to effect ids and reads a stored preference back. impulse.ts is one effect at one point at one instant. That split is what keeps the whole drawing layer testable in node against a recording context — 49 tests, including that no effect paints outside its declared reach (asserted at ten points across each life, at three seeds), that each fades to nothing, and that no two are the same animation wearing two names. Stored per MEMBER in the generic ui-preferences store, so it follows a person between devices and changes nothing for anyone else — no schema, no service change. Every failure path lands on the defaults, because the canvas also runs on /validate and /logout where there is no session to ask. An id that no longer exists falls back per FIELD, so retiring one effect cannot cost a member the other five choices they made. Each row previews live, using the real engine at the org's own colours. At rest the pad draws the effect's own reach as a dashed boundary: every effect is over in under a second, so a pad that only paints while playing spends nearly all its life empty, and six empty boxes in a settings pane read as something that failed to load — which is exactly how it looked when first rendered. Three defects found by driving it in a browser rather than by reasoning about it: - MUI's Select clones role="option" onto EVERY child it is given, ListSubheader included, so the six group headings were announced to screen readers as selectable options. Autocomplete with an explicit renderGroup emits role="presentation" headers wrapping a real role="group" — and brings type-to-filter, which twenty-four options genuinely want. - A sticky group heading sat on top of the first option in its own group and hid its name. - toBinding read stored[event] straight, which walks the prototype chain. Verified end to end in Chrome with real trusted input: the default binding paints converge at 22px, rebinding to shockwave paints at 79px, an unknown id falls back to the default, and No effect allocates no canvas and paints no pixel at all.