etm-powersync-app/lib/theme/etm_tokens.dart
Patrick Schurig 9c19791e9a feat(énergie): les deux graphes — flux corrigé, et l'empilement par consommateur
Maquette validée par Patrick (couleurs comprises). Deux graphes, et trois
défauts d'affichage trouvés en chemin — dont deux à l'écran seulement.

GRAPHE 1 — les trois « on ne voit pas » avaient trois causes.

  production : publiée NÉGATIVE (−103 W). La magnitude est prise AU RENDU, pas
  au transport : le champ reste brut (§7.1), et `.abs()` est licite sur une
  production, dont le signe ne porte aucune information — contrairement au
  réseau, où le signe dit le sens (d'où gridImportW/gridExportW là-bas).

  autoconsommation : `production − export` clampé à 0, donc TOUJOURS zéro avec
  une production négative. Même racine, corrigée par la même magnitude.

  SOC : correctement absent — aucune batterie sur l'installation. Ce qui
  manquait, c'était de le DIRE ; c'est maintenant une note sous le graphe.

  ⚠️ Et un deuxième empêchement, vu seulement à l'écran : le signe réparé, la
  production restait invisible SOUS l'autoconsommation. Tant que rien n'est
  exporté les deux sont égales au pixel près, et la verte était tracée par
  dessus. L'autoconso passe en pointillé — des tirets sur la bande ambre disent
  « tout est consommé sur place », leur décrochage dit l'export. Deux courbes
  qui coïncident doivent pouvoir se VOIR coïncider.

  ⚠️ Ce correctif en créait un autre : le SOC était aussi vert pointillé. Sur
  une installation AVEC batterie les deux auraient été indiscernables — collision
  qu'aucun banc sans batterie ne pouvait montrer. Le SOC passe en indigo.

GRAPHE 2 — qui consomme, empilé.

  `Energy.GetThingPowerLogs`, et la liste des appareils vient de GetLoadConfig :
  chaque charge déclarée porte son `meterThingId`, son label et son domaine —
  vérifié renseigné sur les quatre charges de .75. Rien n'est deviné d'un nom
  d'appareil.

  Un appareil demandé sans réponse sort dans `sansHistorique` et est NOMMÉ sous
  le graphe, jamais tracé à zéro : la V2C ne déclare pas `smartmeter`, donc elle
  n'a pas de passé — ce qui n'est pas « elle n'a rien tiré ».

  Le RESTE du foyer est licite ici, contrairement au Sankey qui l'avait fait
  interdire : chaque part est un compteur dédié et le total est le compteur du
  foyer, donc la soustraction ne porte que du mesuré. Et un reste NÉGATIF n'est
  pas raboté — c'est la preuve qu'un sous-compteur n'est pas dans le périmètre
  du total, et l'écraser effacerait la seule chose qui le dise.

  🔴 Le signe des sous-compteurs n'est PAS vérifié : les trois lisent 0 W à tous
  les pas, donc aucun échantillon non nul n'existe. Le compteur PV publie en
  négatif — rien n'autorise à supposer. La valeur est transportée brute et
  `signeIndetermine` le dit à l'écran.

Couleurs par consommateur ajoutées au thème (pac ambre, ecs sarcelle, ev
indigo) : distinctes des couleurs de FLUX, qui disent d'où vient l'énergie, là
où celles-ci disent qui la prend.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-08 10:25:16 +02:00

172 lines
8.1 KiB
Dart

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
/// Tokens de marque ETM PowerSync — source de vérité unique.
class EtmTokens {
EtmTokens._();
// ── Couleurs de marque (spec Dashboard v4) ────────────────────────────────
static const Color brand = Color(0xFF31A3DD);
static const Color solar = Color(0xFFFEC113);
static const Color solarDark = Color(0xFFFFCB33);
static const Color eco = Color(0xFF28A06A);
static const Color ecoDark = Color(0xFF33B87A);
static const Color heat = Color(0xFFEF8B2A);
static const Color water = Color(0xFF31A3DD);
static const Color gridColor = Color(0xFF8A98A3);
static const Color importColor = Color(0xFFE8806F);
// ── Legacy (compatibilité existante) ─────────────────────────────────────
static const Color navy = Color(0xFF0D2B3B);
static const Color navy2 = Color(0xFF10384C);
static const Color blue = Color(0xFF31A3DD);
static const Color amber = Color(0xFFFEC113);
static const Color green = Color(0xFF1DB86A);
static const Color greenDark= Color(0xFF159C58);
static const Color orange = Color(0xFFE8923A);
static const Color danger = Color(0xFFE8423F);
// ── Couleurs de flux ──────────────────────────────────────────────────────
static const Color solToHouse = Color(0xFFB8C34D);
static const Color solToEcs = Color(0xFF31A3DD);
static const Color solToVe = Color(0xFF28A06A);
static const Color solToBatt = Color(0xFF7FCF72);
static const Color solToGrid = Color(0xFFFEC113);
static const Color battToHouse = Color(0xFF2F6FB0);
static const Color gridToHouse = Color(0xFFE8806F);
// ── Neutres light ─────────────────────────────────────────────────────────
static const Color ink = navy;
static const Color muted= Color(0xFF5D6F7B);
static const Color faint= Color(0xFF8B9AA4);
static const Color line = Color(0xFFE4EAEE);
static const Color bg = Color(0xFFEEF2F5);
static const Color bg2 = Color(0xFFE4EAEE);
static const Color card = Color(0xFFFFFFFF);
// ── Neutres dark ──────────────────────────────────────────────────────────
static const Color bgDark = Color(0xFF06141D);
static const Color bg2Dark = Color(0xFF0A1F2B);
static const Color surfaceDark = Color(0xFF0D2B3B);
static const Color inkDark = Color(0xFFEAF3F7);
static const Color mutedDark = Color(0xFF9BB0BC);
static const Color faintDark = Color(0xFF6C8493);
// ── Soft fills (legacy light) ─────────────────────────────────────────────
static const Color greenSoft = Color(0xFFE7F8EF);
static const Color blueSoft = Color(0xFFE9F5FB);
static const Color amberSoft = Color(0xFFFFF6DD);
// ── Rayons ────────────────────────────────────────────────────────────────
static const double radius = 16;
static const double radiusLg = 22;
static const double radiusCard = 20;
static const double radiusCtrl = 13;
static const double radiusPill = 30;
// ── Context-aware ─────────────────────────────────────────────────────────
static bool _isDark(BuildContext c) =>
Theme.of(c).brightness == Brightness.dark;
static Color bgOf(BuildContext c) => _isDark(c) ? bgDark : bg;
static Color bg2Of(BuildContext c) => _isDark(c) ? bg2Dark : bg2;
static Color surfaceOf(BuildContext c) => _isDark(c) ? surfaceDark : card;
static Color inkOf(BuildContext c) => _isDark(c) ? inkDark : ink;
static Color mutedOf(BuildContext c) => _isDark(c) ? mutedDark : muted;
static Color faintOf(BuildContext c) => _isDark(c) ? faintDark : faint;
static Color ecoOf(BuildContext c) => _isDark(c) ? ecoDark : eco;
static Color solarOf(BuildContext c) => _isDark(c) ? solarDark : solar;
// ── Couleurs par CONSOMMATEUR ─────────────────────────────────────────────
//
// Distinctes des couleurs de FLUX (solaire, maison, réseau, batterie), qui disent d'où
// vient l'énergie. Celles-ci disent **qui la prend**, et il en fallait de nouvelles :
// le thème n'en portait aucune, et réutiliser une couleur de flux ferait lire une
// origine là où il n'y a qu'une destination.
//
// Trois teintes choisies pour ne heurter aucune couleur de flux existante, et validées
// par Patrick le 2026-09-08 sur la maquette `docs/mockups/ecran_energie_refonte.html`.
static const Color pac = Color(0xFFE88C30); // pompe à chaleur — ambre chaud
static const Color pacDark = Color(0xFFF09A45);
static const Color ecs = Color(0xFF2AA7A0); // eau chaude — sarcelle
static const Color ecsDark = Color(0xFF37BDB5);
static const Color ev = Color(0xFF7C6CE4); // bornes — indigo
static const Color evDark = Color(0xFF8E80EA);
static Color pacOf(BuildContext c) => _isDark(c) ? pacDark : pac;
static Color ecsOf(BuildContext c) => _isDark(c) ? ecsDark : ecs;
static Color evOf(BuildContext c) => _isDark(c) ? evDark : ev;
/// La couleur d'un domaine de charge, ou `null` s'il n'en a pas.
///
/// `null` n'est pas un défaut à combler : une charge sans domaine déclaré n'a pas de
/// couleur, et lui en attribuer une par défaut la ferait passer pour classée.
static Color? domaineOf(BuildContext c, String? domaine) => switch (domaine) {
'heating' => pacOf(c),
'ecs' => ecsOf(c),
'ev' => evOf(c),
_ => null,
};
static Color lineOf(BuildContext c) => _isDark(c)
? const Color(0xFFFFFFFF).withValues(alpha: 0.09)
: navy.withValues(alpha: 0.10);
static Color lineStrongOf(BuildContext c) => _isDark(c)
? const Color(0xFFFFFFFF).withValues(alpha: 0.16)
: navy.withValues(alpha: 0.16);
// ── Typographie ───────────────────────────────────────────────────────────
static TextStyle sans({
double size = 14,
FontWeight weight = FontWeight.w400,
Color color = ink,
double? height,
double? letterSpacing,
}) =>
GoogleFonts.ibmPlexSans(
fontSize: size,
fontWeight: weight,
color: color,
height: height,
letterSpacing: letterSpacing,
);
static TextStyle mono({
double size = 14,
FontWeight weight = FontWeight.w600,
Color color = ink,
}) =>
GoogleFonts.ibmPlexMono(
fontSize: size,
fontWeight: weight,
color: color,
fontFeatures: const [FontFeature.tabularFigures()],
);
static TextStyle sectionLabel() => sans(
size: 12,
weight: FontWeight.w600,
color: faint,
letterSpacing: 1.2,
);
// ── Ombres ────────────────────────────────────────────────────────────────
static List<BoxShadow> get cardShadow => [
BoxShadow(
color: navy.withValues(alpha: 0.04),
blurRadius: 2,
offset: const Offset(0, 1),
),
BoxShadow(
color: navy.withValues(alpha: 0.06),
blurRadius: 30,
offset: const Offset(0, 10),
),
];
static List<BoxShadow> cardShadowOf(BuildContext c) => _isDark(c)
? []
: cardShadow;
}