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
| Layer | Before | After |
|---|---|---|
| Header | AppBar with 3 progress dots in the title slot | Custom 2-line header: close button + “Passo N di 3” small-caps + slim 3-segment progress bar with AppTheme.brandGradient fill |
| Step titles | Material headlineMedium, mid weight | Editorial 28px / weight 900 / -0.5 letter-spacing — matches event-details v3 |
| Surface | Theme scaffoldBackgroundColor (warm canvas) | AppTheme.pureWhite everywhere |
| Bottom CTA | Inline 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 inputs | CustomTextField with boxy fill | _BrandedTextField — underline style, small-caps coral label that lights up on focus |
| Event picker | Plain pill chips with title + time | _EventChip with cover image (proxied via imageUrlForPlatform), day + time, selection state. Loader widened to 7 days |
| Step 2 vibe grid | Wrap with uneven pill widths | 2-column GridView of _VibeCard (emoji + label + check icon on selection); rotating vibe colors via AppTheme.vibeColorAt(i) |
| Step 3 size | Slider 3–20 | _MembersStepper with big 32pt numeral + −/+ buttons (44pt tap targets) |
| Step 3 duration | SegmentedButton 3/6/12/24h | 4 × _DurationPill in a Row — coral fill on active, label below (“Stasera” / 6h / 12h / “Tutta la notte”) |
| Summary | Plain card with BadgeSticker + 1-line subtitle | _SummaryCard with brand-gradient tint, large crew name, 3 chips (vibe / members / duration), live-updates |
| Haptics | None | HapticFeedback.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)
| Key | Used by |
|---|---|
crewWizardStepIndicator (placeholders: current/total int) | Header step label |
crewWizardClose | Close button tooltip |
crewWizardEventsThisWeek | Picker section header |
crewWizardNoEventsHint | Hint below event picker |
crewDurationTonight | 3h pill caption |
crewDurationLateNight | 24h pill caption |
crewDurationHoursShort (placeholder hours: int) | 6h / 12h pill caption + summary chip |
crewMembersValue (placeholder count: int) | Stepper caption + summary chip |
crewSummaryReadyTitle | Summary card section label |
crewSummaryReadySub | Summary card subtitle |
crewLaunchCta | Bottom 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).
HapticFeedbackon selection / step transition / submit, with appropriate intensity per action.- Sticky bottom CTA: thumb doesn’t move between steps; visible while scrolling step content.
2026-05-08 follow-up — autolink, karma cap, derived expiry
Three product changes layered on top of the visual rewrite:
Autolink event from event-details CTA
/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_score | Max members |
|---|---|
| < 3.5 | 4 |
| 3.5–3.99 | 6 |
| 4.0–4.49 | 10 |
| ≥ 4.5 | 20 |
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
_DurationPillwidget._DurationOptionmodel.- Step 3’s duration row.
_SummaryCardno longer readsstate.durationHours— takes aderivedHoursparam.
New l10n keys (en/it/es)
| Key | Where 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:
| Field | Source |
|---|---|
| Meeting point | events.location.name (jsonb) — manual input hidden |
| Vibe | events.category via _categoryToVibe mapping — step skipped |
| Duration | event.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.category | Mapped vibe |
|---|---|
nightlife, clubbing | Rave |
music, live_music | Techno |
party | Trash |
food_drinks | Aperitivo |
culture, arts | Indie |
social, networking | Chill |
| anything else | capitalized 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 viaimageUrlForPlatform).
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
| Key | Use |
|---|---|
crewStepBasicsSubtitleLinked | Step 1 subtitle when an event is linked |
crewLinkedEventLabel | _LinkedEventChip header (“LINKED EVENT”) |
crewLinkedEventUnlink | Unlink icon tooltip |
crewInviteSheetTitle / crewInviteSheetSubtitle (crewTitle) | Share sheet header |
crewInviteFriendsSection / crewInviteGroupsSection | Section labels |
crewInviteEmpty | Empty state |
crewInviteSkip / crewInviteSendCtaIdle / crewInviteSendCtaWithCount (count) | Footer CTAs |
crewInviteMessageLead (crewTitle) / crewInviteMessageOpenApp | Sent message body |
crewInviteSentCount (count) / crewInviteSentMixed (sent, failed) | Result SnackBar |
groupChatFallbackName | Group display name fallback |
Build / verify
flutter pub get
flutter gen-l10n
flutter analyze # 0 issues — verified 2026-05-08Related
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