Skip to content

ui(phase-2c): profile-card — plan + implementation#188

Merged
intendednull merged 19 commits into
mainfrom
phase-2c/profile-card
Apr 25, 2026
Merged

ui(phase-2c): profile-card — plan + implementation#188
intendednull merged 19 commits into
mainfrom
phase-2c/profile-card

Conversation

@intendednull
Copy link
Copy Markdown
Owner

Summary

Plan + implementation for docs/specs/2026-04-19-ui-design/profile-card.md
in a single PR.

  • Shared <ProfileCardContent> leaf + desktop <ProfilePopover> + mobile <ProfileSheet> wrappers
  • Event-bus entry (open_profile / close_profile) wired from every avatar surface (message row, member list, me-strip, participant tile)
  • All 17 peer-view fields (crest banner, verification badge, avatar, presence, display name, pronouns, handle + nickname, status, bio, tagline, pinned fragment, shared groves, elsewhere, since, fingerprint, primary + secondary rows)
  • Self-view variant (full-width edit profile + this is you · <3 words> caption)
  • Private nickname inline editor (local-only, localStorage-backed on wasm, in-memory on native)
  • Badge tap → AppState::trust::compare_target → existing <AddFriendDialog> compare flow handoff

New state on Profile: pronouns, bio, tagline, crest_pattern, crest_color, pinned, elsewhere, since. One new EventKind::UpdateProfile(Box<ProfileDelta>). Caps truncation on apply; invalid crest_color values drop to None. #[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 --check passes
  • cargo clippy --workspace -- -D warnings zero warnings
  • cargo test -p willow-state 203 tests green (9 new for UpdateProfile)
  • cargo test -p willow-client 150 tests green (9 new for ProfileView + since_hint + shared_groves, 7 new for NicknameStore)
  • cargo check --target wasm32-unknown-unknown -p willow-web clean
  • just test-browser (CI) — 8 new tests in phase_2c_profile_card module

🤖 Generated with Claude Code

intendednull and others added 15 commits April 21, 2026 14:36
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>
intendednull and others added 3 commits April 21, 2026 15:39
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>
@intendednull intendednull merged commit ba47572 into main Apr 25, 2026
6 checks passed
@intendednull intendednull deleted the phase-2c/profile-card branch April 25, 2026 06:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant