Files
livrets/docs/superpowers/plans/2026-08-31-icons-launcher.md

246 lines
10 KiB
Markdown

# Icônes launcher depuis le logo — Plan d'implémentation
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Décliner `docs/branding/logo.svg` en icônes launcher Android (adaptive + legacy), iOS (appiconset) et desktop, via `docs/branding/generate-icons.sh`.
**Architecture:** 3 variantes SVG committées (square / foreground / monochrome) → script bash idempotent avec `rsvg-convert` (+ `python3` pour les tailles iOS lues dans `Contents.json`, + `magick` pour le background uni) qui écrase les PNG existants in situ.
**Tech Stack:** SVG 1.1, bash, rsvg-convert 2.62, python3, ImageMagick, Compose Multiplatform (`painterResource`).
## Global Constraints
- Palette uniquement : `#1D3557` `#457B9D` `#A8DADC` `#E63946` `#F1FAEE` (+ `#FFFFFF` réservé à la couche monochrome)
- Aucun `<text>` dans les SVG ; aucune référence externe
- Zone sûre Android : cercle de 66 dp de diamètre centré dans le canvas 108 (scale 0.5073)
- `Contents.json` iOS non modifié ; chemins/noms des PNG Android inchangés
- Géométrie tuiles/glyphes : verbatim du master `logo.svg` (spec 2026-08-31-logo-design §3)
---
### Task 1: Variantes SVG sources
**Files:**
- Create: `docs/branding/icon-square.svg`
- Create: `docs/branding/icon-foreground.svg`
- Create: `docs/branding/icon-monochrome.svg`
**Interfaces:**
- Consumes: géométrie du master `logo.svg`
- Produces: 3 SVG sources consommés par `generate-icons.sh` (Task 2)
- [ ] **Step 1: Créer `icon-square.svg`** (plein cadre, sans arrondi — pour iOS)
```svg
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 128 128" role="img" aria-labelledby="title">
<title id="title">Livrets — icône plein cadre</title>
<rect x="0" y="0" width="128" height="128" fill="#1D3557"/>
<rect x="18" y="18" width="42" height="42" rx="12" fill="#A8DADC"/>
<rect x="68" y="18" width="42" height="42" rx="12" fill="#E63946"/>
<rect x="18" y="68" width="42" height="42" rx="12" fill="#F1FAEE"/>
<rect x="68" y="68" width="42" height="42" rx="12" fill="#457B9D"/>
<path d="M29,28 L49,28 L36,50" fill="none" stroke="#1D3557" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M81,31 L97,47 M97,31 L81,47" fill="none" stroke="#F1FAEE" stroke-width="6.5" stroke-linecap="round"/>
<circle cx="39" cy="82.75" r="5.5" fill="#1D3557"/>
<circle cx="39" cy="93.25" r="7.5" fill="#1D3557"/>
<path d="M81,81 C81,73 97,73 97,81 C97,86.5 89,86.5 89,93 L89,95.5" fill="none" stroke="#F1FAEE" stroke-width="6" stroke-linecap="round"/>
<circle cx="89" cy="102.5" r="3.4" fill="#F1FAEE"/>
</svg>
```
- [ ] **Step 2: Créer `icon-foreground.svg`** (viewBox 108, scale 0.5073 vers centre 54,54)
```svg
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 108 108" role="img" aria-labelledby="title">
<title id="title">Livrets — couche foreground adaptive</title>
<g transform="translate(54,54) scale(0.5073) translate(-64,-64)">
<rect x="18" y="18" width="42" height="42" rx="12" fill="#A8DADC"/>
<rect x="68" y="18" width="42" height="42" rx="12" fill="#E63946"/>
<rect x="18" y="68" width="42" height="42" rx="12" fill="#F1FAEE"/>
<rect x="68" y="68" width="42" height="42" rx="12" fill="#457B9D"/>
<path d="M29,28 L49,28 L36,50" fill="none" stroke="#1D3557" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M81,31 L97,47 M97,31 L81,47" fill="none" stroke="#F1FAEE" stroke-width="6.5" stroke-linecap="round"/>
<circle cx="39" cy="82.75" r="5.5" fill="#1D3557"/>
<circle cx="39" cy="93.25" r="7.5" fill="#1D3557"/>
<path d="M81,81 C81,73 97,73 97,81 C97,86.5 89,86.5 89,93 L89,95.5" fill="none" stroke="#F1FAEE" stroke-width="6" stroke-linecap="round"/>
<circle cx="89" cy="102.5" r="3.4" fill="#F1FAEE"/>
</g>
</svg>
```
- [ ] **Step 3: Créer `icon-monochrome.svg`** (tuiles blanches uniquement, même groupe)
```svg
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 108 108" role="img" aria-labelledby="title">
<title id="title">Livrets — couche monochrome adaptive</title>
<g transform="translate(54,54) scale(0.5073) translate(-64,-64)">
<rect x="18" y="18" width="42" height="42" rx="12" fill="#FFFFFF"/>
<rect x="68" y="18" width="42" height="42" rx="12" fill="#FFFFFF"/>
<rect x="18" y="68" width="42" height="42" rx="12" fill="#FFFFFF"/>
<rect x="68" y="68" width="42" height="42" rx="12" fill="#FFFFFF"/>
</g>
</svg>
```
- [ ] **Step 4: Valider**
```bash
for f in docs/branding/icon-square.svg docs/branding/icon-foreground.svg docs/branding/icon-monochrome.svg; do
xmllint --noout "$f" && grep -c "<text" "$f" | grep -q "^0$"
done
# attendu : aucune erreur, aucune sortie
grep -oE "#[0-9A-Fa-f]{6}" docs/branding/icon-monochrome.svg | sort -u # attendu : #FFFFFF uniquement
```
- [ ] **Step 5: Commit**
```bash
git add docs/branding/icon-square.svg docs/branding/icon-foreground.svg docs/branding/icon-monochrome.svg
git commit -m "feat: variantes svg des icônes (square, foreground, monochrome)"
```
---
### Task 2: Script `generate-icons.sh` et génération des PNG
**Files:**
- Create: `docs/branding/generate-icons.sh`
- Modify (généré): `androidApp/src/main/res/mipmap-*/ic_launcher*.png`
- Modify (généré): `iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/*.png`
- Create: `desktopApp/src/main/resources/icons/icon.png`
**Interfaces:**
- Consumes: les 4 SVG de `docs/branding/` (logo, square, foreground, monochrome)
- Produces: tous les PNG launcher + icône desktop ; script réutilisable après retouche du logo
- [ ] **Step 1: Créer le script**
```bash
#!/usr/bin/env bash
# Génère les icônes launcher (Android, iOS, desktop) depuis les SVG de docs/branding/.
# Prérequis : rsvg-convert, magick, python3. Idempotent.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
BRANDING="$ROOT/docs/branding"
RES="$ROOT/androidApp/src/main/res"
IOS_APPSET="$ROOT/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset"
for tool in rsvg-convert magick python3; do
command -v "$tool" >/dev/null 2>&1 || { echo "Erreur : '$tool' requis" >&2; exit 1; }
done
# --- Android : legacy (carte arrondie du master) ---
for pair in mdpi:48 hdpi:72 xhdpi:96 xxhdpi:144 xxxhdpi:192; do
dpi="${pair%%:*}"; size="${pair##*:}"
rsvg-convert -w "$size" -h "$size" "$BRANDING/logo.svg" -o "$RES/mipmap-$dpi/ic_launcher.png"
done
# --- Android : couches adaptive (108 dp) ---
for pair in mdpi:108 hdpi:162 xhdpi:216 xxhdpi:324 xxxhdpi:432; do
dpi="${pair%%:*}"; size="${pair##*:}"
magick -size "${size}x${size}" xc:'#1D3557' "$RES/mipmap-$dpi/ic_launcher_background.png"
rsvg-convert -w "$size" -h "$size" "$BRANDING/icon-foreground.svg" -o "$RES/mipmap-$dpi/ic_launcher_foreground.png"
rsvg-convert -w "$size" -h "$size" "$BRANDING/icon-monochrome.svg" -o "$RES/mipmap-$dpi/ic_launcher_monochrome.png"
done
# --- iOS : tailles lues dans Contents.json ---
python3 - "$BRANDING/icon-square.svg" "$IOS_APPSET" <<'PY'
import json, pathlib, subprocess, sys
svg, appset = sys.argv[1], pathlib.Path(sys.argv[2])
for img in json.loads((appset / "Contents.json").read_text())["images"]:
w, _ = (float(v) for v in img["size"].split("x"))
px = round(w * float(img["scale"].rstrip("x")))
subprocess.run(
["rsvg-convert", "-w", str(px), "-h", str(px), svg, "-o", str(appset / img["filename"])],
check=True,
)
PY
# --- Desktop ---
mkdir -p "$ROOT/desktopApp/src/main/resources/icons"
rsvg-convert -w 512 -h 512 "$BRANDING/logo.svg" -o "$ROOT/desktopApp/src/main/resources/icons/icon.png"
echo "Icônes générées ✓"
```
- [ ] **Step 2: Rendre exécutable et lancer**
```bash
chmod +x docs/branding/generate-icons.sh
./docs/branding/generate-icons.sh
# attendu : « Icônes générées ✓ » sans erreur
```
- [ ] **Step 3: Vérifier les dimensions générées**
```bash
sips -g pixelWidth androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png | grep -q 192 # legacy
sips -g pixelWidth androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png | grep -q 432
sips -g pixelWidth iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png | grep -q 1024
sips -g pixelWidth desktopApp/src/main/resources/icons/icon.png | grep -q 512
# attendu : les 4 grep réussissent (silence = OK)
for f in iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/*.png; do sips -g pixelWidth "$f" | grep -qE "[1-9][0-9]*"; done
# attendu : tous les PNG iOS existent et non vides
```
- [ ] **Step 4: Contrôle visuel companion** (grille des icônes + simulation masque rond Android) puis commit
```bash
git add androidApp/src/main/res docs/branding/generate-icons.sh desktopApp/src/main/resources iosApp/iosApp/Assets.xcassets
git commit -m "feat: générer les icônes launcher depuis le logo (script + assets)"
```
---
### Task 3: Icône de fenêtre desktop
**Files:**
- Modify: `desktopApp/src/main/kotlin/com/livrets/desktop/Main.kt:3-6` (imports) et `:10-14` (Window)
**Interfaces:**
- Consumes: `desktopApp/src/main/resources/icons/icon.png` (Task 2)
- Produces: fenêtre desktop icônée
- [ ] **Step 1: Ajouter l'import et le paramètre icon**
```kotlin
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import com.livrets.App
import org.jetbrains.compose.ui.res.painterResource
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Livrets",
state = rememberWindowState(width = 480.dp, height = 800.dp),
icon = painterResource("icons/icon.png"),
) {
App(filesDirPath = storageDir())
}
}
private fun storageDir(): String =
System.getProperty("user.home") + "/.livrets"
```
- [ ] **Step 2: Compiler et tester**
```bash
./gradlew :desktopApp:compileKotlin --console=plain 2>&1 | tail -3
# attendu : BUILD SUCCESSFUL (sinon ajuster l'import painterResource selon la version compose)
./gradlew :sharedUI:jvmTest :androidApp:assembleDebug --console=plain 2>&1 | tail -3
# attendu : BUILD SUCCESSFUL
```
- [ ] **Step 3: Commit**
```bash
git add desktopApp/src/main/kotlin/com/livrets/desktop/Main.kt
git commit -m "feat: icône de fenêtre desktop"
```