import 'package:flutter/material.dart' hide StepState; import '../about/internal/application/StepTracker.dart'; import '../about/internal/application/TextType.dart'; import '../utils/Colors.dart'; import 'text/Text.dart'; /// Renders an ordered list of deploy/build steps with live state and, for the /// active step, an inline progress bar. class StepList extends StatelessWidget { final List steps; const StepList({super.key, required this.steps}); @override Widget build(BuildContext context) { final jp = context.jp; final doneCount = steps.where((s) => s.state == StepState.done).length; return Container( padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), decoration: PeachTheme.panel(context, glowColor: PeachTheme.seed), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ text( 'PROGRESS', 12, TextType.Regular, color: jp.textDim, letterSpacing: 1.2, weight: FontWeight.w700, ), const Spacer(), text( '$doneCount / ${steps.length}', 12, TextType.Regular, color: PeachTheme.seed, weight: FontWeight.w700, ), ], ), const SizedBox(height: 10), for (var i = 0; i < steps.length; i++) _StepRow(step: steps[i], isLast: i == steps.length - 1, jp: jp), ], ), ); } } class _StepRow extends StatelessWidget { final DeployStep step; final bool isLast; final JpColors jp; const _StepRow({required this.step, required this.isLast, required this.jp}); @override Widget build(BuildContext context) { final active = step.state == StepState.active; final (color, icon) = switch (step.state) { StepState.done => (PeachTheme.seed, Icons.check), StepState.active => (PeachTheme.seed, Icons.autorenew), StepState.failed => (PeachTheme.danger, Icons.close), StepState.skipped => (jp.textFaint, Icons.remove), StepState.pending => (jp.textFaint, Icons.circle_outlined), }; final pct = step.progress == null ? null : (step.progress! * 100).clamp(0, 100).toStringAsFixed(0); return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( children: [ _dot(color, icon, active), if (!isLast) Expanded( child: Container( width: 2, margin: const EdgeInsets.symmetric(vertical: 2), color: step.state == StepState.done ? PeachTheme.seed.withValues(alpha: 0.4) : jp.border, ), ), ], ), const SizedBox(width: 12), Expanded( child: Padding( padding: EdgeInsets.only(bottom: isLast ? 0 : 14, top: 2), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: text( step.label, 13.5, TextType.Regular, color: step.state == StepState.pending ? jp.textFaint : jp.text, weight: active ? FontWeight.w700 : FontWeight.w500, ), ), if (pct != null && active) text( '$pct%', 12, TextType.Regular, color: PeachTheme.seed, weight: FontWeight.w700, ), ], ), if (active && step.progress != null) ...[ const SizedBox(height: 6), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: step.progress, minHeight: 5, backgroundColor: jp.surface2, ), ), ], ], ), ), ), ], ), ); } Widget _dot(Color color, IconData icon, bool active) { return Container( width: 26, height: 26, decoration: BoxDecoration( color: color.withValues(alpha: active ? 0.18 : 0.12), shape: BoxShape.circle, border: Border.all(color: color.withValues(alpha: 0.6)), ), child: active ? const Padding( padding: EdgeInsets.all(5), child: CircularProgressIndicator( strokeWidth: 2, color: PeachTheme.seed, ), ) : Icon(icon, size: 15, color: color), ); } }