diff --git a/COWORK.md b/COWORK.md
index 8ddaf7a..611b26e 100644
--- a/COWORK.md
+++ b/COWORK.md
@@ -25,6 +25,7 @@ Les deux ne communiquent **que via git + ce fichier**. ⚠️ La mémoire de cha
- **Capacitor 8 n'auto-découvre PAS les plugins in-app** → tous enregistrés dans `MainViewController.capacitorDidLoad()` : CoachWorkoutKit, CoachHealthRoute, CoachWorkoutObserver, CoachLiveBridge, **CoachAppleAuth**, **CoachGoogleAuth**.
- **Login natif Apple + Google = OK, validé device.** Apple via `AuthenticationServices` (intégré). Google via **SDK GoogleSignIn** (SPM, lié à la target App) + `GIDClientID`/URL scheme dans Info.plist. Les deux POSTent l'id_token sur `/auth/apple|google/web` (backend) → cookie de session.
- watchOS `CoachWatch` + Live Activity en place (chantiers antérieurs, cf. `HANDOFF-WATCHOS.md`).
+- **Widgets natifs iPhone (2026-06-29)** : code écrit côté dev, **reste le câblage Mac**. Plugin `CoachWidgetBridge` (à enregistrer — DÉJÀ ajouté dans `capacitorDidLoad()`), modèle partagé `CoachWidgetSnapshot.swift`, widgets `CoachWidgets.swift` (Séance du jour + Score de forme) dans l'extension `CoachLiveActivity`. App Group `group.ch.hypnotruck.coach` (entitlements écrits). **Hook web déjà en prod** (`widget-bridge.js`). ⚠️ Étapes Mac obligatoires : créer l'App Group sur le portail + activer la capability sur App ET CoachLiveActivity + Target Membership des 3 nouveaux fichiers. **Tout est détaillé dans `docs/widgets-runbook-mac.md`.**
## 👉 À reprendre
- ✅ **Phase 4 login — FAIT & DÉPLOYÉ** (révocation Apple à la suppression de compte) : code complet côté backend `coach_sportif` (commit `5ae6e2d`) + clé `.p8` déployée sur le VPS prod (vérifié 2026-06-26). Rien à coder. Détails : `coach_sportif/COWORK.md`.
diff --git a/docs/widgets-runbook-mac.md b/docs/widgets-runbook-mac.md
new file mode 100644
index 0000000..791a9d4
--- /dev/null
+++ b/docs/widgets-runbook-mac.md
@@ -0,0 +1,74 @@
+# Runbook Mac — widgets natifs iPhone (Séance du jour + Score de forme)
+
+**Pour le Claude « Cowork » (Mac) / Sylvain.** Le code Swift + entitlements + le
+hook web sont écrits côté `coach_sportif`/`coach-ios` par le Claude dev (Linux).
+Il **reste les étapes Mac/portail Apple** ci-dessous — elles ne peuvent pas se
+faire depuis Linux (provisioning + ajout de fichiers aux cibles dans Xcode).
+
+## Ce qui est déjà fait (committé)
+
+**coach-ios** (natif) :
+- `ios/App/App/CoachWidgetSnapshot.swift` — modèle partagé App ↔ extension (App Group).
+- `ios/App/App/CoachWidgetBridge.swift` — plugin Capacitor `setSnapshot`/`clear` + `WidgetCenter.reloadAllTimelines()`.
+- `ios/App/CoachLiveActivity/CoachWidgets.swift` — 2 widgets (TimelineProvider + vues SwiftUI).
+- `ios/App/CoachLiveActivity/CoachLiveActivityBundle.swift` — widgets ajoutés au `WidgetBundle`.
+- `ios/App/App/MainViewController.swift` — plugin enregistré dans `capacitorDidLoad()`.
+- `ios/App/App/App.entitlements` — App Group `group.ch.hypnotruck.coach` ajouté.
+- `ios/App/CoachLiveActivity/CoachLiveActivity.entitlements` — **créé** avec l'App Group.
+
+**coach_sportif** (web, déjà en prod via webhook) :
+- `web/static/widget-bridge.js` + include dans `_layout.html` — pousse `today` (depuis `/api/today`) + `forme` (recovery_score). No-op hors app native.
+- `web/templates/home.html` — `data-form-score` exposé sur le hero.
+
+## Étapes Mac à faire (dans l'ordre)
+
+### 1. App Group sur le portail Apple
+developer.apple.com → Certificates, IDs & Profiles → **Identifiers** → **App Groups** →
+créer `group.ch.hypnotruck.coach` (si pas déjà créé).
+
+### 2. Activer la capability sur les 2 cibles (Xcode)
+⚠️ **Quitter puis `git pull`** d'abord (règle COWORK : jamais de pull Xcode ouvert).
+Ouvrir `ios/App/App.xcworkspace`, puis pour **App** ET **CoachLiveActivity** :
+- onglet **Signing & Capabilities** → **+ Capability** → **App Groups** →
+ cocher `group.ch.hypnotruck.coach`.
+- Vérifier que `CODE_SIGN_ENTITLEMENTS` pointe bien sur le bon `.entitlements`
+ (App → `App/App.entitlements` ; CoachLiveActivity → `CoachLiveActivity/CoachLiveActivity.entitlements`,
+ à régler si Xcode ne l'a pas pris).
+
+### 3. Ajouter les nouveaux fichiers aux bonnes cibles (Target Membership)
+Les fichiers existent sur disque mais doivent être référencés par le projet :
+- `CoachWidgetSnapshot.swift` → **cocher App ET CoachLiveActivity** (fichier partagé).
+- `CoachWidgetBridge.swift` → **cible App** uniquement.
+- `CoachWidgets.swift` → **cible CoachLiveActivity** uniquement.
+- `CoachLiveActivity.entitlements` → pas une source (référencé par le build setting).
+
+Méthode : clic droit sur le dossier de la cible → *Add Files to "App"…* (décocher
+*Copy items*, cocher la bonne cible), ou sélectionner le fichier → inspecteur de
+droite → *Target Membership*.
+
+### 4. Build + test device
+- **Clean Build Folder** (`Cmd+Shift+K`) → **Run** sur l'iPhone.
+- Ouvrir l'app (charge la prod) une fois pour que `widget-bridge.js` pousse le snapshot.
+- Écran d'accueil → ajouter les widgets **Coach** : « Séance du jour » (small/medium) et « Score de forme » (small).
+- Vérifier qu'ils affichent les vraies données du jour. Tap → ouvre `/calendar` resp. `/forme`.
+
+### 5. Debug si le widget reste vide
+- Vérifier le snapshot écrit : Safari → Développement → iPhone → console →
+ `await Capacitor.Plugins.CoachWidgetBridge.setSnapshot({today:{sport:"running",title:"Test",done:false}})`
+ puis long-press l'écran d'accueil → le widget doit se mettre à jour.
+- Si rien : l'App Group n'est pas partagé (étape 2) → `UserDefaults(suiteName:)` renvoie nil.
+
+## Modèle de données (contrat web ↔ widget)
+
+```jsonc
+{
+ "today": { "sport": "running", "title": "Sortie longue", "subtitle": "90 min · Z2", "done": false },
+ "forme": { "score": 72, "label": "Prêt" } // null si pas de score
+}
+```
+`sport` ∈ running | cycling | strength | mobility | hiking | walking | rest.
+
+## Suite possible (backlog)
+- Complication **Watch** (nouvelle target widget watchOS — à créer dans Xcode).
+- Lockscreen widgets (`accessoryRectangular` / `accessoryCircular`) — ajouter les familles.
+- Background refresh (App Background Tasks) si on veut un widget à jour sans ouvrir l'app.
diff --git a/ios/App/App/App.entitlements b/ios/App/App/App.entitlements
index 5aacad2..fb4b48c 100644
--- a/ios/App/App/App.entitlements
+++ b/ios/App/App/App.entitlements
@@ -12,5 +12,9 @@
Default
+ com.apple.security.application-groups
+
+ group.ch.hypnotruck.coach
+
diff --git a/ios/App/App/CoachWidgetBridge.swift b/ios/App/App/CoachWidgetBridge.swift
new file mode 100644
index 0000000..944a8fb
--- /dev/null
+++ b/ios/App/App/CoachWidgetBridge.swift
@@ -0,0 +1,55 @@
+// CoachWidgetBridge.swift
+// Plugin Capacitor (cible App) : la WebView pousse les données du jour
+// (séance + score de forme) vers l'App Group, puis on rafraîchit les widgets
+// d'écran d'accueil. Aucun accès réseau côté widget : tout passe par ce pont.
+//
+// Enregistrement : MainViewController.capacitorDidLoad() ->
+// bridge?.registerPluginInstance(CoachWidgetBridgePlugin())
+//
+// Usage JS (no-op gracieux hors app native) :
+// const W = window.Capacitor?.Plugins?.CoachWidgetBridge;
+// await W?.setSnapshot({
+// today: { sport:"running", title:"Sortie longue", subtitle:"1h30 · Z2", done:false },
+// forme: { score: 72, label:"Prêt" }
+// });
+import Foundation
+import Capacitor
+import WidgetKit
+
+@objc(CoachWidgetBridgePlugin)
+public class CoachWidgetBridgePlugin: CAPPlugin, CAPBridgedPlugin {
+ public let identifier = "CoachWidgetBridgePlugin"
+ public let jsName = "CoachWidgetBridge"
+ public let pluginMethods: [CAPPluginMethod] = [
+ CAPPluginMethod(name: "setSnapshot", returnType: CAPPluginReturnPromise),
+ CAPPluginMethod(name: "clear", returnType: CAPPluginReturnPromise),
+ ]
+
+ @objc func setSnapshot(_ call: CAPPluginCall) {
+ var today: CoachWidgetSnapshot.TodaySession?
+ if let t = call.getObject("today") {
+ today = CoachWidgetSnapshot.TodaySession(
+ sport: t["sport"] as? String ?? "rest",
+ title: t["title"] as? String ?? "",
+ subtitle: t["subtitle"] as? String,
+ done: t["done"] as? Bool ?? false
+ )
+ }
+ var forme: CoachWidgetSnapshot.FormeScore?
+ if let f = call.getObject("forme") {
+ forme = CoachWidgetSnapshot.FormeScore(
+ score: f["score"] as? Int,
+ label: f["label"] as? String
+ )
+ }
+ CoachWidgetStore.save(CoachWidgetSnapshot(updatedAt: Date(), today: today, forme: forme))
+ if #available(iOS 14.0, *) { WidgetCenter.shared.reloadAllTimelines() }
+ call.resolve(["ok": true])
+ }
+
+ @objc func clear(_ call: CAPPluginCall) {
+ CoachWidgetStore.clear()
+ if #available(iOS 14.0, *) { WidgetCenter.shared.reloadAllTimelines() }
+ call.resolve(["ok": true])
+ }
+}
diff --git a/ios/App/App/CoachWidgetSnapshot.swift b/ios/App/App/CoachWidgetSnapshot.swift
new file mode 100644
index 0000000..393d594
--- /dev/null
+++ b/ios/App/App/CoachWidgetSnapshot.swift
@@ -0,0 +1,56 @@
+// CoachWidgetSnapshot.swift
+// Modèle de données PARTAGÉ entre l'app (cible App) et l'extension widget
+// (cible CoachLiveActivity). L'app écrit le snapshot du jour dans l'App Group
+// via CoachWidgetBridge ; les widgets d'écran d'accueil le lisent.
+//
+// ⚠️ Ce fichier doit appartenir aux DEUX cibles (App + CoachLiveActivity) —
+// cf. docs/widgets-runbook-mac.md (étape « Target Membership »).
+//
+// App Group requis : group.ch.hypnotruck.coach (à créer sur le portail Apple
+// et activer comme capability sur les deux cibles).
+import Foundation
+
+enum CoachWidgetStore {
+ /// Identifiant App Group partagé app ↔ extension. Doit correspondre
+ /// EXACTEMENT à la capability "App Groups" des deux cibles.
+ static let appGroup = "group.ch.hypnotruck.coach"
+ static let snapshotKey = "coach_widget_snapshot"
+
+ static var defaults: UserDefaults? { UserDefaults(suiteName: appGroup) }
+
+ static func save(_ snapshot: CoachWidgetSnapshot) {
+ guard let d = defaults, let data = try? JSONEncoder().encode(snapshot) else { return }
+ d.set(data, forKey: snapshotKey)
+ }
+
+ static func load() -> CoachWidgetSnapshot? {
+ guard let d = defaults, let data = d.data(forKey: snapshotKey) else { return nil }
+ return try? JSONDecoder().decode(CoachWidgetSnapshot.self, from: data)
+ }
+
+ static func clear() {
+ defaults?.removeObject(forKey: snapshotKey)
+ }
+}
+
+/// Instantané des données affichées par les widgets. Volontairement minimal :
+/// le widget n'a pas d'accès réseau, tout vient de ce snapshot poussé par l'app.
+struct CoachWidgetSnapshot: Codable {
+ var updatedAt: Date
+ var today: TodaySession?
+ var forme: FormeScore?
+
+ /// Séance planifiée du jour (nil = jour OFF / pas de séance).
+ struct TodaySession: Codable {
+ var sport: String // running, cycling, strength, mobility, hiking, rest…
+ var title: String // "Sortie longue", "Renfo bas du corps"…
+ var subtitle: String? // objectif court : "1h30 · Z2", "4×12"…
+ var done: Bool // séance déjà réalisée aujourd'hui
+ }
+
+ /// Score de forme / récupération (nil = pas encore de score).
+ struct FormeScore: Codable {
+ var score: Int? // 0–100
+ var label: String? // "Prêt", "Récup", "Fatigue"…
+ }
+}
diff --git a/ios/App/App/MainViewController.swift b/ios/App/App/MainViewController.swift
index 24c68c2..0bc0009 100644
--- a/ios/App/App/MainViewController.swift
+++ b/ios/App/App/MainViewController.swift
@@ -28,6 +28,9 @@ class MainViewController: CAPBridgeViewController {
// Plugin natif Sign in with Google — signIn() via SDK GoogleSignIn
// → id_token POSTé sur /auth/google/web (cf. CoachGoogleAuth.swift).
bridge?.registerPluginInstance(CoachGoogleAuthPlugin())
+ // Pont widgets : la WebView pousse les données du jour vers l'App Group
+ // et rafraîchit les widgets d'écran d'accueil (cf. CoachWidgetBridge.swift).
+ bridge?.registerPluginInstance(CoachWidgetBridgePlugin())
}
override func viewDidLoad() {
diff --git a/ios/App/CoachLiveActivity/CoachLiveActivity.entitlements b/ios/App/CoachLiveActivity/CoachLiveActivity.entitlements
new file mode 100644
index 0000000..7072a81
--- /dev/null
+++ b/ios/App/CoachLiveActivity/CoachLiveActivity.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.ch.hypnotruck.coach
+
+
+
diff --git a/ios/App/CoachLiveActivity/CoachLiveActivityBundle.swift b/ios/App/CoachLiveActivity/CoachLiveActivityBundle.swift
index 46c67ad..c344775 100644
--- a/ios/App/CoachLiveActivity/CoachLiveActivityBundle.swift
+++ b/ios/App/CoachLiveActivity/CoachLiveActivityBundle.swift
@@ -1,6 +1,7 @@
// CoachLiveActivityBundle.swift
-// Entry point @main de l'extension WidgetKit. Contient le ou les Widgets
-// exposés par cette extension. Ici juste la Live Activity du live workout.
+// Entry point @main de l'extension WidgetKit. Contient les Widgets exposés par
+// cette extension : la Live Activity du live workout + les widgets d'écran
+// d'accueil (séance du jour, score de forme).
import WidgetKit
import SwiftUI
@@ -9,5 +10,7 @@ import SwiftUI
struct CoachLiveActivityBundle: WidgetBundle {
var body: some Widget {
CoachLiveActivityWidget()
+ CoachTodaySessionWidget()
+ CoachFormeScoreWidget()
}
}
diff --git a/ios/App/CoachLiveActivity/CoachWidgets.swift b/ios/App/CoachLiveActivity/CoachWidgets.swift
new file mode 100644
index 0000000..5634bf8
--- /dev/null
+++ b/ios/App/CoachLiveActivity/CoachWidgets.swift
@@ -0,0 +1,258 @@
+// CoachWidgets.swift
+// Widgets d'écran d'accueil (cible CoachLiveActivity). Lisent le snapshot
+// poussé par l'app dans l'App Group (CoachWidgetStore) — aucun accès réseau.
+// Deux widgets : « Séance du jour » et « Score de forme ».
+//
+// ⚠️ Ce fichier appartient à la cible CoachLiveActivity. Il référence
+// CoachWidgetSnapshot.swift, qui doit AUSSI appartenir à cette cible
+// (Target Membership) — cf. docs/widgets-runbook-mac.md.
+
+import WidgetKit
+import SwiftUI
+
+// MARK: - Timeline
+
+struct CoachSnapshotEntry: TimelineEntry {
+ let date: Date
+ let snapshot: CoachWidgetSnapshot?
+}
+
+struct CoachWidgetProvider: TimelineProvider {
+ func placeholder(in context: Context) -> CoachSnapshotEntry {
+ CoachSnapshotEntry(date: Date(), snapshot: nil)
+ }
+
+ func getSnapshot(in context: Context, completion: @escaping (CoachSnapshotEntry) -> Void) {
+ completion(CoachSnapshotEntry(date: Date(), snapshot: CoachWidgetStore.load()))
+ }
+
+ func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
+ let entry = CoachSnapshotEntry(date: Date(), snapshot: CoachWidgetStore.load())
+ // L'app rafraîchit explicitement (WidgetCenter.reloadAllTimelines) à
+ // chaque setSnapshot. On planifie en plus un refresh horaire de courtoisie.
+ let next = Calendar.current.date(byAdding: .hour, value: 1, to: Date())
+ ?? Date().addingTimeInterval(3600)
+ completion(Timeline(entries: [entry], policy: .after(next)))
+ }
+}
+
+// MARK: - Palette / icônes (alignées sur les couleurs sport de l'app)
+
+private func sportColor(_ sport: String) -> Color {
+ switch sport {
+ case "running": return Color(red: 1.00, green: 0.45, blue: 0.21)
+ case "cycling": return Color(red: 0.20, green: 0.78, blue: 0.35)
+ case "strength": return Color(red: 0.69, green: 0.32, blue: 0.87)
+ case "mobility": return Color(red: 0.35, green: 0.78, blue: 0.98)
+ case "hiking", "walking": return Color(red: 0.80, green: 0.62, blue: 0.30)
+ default: return Color.gray
+ }
+}
+
+private func sportSymbol(_ sport: String) -> String {
+ switch sport {
+ case "cycling": return "bicycle"
+ case "walking": return "figure.walk"
+ case "hiking": return "figure.hiking"
+ case "strength": return "dumbbell.fill"
+ case "mobility": return "figure.yoga"
+ case "rest": return "moon.zzz.fill"
+ default: return "figure.run"
+ }
+}
+
+private func sportLabel(_ sport: String) -> String {
+ switch sport {
+ case "running": return "Course"
+ case "cycling": return "Vélo"
+ case "walking": return "Marche"
+ case "hiking": return "Rando"
+ case "strength": return "Renfo"
+ case "mobility": return "Mobilité"
+ case "rest": return "Repos"
+ default: return sport.capitalized
+ }
+}
+
+// Applique le fond container (requis iOS 17+) sans casser le build < 17.
+private extension View {
+ @ViewBuilder
+ func coachWidgetBackground() -> some View {
+ if #available(iOS 17.0, *) {
+ self.containerBackground(.fill.tertiary, for: .widget)
+ } else {
+ self.padding()
+ }
+ }
+}
+
+// MARK: - Widget « Séance du jour »
+
+struct CoachTodaySessionEntryView: View {
+ var entry: CoachSnapshotEntry
+ @Environment(\.widgetFamily) private var family
+
+ var body: some View {
+ let today = entry.snapshot?.today
+ Group {
+ if let s = today {
+ if family == .systemMedium {
+ mediumView(s)
+ } else {
+ smallView(s)
+ }
+ } else {
+ emptyView
+ }
+ }
+ .coachWidgetBackground()
+ .widgetURL(URL(string: "https://coach.hypnotruck.ch/calendar"))
+ }
+
+ private func header(_ s: CoachWidgetSnapshot.TodaySession) -> some View {
+ HStack(spacing: 6) {
+ Image(systemName: sportSymbol(s.sport))
+ .foregroundStyle(sportColor(s.sport))
+ Text(sportLabel(s.sport).uppercased())
+ .font(.caption2.weight(.bold))
+ .foregroundStyle(.secondary)
+ Spacer()
+ if s.done {
+ Image(systemName: "checkmark.seal.fill")
+ .foregroundStyle(.green)
+ .font(.caption)
+ }
+ }
+ }
+
+ private func smallView(_ s: CoachWidgetSnapshot.TodaySession) -> some View {
+ VStack(alignment: .leading, spacing: 6) {
+ header(s)
+ Spacer(minLength: 0)
+ Text(s.title)
+ .font(.headline)
+ .lineLimit(2)
+ .minimumScaleFactor(0.8)
+ if let sub = s.subtitle, !sub.isEmpty {
+ Text(sub).font(.caption).foregroundStyle(.secondary).lineLimit(1)
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
+ }
+
+ private func mediumView(_ s: CoachWidgetSnapshot.TodaySession) -> some View {
+ HStack(spacing: 14) {
+ ZStack {
+ Circle().fill(sportColor(s.sport).opacity(0.18))
+ Image(systemName: sportSymbol(s.sport))
+ .font(.title2)
+ .foregroundStyle(sportColor(s.sport))
+ }
+ .frame(width: 56, height: 56)
+ VStack(alignment: .leading, spacing: 4) {
+ Text("Séance du jour")
+ .font(.caption2.weight(.bold))
+ .foregroundStyle(.secondary)
+ Text(s.title).font(.headline).lineLimit(2)
+ if let sub = s.subtitle, !sub.isEmpty {
+ Text(sub).font(.subheadline).foregroundStyle(.secondary).lineLimit(1)
+ }
+ if s.done {
+ Label("Réalisée", systemImage: "checkmark.seal.fill")
+ .font(.caption2).foregroundStyle(.green)
+ }
+ }
+ Spacer(minLength: 0)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
+ }
+
+ private var emptyView: some View {
+ VStack(alignment: .leading, spacing: 6) {
+ HStack(spacing: 6) {
+ Image(systemName: "moon.zzz.fill").foregroundStyle(.secondary)
+ Text("JOUR OFF").font(.caption2.weight(.bold)).foregroundStyle(.secondary)
+ }
+ Spacer(minLength: 0)
+ Text("Pas de séance prévue")
+ .font(.subheadline).foregroundStyle(.secondary)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
+ }
+}
+
+struct CoachTodaySessionWidget: Widget {
+ let kind = "CoachTodaySessionWidget"
+ var body: some WidgetConfiguration {
+ StaticConfiguration(kind: kind, provider: CoachWidgetProvider()) { entry in
+ CoachTodaySessionEntryView(entry: entry)
+ }
+ .configurationDisplayName("Séance du jour")
+ .description("La séance prévue aujourd'hui dans ton programme.")
+ .supportedFamilies([.systemSmall, .systemMedium])
+ }
+}
+
+// MARK: - Widget « Score de forme »
+
+struct CoachFormeScoreEntryView: View {
+ var entry: CoachSnapshotEntry
+
+ private func ringColor(_ score: Int) -> Color {
+ switch score {
+ case 75...: return Color(red: 0.20, green: 0.78, blue: 0.35)
+ case 50..<75: return Color(red: 1.00, green: 0.80, blue: 0.0)
+ default: return Color(red: 1.00, green: 0.27, blue: 0.23)
+ }
+ }
+
+ var body: some View {
+ let forme = entry.snapshot?.forme
+ VStack(spacing: 8) {
+ Text("FORME")
+ .font(.caption2.weight(.bold))
+ .foregroundStyle(.secondary)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ Spacer(minLength: 0)
+ if let score = forme?.score {
+ ZStack {
+ Circle().stroke(Color.secondary.opacity(0.18), lineWidth: 9)
+ Circle()
+ .trim(from: 0, to: min(max(Double(score) / 100.0, 0), 1))
+ .stroke(ringColor(score), style: StrokeStyle(lineWidth: 9, lineCap: .round))
+ .rotationEffect(.degrees(-90))
+ VStack(spacing: 0) {
+ Text("\(score)").font(.system(size: 30, weight: .bold, design: .rounded))
+ if let l = forme?.label, !l.isEmpty {
+ Text(l).font(.caption2).foregroundStyle(.secondary)
+ }
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ } else {
+ VStack(spacing: 4) {
+ Image(systemName: "bolt.heart")
+ .font(.title)
+ .foregroundStyle(.secondary)
+ Text("Pas de score")
+ .font(.caption).foregroundStyle(.secondary)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+ }
+ .coachWidgetBackground()
+ .widgetURL(URL(string: "https://coach.hypnotruck.ch/forme"))
+ }
+}
+
+struct CoachFormeScoreWidget: Widget {
+ let kind = "CoachFormeScoreWidget"
+ var body: some WidgetConfiguration {
+ StaticConfiguration(kind: kind, provider: CoachWidgetProvider()) { entry in
+ CoachFormeScoreEntryView(entry: entry)
+ }
+ .configurationDisplayName("Score de forme")
+ .description("Ton score de forme / récupération du jour.")
+ .supportedFamilies([.systemSmall])
+ }
+}