feat(web): rich reusable avatar picker replacing the native file input (BUNYIP-408) #399
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/BUNYIP-408-avatar-picker"
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?
What
Replaces the unstyled native
<input type="file">+ separate Upload button in Settings with a single reusable avatar picker component (views::avatar_picker), addressing every point raised: off-theme chrome, two-step interaction with no feedback, no preview, no client-side validation, non-interactive circle, and misaligned vertical rhythm.Behaviour
<label>around a visually-clipped-but-focusable input, so mouse and keyboard both work with no JS. A secondary "Change photo" button adds discoverability.aria-live="polite", and clear on the next valid pick.[data-avatar-slot], so a successful upload/removal repaints the header user-menu avatar in place, no full-page reload;/me/avataris cache-busted per change. (The header reads the same server source on next navigation, so there is no stale cache.)Progressive enhancement
Without JS the control is a plain multipart form (choose + Upload) plus a remove form; JS hides those and drives the enhanced flow. The BFF
/settings/avatarand/settings/avatar/removehandlers now content-negotiate: JSON ({ok}/{error}) for the picker's XHR, redirect + toast for the no-JS form.Files
src/views/avatar_picker.rs- the component (Maud markup + theAVATAR_PICKER_JScontroller) and render tests.input.css-.avatar-picker*/.avatar-slot__imgcomponent styles keyed on theme tokens;assets/styles.cssrebuilt with the pinned Tailwind toolchain.src/views/layout.rs- mounts the controller;avatar_badgenow renders[data-avatar-slot]for propagation.src/handlers/dashboard.rs- Settings card uses the component; upload/remove handlers content-negotiate JSON vs redirect.Verification
just check-containergreen: fmt + clippy-D warnings+ all test binaries (bunyip-web 116 tests, incl. 3 new picker render tests for the empty/set states).rust-builder-glibcimage; component + new utility classes confirmed present inassets/styles.css.Note: interactive behaviour (drag/drop, canvas downscale, progress, propagation) is client-side JS and is not covered by the Rust test suite. Happy to spin up a live preview of the states if useful.
🤖 Generated with Claude Code
The Settings avatar control was an unstyled native <input type="file"> plus a separate Upload button: off-theme chrome and focus ring, a two-step interaction with no feedback, no preview, no client-side validation, and a non-interactive avatar circle. This replaces it with a single reusable component (`views::avatar_picker`) used anywhere an image upload appears. Interaction: the avatar circle is the primary target - hovering (or focusing) shows a "Change" overlay and it opens the file dialog on click, Enter, or Space. The circle is a <label> wrapping a visually-clipped-but-focusable file input, so click and keyboard both work with no JS, and a secondary "Change photo" button adds discoverability. Drag and drop onto the circle is supported with a visible drop-target ring, and page-wide drop is prevented so a stray drop never navigates away. Selection previews immediately in the circle and the upload fires automatically (no Upload button) with a determinate progress ring. Client-side validation before any request: the MIME is sniffed from magic bytes (not the extension), anything outside PNG/JPEG/WebP/GIF is rejected, and over-2-MB files are rejected with the file's actual size in the message. Valid images are center-cropped to a square and downscaled to a 512px edge on a canvas (exported as WebP), so a 1.9 MB photo is stored as a few KB. Errors render inline in the theme error color, are announced via aria-live=polite, and clear on the next valid pick. The error line reserves its height so states never shift the fields below. States covered: empty (letter fallback), preview-pending, uploading (progress ring + disabled controls), success (toast), error, and a Remove action with a confirm step (removal is irreversible). Propagation: every avatar surface renders `[data-avatar-slot]`, so a successful upload/removal repaints the header user-menu avatar in place, with no full-page reload; the `/me/avatar` URL is cache-busted per change. Theming and a11y: the focus ring is the app's reed focus-visible ring (never the browser default), the overlay scrim clears WCAG AA over any image, the hidden input keeps a real label association and the image real alt text, and the component is keyed on theme tokens so light, dark, and the high-contrast toggle all adapt. Progressive enhancement: without JS the control is a plain multipart form (choose + Upload) plus a remove form; JS hides those and drives the enhanced flow. The BFF upload/remove handlers now content-negotiate - JSON (`{ok}`/`{error}`) for the picker's XHR, redirect + toast for the no-JS form. Component styles live in `input.css` (`.avatar-picker*`, `.avatar-slot__img`); `assets/styles.css` was rebuilt with the pinned Tailwind toolchain. Verified: `just check-container` green (fmt + clippy -D warnings + all test binaries), including new render tests for the picker's empty/set states. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5dcYueNHByRnWJDYoDX1W