10 KiB
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(+#FFFFFFré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.jsoniOS 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 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 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 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
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
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
#!/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
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
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
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
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
./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
git add desktopApp/src/main/kotlin/com/livrets/desktop/Main.kt
git commit -m "feat: icône de fenêtre desktop"