import 'package:flutter/material.dart'; import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart'; enum MwButtonVariant { primary, secondary, ghost, destructive } class MwButton extends StatelessWidget { const MwButton({ super.key, required this.label, required this.onPressed, this.icon, this.variant = MwButtonVariant.primary, this.loading = false, this.expand = false, }); final String label; final VoidCallback? onPressed; final IconData? icon; final MwButtonVariant variant; final bool loading; final bool expand; @override Widget build(BuildContext context) { final tokens = MoonWellDesignTokens.of(context); final scheme = Theme.of(context).colorScheme; final child = loading ? const SizedBox.square( dimension: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ if (icon != null) ...[ Icon(icon, size: 18), const SizedBox(width: 8), ], Flexible(child: Text(label, overflow: TextOverflow.ellipsis)), ], ); final callback = loading ? null : onPressed; final button = switch (variant) { MwButtonVariant.primary => ElevatedButton( onPressed: callback, child: child, ), MwButtonVariant.secondary => OutlinedButton( onPressed: callback, child: child, ), MwButtonVariant.ghost => TextButton(onPressed: callback, child: child), MwButtonVariant.destructive => ElevatedButton( onPressed: callback, style: ElevatedButton.styleFrom( backgroundColor: scheme.error, foregroundColor: scheme.onError, disabledBackgroundColor: scheme.error.withAlpha(50), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(tokens.radiusMedium), ), ), child: child, ), }; return Semantics( button: true, enabled: callback != null, child: SizedBox(width: expand ? double.infinity : null, child: button), ); } } class MwIconButton extends StatelessWidget { const MwIconButton({ super.key, required this.icon, required this.tooltip, required this.onPressed, this.selected = false, this.destructive = false, }); final IconData icon; final String tooltip; final VoidCallback? onPressed; final bool selected; final bool destructive; @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return Tooltip( message: tooltip, child: Semantics( label: tooltip, button: true, enabled: onPressed != null, selected: selected, child: IconButton( onPressed: onPressed, isSelected: selected, icon: Icon(icon), color: destructive ? scheme.error : scheme.onSurfaceVariant, style: IconButton.styleFrom(minimumSize: const Size(44, 44)), ), ), ); } } class MwTextField extends StatelessWidget { const MwTextField({ super.key, required this.label, this.controller, this.hint, this.errorText, this.prefixIcon, this.enabled = true, this.obscureText = false, this.textInputAction, this.onSubmitted, }); final String label; final TextEditingController? controller; final String? hint; final String? errorText; final IconData? prefixIcon; final bool enabled; final bool obscureText; final TextInputAction? textInputAction; final ValueChanged? onSubmitted; @override Widget build(BuildContext context) { return TextField( controller: controller, enabled: enabled, obscureText: obscureText, textInputAction: textInputAction, onSubmitted: onSubmitted, decoration: InputDecoration( labelText: label, hintText: hint, errorText: errorText, prefixIcon: prefixIcon == null ? null : Icon(prefixIcon), ), ); } } class MwCheckbox extends StatelessWidget { const MwCheckbox({ super.key, required this.label, required this.value, required this.onChanged, }); final String label; final bool value; final ValueChanged? onChanged; @override Widget build(BuildContext context) { return CheckboxListTile( value: value, onChanged: onChanged, title: Text(label), controlAffinity: ListTileControlAffinity.leading, contentPadding: EdgeInsets.zero, dense: true, ); } } class MwTabs extends StatelessWidget { const MwTabs({ super.key, required this.labels, required this.selectedIndex, required this.onSelected, this.disabledIndices = const {}, }); final List labels; final int selectedIndex; final ValueChanged onSelected; final Set disabledIndices; @override Widget build(BuildContext context) { return Wrap( spacing: 4, children: [ for (var index = 0; index < labels.length; index++) ChoiceChip( label: Text(labels[index]), selected: selectedIndex == index, onSelected: disabledIndices.contains(index) ? null : (_) => onSelected(index), ), ], ); } } enum MwBadgeTone { neutral, success, warning, error, comingSoon } class MwBadge extends StatelessWidget { const MwBadge({ super.key, required this.label, this.tone = MwBadgeTone.neutral, }); final String label; final MwBadgeTone tone; @override Widget build(BuildContext context) { final tokens = MoonWellDesignTokens.of(context); final scheme = Theme.of(context).colorScheme; final color = switch (tone) { MwBadgeTone.neutral => scheme.onSurfaceVariant, MwBadgeTone.success => tokens.success, MwBadgeTone.warning => tokens.warning, MwBadgeTone.error => scheme.error, MwBadgeTone.comingSoon => tokens.accentMuted, }; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: color.withAlpha(24), border: Border.all(color: color.withAlpha(130)), borderRadius: BorderRadius.circular(99), ), child: Text( label, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: color, fontWeight: FontWeight.w700, ), ), ); } } enum MwPanelLevel { low, regular, high } class MwPanel extends StatelessWidget { const MwPanel({ super.key, required this.child, this.level = MwPanelLevel.regular, this.padding = const EdgeInsets.all(24), }); final Widget child; final MwPanelLevel level; final EdgeInsetsGeometry padding; @override Widget build(BuildContext context) { final tokens = MoonWellDesignTokens.of(context); final color = switch (level) { MwPanelLevel.low => tokens.surfaceLow, MwPanelLevel.regular => tokens.surface, MwPanelLevel.high => tokens.surfaceHigh, }; return DecoratedBox( decoration: BoxDecoration( color: color.withAlpha(238), border: Border.fromBorderSide(tokens.border), borderRadius: BorderRadius.circular(tokens.radiusLarge), boxShadow: tokens.panelShadow, ), child: Padding(padding: padding, child: child), ); } } enum MwProgressState { active, paused, completed, failure } class MwProgressBar extends StatelessWidget { const MwProgressBar({ super.key, this.value, this.label, this.state = MwProgressState.active, }); final double? value; final String? label; final MwProgressState state; @override Widget build(BuildContext context) { final tokens = MoonWellDesignTokens.of(context); final scheme = Theme.of(context).colorScheme; final color = switch (state) { MwProgressState.active => scheme.primary, MwProgressState.paused => tokens.warning, MwProgressState.completed => tokens.success, MwProgressState.failure => scheme.error, }; return Semantics( label: label ?? 'Прогресс', value: value == null ? null : '${(value! * 100).round()}%', child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (label != null) ...[ Text(label!, style: Theme.of(context).textTheme.bodySmall), const SizedBox(height: 8), ], ClipRRect( borderRadius: BorderRadius.circular(99), child: LinearProgressIndicator( value: value, color: color, backgroundColor: color.withAlpha(35), minHeight: 7, ), ), ], ), ); } } enum MwStatusTone { operational, success, warning, error } class MwStatusText extends StatelessWidget { const MwStatusText({ super.key, required this.text, this.tone = MwStatusTone.operational, this.monospace = false, }); final String text; final MwStatusTone tone; final bool monospace; @override Widget build(BuildContext context) { final tokens = MoonWellDesignTokens.of(context); final scheme = Theme.of(context).colorScheme; final color = switch (tone) { MwStatusTone.operational => scheme.onSurfaceVariant, MwStatusTone.success => tokens.success, MwStatusTone.warning => tokens.warning, MwStatusTone.error => scheme.error, }; return Text( text, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: color, fontFamily: monospace ? tokens.monospaceFontFamily : null, ), ); } } enum MwStateKind { loading, empty, error } class MwStateView extends StatelessWidget { const MwStateView({ super.key, required this.kind, required this.title, this.description, this.onRetry, }); final MwStateKind kind; final String title; final String? description; final VoidCallback? onRetry; @override Widget build(BuildContext context) { final icon = switch (kind) { MwStateKind.loading => null, MwStateKind.empty => Icons.inbox_outlined, MwStateKind.error => Icons.cloud_off_outlined, }; return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ if (icon == null) const SizedBox.square( dimension: 28, child: CircularProgressIndicator(strokeWidth: 2), ) else Icon(icon, size: 34), const SizedBox(height: 12), Text(title, textAlign: TextAlign.center), if (description != null) ...[ const SizedBox(height: 6), Text( description!, textAlign: TextAlign.center, style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], if (onRetry != null) ...[ const SizedBox(height: 16), MwButton( label: 'Повторить', onPressed: onRetry, variant: MwButtonVariant.secondary, ), ], ], ), ), ); } } class MwDisabledFeature extends StatelessWidget { const MwDisabledFeature({ super.key, required this.message, required this.child, }); final String message; final Widget child; @override Widget build(BuildContext context) { return Tooltip(message: message, child: child); } }