Summary

End-to-end visual rewrite of lib/features/social/screens/create_crew_screen.dart (~720 lines). State management is unchanged — createCrewWizardProvider still drives all transitions; this pass touches only the visual + ergonomic layer.

Branch: edit/mobile-audit-ux-polish · Date: 2026-05-08.

What changed

LayerBeforeAfter
HeaderAppBar with 3 progress dots in the title slotCustom 2-line header: close button + “Passo N di 3” small-caps + slim 3-segment progress bar with AppTheme.brandGradient fill
Step titlesMaterial headlineMedium, mid weightEditorial 28px / weight 900 / -0.5 letter-spacing — matches event-details v3
SurfaceTheme scaffoldBackgroundColor (warm canvas)AppTheme.pureWhite everywhere
Bottom CTAInline CustomButton per step (jumped position step-to-step)Sticky 54pt coral pill in a Container with thin top border. Same screen position across all 3 steps
Step 1 inputsCustomTextField with boxy fill_BrandedTextField — underline style, small-caps coral label that lights up on focus
Event pickerPlain pill chips with title + time_EventChip with cover image (proxied via imageUrlForPlatform), day + time, selection state. Loader widened to 7 days
Step 2 vibe gridWrap with uneven pill widths2-column GridView of _VibeCard (emoji + label + check icon on selection); rotating vibe colors via AppTheme.vibeColorAt(i)
Step 3 sizeSlider 3–20_MembersStepper with big 32pt numeral + −/+ buttons (44pt tap targets)
Step 3 durationSegmentedButton 3/6/12/24h4 × _DurationPill in a Row — coral fill on active, label below (“Stasera” / 6h / 12h / “Tutta la notte”)
SummaryPlain card with BadgeSticker + 1-line subtitle_SummaryCard with brand-gradient tint, large crew name, 3 chips (vibe / members / duration), live-updates
HapticsNoneHapticFeedback.selectionClick() on every meaningful tap; lightImpact on step transitions; mediumImpact on launch

Files changed

  • lib/features/social/screens/create_crew_screen.dart — full rewrite (584 → ~720 lines)
  • lib/l10n/app_en.arb / app_it.arb / app_es.arb — 11 new keys

New l10n keys (en/it/es)

KeyUsed by
crewWizardStepIndicator (placeholders: current/total int)Header step label
crewWizardCloseClose button tooltip
crewWizardEventsThisWeekPicker section header
crewWizardNoEventsHintHint below event picker
crewDurationTonight3h pill caption
crewDurationLateNight24h pill caption
crewDurationHoursShort (placeholder hours: int)6h / 12h pill caption + summary chip
crewMembersValue (placeholder count: int)Stepper caption + summary chip
crewSummaryReadyTitleSummary card section label
crewSummaryReadySubSummary card subtitle
crewLaunchCtaBottom CTA on step 3

Schema fix on the way through

_loadUpcomingEvents previously selected events.location_name — that column does NOT exist (the event location lives inside the events.location jsonb). Fixed to select image_url, location and extract location.name client-side. This silently broke the picker on every render before this pass.

Touch ergonomics

  • All tappable elements are >= 44pt tall (iOS HIG / Material 3 minimum tap target).
  • HapticFeedback on selection / step transition / submit, with appropriate intensity per action.
  • Sticky bottom CTA: thumb doesn’t move between steps; visible while scrolling step content.

Three product changes layered on top of the visual rewrite:

/social/create-crew?eventId=… now pre-loads that event row, prefills linkedEventId / linkedEventTitle / location (extracted from events.location.name), and stamps end_date for the derived expiry.

CreateCrewScreen constructor takes an optional initialEventId. Router:

GoRoute(
  path: 'create-crew',
  builder: (context, state) {
    final eventId = state.uri.queryParameters['eventId'];
    return CreateCrewScreen(initialEventId: eventId);
  },
),

The user no longer has to find the same event again from the picker after tapping “Create crew” on the event-details empty state.

Karma-tier max member cap

Default karma is 3.0 (app_config.karma_default). Tiers:

karma_scoreMax members
< 3.54
3.5–3.996
4.0–4.4910
≥ 4.520

Helper maxCrewSizeForKarma(double) lives in create_crew_wizard_notifier.dart so other surfaces can apply the same gate. The wizard’s _MembersStepper max prop is wired to it; the initial state.maxMembers (default 10) is clamped to the cap on first build. A _KarmaCapHint banner (warm-sun toned) appears under the stepper only when the user has hit their tier ceiling.

karma_score is the canonical “experience + reliability” signal for this feature — it’s bounded, well-defined, and updated by crew_feedback. flow_score / vibe_score / xp untouched.

Derived crew expiry

The duration step is gone. Crew expiry is now derived:

  • Linked event: expires event.end_date + 6h (covers after-party / journey home).
  • Standalone: 12h flat from creation.

Helper crewExpiryHoursFor({now, eventEndDate, bufferAfterEnd = 6, standaloneHours = 12}) returns the duration-hours int that crewNotifier.createCrew expects, clamped to [1, 72].

The wizard surfaces this as a read-only _DerivedExpiryRow (“Closes in Nh” + subtitle “6 hours after event ends” / “Standalone crews stay open for 12 hours”) so the user understands the policy without choosing.

New widgets / helpers

  • _KarmaCapHint — warm-sun banner explaining the cap.
  • _DerivedExpiryRow — read-only expiry display.
  • maxCrewSizeForKarma(double karma) — tier function (notifier file).
  • crewExpiryHoursFor({now, eventEndDate, ...}) — expiry function (notifier file).

Removed

  • _DurationPill widget.
  • _DurationOption model.
  • Step 3’s duration row.
  • _SummaryCard no longer reads state.durationHours — takes a derivedHours param.

New l10n keys (en/it/es)

KeyWhere used
crewMaxCapTitle (placeholder max: int)_KarmaCapHint title
crewMaxCapSubtitle (placeholder karma: String)_KarmaCapHint subtitle
crewExpiryDerivedTitle (placeholder hours: int)_DerivedExpiryRow title
crewExpiryDerivedSubtitleEvent_DerivedExpiryRow subtitle (event-anchored case)
crewExpiryDerivedSubtitleStandalone_DerivedExpiryRow subtitle (no event)

2026-05-09 — Adaptive steps when event is linked + invite share sheet

Adaptive step count

When the wizard opens via /social/create-crew?eventId=X:

FieldSource
Meeting pointevents.location.name (jsonb) — manual input hidden
Vibeevents.category via _categoryToVibe mapping — step skipped
Durationevent.end_date + 6h (existing derived expiry)

Total steps drops 3 → 2: Basics → Size. The header indicator and progress bar are driven by _totalStepsFor(state) so the user sees “Step 1 of 2” rather than the stale “1 of 3”.

PageView keeps 3 fixed children (no mid-flight rebuild). _pageIndexFor(state, step) hops 0 → 2 in linked mode, so step transitions stay smooth without losing the PageController state.

A _LinkedEventChip above the name input shows the linked event title and offers a tap-to-unlink affordance. Unlinking clears the prefilled meeting point so the standalone fallback doesn’t keep a stale venue name.

Category → vibe mapping

events.categoryMapped vibe
nightlife, clubbingRave
music, live_musicTechno
partyTrash
food_drinksAperitivo
culture, artsIndie
social, networkingChill
anything elsecapitalized passthrough

Post-creation invite share sheet

New widget CrewInviteShareSheet (lib/features/social/widgets/crew_invite_share_sheet.dart) opens after _submitCrew succeeds. Two sections:

  • Friends — from FriendService.getFriends(), multi-select with avatar + name.
  • Group chats — from MessagingApiService.getChats(type: group), multi-select with cover (proxied via imageUrlForPlatform).

Sticky footer: “Skip” + coral “Send invites (N)” CTA.

Per target the sheet posts a localized message via MessagingApiService.sendMessage with metadata.type = 'crew_invite' (+ crew_id / crew_title / vibe). Friends route through createDirectChat to find/create the DM. Per-target failures are tolerated; the final SnackBar reports either "Sent N invites" or "Sent N, M failed".

New l10n keys

KeyUse
crewStepBasicsSubtitleLinkedStep 1 subtitle when an event is linked
crewLinkedEventLabel_LinkedEventChip header (“LINKED EVENT”)
crewLinkedEventUnlinkUnlink icon tooltip
crewInviteSheetTitle / crewInviteSheetSubtitle (crewTitle)Share sheet header
crewInviteFriendsSection / crewInviteGroupsSectionSection labels
crewInviteEmptyEmpty state
crewInviteSkip / crewInviteSendCtaIdle / crewInviteSendCtaWithCount (count)Footer CTAs
crewInviteMessageLead (crewTitle) / crewInviteMessageOpenAppSent message body
crewInviteSentCount (count) / crewInviteSentMixed (sent, failed)Result SnackBar
groupChatFallbackNameGroup display name fallback

Build / verify

flutter pub get
flutter gen-l10n
flutter analyze   # 0 issues — verified 2026-05-08

fix-crew-creation-v2 — earlier wizard pass (still active for state management) feat-event-details-v3 — design system reference for editorial style localization — l10n key reference