WIP: feat(ui): Migrate to new UI implementation
This commit is contained in:
@@ -0,0 +1,448 @@
|
||||
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<String>? 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<bool?>? 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<String> labels;
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int> onSelected;
|
||||
final Set<int> 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user