959 lines
28 KiB
Dart
959 lines
28 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../theme/theme_extensions.dart';
|
|
import '../services/brand_service.dart';
|
|
import '../../core/services/enhanced_accessibility_service.dart';
|
|
import '../../core/services/platform_service.dart';
|
|
import '../../core/services/settings_service.dart';
|
|
|
|
/// Unified component library following Conduit design patterns
|
|
/// This provides consistent, reusable UI components throughout the app
|
|
|
|
class ConduitButton extends ConsumerWidget {
|
|
final String text;
|
|
final VoidCallback? onPressed;
|
|
final bool isLoading;
|
|
final bool isDestructive;
|
|
final bool isSecondary;
|
|
final IconData? icon;
|
|
final double? width;
|
|
final bool isFullWidth;
|
|
final bool isCompact;
|
|
|
|
const ConduitButton({
|
|
super.key,
|
|
required this.text,
|
|
this.onPressed,
|
|
this.isLoading = false,
|
|
this.isDestructive = false,
|
|
this.isSecondary = false,
|
|
this.icon,
|
|
this.width,
|
|
this.isFullWidth = false,
|
|
this.isCompact = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final hapticEnabled = ref.watch(hapticEnabledProvider);
|
|
Color backgroundColor;
|
|
Color textColor;
|
|
|
|
if (isDestructive) {
|
|
backgroundColor = context.conduitTheme.error;
|
|
textColor = context.conduitTheme.buttonPrimaryText;
|
|
} else if (isSecondary) {
|
|
backgroundColor = context.conduitTheme.buttonSecondary;
|
|
textColor = context.conduitTheme.buttonSecondaryText;
|
|
} else {
|
|
backgroundColor = context.conduitTheme.buttonPrimary;
|
|
textColor = context.conduitTheme.buttonPrimaryText;
|
|
}
|
|
|
|
// Build semantic label
|
|
String semanticLabel = text;
|
|
if (isLoading) {
|
|
semanticLabel = 'Loading: $text';
|
|
} else if (isDestructive) {
|
|
semanticLabel = 'Warning: $text';
|
|
}
|
|
|
|
return Semantics(
|
|
label: semanticLabel,
|
|
button: true,
|
|
enabled: !isLoading && onPressed != null,
|
|
child: SizedBox(
|
|
width: isFullWidth ? double.infinity : width,
|
|
height: isCompact ? TouchTarget.medium : TouchTarget.comfortable,
|
|
child: ElevatedButton(
|
|
onPressed: isLoading
|
|
? null
|
|
: () {
|
|
if (onPressed != null) {
|
|
PlatformService.hapticFeedbackWithSettings(
|
|
type: isDestructive
|
|
? HapticType.warning
|
|
: HapticType.light,
|
|
hapticEnabled: hapticEnabled,
|
|
);
|
|
onPressed!();
|
|
}
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: backgroundColor,
|
|
foregroundColor: textColor,
|
|
disabledBackgroundColor: context.conduitTheme.buttonDisabled,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.button),
|
|
),
|
|
elevation: Elevation.none,
|
|
shadowColor: backgroundColor.withValues(alpha: Alpha.standard),
|
|
minimumSize: Size(
|
|
TouchTarget.minimum,
|
|
isCompact ? TouchTarget.medium : TouchTarget.comfortable,
|
|
),
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: isCompact ? Spacing.md : Spacing.buttonPadding,
|
|
vertical: isCompact ? Spacing.sm : Spacing.sm,
|
|
),
|
|
),
|
|
child: isLoading
|
|
? Semantics(
|
|
label: 'Loading',
|
|
excludeSemantics: true,
|
|
child: SizedBox(
|
|
width: IconSize.small,
|
|
height: IconSize.small,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation<Color>(textColor),
|
|
),
|
|
),
|
|
)
|
|
: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
if (icon != null) ...[
|
|
Icon(icon, size: IconSize.small),
|
|
SizedBox(width: Spacing.iconSpacing),
|
|
],
|
|
Flexible(
|
|
child: EnhancedAccessibilityService.createAccessibleText(
|
|
text,
|
|
style: AppTypography.standard.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
color: textColor,
|
|
),
|
|
maxLines: 1,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ConduitInput extends StatelessWidget {
|
|
final String? label;
|
|
final String? hint;
|
|
final TextEditingController? controller;
|
|
final ValueChanged<String>? onChanged;
|
|
final VoidCallback? onTap;
|
|
final bool obscureText;
|
|
final bool enabled;
|
|
final String? errorText;
|
|
final int? maxLines;
|
|
final Widget? suffixIcon;
|
|
final Widget? prefixIcon;
|
|
final TextInputType? keyboardType;
|
|
final bool autofocus;
|
|
final String? semanticLabel;
|
|
final ValueChanged<String>? onSubmitted;
|
|
final bool isRequired;
|
|
|
|
const ConduitInput({
|
|
super.key,
|
|
this.label,
|
|
this.hint,
|
|
this.controller,
|
|
this.onChanged,
|
|
this.onTap,
|
|
this.obscureText = false,
|
|
this.enabled = true,
|
|
this.errorText,
|
|
this.maxLines = 1,
|
|
this.suffixIcon,
|
|
this.prefixIcon,
|
|
this.keyboardType,
|
|
this.autofocus = false,
|
|
this.semanticLabel,
|
|
this.onSubmitted,
|
|
this.isRequired = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (label != null) ...[
|
|
Row(
|
|
children: [
|
|
Text(
|
|
label!,
|
|
style: AppTypography.standard.copyWith(
|
|
fontWeight: FontWeight.w500,
|
|
color: context.conduitTheme.textPrimary,
|
|
),
|
|
),
|
|
if (isRequired) ...[
|
|
SizedBox(width: Spacing.textSpacing),
|
|
Text(
|
|
'*',
|
|
style: AppTypography.standard.copyWith(
|
|
color: context.conduitTheme.error,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
SizedBox(height: Spacing.sm),
|
|
],
|
|
Semantics(
|
|
label: semanticLabel ?? label ?? 'Input field',
|
|
textField: true,
|
|
child: TextField(
|
|
controller: controller,
|
|
onChanged: onChanged,
|
|
onTap: onTap,
|
|
onSubmitted: onSubmitted,
|
|
obscureText: obscureText,
|
|
enabled: enabled,
|
|
maxLines: maxLines,
|
|
keyboardType: keyboardType,
|
|
autofocus: autofocus,
|
|
style: AppTypography.standard.copyWith(
|
|
color: context.conduitTheme.textPrimary,
|
|
),
|
|
decoration: InputDecoration(
|
|
hintText: hint,
|
|
hintStyle: AppTypography.standard.copyWith(
|
|
color: context.conduitTheme.inputPlaceholder,
|
|
),
|
|
filled: true,
|
|
fillColor: context.conduitTheme.inputBackground,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.input),
|
|
borderSide: BorderSide(
|
|
color: context.conduitTheme.inputBorder,
|
|
width: BorderWidth.standard,
|
|
),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.input),
|
|
borderSide: BorderSide(
|
|
color: context.conduitTheme.inputBorder,
|
|
width: BorderWidth.standard,
|
|
),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.input),
|
|
borderSide: BorderSide(
|
|
color: context.conduitTheme.buttonPrimary,
|
|
width: BorderWidth.thick,
|
|
),
|
|
),
|
|
errorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.input),
|
|
borderSide: BorderSide(
|
|
color: context.conduitTheme.error,
|
|
width: BorderWidth.standard,
|
|
),
|
|
),
|
|
focusedErrorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.input),
|
|
borderSide: BorderSide(
|
|
color: context.conduitTheme.error,
|
|
width: BorderWidth.thick,
|
|
),
|
|
),
|
|
contentPadding: EdgeInsets.symmetric(
|
|
horizontal: Spacing.inputPadding,
|
|
vertical: Spacing.md,
|
|
),
|
|
suffixIcon: suffixIcon,
|
|
prefixIcon: prefixIcon,
|
|
errorText: errorText,
|
|
errorStyle: AppTypography.small.copyWith(
|
|
color: context.conduitTheme.error,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class ConduitCard extends StatelessWidget {
|
|
final Widget child;
|
|
final EdgeInsetsGeometry? padding;
|
|
final VoidCallback? onTap;
|
|
final bool isSelected;
|
|
final bool isElevated;
|
|
final bool isCompact;
|
|
|
|
const ConduitCard({
|
|
super.key,
|
|
required this.child,
|
|
this.padding,
|
|
this.onTap,
|
|
this.isSelected = false,
|
|
this.isElevated = false,
|
|
this.isCompact = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
padding:
|
|
padding ??
|
|
EdgeInsets.all(isCompact ? Spacing.md : Spacing.cardPadding),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? context.conduitTheme.buttonPrimary.withValues(
|
|
alpha: Alpha.highlight,
|
|
)
|
|
: context.conduitTheme.cardBackground,
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.card),
|
|
border: Border.all(
|
|
color: isSelected
|
|
? context.conduitTheme.buttonPrimary.withValues(
|
|
alpha: Alpha.standard,
|
|
)
|
|
: context.conduitTheme.cardBorder,
|
|
width: BorderWidth.standard,
|
|
),
|
|
boxShadow: isElevated ? ConduitShadows.card : null,
|
|
),
|
|
child: child,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ConduitIconButton extends ConsumerWidget {
|
|
final IconData icon;
|
|
final VoidCallback? onPressed;
|
|
final String? tooltip;
|
|
final bool isActive;
|
|
final Color? backgroundColor;
|
|
final Color? iconColor;
|
|
final bool isCompact;
|
|
final bool isCircular;
|
|
|
|
const ConduitIconButton({
|
|
super.key,
|
|
required this.icon,
|
|
this.onPressed,
|
|
this.tooltip,
|
|
this.isActive = false,
|
|
this.backgroundColor,
|
|
this.iconColor,
|
|
this.isCompact = false,
|
|
this.isCircular = true,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final hapticEnabled = ref.watch(hapticEnabledProvider);
|
|
final effectiveBackgroundColor =
|
|
backgroundColor ??
|
|
(isActive
|
|
? context.conduitTheme.buttonPrimary.withValues(
|
|
alpha: Alpha.highlight,
|
|
)
|
|
: Colors.transparent);
|
|
final effectiveIconColor =
|
|
iconColor ??
|
|
(isActive
|
|
? context.conduitTheme.buttonPrimary
|
|
: context.conduitTheme.iconSecondary);
|
|
|
|
// Build semantic label with context
|
|
String semanticLabel = tooltip ?? 'Button';
|
|
if (isActive) {
|
|
semanticLabel = '$semanticLabel, active';
|
|
}
|
|
|
|
return Semantics(
|
|
label: semanticLabel,
|
|
button: true,
|
|
enabled: onPressed != null,
|
|
child: Tooltip(
|
|
message: tooltip ?? '',
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
if (onPressed != null) {
|
|
PlatformService.hapticFeedbackWithSettings(
|
|
type: HapticType.selection,
|
|
hapticEnabled: hapticEnabled,
|
|
);
|
|
onPressed!();
|
|
}
|
|
},
|
|
child: Container(
|
|
width: isCompact ? TouchTarget.medium : TouchTarget.minimum,
|
|
height: isCompact ? TouchTarget.medium : TouchTarget.minimum,
|
|
decoration: BoxDecoration(
|
|
color: effectiveBackgroundColor,
|
|
borderRadius: BorderRadius.circular(
|
|
isCircular
|
|
? AppBorderRadius.circular
|
|
: AppBorderRadius.standard,
|
|
),
|
|
border: isActive
|
|
? Border.all(
|
|
color: context.conduitTheme.buttonPrimary.withValues(
|
|
alpha: Alpha.standard,
|
|
),
|
|
width: BorderWidth.standard,
|
|
)
|
|
: null,
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
size: isCompact ? IconSize.small : IconSize.medium,
|
|
color: effectiveIconColor,
|
|
semanticLabel: tooltip,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ConduitLoadingIndicator extends StatelessWidget {
|
|
final String? message;
|
|
final double size;
|
|
final bool isCompact;
|
|
|
|
const ConduitLoadingIndicator({
|
|
super.key,
|
|
this.message,
|
|
this.size = 24,
|
|
this.isCompact = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
SizedBox(
|
|
width: size,
|
|
height: size,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: isCompact ? 2 : 3,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
context.conduitTheme.buttonPrimary,
|
|
),
|
|
),
|
|
),
|
|
if (message != null) ...[
|
|
SizedBox(height: isCompact ? Spacing.sm : Spacing.md),
|
|
Text(
|
|
message!,
|
|
style: AppTypography.standard.copyWith(
|
|
color: context.conduitTheme.textSecondary,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class ConduitEmptyState extends StatelessWidget {
|
|
final IconData icon;
|
|
final String title;
|
|
final String message;
|
|
final Widget? action;
|
|
final bool isCompact;
|
|
|
|
const ConduitEmptyState({
|
|
super.key,
|
|
required this.icon,
|
|
required this.title,
|
|
required this.message,
|
|
this.action,
|
|
this.isCompact = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(isCompact ? Spacing.md : Spacing.lg),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
width: isCompact ? IconSize.xxl : IconSize.xxl + Spacing.md,
|
|
height: isCompact ? IconSize.xxl : IconSize.xxl + Spacing.md,
|
|
decoration: BoxDecoration(
|
|
color: context.conduitTheme.surfaceBackground,
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.circular),
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
size: isCompact ? IconSize.xl : TouchTarget.minimum,
|
|
color: context.conduitTheme.iconSecondary,
|
|
),
|
|
),
|
|
SizedBox(height: isCompact ? Spacing.sm : Spacing.md),
|
|
Text(
|
|
title,
|
|
style: AppTypography.headlineSmallStyle.copyWith(
|
|
color: context.conduitTheme.textPrimary,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
SizedBox(height: Spacing.sm),
|
|
Text(
|
|
message,
|
|
style: AppTypography.standard.copyWith(
|
|
color: context.conduitTheme.textSecondary,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
if (action != null) ...[
|
|
SizedBox(height: isCompact ? Spacing.md : Spacing.lg),
|
|
action!,
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ConduitAvatar extends StatelessWidget {
|
|
final double size;
|
|
final IconData? icon;
|
|
final String? text;
|
|
final bool isCompact;
|
|
|
|
const ConduitAvatar({
|
|
super.key,
|
|
this.size = 32,
|
|
this.icon,
|
|
this.text,
|
|
this.isCompact = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BrandService.createBrandAvatar(
|
|
size: isCompact ? size * 0.8 : size,
|
|
fallbackText: text,
|
|
);
|
|
}
|
|
}
|
|
|
|
class ConduitBadge extends StatelessWidget {
|
|
final String text;
|
|
final Color? backgroundColor;
|
|
final Color? textColor;
|
|
final bool isCompact;
|
|
|
|
const ConduitBadge({
|
|
super.key,
|
|
required this.text,
|
|
this.backgroundColor,
|
|
this.textColor,
|
|
this.isCompact = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: isCompact ? Spacing.sm : Spacing.md,
|
|
vertical: isCompact ? Spacing.xs : Spacing.sm,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color:
|
|
backgroundColor ??
|
|
context.conduitTheme.buttonPrimary.withValues(
|
|
alpha: Alpha.badgeBackground,
|
|
),
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.badge),
|
|
),
|
|
child: Text(
|
|
text,
|
|
style: AppTypography.small.copyWith(
|
|
color: textColor ?? context.conduitTheme.buttonPrimary,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ConduitChip extends StatelessWidget {
|
|
final String label;
|
|
final VoidCallback? onTap;
|
|
final bool isSelected;
|
|
final IconData? icon;
|
|
final bool isCompact;
|
|
|
|
const ConduitChip({
|
|
super.key,
|
|
required this.label,
|
|
this.onTap,
|
|
this.isSelected = false,
|
|
this.icon,
|
|
this.isCompact = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: isCompact ? Spacing.sm : Spacing.md,
|
|
vertical: isCompact ? Spacing.xs : Spacing.sm,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? context.conduitTheme.buttonPrimary.withValues(
|
|
alpha: Alpha.highlight,
|
|
)
|
|
: context.conduitTheme.surfaceContainer,
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.chip),
|
|
border: Border.all(
|
|
color: isSelected
|
|
? context.conduitTheme.buttonPrimary.withValues(
|
|
alpha: Alpha.standard,
|
|
)
|
|
: context.conduitTheme.dividerColor,
|
|
width: BorderWidth.standard,
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (icon != null) ...[
|
|
Icon(
|
|
icon,
|
|
size: isCompact ? IconSize.xs : IconSize.small,
|
|
color: isSelected
|
|
? context.conduitTheme.buttonPrimary
|
|
: context.conduitTheme.iconSecondary,
|
|
),
|
|
SizedBox(width: Spacing.iconSpacing),
|
|
],
|
|
Text(
|
|
label,
|
|
style: AppTypography.small.copyWith(
|
|
color: isSelected
|
|
? context.conduitTheme.buttonPrimary
|
|
: context.conduitTheme.textSecondary,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ConduitDivider extends StatelessWidget {
|
|
final bool isCompact;
|
|
final Color? color;
|
|
|
|
const ConduitDivider({super.key, this.isCompact = false, this.color});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
height: BorderWidth.standard,
|
|
color: color ?? context.conduitTheme.dividerColor,
|
|
margin: EdgeInsets.symmetric(
|
|
vertical: isCompact ? Spacing.sm : Spacing.md,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ConduitSpacer extends StatelessWidget {
|
|
final double height;
|
|
final bool isCompact;
|
|
|
|
const ConduitSpacer({super.key, this.height = 16, this.isCompact = false});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox(height: isCompact ? height * 0.5 : height);
|
|
}
|
|
}
|
|
|
|
/// Enhanced form field with better accessibility and validation
|
|
class AccessibleFormField extends StatelessWidget {
|
|
final String? label;
|
|
final String? hint;
|
|
final TextEditingController? controller;
|
|
final ValueChanged<String>? onChanged;
|
|
final ValueChanged<String>? onSubmitted;
|
|
final VoidCallback? onTap;
|
|
final bool obscureText;
|
|
final bool enabled;
|
|
final String? errorText;
|
|
final int? maxLines;
|
|
final Widget? suffixIcon;
|
|
final Widget? prefixIcon;
|
|
final TextInputType? keyboardType;
|
|
final bool autofocus;
|
|
final String? semanticLabel;
|
|
final String? Function(String?)? validator;
|
|
final bool isRequired;
|
|
final bool isCompact;
|
|
|
|
const AccessibleFormField({
|
|
super.key,
|
|
this.label,
|
|
this.hint,
|
|
this.controller,
|
|
this.onChanged,
|
|
this.onSubmitted,
|
|
this.onTap,
|
|
this.obscureText = false,
|
|
this.enabled = true,
|
|
this.errorText,
|
|
this.maxLines = 1,
|
|
this.suffixIcon,
|
|
this.prefixIcon,
|
|
this.keyboardType,
|
|
this.autofocus = false,
|
|
this.semanticLabel,
|
|
this.validator,
|
|
this.isRequired = false,
|
|
this.isCompact = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (label != null) ...[
|
|
Row(
|
|
children: [
|
|
Text(
|
|
label!,
|
|
style: AppTypography.standard.copyWith(
|
|
fontWeight: FontWeight.w500,
|
|
color: context.conduitTheme.textPrimary,
|
|
),
|
|
),
|
|
if (isRequired) ...[
|
|
SizedBox(width: Spacing.textSpacing),
|
|
Text(
|
|
'*',
|
|
style: AppTypography.standard.copyWith(
|
|
color: context.conduitTheme.error,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
SizedBox(height: isCompact ? Spacing.xs : Spacing.sm),
|
|
],
|
|
Semantics(
|
|
label: semanticLabel ?? label ?? 'Input field',
|
|
textField: true,
|
|
child: TextFormField(
|
|
controller: controller,
|
|
onChanged: onChanged,
|
|
onTap: onTap,
|
|
onFieldSubmitted: onSubmitted,
|
|
obscureText: obscureText,
|
|
enabled: enabled,
|
|
maxLines: maxLines,
|
|
keyboardType: keyboardType,
|
|
autofocus: autofocus,
|
|
validator: validator,
|
|
style: AppTypography.standard.copyWith(
|
|
color: context.conduitTheme.textPrimary,
|
|
),
|
|
decoration: InputDecoration(
|
|
hintText: hint,
|
|
hintStyle: AppTypography.standard.copyWith(
|
|
color: context.conduitTheme.inputPlaceholder,
|
|
),
|
|
filled: true,
|
|
fillColor: context.conduitTheme.inputBackground,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.input),
|
|
borderSide: BorderSide(
|
|
color: context.conduitTheme.inputBorder,
|
|
width: BorderWidth.standard,
|
|
),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.input),
|
|
borderSide: BorderSide(
|
|
color: context.conduitTheme.inputBorder,
|
|
width: BorderWidth.standard,
|
|
),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.input),
|
|
borderSide: BorderSide(
|
|
color: context.conduitTheme.buttonPrimary,
|
|
width: BorderWidth.thick,
|
|
),
|
|
),
|
|
errorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.input),
|
|
borderSide: BorderSide(
|
|
color: context.conduitTheme.error,
|
|
width: BorderWidth.standard,
|
|
),
|
|
),
|
|
focusedErrorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.input),
|
|
borderSide: BorderSide(
|
|
color: context.conduitTheme.error,
|
|
width: BorderWidth.thick,
|
|
),
|
|
),
|
|
contentPadding: EdgeInsets.symmetric(
|
|
horizontal: isCompact ? Spacing.md : Spacing.inputPadding,
|
|
vertical: isCompact ? Spacing.sm : Spacing.md,
|
|
),
|
|
suffixIcon: suffixIcon,
|
|
prefixIcon: prefixIcon,
|
|
errorText: errorText,
|
|
errorStyle: AppTypography.small.copyWith(
|
|
color: context.conduitTheme.error,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Enhanced section header with better typography
|
|
class ConduitSectionHeader extends StatelessWidget {
|
|
final String title;
|
|
final String? subtitle;
|
|
final Widget? action;
|
|
final bool isCompact;
|
|
|
|
const ConduitSectionHeader({
|
|
super.key,
|
|
required this.title,
|
|
this.subtitle,
|
|
this.action,
|
|
this.isCompact = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: isCompact ? Spacing.md : Spacing.pagePadding,
|
|
vertical: isCompact ? Spacing.sm : Spacing.md,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: AppTypography.headlineSmallStyle.copyWith(
|
|
color: context.conduitTheme.textPrimary,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
if (subtitle != null) ...[
|
|
SizedBox(height: Spacing.textSpacing),
|
|
Text(
|
|
subtitle!,
|
|
style: AppTypography.standard.copyWith(
|
|
color: context.conduitTheme.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
if (action != null) ...[SizedBox(width: Spacing.md), action!],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Enhanced list item with better consistency
|
|
class ConduitListItem extends StatelessWidget {
|
|
final Widget leading;
|
|
final Widget title;
|
|
final Widget? subtitle;
|
|
final Widget? trailing;
|
|
final VoidCallback? onTap;
|
|
final bool isSelected;
|
|
final bool isCompact;
|
|
|
|
const ConduitListItem({
|
|
super.key,
|
|
required this.leading,
|
|
required this.title,
|
|
this.subtitle,
|
|
this.trailing,
|
|
this.onTap,
|
|
this.isSelected = false,
|
|
this.isCompact = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
padding: EdgeInsets.all(
|
|
isCompact ? Spacing.sm : Spacing.listItemPadding,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? context.conduitTheme.buttonPrimary.withValues(
|
|
alpha: Alpha.highlight,
|
|
)
|
|
: Colors.transparent,
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.standard),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
leading,
|
|
SizedBox(width: isCompact ? Spacing.sm : Spacing.md),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
title,
|
|
if (subtitle != null) ...[
|
|
SizedBox(height: Spacing.textSpacing),
|
|
subtitle!,
|
|
],
|
|
],
|
|
),
|
|
),
|
|
if (trailing != null) ...[
|
|
SizedBox(width: isCompact ? Spacing.sm : Spacing.md),
|
|
trailing!,
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|