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

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 (+ #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 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"