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:
@@ -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" |
|
||||
Reference in New Issue
Block a user