ui(phase-2c): profile-card — plan + implementation#188
Merged
Conversation
Translates docs/specs/2026-04-19-ui-design/profile-card.md into a checkbox-tracked implementation plan. Plan + impl ship as one PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the 8 new optional fields on willow-state::Profile +
CrestPattern / PinnedFragment / PinnedKind + per-field caps. All new
fields carry #[serde(default)] so pre-phase-2c serialized payloads
still decode. Refactors the two remaining Profile { .. } struct
literals to use Profile::new(peer_id).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the single delta event carrying optional updates for every grove-propagated profile field. apply_event upserts a Profile if missing (matches SetProfile contract), truncates every string field to its spec cap rather than rejecting the event, drops invalid crest_color values to None. Permission = none beyond self-authorship. Ships 9 state tests: merge / clear / preserve / idempotent / caps / create-on-missing / invalid-crest-color / elsewhere-cap / pinned-round-trip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Large-enum-variant clippy fires on willow-common::WireMessage because EventKind now carries an UpdateProfile arm whose fields sum to ~350 bytes (9 Option<Option<...>> / Option<Vec<String>>). Extracting the payload into a ProfileDelta struct carried via Box<ProfileDelta> drops the enum back to its pre-phase-2c size. All tests continue to pass with the new shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches the TrustStore shape so the web crate can ship a localStorage-backed WebNicknameStore without ceremony. v1 of the profile-card nickname feature is local-only per spec — no EventKind, no propagation. Ships 7 unit tests (set/get round-trip, clear, empty clears, version bumps, caps, snapshot). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- willow-crypto: new peer_fingerprint(peer) -> 6 words helper + 4 tests. DS tag PEER_FINGERPRINT_DS_TAG distinct from SAS so the fingerprint domain never collides with the SAS compare flow. - willow-state: re-export CrestPattern / PinnedFragment / PinnedKind / ProfileDelta from the crate root so downstream consumers don't need willow_state::types::* paths. - willow-client: ProfileView struct aggregating every field the profile card renders (handle, display name, pronouns, bio, tagline, crest, pinned, elsewhere, since, short+full fingerprint, is_self flag). profile_view_of(peer, local) accessor on ClientViewHandle reads from the event-sourced Profile + global ProfileState. - ClientMutations::update_profile_fields(delta) builds + applies + broadcasts the UpdateProfile event. - since_hint(earliest_ms, now_ms) soft-time formatter (season · yr N). - ServerRegistry::shared_groves scaffold (returns empty until multi- grove membership plumbing lands; TODO comment pins the follow-up). 9 new client tests in tests/profile_view.rs cover: field merges, defaults, is_self, handle derivation, since_hint shape, delta default, broadcast round-trip, shared-groves empty contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ckname Adds crates/web/src/profile/ with: - bus.rs — open_profile / close_profile / event constants (CustomEvent on window). - controller.rs — use_profile_controller() installing window listeners once, resolving user_id into ProfileView via ClientViewHandle. - copy.rs — exact §Copy strings from the spec as load-bearing consts. - crest.rs — CrestBanner procedural SVG (fronds / rings / leaf) seeded by blake3(peer_id). 6 unit tests cover crest_defaults resolution + seed determinism. - nickname_store.rs — WebNicknameStore localStorage impl with native in-memory fallback. 3 unit tests. Wires AppState.profile + AppWriteSignals.profile signal pair so the controller can push ProfileState through the existing create_signals bundle. Adds CustomEvent / CustomEventInit / History / DomRect to web-sys features. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the 68-line ProfileCardStub with the real content leaf. Renders
crest banner, verification badge, close button, avatar, presence dot,
display name, pronouns pill, handle + nickname line, status pill, bio,
tagline, pinned fragment (quote curly quotes / fragment plain),
elsewhere chips, since + fingerprint meta rows, primary action row
(peer: message/call/whisper/more; self: edit profile full-width), and
secondary row (peer: copy fingerprint / set nickname / block; self:
\"this is you · <3 words>\" caption).
- Variant enum ProfileVariant { Peer, Self_ } drives every layout
branch the spec calls out in §Self view.
- Verification badge tap sets AppState.trust.compare_target + closes
the card — the existing <AddFriendDialog> takes over.
- edit profile tap (self) opens Settings tab = Profile.
- Inline nickname editor: Enter saves, Escape cancels, blur saves,
empty clears.
- copy fingerprint routes through util::copy_to_clipboard and does
NOT close the card.
- Old ProfileCardStub kept as a #[deprecated] thin wrapper so
phase-1e presence surfaces keep compiling until migration.
CSS at crates/web/style.css appends 17-selector block under
\"/* Phase 2c Profile card */\". Foundation tokens only; desktop/mobile
breakpoint via @media (max-width: 720px).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- <ProfilePopover>: reads use_profile_controller() signal, measures anchor rect, chooses position (right of anchor → flip left → clamp), renders <ProfileCardContent>. Hidden on mobile via CSS media query. - <ProfileSheet>: scrim + slide-up sheet with 22px top corners, drag handle pill, wraps <ProfileCardContent>. Hidden on desktop. - Both dispatch close_profile() on any navigating action. - App root now provides NicknameStoreHandle in context so the nickname editor reads/writes without touching ClientHandle. - Both wrappers mounted once at the app root next to <AddFriendDialog>. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every avatar surface now dispatches crate::profile::open_profile(&peer_id, Some(anchor)) so the global controller resolves it into a ProfileView and pushes onto the open signal — the popover / sheet wrapper takes over from there. - MessageView author button (`.author-btn`): uses the button itself as the anchor so the desktop popover positions against it. - MemberList row: new `.member-name-btn` wraps the display name + short peer id. - Channel-sidebar me-strip: opens profile-card in self variant; the `on_settings_click` prop is retained (kept compiling; superseded by the card's \"edit profile\" CTA internally). - ParticipantTile (voice call): tile-avatar click opens the card; stop_propagation prevents the tile-focus handler from firing. Grove-rail tiles represent servers (not peers) so they don't route through the profile bus. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 wasm-pack tests covering the profile card surface: - leaf_renders_all_peer_fields — 17-field peer view spec compliance. - leaf_self_variant_shows_edit_profile — variant flag flips. - leaf_omits_missing_peer_fields_on_peer_variant — spec §Edge cases. - leaf_has_role_dialog_and_aria_label — a11y contract. - crest_defaults_to_leaf_moss_when_unset — foundation fallback. - crest_is_deterministic_for_same_peer_id — seed stability. - badge_click_sets_compare_target — trust-verification handoff. - nickname_editor_save_on_enter / escape_cancels — local-only editor. - open_profile_and_close_profile_dispatch_window_events — bus contract. Each test provides fresh AppState + AppWriteSignals + TrustStore + NicknameStore in context so reactive effects fire without bleeding between tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ProfilePopover installs an Effect that (1) captures the currently- focused element before the card opens, (2) moves focus to the first focusable button inside .profile-popover on the next tick, and (3) restores focus to the previously-focused element when the card closes. - ProfileSheet mirrors the same pattern for the mobile wrapper. Spec §Accessibility: \"On open, focus moves to the card (first focusable element in the primary action row, or the close button on desktop if present). On close, focus returns to the anchor element.\" Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final sweep flips every completed checkbox in the phase-2c plan and the self-review section. All 14 tasks + 17 acceptance rows mapped to either shipped code or explicit TODO references (sync-queue / letters-dms / call-experience / whisper-mode / thread-pane all land in their own phases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves conflict at EOF of crates/web/tests/browser.rs — both sides appended independent test modules (phase_2c_profile_card here, then foundation_tokens from PR #184). Keep both. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
web_sys 0.3.91 deprecates EventInit::bubbles(bool) in favor of set_bubbles(bool). CI's rustc/web-sys toolchain flags it under -D warnings. Move the init construction out of the call site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Apr 22, 2026
# Conflicts: # crates/client/src/lib.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Plan + implementation for docs/specs/2026-04-19-ui-design/profile-card.md
in a single PR.
<ProfileCardContent>leaf + desktop<ProfilePopover>+ mobile<ProfileSheet>wrappersopen_profile/close_profile) wired from every avatar surface (message row, member list, me-strip, participant tile)edit profile+this is you · <3 words>caption)AppState::trust::compare_target→ existing<AddFriendDialog>compare flow handoffNew state on
Profile:pronouns,bio,tagline,crest_pattern,crest_color,pinned,elsewhere,since. One newEventKind::UpdateProfile(Box<ProfileDelta>). Caps truncation on apply; invalidcrest_colorvalues drop toNone.#[serde(default)]keeps pre-phase-2c events wire-compatible.New helpers:
willow_crypto::peer_fingerprint(peer)(per-peer 6-word, distinct DS tag from SAS),willow_client::ProfileView+profile_view_of(peer, local)selector,willow_client::ProfileDelta,ClientMutations::update_profile_fields,since_hint(earliest_ms, now_ms).Test plan
cargo fmt --checkpassescargo clippy --workspace -- -D warningszero warningscargo test -p willow-state203 tests green (9 new forUpdateProfile)cargo test -p willow-client150 tests green (9 new forProfileView+since_hint+shared_groves, 7 new forNicknameStore)cargo check --target wasm32-unknown-unknown -p willow-webcleanjust test-browser(CI) — 8 new tests inphase_2c_profile_cardmodule🤖 Generated with Claude Code