import 'package:flutter/material.dart'; import '../theme/etm_tokens.dart'; import '../main.dart' show DrawerMenuButton; // ───────────────────────────────────────────────────────────────────────────── // ACScreen — Climatisation / Chauffage // // Structure (brief) : // ① Thermostats par pièce EN HAUT (geste fréquent) // - pièces actives : expandées avec temp + modes // - pièces éteintes : compactes // ② Sources pilotées par Héos EN BAS // - PAC Atlantic (SG-Ready : 4 états) // - Chauffe-eau thermodynamique (Surplus / Eco / Boost) // - Climatiseur (rafraîchissement anticipé) // // Saison-conscient : hiver = PAC + ECS, été = Clim + ECS. // La maquette les montre tous pour valider les widgets. // ───────────────────────────────────────────────────────────────────────────── // ── Enums ──────────────────────────────────────────────────────────────────── enum ACMode { heat, cool, auto, fan } enum SGReadyState { blocked, normal, recommended, forced } enum DHWMode { surplus, eco, boost } // ── Data models ─────────────────────────────────────────────────────────────── class _Zone { final String name; final double currentTemp; final double targetTemp; final ACMode mode; final bool isOn; final bool heosActive; // Héos pilote en ce moment const _Zone({ required this.name, required this.currentTemp, required this.targetTemp, required this.mode, required this.isOn, this.heosActive = false, }); _Zone copyWith({double? targetTemp, ACMode? mode, bool? isOn}) => _Zone( name: name, currentTemp: currentTemp, targetTemp: targetTemp ?? this.targetTemp, mode: mode ?? this.mode, isOn: isOn ?? this.isOn, heosActive: heosActive, ); } // ───────────────────────────────────────────────────────────────────────────── class ACScreen extends StatefulWidget { const ACScreen({super.key}); @override State createState() => _ACScreenState(); } class _ACScreenState extends State { final List<_Zone> _zones = [ const _Zone(name: 'Salon', currentTemp: 19.5, targetTemp: 21.0, mode: ACMode.heat, isOn: true, heosActive: true), const _Zone(name: 'Chambre', currentTemp: 18.0, targetTemp: 19.0, mode: ACMode.cool, isOn: false), const _Zone(name: 'Bureau', currentTemp: 20.0, targetTemp: 22.0, mode: ACMode.heat, isOn: true), const _Zone(name: 'Cuisine', currentTemp: 21.5, targetTemp: 21.0, mode: ACMode.auto, isOn: false), ]; SGReadyState _sgReady = SGReadyState.forced; bool _sgAuto = true; // Héos pilote SG-Ready DHWMode _dhwMode = DHWMode.surplus; double _dhwCurrent = 52; double _dhwTarget = 60; double _climCurrent = 24.0; double _climTarget = 22.0; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: EtmTokens.bg, body: CustomScrollView( slivers: [ SliverAppBar( floating: true, backgroundColor: EtmTokens.bg, elevation: 0, leading: const DrawerMenuButton(), leadingWidth: 64, title: Text('Climatisation / Chauffage', style: EtmTokens.sans(size: 18, weight: FontWeight.w600)), actions: [ Padding( padding: const EdgeInsets.only(right: 8), child: IconButton( icon: const Icon(Icons.schedule_rounded, color: EtmTokens.muted, size: 22), onPressed: () {}, tooltip: 'Planning hebdomadaire', ), ), ], ), SliverPadding( padding: const EdgeInsets.fromLTRB(18, 4, 18, 32), sliver: SliverList( delegate: SliverChildListDelegate([ // ── ① Thermostats par pièce ───────────────────────────────── ...List.generate(_zones.length, (i) { final zone = _zones[i]; return Padding( padding: const EdgeInsets.only(bottom: 12), child: zone.isOn ? _ActiveZoneCard( zone: zone, onChanged: (z) => setState(() => _zones[i] = z), ) : _InactiveZoneCard( zone: zone, onTurnOn: () => setState( () => _zones[i] = zone.copyWith(isOn: true)), ), ); }), const SizedBox(height: 8), // ── ② Sources pilotées par Héos ───────────────────────────── _SectionLabel('SOURCES PILOTÉES PAR HÉOS'), const SizedBox(height: 12), // PAC Atlantic — SG-Ready _PACCard( sgState: _sgReady, sgAuto: _sgAuto, onStateChanged: (s) => setState(() { _sgReady = s; _sgAuto = false; }), onAutoToggle: () => setState(() => _sgAuto = !_sgAuto), ), const SizedBox(height: 12), // Chauffe-eau thermodynamique _DHWCard( mode: _dhwMode, currentTemp: _dhwCurrent, targetTemp: _dhwTarget, onModeChanged: (m) => setState(() => _dhwMode = m), onTargetChanged: (t) => setState(() => _dhwTarget = t), ), const SizedBox(height: 12), // Climatiseur — rafraîchissement anticipé _ClimCard( currentTemp: _climCurrent, targetTemp: _climTarget, onTargetChanged: (t) => setState(() => _climTarget = t), ), ]), ), ), ], ), ); } } // ─────────────────────────── Helpers de style ────────────────────────────────── Color _modeColor(ACMode m) => switch (m) { ACMode.heat => EtmTokens.orange, ACMode.cool => EtmTokens.blue, ACMode.auto => EtmTokens.green, ACMode.fan => EtmTokens.muted, }; IconData _modeIcon(ACMode m) => switch (m) { ACMode.heat => Icons.whatshot_rounded, ACMode.cool => Icons.ac_unit_rounded, ACMode.auto => Icons.autorenew_rounded, ACMode.fan => Icons.air_rounded, }; String _modeLabel(ACMode m) => switch (m) { ACMode.heat => 'Chauffage', ACMode.cool => 'Clim', ACMode.auto => 'Auto', ACMode.fan => 'Ventilation', }; String _modeLabelShort(ACMode m) => switch (m) { ACMode.heat => 'Chauf.', ACMode.cool => 'Clim', ACMode.auto => 'Auto', ACMode.fan => 'Vent.', }; // ─────────────────────────── Carte zone active ───────────────────────────────── class _ActiveZoneCard extends StatelessWidget { final _Zone zone; final ValueChanged<_Zone> onChanged; const _ActiveZoneCard({required this.zone, required this.onChanged}); @override Widget build(BuildContext context) { final color = _modeColor(zone.mode); return _Card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: color.withValues(alpha: 0.14), borderRadius: BorderRadius.circular(12), ), child: Icon(_modeIcon(zone.mode), color: color, size: 24), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(zone.name, style: EtmTokens.sans(size: 16, weight: FontWeight.w600)), Text(_modeLabel(zone.mode), style: EtmTokens.sans(size: 12, color: color)), ], ), ), Switch( value: zone.isOn, onChanged: (v) => onChanged(zone.copyWith(isOn: v)), activeColor: color, ), ], ), const Padding( padding: EdgeInsets.symmetric(vertical: 14), child: Divider(height: 1, color: EtmTokens.line), ), // Températures Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Actuelle', style: EtmTokens.sans(size: 11, color: EtmTokens.muted)), Text('${zone.currentTemp.toStringAsFixed(1)}°C', style: EtmTokens.mono(size: 28, weight: FontWeight.w700)), ], ), Icon(Icons.arrow_forward_rounded, color: EtmTokens.faint, size: 20), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text('Cible', style: EtmTokens.sans(size: 11, color: EtmTokens.muted)), Row( children: [ _TempButton( icon: Icons.remove, color: color, onTap: () => onChanged( zone.copyWith(targetTemp: zone.targetTemp - 0.5)), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Text('${zone.targetTemp.toStringAsFixed(1)}°C', style: EtmTokens.mono(size: 24, weight: FontWeight.w700, color: color)), ), _TempButton( icon: Icons.add, color: color, onTap: () => onChanged( zone.copyWith(targetTemp: zone.targetTemp + 0.5)), ), ], ), ], ), ], ), const SizedBox(height: 14), // Sélecteur mode Row( children: ACMode.values.map((m) { final sel = zone.mode == m; final mcol = _modeColor(m); return Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: GestureDetector( onTap: () => onChanged(zone.copyWith(mode: m)), child: AnimatedContainer( duration: const Duration(milliseconds: 180), padding: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( color: sel ? mcol.withValues(alpha: 0.12) : EtmTokens.bg, borderRadius: BorderRadius.circular(10), border: Border.all( color: sel ? mcol : EtmTokens.line, width: sel ? 1.5 : 1, ), ), child: Column( children: [ Icon(_modeIcon(m), size: 18, color: sel ? mcol : EtmTokens.faint), const SizedBox(height: 3), Text(_modeLabelShort(m), style: EtmTokens.sans( size: 10, color: sel ? mcol : EtmTokens.muted, weight: sel ? FontWeight.w600 : FontWeight.w400)), ], ), ), ), ), ); }).toList(), ), // Chip Héos si actif if (zone.heosActive) ...[ const SizedBox(height: 12), _HeosChip('Chauffe au solaire en ce moment'), ], ], ), ); } } // ─────────────────────────── Carte zone inactive ─────────────────────────────── class _InactiveZoneCard extends StatelessWidget { final _Zone zone; final VoidCallback onTurnOn; const _InactiveZoneCard({required this.zone, required this.onTurnOn}); @override Widget build(BuildContext context) { return _Card( padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: EtmTokens.bg, borderRadius: BorderRadius.circular(12), ), child: Icon(_modeIcon(zone.mode), color: EtmTokens.faint, size: 22), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(zone.name, style: EtmTokens.sans(size: 15, weight: FontWeight.w600)), Text('Éteint', style: EtmTokens.sans(size: 12, color: EtmTokens.faint)), ], ), ), Switch(value: false, onChanged: (_) => onTurnOn(), activeColor: EtmTokens.green), ], ), ); } } // ─────────────────────────── PAC SG-Ready ────────────────────────────────────── class _PACCard extends StatelessWidget { final SGReadyState sgState; final bool sgAuto; final ValueChanged onStateChanged; final VoidCallback onAutoToggle; const _PACCard({ required this.sgState, required this.sgAuto, required this.onStateChanged, required this.onAutoToggle, }); @override Widget build(BuildContext context) { return _Card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: EtmTokens.orange.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), ), child: const Icon(Icons.heat_pump_outlined, color: EtmTokens.orange, size: 24), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('PAC Atlantic', style: EtmTokens.sans(size: 15, weight: FontWeight.w600)), Text('Source de chauffage · SG-Ready', style: EtmTokens.sans(size: 11, color: EtmTokens.muted)), ], ), ), _HeosBadgeSmall(), ], ), const SizedBox(height: 14), // Label + toggle Auto Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('MODE SG-READY', style: EtmTokens.sectionLabel()), GestureDetector( onTap: onAutoToggle, child: AnimatedContainer( duration: const Duration(milliseconds: 180), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: sgAuto ? EtmTokens.greenSoft : EtmTokens.bg, borderRadius: BorderRadius.circular(99), border: Border.all( color: sgAuto ? EtmTokens.green : EtmTokens.line, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.auto_awesome_rounded, size: 12, color: sgAuto ? EtmTokens.green : EtmTokens.muted), const SizedBox(width: 4), Text('Auto', style: EtmTokens.sans( size: 11, weight: FontWeight.w600, color: sgAuto ? EtmTokens.green : EtmTokens.muted)), ], ), ), ), ], ), const SizedBox(height: 10), // 4 états SG-Ready Row( children: SGReadyState.values.map((s) { final isBlocked = s == SGReadyState.blocked; final sel = !sgAuto && sgState == s; final (label, color) = switch (s) { SGReadyState.blocked => ('Bloqué', EtmTokens.danger), SGReadyState.normal => ('Normal', EtmTokens.muted), SGReadyState.recommended => ('Recommandé', EtmTokens.amber), SGReadyState.forced => ('Forcé', EtmTokens.green), }; return Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 2), child: GestureDetector( onTap: isBlocked ? null : () => onStateChanged(s), child: AnimatedContainer( duration: const Duration(milliseconds: 180), padding: const EdgeInsets.symmetric(vertical: 9), decoration: BoxDecoration( color: isBlocked ? EtmTokens.bg : sel ? color.withValues(alpha: 0.14) : EtmTokens.bg, borderRadius: BorderRadius.circular(10), border: Border.all( color: sel ? color : EtmTokens.line, width: sel ? 1.5 : 1, ), ), child: Text( label, textAlign: TextAlign.center, style: EtmTokens.sans( size: 11, weight: sel ? FontWeight.w600 : FontWeight.w400, color: isBlocked ? EtmTokens.faint : sel ? color : EtmTokens.muted, ), ), ), ), ), ); }).toList(), ), const SizedBox(height: 14), // Info surplus _SurplusInfo( label: 'Surplus solaire 2,4 kW', detail: 'chauffe au solaire en stockant l\'énergie gratuite.', powerKw: 1.8, solarPct: 100, ), ], ), ); } } // ─────────────────────────── Chauffe-eau ─────────────────────────────────────── class _DHWCard extends StatelessWidget { final DHWMode mode; final double currentTemp; final double targetTemp; final ValueChanged onModeChanged; final ValueChanged onTargetChanged; const _DHWCard({ required this.mode, required this.currentTemp, required this.targetTemp, required this.onModeChanged, required this.onTargetChanged, }); @override Widget build(BuildContext context) { final (modeColor, modeIcon, modeLabel) = switch (mode) { DHWMode.surplus => (EtmTokens.green, Icons.wb_sunny_rounded, 'Surplus'), DHWMode.eco => (EtmTokens.blue, Icons.eco_rounded, 'Éco'), DHWMode.boost => (EtmTokens.amber, Icons.bolt_rounded, 'Boost'), }; return _Card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: EtmTokens.blue.withValues(alpha: 0.10), borderRadius: BorderRadius.circular(12), ), child: const Icon(Icons.water_drop_outlined, color: EtmTokens.blue, size: 24), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Chauffe-eau', style: EtmTokens.sans(size: 15, weight: FontWeight.w600)), Text('Ballon thermodynamique', style: EtmTokens.sans(size: 11, color: EtmTokens.muted)), ], ), ), _HeosBadgeSmall(), ], ), const Padding( padding: EdgeInsets.symmetric(vertical: 14), child: Divider(height: 1, color: EtmTokens.line), ), // Températures Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Température eau', style: EtmTokens.sans(size: 11, color: EtmTokens.muted)), Text('${currentTemp.toStringAsFixed(0)}°C', style: EtmTokens.mono(size: 28, weight: FontWeight.w700)), ], ), Icon(Icons.arrow_forward_rounded, color: EtmTokens.faint, size: 20), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text('Cible', style: EtmTokens.sans(size: 11, color: EtmTokens.muted)), Row( children: [ _TempButton( icon: Icons.remove, color: modeColor, onTap: () => onTargetChanged(targetTemp - 1)), Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Text('${targetTemp.toStringAsFixed(0)}°C', style: EtmTokens.mono(size: 24, weight: FontWeight.w700, color: modeColor)), ), _TempButton( icon: Icons.add, color: modeColor, onTap: () => onTargetChanged(targetTemp + 1)), ], ), ], ), ], ), const SizedBox(height: 14), // 3 modes DHW Row( children: DHWMode.values.map((m) { final sel = mode == m; final (mc, mi, ml) = switch (m) { DHWMode.surplus => (EtmTokens.green, Icons.wb_sunny_rounded, 'Surplus'), DHWMode.eco => (EtmTokens.blue, Icons.eco_rounded, 'Éco'), DHWMode.boost => (EtmTokens.amber, Icons.bolt_rounded, 'Boost'), }; return Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 3), child: GestureDetector( onTap: () => onModeChanged(m), child: AnimatedContainer( duration: const Duration(milliseconds: 180), padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( color: sel ? mc.withValues(alpha: 0.12) : EtmTokens.bg, borderRadius: BorderRadius.circular(12), border: Border.all( color: sel ? mc : EtmTokens.line, width: sel ? 1.5 : 1, ), ), child: Column( children: [ Icon(mi, size: 20, color: sel ? mc : EtmTokens.faint), const SizedBox(height: 4), Text(ml, style: EtmTokens.sans( size: 12, weight: sel ? FontWeight.w600 : FontWeight.w400, color: sel ? mc : EtmTokens.muted)), ], ), ), ), ), ); }).toList(), ), const SizedBox(height: 14), // Info _SurplusInfo( label: 'Surplus solaire', detail: 'chauffe l\'eau avant le soir', powerKw: 1.2, solarPct: 100, ), ], ), ); } } // ─────────────────────────── Climatiseur anticipé ────────────────────────────── class _ClimCard extends StatelessWidget { final double currentTemp; final double targetTemp; final ValueChanged onTargetChanged; const _ClimCard({ required this.currentTemp, required this.targetTemp, required this.onTargetChanged, }); @override Widget build(BuildContext context) { return _Card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: EtmTokens.blue.withValues(alpha: 0.10), borderRadius: BorderRadius.circular(12), ), child: const Icon(Icons.ac_unit_rounded, color: EtmTokens.blue, size: 24), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Climatiseur', style: EtmTokens.sans(size: 15, weight: FontWeight.w600)), Text('Rafraîchissement anticipé', style: EtmTokens.sans(size: 11, color: EtmTokens.muted)), ], ), ), ], ), const Padding( padding: EdgeInsets.symmetric(vertical: 14), child: Divider(height: 1, color: EtmTokens.line), ), // Températures Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Actuelle', style: EtmTokens.sans(size: 11, color: EtmTokens.muted)), Text('${currentTemp.toStringAsFixed(1)}°C', style: EtmTokens.mono(size: 28, weight: FontWeight.w700)), ], ), const Icon(Icons.arrow_forward_rounded, color: EtmTokens.faint, size: 20), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text('Cible', style: EtmTokens.sans(size: 11, color: EtmTokens.muted)), Row( children: [ _TempButton( icon: Icons.remove, color: EtmTokens.blue, onTap: () => onTargetChanged(targetTemp - 0.5)), Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Text('${targetTemp.toStringAsFixed(1)}°C', style: EtmTokens.mono(size: 24, weight: FontWeight.w700, color: EtmTokens.blue)), ), _TempButton( icon: Icons.add, color: EtmTokens.blue, onTap: () => onTargetChanged(targetTemp + 0.5)), ], ), ], ), ], ), const SizedBox(height: 14), // Info Héos Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: EtmTokens.blueSoft, borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.schedule_rounded, size: 14, color: EtmTokens.blue), const SizedBox(width: 6), Text('Pré-refroidissement 13h–16h', style: EtmTokens.sans(size: 12, weight: FontWeight.w600, color: EtmTokens.blue)), ], ), const SizedBox(height: 4), Text( 'Sur surplus solaire, pour traverser le pic tarifaire ' '17h–21h sans clim payante.', style: EtmTokens.sans(size: 11, color: EtmTokens.muted), ), const SizedBox(height: 8), Row( children: [ Text('820 W', style: EtmTokens.mono(size: 12, color: EtmTokens.blue)), Text(' · 100% solaire', style: EtmTokens.sans(size: 12, color: EtmTokens.muted)), ], ), ], ), ), ], ), ); } } // ─────────────────────────── Widgets partagés ────────────────────────────────── class _Card extends StatelessWidget { final Widget child; final EdgeInsets? padding; const _Card({required this.child, this.padding}); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: EtmTokens.card, borderRadius: BorderRadius.circular(EtmTokens.radiusLg), boxShadow: EtmTokens.cardShadow, ), padding: padding ?? const EdgeInsets.all(20), child: child, ); } } class _TempButton extends StatelessWidget { final IconData icon; final Color color; final VoidCallback onTap; const _TempButton({required this.icon, required this.color, required this.onTap}); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( width: 30, height: 30, decoration: BoxDecoration( color: color.withValues(alpha: 0.10), shape: BoxShape.circle, ), child: Icon(icon, size: 16, color: color), ), ); } } class _HeosChip extends StatelessWidget { final String label; const _HeosChip(this.label); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: EtmTokens.greenSoft, borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.wb_sunny_rounded, size: 12, color: EtmTokens.greenDark), const SizedBox(width: 5), Text(label, style: EtmTokens.sans(size: 11, weight: FontWeight.w500, color: EtmTokens.greenDark)), ], ), ); } } class _HeosBadgeSmall extends StatelessWidget { const _HeosBadgeSmall(); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: EtmTokens.greenSoft, borderRadius: BorderRadius.circular(6), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.auto_awesome_rounded, size: 10, color: EtmTokens.greenDark), const SizedBox(width: 4), Text('Piloté par Héos', style: EtmTokens.sans(size: 10, weight: FontWeight.w600, color: EtmTokens.greenDark)), ], ), ); } } class _SurplusInfo extends StatelessWidget { final String label; final String detail; final double powerKw; final int solarPct; const _SurplusInfo({ required this.label, required this.detail, required this.powerKw, required this.solarPct, }); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: EtmTokens.greenSoft, borderRadius: BorderRadius.circular(10), ), child: Row( children: [ const Icon(Icons.wb_sunny_rounded, size: 14, color: EtmTokens.greenDark), const SizedBox(width: 8), Expanded( child: RichText( text: TextSpan( style: EtmTokens.sans(size: 11, color: EtmTokens.muted), children: [ TextSpan( text: '$label ', style: EtmTokens.sans(size: 11, weight: FontWeight.w600, color: EtmTokens.greenDark), ), TextSpan(text: detail), ], ), ), ), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text('${powerKw.toStringAsFixed(1)} kW', style: EtmTokens.mono(size: 11, color: EtmTokens.green)), Text('$solarPct% solaire', style: EtmTokens.sans(size: 10, color: EtmTokens.muted)), ], ), ], ), ); } } class _SectionLabel extends StatelessWidget { final String text; const _SectionLabel(this.text); @override Widget build(BuildContext context) { return Text(text, style: EtmTokens.sectionLabel()); } }