feat: implement core architecture, design system, localization, and trip features for both driver and rider apps.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../design/tokens.dart';
|
||||
|
||||
/// حالة اللاشيء — رسالة + أيقونة.
|
||||
/// docs/26 §7
|
||||
class EmptyView extends StatelessWidget {
|
||||
const EmptyView({
|
||||
super.key,
|
||||
this.icon = Icons.inbox_rounded,
|
||||
this.message,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String? message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(T.s32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 64, color: cs.onSurfaceVariant.withValues(alpha: 0.4)),
|
||||
const SizedBox(height: T.s16),
|
||||
Text(
|
||||
message ?? 'لا توجد عناصر بعد',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../design/tokens.dart';
|
||||
|
||||
/// حالة الخطأ — رسالة بالعربية + زر إعادة المحاولة.
|
||||
/// docs/26 §7
|
||||
class ErrorView extends StatelessWidget {
|
||||
const ErrorView({
|
||||
super.key,
|
||||
this.message,
|
||||
this.onRetry,
|
||||
});
|
||||
|
||||
final String? message;
|
||||
final VoidCallback? onRetry;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(T.s32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.error_outline_rounded,
|
||||
size: 64, color: cs.error.withValues(alpha: 0.6)),
|
||||
const SizedBox(height: T.s16),
|
||||
Text(
|
||||
message ?? 'حدث خطأ ما',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: cs.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (onRetry != null) ...[
|
||||
const SizedBox(height: T.s16),
|
||||
FilledButton.icon(
|
||||
onPressed: onRetry,
|
||||
icon: const Icon(Icons.refresh_rounded, size: 20),
|
||||
label: const Text('أعد المحاولة'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../design/tokens.dart';
|
||||
|
||||
/// هيكل التحميل — تأثير نبض على عناصر مستوحاة من الشاشة الفعلية.
|
||||
/// docs/26 §7
|
||||
class Skeleton extends StatelessWidget {
|
||||
const Skeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final baseColor = cs.surfaceContainerHighest;
|
||||
final highlightColor = cs.surfaceContainerHigh;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(T.pagePadding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_PulseBox(width: double.infinity, height: 200, base: baseColor, highlight: highlightColor),
|
||||
const SizedBox(height: T.s16),
|
||||
_PulseBox(width: 200, height: 20, base: baseColor, highlight: highlightColor),
|
||||
const SizedBox(height: T.s12),
|
||||
_PulseBox(width: 140, height: 16, base: baseColor, highlight: highlightColor),
|
||||
const SizedBox(height: T.s24),
|
||||
Row(
|
||||
children: [
|
||||
_PulseBox(width: 48, height: 48, base: baseColor, highlight: highlightColor, radius: T.rCircle),
|
||||
const SizedBox(width: T.s12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_PulseBox(width: double.infinity, height: 16, base: baseColor, highlight: highlightColor),
|
||||
const SizedBox(height: T.s8),
|
||||
_PulseBox(width: 120, height: 14, base: baseColor, highlight: highlightColor),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PulseBox extends StatefulWidget {
|
||||
const _PulseBox({
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.base,
|
||||
required this.highlight,
|
||||
this.radius = 8,
|
||||
});
|
||||
|
||||
final double width;
|
||||
final double height;
|
||||
final Color base;
|
||||
final Color highlight;
|
||||
final double radius;
|
||||
|
||||
@override
|
||||
State<_PulseBox> createState() => _PulseBoxState();
|
||||
}
|
||||
|
||||
class _PulseBoxState extends State<_PulseBox>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _ctrl;
|
||||
late final Animation<double> _anim;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 1200))
|
||||
..repeat();
|
||||
_anim = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _anim,
|
||||
builder: (_, __) {
|
||||
return Container(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
decoration: BoxDecoration(
|
||||
color: Color.lerp(widget.base, widget.highlight, _anim.value),
|
||||
borderRadius: BorderRadius.circular(widget.radius),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../design/tokens.dart';
|
||||
|
||||
/// شريط حالة الرحلة الحي.
|
||||
/// docs/26 §7
|
||||
class StatusBanner extends StatelessWidget {
|
||||
const StatusBanner({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.color,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final Color? color;
|
||||
final IconData? icon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final effectiveColor = color ?? cs.primary;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: T.s16,
|
||||
vertical: T.s12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: effectiveColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(T.r8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(icon, color: effectiveColor, size: T.iconSm),
|
||||
const SizedBox(width: T.s8),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: effectiveColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../design/tokens.dart';
|
||||
|
||||
/// زر موحّد: primary (معبّأ) · secondary (محدّد) · text — مع حالة تحميل داخلية.
|
||||
/// docs/26 §7
|
||||
enum TripzButtonStyle { primary, secondary, text }
|
||||
|
||||
class TripzButton extends StatelessWidget {
|
||||
const TripzButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.onPressed,
|
||||
this.style = TripzButtonStyle.primary,
|
||||
this.loading = false,
|
||||
this.icon,
|
||||
this.fullWidth = true,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
final TripzButtonStyle style;
|
||||
final bool loading;
|
||||
final IconData? icon;
|
||||
final bool fullWidth;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final cs = theme.colorScheme;
|
||||
final labelStyle = theme.textTheme.labelLarge;
|
||||
|
||||
final effectiveOnPressed = loading ? null : onPressed;
|
||||
|
||||
switch (style) {
|
||||
case TripzButtonStyle.primary:
|
||||
return SizedBox(
|
||||
width: fullWidth ? double.infinity : null,
|
||||
height: T.buttonHeight,
|
||||
child: FilledButton(
|
||||
onPressed: effectiveOnPressed,
|
||||
child: _buildChild(cs.primary, labelStyle),
|
||||
),
|
||||
);
|
||||
case TripzButtonStyle.secondary:
|
||||
return SizedBox(
|
||||
width: fullWidth ? double.infinity : null,
|
||||
height: T.buttonHeight,
|
||||
child: OutlinedButton(
|
||||
onPressed: effectiveOnPressed,
|
||||
child: _buildChild(cs.primary, labelStyle),
|
||||
),
|
||||
);
|
||||
case TripzButtonStyle.text:
|
||||
return TextButton(
|
||||
onPressed: effectiveOnPressed,
|
||||
child: _buildChild(cs.primary, labelStyle),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildChild(Color color, TextStyle? style) {
|
||||
if (loading) {
|
||||
return const SizedBox(
|
||||
height: 22,
|
||||
width: 22,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
);
|
||||
}
|
||||
|
||||
if (icon != null) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 20),
|
||||
const SizedBox(width: T.s8),
|
||||
Text(label, style: style),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Text(label, style: style);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../design/tokens.dart';
|
||||
|
||||
/// بطاقة موحّدة: رحلة / سائق / محفظة — زوايا 12، حشوة 16.
|
||||
/// docs/26 §7
|
||||
class TripzCard extends StatelessWidget {
|
||||
const TripzCard({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.padding,
|
||||
this.onTap,
|
||||
this.color,
|
||||
this.borderColor,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final VoidCallback? onTap;
|
||||
final Color? color;
|
||||
final Color? borderColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final card = Card(
|
||||
color: color,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(T.r12),
|
||||
side: borderColor != null
|
||||
? BorderSide(color: borderColor!)
|
||||
: BorderSide.none,
|
||||
),
|
||||
child: Padding(
|
||||
padding: padding ?? const EdgeInsets.all(T.pagePadding),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
||||
if (onTap != null) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(T.r12),
|
||||
child: card,
|
||||
);
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../design/tokens.dart';
|
||||
|
||||
/// السكافولد الموحّد — كل شاشة في التطبيق تُبنى داخله.
|
||||
/// يحوّل `status` من Cubit/Bloc إلى: skeleton / error / empty / محتوى.
|
||||
/// docs/26 §6
|
||||
class TripzScaffold extends StatelessWidget {
|
||||
const TripzScaffold({
|
||||
super.key,
|
||||
this.title,
|
||||
this.status,
|
||||
this.onRetry,
|
||||
this.empty,
|
||||
this.bottomAction,
|
||||
this.actions,
|
||||
this.showBackButton = false,
|
||||
this.onBack,
|
||||
this.safeAreaTop = true,
|
||||
this.safeAreaBottom = true,
|
||||
this.padding = true,
|
||||
this.backgroundColor,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final String? title;
|
||||
final Enum? status;
|
||||
final VoidCallback? onRetry;
|
||||
final Widget? empty;
|
||||
final Widget? bottomAction;
|
||||
final List<Widget>? actions;
|
||||
final bool showBackButton;
|
||||
final VoidCallback? onBack;
|
||||
final bool safeAreaTop;
|
||||
final bool safeAreaBottom;
|
||||
final bool padding;
|
||||
final Color? backgroundColor;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final cs = theme.colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: backgroundColor ?? cs.surface,
|
||||
appBar: title != null
|
||||
? AppBar(
|
||||
title: Text(title!),
|
||||
leading: showBackButton
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20),
|
||||
onPressed: onBack ?? () => Navigator.maybePop(context),
|
||||
)
|
||||
: null,
|
||||
actions: actions,
|
||||
)
|
||||
: null,
|
||||
body: SafeArea(
|
||||
top: safeAreaTop,
|
||||
bottom: safeAreaBottom,
|
||||
child: _buildBody(context),
|
||||
),
|
||||
bottomNavigationBar: bottomAction != null
|
||||
? Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
T.pagePadding,
|
||||
0,
|
||||
T.pagePadding,
|
||||
MediaQuery.of(context).viewInsets.bottom > 0
|
||||
? MediaQuery.of(context).viewInsets.bottom + 8
|
||||
: T.pagePadding,
|
||||
),
|
||||
child: bottomAction,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(BuildContext context) {
|
||||
if (status != null) {
|
||||
if (_isError) return ErrorView(onRetry: onRetry);
|
||||
if (_isLoading) return const Skeleton();
|
||||
if (_isEmpty) return empty ?? const EmptyView();
|
||||
}
|
||||
|
||||
return padding
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(T.pagePadding),
|
||||
child: child,
|
||||
)
|
||||
: child;
|
||||
}
|
||||
|
||||
bool get _isLoading =>
|
||||
status != null && status!.name == 'loading';
|
||||
|
||||
bool get _isError =>
|
||||
status != null && status!.name == 'error';
|
||||
|
||||
bool get _isEmpty =>
|
||||
status != null && status!.name == 'empty';
|
||||
}
|
||||
|
||||
class ErrorView extends StatelessWidget {
|
||||
final VoidCallback? onRetry;
|
||||
const ErrorView({super.key, this.onRetry});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(T.pagePadding),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.error_outline_rounded, size: 48, color: cs.error),
|
||||
const SizedBox(height: T.s16),
|
||||
Text(
|
||||
'حدث خطأ',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (onRetry != null) ...[
|
||||
const SizedBox(height: T.s16),
|
||||
FilledButton(onPressed: onRetry, child: const Text('إعادة المحاولة')),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Skeleton extends StatelessWidget {
|
||||
const Skeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyView extends StatelessWidget {
|
||||
const EmptyView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'لا توجد بيانات',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../design/tokens.dart';
|
||||
|
||||
/// كل bottom sheet في التطبيق يمرّ من هنا: مقبض علوي، زوايا 24.
|
||||
/// docs/26 §7
|
||||
class TripzSheet extends StatelessWidget {
|
||||
const TripzSheet({
|
||||
super.key,
|
||||
this.title,
|
||||
this.child,
|
||||
this.bottomAction,
|
||||
this.isScrollControlled = true,
|
||||
});
|
||||
|
||||
final String? title;
|
||||
final Widget? child;
|
||||
final Widget? bottomAction;
|
||||
final bool isScrollControlled;
|
||||
|
||||
/// مساعد للاستدعاء بأمر واحد
|
||||
static Future<T?> show<T>(
|
||||
BuildContext context, {
|
||||
String? title,
|
||||
Widget? child,
|
||||
Widget? bottomAction,
|
||||
bool isScrollControlled = true,
|
||||
}) {
|
||||
return showModalBottomSheet<T>(
|
||||
context: context,
|
||||
isScrollControlled: isScrollControlled,
|
||||
useSafeArea: true,
|
||||
builder: (_) => TripzSheet(
|
||||
title: title,
|
||||
child: child,
|
||||
bottomAction: bottomAction,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final cs = theme.colorScheme;
|
||||
final bottomPadding = MediaQuery.of(context).viewInsets.bottom;
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(0, 0, 0, bottomPadding),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: T.pagePadding),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (title != null) ...[
|
||||
Text(
|
||||
title!,
|
||||
style: theme.textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: T.s16),
|
||||
],
|
||||
if (child != null) child!,
|
||||
if (bottomAction != null) ...[
|
||||
const SizedBox(height: T.s16),
|
||||
bottomAction!,
|
||||
const SizedBox(height: T.s8),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../design/tokens.dart';
|
||||
|
||||
/// حقل إدخال موحّد: هاتف · OTP · بحث — رسالة الخطأ تحته بالعربية.
|
||||
/// docs/26 §7
|
||||
class TripzTextField extends StatelessWidget {
|
||||
const TripzTextField({
|
||||
super.key,
|
||||
this.controller,
|
||||
this.label,
|
||||
this.hint,
|
||||
this.prefix,
|
||||
this.suffix,
|
||||
this.keyboardType,
|
||||
this.obscureText = false,
|
||||
this.errorText,
|
||||
this.enabled = true,
|
||||
this.onChanged,
|
||||
this.onSubmitted,
|
||||
this.autofocus = false,
|
||||
this.textAlign,
|
||||
this.maxLength,
|
||||
this.contentPadding,
|
||||
this.focusNode,
|
||||
});
|
||||
|
||||
final TextEditingController? controller;
|
||||
final String? label;
|
||||
final String? hint;
|
||||
final Widget? prefix;
|
||||
final Widget? suffix;
|
||||
final TextInputType? keyboardType;
|
||||
final bool obscureText;
|
||||
final String? errorText;
|
||||
final bool enabled;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final ValueChanged<String>? onSubmitted;
|
||||
final bool autofocus;
|
||||
final TextAlign? textAlign;
|
||||
final int? maxLength;
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
final FocusNode? focusNode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: T.inputHeight,
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
keyboardType: keyboardType,
|
||||
obscureText: obscureText,
|
||||
enabled: enabled,
|
||||
onChanged: onChanged,
|
||||
onSubmitted: onSubmitted,
|
||||
autofocus: autofocus,
|
||||
textAlign: textAlign ?? TextAlign.start,
|
||||
maxLength: maxLength,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
hintText: hint,
|
||||
prefixIcon: prefix,
|
||||
suffixIcon: suffix,
|
||||
counterText: '',
|
||||
contentPadding: contentPadding ??
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorText != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
errorText!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: cs.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user