docs: add UI redesign design spec (teal arctique, moderne & épuré)

Spec for complete visual overhaul of MyIce KMP app:
- Teal arctique color scheme (full Material 3 slots, light/dark)
- Typography scale, shape tokens, spacing tokens (Dimens)
- 4 screens redesigned (Login, Schedule, EventDetail + navigation)
- New components: SkeletonCard, EmptyState, ErrorState, FilterChips
- EventCard rework: color border, type badge, leading icons
- Sticky date headers in schedule list
- French accents corrected, hardcoded colors removed
- Branded as 'MyIce' (dropped tech codename 'K')

Refs: brainstorming session 2026-08-06
This commit is contained in:
2026-08-06 15:35:22 +02:00
parent efaa93d907
commit d4f43de933
@@ -0,0 +1,277 @@
# MyIce UI Redesign — Design Spec
**Date:** 2026-08-06
**Status:** Approved (pending review)
**Scope:** Refonte visuelle complète — thème, 4 écrans, composants
## Contexte
L'app MyIce (KMP, Compose Multiplatform) est fonctionnellement complète mais visuellement squelettique: thème à 4 slots Material 3, zéro icônerie, listes plates, couleurs hardcodées, accents français manquants, pas de regroupement par date, pas de typographie dédiée.
Ce spec décrit une refonte visuelle complète dans une direction **moderne & épurée** avec un accent **teal arctique**.
## Direction visuelle
- **Style:** Moderne & épuré — beaucoup d'air, surfaces propres, typo hiérarchisée, accents subtils
- **Accent:** Teal arctique (`#0D9488` light / `#2DD4BF` dark) — évoque la glace et le hockey
- **Philosophie:** Le chrome disparaît, les données sont les vedettes
## 1. Thème & Design Tokens
### 1.1 Palette (Material 3 complet)
**Light scheme:**
| Slot | Valeur | Note |
|------|--------|------|
| primary | `#0D9488` (teal-600) | |
| onPrimary | `#FFFFFF` | |
| primaryContainer | `#99F6E4` (teal-100) | |
| onPrimaryContainer | `#134E4A` (teal-900) | |
| secondary | `#475569` (slate-600) | |
| secondaryContainer | `#E2E8F0` (slate-200) | |
| onSecondaryContainer | `#1E293B` | |
| tertiary | `#0EA5E9` (sky-500) | états interactifs secondaires |
| tertiaryContainer | `#E0F2FE` | |
| error | `#DC2626` | |
| onError | `#FFFFFF` | |
| errorContainer | `#FEE2E2` | |
| onErrorContainer | `#7F1D1D` | |
| background | `#FAFAFA` (warm white) | |
| onBackground | `#1C1917` | |
| surface | `#FFFFFF` | |
| onSurface | `#1C1917` (warm dark) | |
| surfaceVariant | `#F1F5F9` (slate-100) | |
| onSurfaceVariant | `#475569` | |
| outline | `#CBD5E1` (slate-300) | |
**Dark scheme:**
| Slot | Valeur | Note |
|------|--------|------|
| primary | `#2DD4BF` (teal-400) | |
| onPrimary | `#003833` | |
| primaryContainer | `#0D9488` | |
| onPrimaryContainer | `#99F6E4` | |
| secondary | `#94A3B8` (slate-400) | |
| secondaryContainer | `#334155` (slate-700) | |
| tertiary | `#0EA5E9` | |
| background | `#0F172A` (slate-900) | |
| onBackground | `#F1F5F9` | |
| surface | `#1E293B` (slate-800) | |
| onSurface | `#F1F5F9` | |
| surfaceVariant | `#334155` | |
| onSurfaceVariant | `#94A3B8` | |
| outline | `#475569` | |
| error | `#F87171` | |
| onError | `#7F1D1D` | |
| errorContainer | `#7F1D1D` | |
| onErrorContainer | `#FECACA` | |
### 1.2 Typographie
Échelle système (pas de custom font — trop complexe en KMP), mais poids et tailles définis explicitement via `Typography()`:
| Style | Taille | Poids | Usage |
|-------|--------|-------|-------|
| headlineLarge | 28sp | Bold | App name (login) |
| titleLarge | 22sp | SemiBold | Screen titles (TopAppBar) |
| titleMedium | 16sp | SemiBold | Card titles |
| bodyLarge | 15sp | Regular | Main content |
| bodyMedium | 14sp | Regular | Secondary info |
| bodySmall | 13sp | Regular | Metadata, labels |
| labelMedium | 12sp | Medium | Chips, badges, section headers |
### 1.3 Shapes
| Nom | Rayon | Usage |
|-----|-------|-------|
| small | 8dp | Chips, badges |
| medium | 12dp | Cards, inputs |
| large | 16dp | Sheets, big cards |
### 1.4 Spacing tokens (`Dimens` object)
| Token | Valeur |
|-------|--------|
| xs | 4dp |
| sm | 8dp |
| md | 12dp |
| lg | 16dp |
| xl | 24dp |
| xxl | 32dp |
### 1.5 Elevation
| Niveau | Valeur | Usage |
|--------|--------|-------|
| level0 | 0dp | Flat surfaces |
| level1 | 1dp | Cards |
| level2 | 3dp | App bar, dialogs |
## 2. Écrans
### 2.1 LoginScreen
Layout centré vertical:
- Icône `SportsHockey` (Material) 48dp dans un cercle `primaryContainer` 80dp, en haut
- `"MyIce"` en `headlineLarge` (le "K" de "MyIceK" était un codename tech — supprimé)
- Sous-titre `"Votre calendrier hockey"` en `titleMedium` `onSurfaceVariant`
- Spacer `xxl` (32dp)
- `Button` plein, 48dp de haut, icône `Login` leading: `"Se connecter avec Infomaniak"`
- Snackbar erreur en `errorContainer`/`onErrorContainer`
- Fond `background`, contenu centré verticalement (pas collé au haut)
### 2.2 ScheduleScreen
**TopAppBar:**
- Titre `"Calendrier"` (au lieu de "MyIceK")
- Actions: icône `Refresh` + icône `Logout` uniquement (Material icons, pas de texte)
- L'email utilisateur est supprimé de la barre (trop encombrant sur mobile). Il n'est pas relocalisé — un menu compte futur est hors scope.
**Filtres:**
- `SegmentedButton` (singleChoice, 3 segments) sur une ligne: `"Tous"` / `"Matchs"` / `"Entraînements"` (remplace le `FilterDropdown` pour le type)
- `AssistChip` cliquables pour Compte / Age / Sous-groupe (affichent la valeur sélectionnée, `"Tous"` par défaut). Utilisent `account.label` au lieu de `account.name` pour le dropdown Compte.
- `AssistChip` "Effacer" avec icône `Close` apparaît si au moins un filtre est actif
- Section filtres dans une `Card` `surfaceVariant`, `medium` shape, padding `lg`
**Liste d'événements:**
- `LazyColumn` avec **sticky headers** par jour: `"Samedi 15 mars"` en `labelMedium` uppercase `onSurfaceVariant`, fond `surface`, divider subtil en dessous
- `EventCard` repensé (voir Section 3)
- Indicateur `"Dernière mise à jour: ..."` en bas de liste, `bodySmall` `onSurfaceVariant` (utilise `lastUpdated` du state — actuellement non affiché)
- `PullToRefreshBox` conservé (Material 3 par défaut)
**États partagés:**
- Loading: 3-4 `SkeletonCard` animés (voir Section 3)
- Error: `ErrorState` avec icône `CloudOff`, message, bouton `"Réessayer"`
- Empty: `EmptyState` avec icône `CalendarClear`, `"Aucun événement disponible"`
### 2.3 EventDetailScreen
**TopAppBar:**
- Navigation icon: `ArrowBack` (icône, pas `TextButton("Retour")`)
- Titre: titre de l'événement tronqué
- `PlatformBackHandler` conservé
**Section infos (carte unique, `medium` shape, elevation 1dp):**
- Chaque ligne: icône leading 18dp `onSurfaceVariant` + label `bodySmall` uppercase `onSurfaceVariant` + valeur `bodyLarge` `onSurface`
- Lignes: `Event` → type, `Place` → lieu, `CalendarMonth` → date, `Schedule` → heure
- Plus de `"Type: Match"` interpolé — label et valeur visuellement distincts
**Convocation:**
- En-tête `"Joueurs (N)"` en `titleMedium` SemiBold + `BadgedBox` avec count
- **Joueurs:** `ListItem` Material 3 — numéro dans un cercle `primaryContainer` 40dp (avatar), nom `bodyLarge`, position + `"(née 2008)"` en `bodySmall` dessous. Triés par numéro.
- Si aucun joueur: carte `errorContainer` avec icône `Warning` + `"Aucun joueur ni personnel convoqué"`
- **Personnel:** `ListItem` avec icône `Person` leading, nom `bodyLarge`, role en `AssistChip` `secondaryContainer` à droite
- En-tête `"Personnel"` en `titleMedium` SemiBold
**États partagés:**
- Loading: `LoadingIndicator` conservé
- Error: `ErrorState` avec icône `ErrorOutline`, message, bouton `"Réessayer"`
### 2.4 Navigation & transitions
- Navigation manuelle conservée (sealed class + `mutableStateOf` — pas de lib externe)
- **`AnimatedContent`** ajouté dans `App.kt` pour transitions fondues entre écrans (`tween(300ms)`)
- `PlatformBackHandler` conservé (known limitation: no-op sur desktop/iOS)
## 3. Composants
### 3.1 EventCard (repensé)
Carte `medium` shape (12dp), elevation 1dp, cliquable:
- **Barre couleur** 4dp à gauche via `Modifier.border` sur le `Card` entier (pas un `Row`) — couleur `event.color` (hex parsé), fallback `primary`
- **Badge type** en haut-droite: `AssistChip` `"Match"` (couleur teal/primary) ou `"Entraînement"` (couleur secondary/slate)
- **Titre:** pour un match (`eventType == "Jeu"`): `"Match vs {opponent}"`. Pour un entraînement: `"Entraînement"`. Suppression de la ligne `"Adversaire:"` pour les entraînements.
- **Lignes info** avec icônes leading 18dp `onSurfaceVariant`:
- `Place` 18dp + lieu en `bodyMedium`
- `Schedule` 18dp + heure condensée (`formatEventTimeRange`: `"18h00 19h30"`)
- **Âge + nom** (`"{agegroup} - {name}"`) en `titleMedium` SemiBold
- La date est dans le header de section (pas dans la card)
### 3.2 SkeletonCard
Carte placeholder grise aux mêmes proportions que `EventCard`:
- Fond `surfaceVariant`, `alpha(0.3f)`
- Animation shimmer: translation horizontale d'un dégradé clair (`InfiniteTransition` + `rememberInfiniteTransition`)
- 4 lignes fictives (titre + 2 lignes info + badge)
- Réutilisé pour le loading state du Schedule
### 3.3 EmptyState / ErrorState (composables partagés)
```
EmptyState(icon: ImageVector, title: String, message: String)
ErrorState(icon: ImageVector, title: String, message: String, onRetry: () -> Unit)
```
- Centrés verticalement dans un `Column` (`Arrangement.Center`, `HorizontalAlignment.CenterHorizontally`)
- Icône 48dp `onSurfaceVariant`
- Titre `titleMedium` `onSurface`
- Message `bodyMedium` `onSurfaceVariant`
- `ErrorState` ajoute un `Button` `"Réessayer"` avec `onRetry`
- Réutilisés sur ScheduleScreen et EventDetailScreen
### 3.4 Wrappers filtres
- `SegmentedButton` (singleChoice) pour le type: 3 options `"Tous"/"Matchs"/"Entraînements"`, mappe vers `TypeFilter.ALL/GAMES/PRACTICES`
- `AssistChip` wrapper pour Compte/Age/Sous-groupe: affiche label + valeur sélectionnée, clic ouvre un `DropdownMenu`
- `AssistChip` "Effacer" avec icône `Close`: visible si au moins un filtre non-défaut est actif, reset tous les filtres
## 4. Corrections transversales
### 4.1 Accents français
Toutes les strings UI sont corrigées:
- "Rafraichir" → "Rafraîchir"
- "Deconnexion" → "Déconnexion"
- "Reessayer" → "Réessayer"
- "Aucun evenement" → "Aucun événement"
- "Entrainements" → "Entraînements"
- "convoque" → "convoqué"
- "donnees" → "données"
### 4.2 Couleurs hardcodées supprimées
- `Color.Gray` (ScheduleScreen) → `onSurfaceVariant`
- `Color(0xFFFFC107)` (EventDetailScreen warning) → `errorContainer`/`onErrorContainer`
- `Color(0xFF000000 or value)` (EventCard color mask) → `event.color` direct ou `primary` fallback via une util helper `parseHexColor`
### 4.3 Branding
- "MyIceK" → "MyIce" partout (TopAppBar, LoginScreen, titre fenêtre desktop)
- `account.label` utilisé au lieu de `account.name` dans le dropdown Compte
### 4.4 Strings
Toutes les strings restent hardcodées inline (pas de localization infrastructure — hors scope). Pas de `object Strings` non plus: ajouter une couche d'indirection sans localization réelle est du YAGNI. Les accents français (4.1) sont corrigés directement dans les strings inline.
## 5. Hors scope
- Migration vers Navigation Compose library
- `viewModel()` au lieu de `remember{}` (known limitation — perte de state rotation Android)
- Localization infrastructure (string resources)
- Custom fonts (trop complexe en KMP)
- Tests UI (Composition Testing) — le projet n'a pas de framework de test UI configuré
- iOS `.xcodeproj` creation
- Desktop window icon / menu bar
## 6. Fichiers impactés
| Fichier | Changement |
|---------|------------|
| `ui/theme/Theme.kt` | Réécriture complète: palette, typo, shapes, elevation, `Dimens` |
| `ui/theme/Color.kt` | Nouveau — définitions de couleur teal/slate |
| `ui/theme/Dimens.kt` | Nouveau — spacing tokens |
| `ui/screens/LoginScreen.kt` | Refonte: icône, branding, layout |
| `ui/screens/ScheduleScreen.kt` | Refonte: TopAppBar, chips filtres, sticky headers, états partagés, `lastUpdated` |
| `ui/screens/EventDetailScreen.kt` | Refonte: icône back, section infos stylisée, ListItems pour joueurs/staff |
| `ui/components/EventCard.kt` | Refonte: border couleur, badge type, icônes leading, logique match/entraînement |
| `ui/components/SkeletonCard.kt` | Nouveau — skeleton loading |
| `ui/components/EmptyState.kt` | Nouveau — état vide partagé |
| `ui/components/ErrorState.kt` | Nouveau — état erreur partagé |
| `ui/components/FilterChips.kt` | Nouveau — SegmentedButton + AssistChips (remplace FilterDropdown) |
| `ui/components/FilterDropdown.kt` | Supprimé — remplacé par FilterChips |
| `ui/components/LoadingIndicator.kt` | Conservé (utilisé par EventDetail) |
| `App.kt` | `AnimatedContent` pour transitions, branding "MyIce" |
| `util/EventDateTimeFormatter.kt` | Ajout `formatEventTimeRange` condensé (existant conservé) |
| `desktopMain/Main.kt` | Titre fenêtre "MyIce" |