feat: new user card, hover triggers and scoped roles

Draft
IV
3mo ago
+1586-709

feat: New user card, hover triggers and scoped roles

Task: CORE-1962

Summary

Implements the new user card design (Figma spec), changes how it is triggered (hover instead of click) and reworks how user roles are presented across the room UI, splitting them by workspace and room scope everywhere they appear.

New card anatomy

  • Workspace roles band at the top (Slack-like) with a "Workspace roles" tooltip: the user's global public roles, visually distinct from room roles
  • Compact header: 36px avatar, presence status, name (titles-labels token) with an inline lightweight nickname, and custom status
  • Icon-led info list with tooltips on the icons: username first, job title (business icon), room role tags (outline shield, fuselage medium tags), local time with the UTC offset ("3:04 PM local time (UTC -3)") — the bio stays in the user info contextual bar — plus a "See member profile" link
  • Labeled action buttons with icons: Message and Mute, plus the overflow kebab menu with a reorganized taxonomy: a "Manage room roles" section (owner/leader/moderator toggles), moderation actions (remove from room, ban) and an admin section with the moderation console shortcut
  • No explicit close button: the card is dismissed by hovering away, Escape or interacting outside
  • 400px-wide (25rem) dialog, 8px radius — all spacing uses rem-based fuselage tokens

Hover behavior

  • 500ms open delay / 300ms close delay (hover intent); click on the collapsed role tags opens it immediately
  • While open, hover is tracked geometrically (document mousemove against the card, trigger and any spawned menu rects). Synthetic mouseenter/mouseleave proved unreliable on the portaled popover: the local-time clock re-renders every second under a resting pointer, firing spurious leave events that closed the card.
  • Message author names are underlined on hover; the card anchors 0.25rem from its trigger

Roles by scope

  • Message header: one collapsed tag per scope — "admin +2" (workspace) and "owner +1" (room) — each with a tooltip listing that scope's roles; tags open the user card on click
  • User card: workspace roles in the top band, room roles as tags in the info list
  • User info contextual bar: separate "Workspace roles" and "Room roles" fields (the latter only in a room context), both reactive
  • All three surfaces share the same scope-aware source (useUserRolesByScope)

Message header refinements

  • The @username no longer renders next to the real name (it remains in the name tooltip and in the card)
  • Name and avatar tooltips honor the "Mentions with @ symbol" preference
  • Timestamps are smaller (10px) in the secondary font color, on regular and system messages; system message names use the titles-labels token
  • Quoted message timestamps underline on hover, since they are a jump action

User info contextual bar

  • Username leads the fields; roles split by scope as above
  • Every text info (username, nickname, bio, title, nationality, languages, phone, email, voice extension, custom fields) reveals a copy-to-clipboard button on hover/focus
  • The @username also left the system message header when real names are displayed

New profile fields

Three optional user fields — title, nationality and languages — editable in Account > Profile (languages as a comma-separated input stored as an array), validated and persisted by saveUserProfile, exposed through users.info, displayed in the contextual bar and (title) in the user card.

Pre-existing upstream bugs fixed along the way

Live role updates were broken for any consumer (message header tags, user card), not just this feature — a full reload was required to see role changes:

  • useRoomRolesQuery's roles-change handler kept the guard from the global-roles variant in its removed case (!!scope), so every room-scoped role removal was silently ignored.
  • Both useRoomRolesQuery and useUserRolesQuery handlers mutated the cached records in place (data[index] = ...) before returning a copy. react-query's structural sharing then compared the new data against the already-mutated old data, found them deep-equal, kept the old reference and never notified observers — the cache held the right value (hence reload "fixing" it) but the UI never re-rendered.

Behavior changes needing sign-off

  • Click → hover: user-facing interaction change on every message
  • @username removed from the message header when real names are displayed

Keyboard support

Triggers (author name, system-message user and @mentions) are focusable buttons: Enter/Space opens the card immediately, focus moves into the dialog, Escape closes it and returns focus to the trigger. A keyboard-opened card is not dismissed by stray pointer movement.

Accessibility audit (WCAG 2.1 AA)

Audited against the rendered component: all text passes contrast (≥ 6.17:1), every action is a real button with a visible label and focus ring, the info list is a description list with visually hidden terms, and the workspace roles band is announced as "Workspace roles: …". Fixed during the audit: the profile link had no visible focus indicator (2.4.7) and the band's aria-label suppressed its content for screen readers.

Testing

  • e2e page object and keyboard-navigation spec updated; UserInfo and RoomMessage unit specs green with updated snapshots
  • Storybook stories updated for the new card anatomy and scoped MessageRoles API
  • Verified live: hover open/close, card and kebab-menu hover persistence, click on collapsed role tags, reactive role updates in both directions, tooltips, truncation

🤖 Generated with Claude Code