Feed Widgets — Design System

Direzione: Light Foundation + Cinematic Pockets

Basata sulla ricerca estetica su Airbnb (2025), Eventbrite “The Path”, e Duolingo “Juicy System”. Tutti e tre usano una base chiara con accenti scuri/colorati strategici — lo stesso principio di Flow.


Filosofia

Il feed di Flow alterna tre “toni” visivi:

TonoWidgetEffetto
CinematicHeroEventCardScuro, pieno schermo, foto + gradiente
ElevatedMinimalEventCard (standard)Bianco puro, shadow Airbnb
Energy AuraSquadBentoSectionFull-bleed con il colore della vibe
Dark compactSocialInviteCardInk black, compatto, CTA coral
EditorialVibeQuoteWidgetTesto flottante senza container

Il ritmo del feed deve mai mettere due pocket scuri consecutivi.


Palette

TokenValoreUso
AppTheme.warmCanvas#F0EEE9Scaffold background (tema chiaro)
AppTheme.inkBlack#121212Pocket scuri, card dark
AppTheme.electricCoral#FF5E57Accento primario, date, CTA
AppTheme.pureWhite#FFFFFFCard bianche (MinimalEventCard)

Lo scaffold del tema chiaro usa warmCanvas (leggermente caldo) invece del bianco puro. Questo evita l’effetto “blank page” e fa risaltare le card bianche con shadow leggere.


HeroEventCard

Non modificare — è il riferimento cinematico del feed.

  • Background scuro con gradiente to-bottom
  • Testo bianco, badge coral SCELTO PER TE 🔥
  • Shadow coral (electricCoral.withValues(alpha: 0.25))
  • borderRadius: 28, height: 400

MinimalEventCard (standard)

Airbnb elevation style.

┌────────────────────────────┐
│  [immagine 16:9]   [€ chip]│
├────────────────────────────┤
│ VEN 21 MAR · 22:00         │  ← coral, uppercase, 11px w800
│ Titolo Evento Bold         │  ← 18px w900 letterSpacing -0.3
│ 📍 Location     [68 ci vanno] │
└────────────────────────────┘
  • Background: Colors.white (light) / Color(0xFF1E1E1E) (dark)
  • Shadow: 0 6px 20px rgba(0,0,0,0.07) + 0 2px 4px rgba(0,0,0,0.03)
  • borderRadius: 24 (non 32 — più moderno, meno rotondo)
  • Data in electricCoral, nessun box calendario
  • “N ci vanno” pill: Color(0xFFF4F4F5) background
  • Urgency labels: ORA, OGGI, DOMANI, TRA N GIORNI, o GIO 21 MAR

CrewBentoSection — Energy Aura

Nota terminologica: “Squad” è il termine interno nel codice (file, provider, modelli). Tutta la UI visibile all’utente usa “Crew”.

Ogni crew card usa il colore della vibe come sfondo full-bleed con gradiente verso il basso scuro.

Modello dati

Il modello Squad ha un campo name (mappa alla colonna title nel DB). Se title è vuoto, Squad.fromJson usa vibe_tag come fallback.

name: (json['title'] as String? ?? '').isNotEmpty
    ? json['title'] as String
    : json['vibe_tag'] as String,

Colori vibe

Le vibe conosciute hanno colori fissi (vedi _vibeColors in squad_bento_section.dart).

Per vibe sconosciute, viene usato un hash DJB2 deterministico sulla stringa vibe che seleziona dalla _fallbackPalette di 10 energy colors. Il risultato è consistente tra sessioni (a differenza di String.hashCode che Dart randomizza).

// Esempio: "Reggaeton" → hash → sempre lo stesso colore
Color _colorForVibe(String vibe) =>
    _vibeColors[vibe] ?? _fallbackPalette[_djb2(vibe) % _fallbackPalette.length];

Layout card

┌────────────────────┐
│  ✨ (emoji bianca) │  ← 22px, ShaderMask BlendMode.srcIn
│  Nome Crew         │  ← 15px w900 white, maxLines:1
│  [  vibe tag  ]    │  ← pill translucente white/18%
│  ████░░ 3/5        │  ← progress bar white
│  3/5 nel Flow      │  ← white70
│                    │
│  ⏱ 4h rimaste     │  ← white55
│  [  UNISCITI  ]    │  ← chip bianca, testo vibeColor
└────────────────────┘
  • Card: width: 168, padding: 14px tutti i lati, SizedBox(height: 232) per la sezione
  • Badge “TUA” in top-right per la propria crew
  • Action button: bianca con testo vibeColor (UNISCITI), o translucente per AL COMPLETO / LA MIA CREW
  • Cerchi decorativi white.withValues(alpha: 0.10) per profondità
  • Shadow: vibeColor.withValues(alpha: 0.35) — ListView ha bottom padding 20 per non tagliare il glow
  • Emoji bianca via ShaderMask(blendMode: BlendMode.srcIn) — non usare ColorFilter.matrix (i valori offset sono in spazio [0, 255], emoji su Android ignorano il matrix filter)

Stringhe UI → Crew

Vecchio (codice)Visibile all’utente
LA MIA SQUADLA MIA CREW
Lasciare la Squad?Lasciare la Crew?
Lancia la tua SquadLancia la tua Crew
Squad in corsoCrew in corso

SocialInviteCard

Dark compact pocket — rompe il ritmo tra card bianche senza essere imponente come HeroEventCard.

┌──────────────────────────────────┐
│ [🔴][🟣]  Il Flow è meglio      │
│            insieme               │  [Invita]
│            +100 Karma per amico  │
└──────────────────────────────────┘
  • Background: AppTheme.inkBlack
  • Avatar circles sovrapposti con gradient coral + violet
  • CTA button coral con shadow coral
  • Layout Row (non Column) — compatto, max 80px di altezza

VibeQuoteWidget

Editorial float — non ha container, “galleggia” sullo scaffold.

VIBRA DEL GIORNO          ← coral, 11px uppercase tracking
"La musica è l'unica..."  ← 22px w900 inkBlack, letterSpacing -0.5
— Flow                     ← grey500, 13px
  • Nessun Container o BoxDecoration
  • Padding: horizontal: 24, vertical: 8
  • Non uppercase (a differenza del vecchio widget) — più editoriale

Ritmo consigliato del feed

HeroEventCard         ← cinema pocket (scuro)
MinimalEventCard × 2  ← elevated white
SquadBentoSection     ← energy aura (colorato)
SocialInviteCard      ← dark compact
VibeQuoteWidget       ← editorial float
MinimalEventCard × 2  ← elevated white
...

Regola: non mettere mai HeroEventCard e SocialInviteCard consecutivi.


Mappa — eventi sopra la bottom nav

RadarMapScreen viene usata in modalità embedded: true dentro il PageView del feed. Il Scaffold principale ha extendBody: true, il che fa sì che il body si estenda dietro la bottom navigation bar. I widget Positioned in fondo alla mappa devono compensare questa sovrapposizione:

final bottomInset = widget.embedded
    ? kBottomNavigationBarHeight + MediaQuery.of(context).padding.bottom
    : MediaQuery.of(context).padding.bottom;
 
// Carousel eventi:  bottom: bottomInset + 24
// Pulsanti zoom/centro: bottom: bottomInset + 260

Regola: non usare valori bottom fissi nella mappa quando embedded: true. Usare sempre bottomInset calcolato come sopra.


File coinvolti

FileCosa cambia
lib/core/theme/app_theme.dartwarmCanvas (#F0EEE9), scaffold light = warmCanvas
lib/shared/widgets/minimal_event_card.dart_buildStandardCard redesign Airbnb-style
lib/shared/widgets/squad_bento_section.dart_SquadCard Energy Aura, crew name+vibe pill, DJB2 hash, ShaderMask emoji
lib/shared/widgets/feed_interruption_widgets.dartSocialInviteCard dark, VibeQuoteWidget editorial
lib/shared/models/squad_model.dartCampo name da colonna title DB
lib/features/events/screens/radar_map_screen.dartbottomInset per extendBody:true
lib/features/feed/screens/feed_screen.dartScrollController feed, refresh button completo
lib/features/social/screens/create_squad_screen.dartUI Crew rename
lib/shared/widgets/squad_detail_sheet.dartUI Crew rename