Profile + Companion-as-Proxy — Shell Chrome Redesign — Design Spec
Date: 2026-07-26
Status: Draft for owner review
Branch: feat/mvp1-personas-authz
Depends on: the shipped My Cosmos companion + the account-switcher / selected-member lens (existing).
Goal
Reframe the shell chrome so the celestial companion is the active member's identity, shown consistently top and bottom, and fold account/settings into a single Profile tab. Concretely: move the companion into the top app bar as the active member's avatar; layer its cosmic ambiance correctly behind content; replace the bottom-nav More tab with a Profile tab whose icon dynamically shows the active member's companion (emoji fallback); and make Profile the one place you switch the active member and manage the login account.
Core model — the active-member lens (with authz invariant)
- Active member = the existing
SelectedMemberRepositoryselection (today surfaced as "Browsing as {member}"). Switching it re-scopes what the UI displays — companion, avatar, balance, catalogs — to that member. - AUTHZ INVARIANT (load-bearing, do NOT weaken): the selection is a display lens only. The authenticated (login) member (
CurrentMemberRepository.current) remains the sole real actor for every gated call and for RLS. Proxying into a member never elevates authorization. Richer "act-as" IAM is explicitly a future phase, out of scope here. - The switcher stays capability-gated: only viewers who already may select a non-self identity see other members in the switch list (a child is locked to self; the existing predicate governs this).
The companion splits into two layers
Today DsCompanionScene bundles the creature + a whole-screen cosmic-dim + the "+N" float, anchored bottom-left over the horizon. This redesign splits it:
- Creature → top-left of the top app bar (a compact, animated avatar), on Today / Earn / Rewards / Household. It renders the active member's companion (type + colorKey + stage + adornments). Tap → the existing tap-to-focus companion sheet (creature + adornments + Stardust balance). Proxy read-only rule: when the active member ≠ the authenticated self, the sheet is read-only — buy/equip/rename are the owner's self-action, and the SDK's self-only companion mutation already forbids them, so the UI hides/disables those affordances when proxied (viewing another member shows their companion + balance, not editable). Future hook (not this build): this avatar becomes the entry point for companion-driven chore recommendations / task breakdowns — so it is built as an interactive affordance, not décor.
- Ambient (cosmic-dim twinkles / glow) → a backdrop atmosphere layer that sits behind the content and in front of the houses/horizon backdrop (z-order: houses backdrop → cosmic ambient → content). The "dim as days-idle grows" behavior (capped, forgiving, reduced-motion-safe) moves here; the whole-screen bottom-left ambient scene widget is retired.
- The "+N" Stardust float anchors near the top-left companion on a reaction (owner completes a chore).
The dynamic identity avatar (one resolver, used in two places)
A single DS/app helper resolves the active member's identity avatar:
identityAvatar(member, companionView):
if companionView != null AND companionView.isCreated AND enabled(member)
→ the companion creature (DsCompanionCreature: type/colorKey/stage), full colour
else
→ the member's emoji avatar (member.emoji + member.colorKey)
The SAME resolver drives BOTH:
- the top-left app-bar avatar (larger, animated, tappable → companion sheet), and
- the bottom-nav Profile tab icon (nav-icon size, full-colour, independent of the nav's selected/unselected tint — like a Google account photo).
So the identity is always identical top and bottom, and updates together when the active member switches.
Navigation — More → Profile
- The bottom-nav 5th slot (adults) / 4th slot (children) becomes Profile, replacing More in BOTH role nav sets (
MainShellPageadult + child destination lists). Household stays adult-only. - Profile tab icon = the identity avatar (companion-when-configured, else emoji) of the active member + a short name label (the member's display name, ellipsized if too long).
- On the tablet nav rail, Profile sits top-left of the rail (identity-at-top convention); on phone it is the trailing bottom-nav slot (bottom-right).
The Profile page
Anchored on the login (authenticated) account, with member-scoped subsections following the active member:
- Login-account frame (authenticated self): account settings, About, Help, Join-with-code, and Logout at the very bottom of the page (removed from the top header — reclaims that space).
- Active-member switcher: the account-switcher list (Google-style), capability-gated. Selecting re-scopes the app; the top-bar avatar + the Profile nav icon update together.
- Active-member subsections: My Cosmos (the companion settings page) and per-member appearance/companion preferences reflect the active member, not the login self. The same proxy read-only rule applies: editing My Cosmos (rename/type/color/adornments) is enabled only when the active member IS the authenticated self; while proxied it is a read-only view of that member's cosmos (SDK self-only mutation enforces this).
Top app bar context strip
Across Today / Earn / Rewards / Household, the top app bar carries: the companion avatar (top-left) + a "viewing {member}" context cue shown only when proxied (active member ≠ authenticated self; hidden when self, where it is redundant). It carries NO balance pill. This replaces the inline _EarnBrowseLabel ("Browsing as {name}") on Earn/Rewards and the old top-header account-switcher chip.
No token balance in the chrome (owner decision 2026-07-26). The top-bar number would be only the member's UNALLOCATED / spendable pool — it excludes tokens set aside for goals, budgets, etc. — so a lone figure repeated in every header is a partial signal that trains the wrong mental model. The full wallet (unallocated + goals + set-aside) stays authoritative in ONE place, the Household tab; Rewards guides spend via each reward's own affordability cue. ActiveIdentity_Cubit therefore carries only member + companionView (no wallet/balance).
Idle animation
The companion idle currently runs (didChangeDependencies → _idle.repeat, honoring MediaQuery.disableAnimations) but is a near-invisible 2px bob. Add a breathing scale (~±4%) + a soft glow pulse on the same ~2.6s period, layered on the bob, reduced-motion-safe (frozen to the static happy pose under reduced motion, exactly as now). Applies wherever DsCompanionCreature renders (top-bar avatar, sheet, My Cosmos, nav icon).
Fallbacks & roles
- No companion / not created / watch-only / companion disabled → the identity avatar falls back to the member's emoji (both top-bar and nav icon), so there is never an empty avatar.
- Children (
MemberKind.child): 4-tab nav (Home/Earn/Rewards/Profile), no Household; the switcher shows only self (capability-gated), so their Profile is self-only + settings + logout. - Reduced motion: the ambient atmosphere and the idle both honor
MediaQuery.disableAnimations(static, no motion requirement).
What is retired
- The standalone bottom-left ambient companion scene widget (
companion_layer.dart's bottom-anchoredDsCompanionScene) — its creature moves to the top-bar avatar; its ambiance moves to the backdrop atmosphere layer. - The inline
_EarnBrowseLabel("Browsing as {name}") on Earn/Rewards. - The top-header account-switcher chip + the top-header logout affordance (logout → Profile bottom).
Architecture / one data path
- Presentation only: Bloc/Cubit → Repository →
client_sdkfacade. No new SDK domain rules; the account-switcher/SelectedMemberRepository+CompanionCubit+CurrentMemberRepositoryalready expose what's needed. The identity-avatar resolver is a pure presentation/DS helper. - Typed errors only; immutable state; new app classes use the gadfly underscore
{Name}_Suffixconvention; DS additions areDs-prefixed; brand copy viaStrings.
Scope boundary
In scope: companion layer split (creature → top-bar avatar; ambient → backdrop layer); the identity-avatar resolver (companion-or-emoji) used by the top-bar avatar + the Profile nav icon; idle breathing/glow animation; nav More→Profile (both role sets, phone + rail); the Profile page (login-account frame + active-member switcher + active-member subsections incl. My Cosmos + logout at bottom); the top-bar context strip (companion + viewing-as + balance); retiring the old ambient widget + _EarnBrowseLabel + top-header chip/logout; tests (widget/golden for the avatar resolver + nav icon + idle; flow stories for switch-updates-identity + Profile-hosts-settings/logout).
NOT in scope (future phases): companion-driven chore recommendations / task breakdowns; real "act-as" IAM / role-based authorization beyond the current display lens; any change to the SP-C economy, RLS, or the companion data model.
Testing
- DS golden/widget:
DsCompanionCreatureat nav-icon size (companion avatar) + the breathing/glow idle (golden at rest); the identity-avatar resolver's two branches (companion vs emoji). - App widget: Profile nav icon shows companion when configured, emoji otherwise, and updates on active-member switch; Profile page hosts the switcher + settings + logout-at-bottom.
- Flow (gadfly-canonical, per-epic): a shell/identity epic — switching the active member updates the top-bar companion + the Profile nav icon together; Profile hosts settings + logout; the top-bar context shows "viewing {member}" + balance. Reduced-motion path holds the static pose. Baselines must not drop.