diff --git a/supabase-ws-flutter-mobile/lib/catalog_main.dart b/supabase-ws-flutter-mobile/lib/catalog_main.dart new file mode 100644 index 0000000..ecf440c --- /dev/null +++ b/supabase-ws-flutter-mobile/lib/catalog_main.dart @@ -0,0 +1,133 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import 'package:flutter_template/core/common/logger.dart'; +import 'package:flutter_template/ui/resources.dart'; +import 'package:flutter_template/ui/theme/app_theme.dart'; +import 'package:flutter_template/ui/widgets/design_system/buttons/base_button.dart'; +import 'package:flutter_template/ui/widgets/design_system/buttons/primary_button.dart'; +import 'package:flutter_template/ui/widgets/design_system/text_fields/input_text.dart'; + +void main() { + runZonedGuarded( + () { + runApp(const MyCatalogApp()); + }, + (exception, stackTrace) => + Logger.fatal(error: exception, stackTrace: stackTrace), + ); +} + +class MyCatalogApp extends StatelessWidget { + const MyCatalogApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => ScreenUtilInit( + designSize: const Size(375, 812), + minTextAdapt: false, + splitScreenMode: true, + builder: (_, __) => const _CatalogAppContentScreen(), + ); +} + +class _CatalogAppContentScreen extends StatefulWidget { + const _CatalogAppContentScreen({ + Key? key, + }) : super(key: key); + + @override + State<_CatalogAppContentScreen> createState() => + _CatalogAppContentScreenState(); +} + +class _CatalogAppContentScreenState extends State<_CatalogAppContentScreen> { + final _messengerKey = GlobalKey(); + + @override + Widget build(BuildContext context) => MaterialApp( + scaffoldMessengerKey: _messengerKey, + home: SafeArea( + child: Scaffold( + body: Padding( + padding: const EdgeInsets.all(20.0), + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: AppPrimaryButton( + text: 'Filled Button', + onPressed: () { + _messengerKey.currentState?.showSnackBar( + const SnackBar( + content: Text('Filled button pressed!'), + ), + ); + }, + style: StyleButton.filled, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: AppPrimaryButton( + text: 'Stroke Button', + onPressed: () { + _messengerKey.currentState?.showSnackBar( + const SnackBar( + content: Text('Stroke button pressed!'), + ), + ); + }, + style: StyleButton.stroke, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: AppPrimaryButton( + text: 'Ghost Button', + onPressed: () { + _messengerKey.currentState?.showSnackBar( + const SnackBar( + content: Text('Ghost button pressed!'), + ), + ); + }, + style: StyleButton.ghost, + ), + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: AppTextInputField( + hintText: 'Write your text...', + labelText: 'Label', + ), + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: AppTextInputField( + hintText: 'Write your text...', + labelText: 'Label', + error: 'Error text', + ), + ), + ], + ), + ), + ), + ), + builder: (context, child) { + Resources.setup(context); + return child!; + }, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + theme: AppTheme.provideAppTheme(context), + ); +} diff --git a/supabase-ws-flutter-mobile/lib/ui/theme/app_colors.dart b/supabase-ws-flutter-mobile/lib/ui/theme/app_colors.dart index 754d5bc..9d77d68 100644 --- a/supabase-ws-flutter-mobile/lib/ui/theme/app_colors.dart +++ b/supabase-ws-flutter-mobile/lib/ui/theme/app_colors.dart @@ -56,7 +56,7 @@ class AppColors extends ColorScheme { static AppColors getColorScheme() => AppColors( colorScheme: ColorScheme.fromSeed( seedColor: const MaterialColor( - 0x0E9F6F, + 0xFF0E9F6F, { 100: Color.fromRGBO(248, 254, 252, 1), 200: Color.fromRGBO(225, 247, 240, 1), @@ -73,7 +73,7 @@ class AppColors extends ColorScheme { ), brightness: Brightness.dark, primary: const MaterialColor( - 0x0E9F6F, + 0xFF0E9F6F, { 100: Color.fromRGBO(248, 254, 252, 1), 200: Color.fromRGBO(225, 247, 240, 1), @@ -89,7 +89,7 @@ class AppColors extends ColorScheme { ), onPrimary: Colors.white, secondary: const MaterialColor( - 0xE0E0E0, + 0xFFE0E0E0, { 100: Color.fromRGBO(255, 255, 255, 1), 200: Color.fromRGBO(249, 249, 249, 1), @@ -105,7 +105,7 @@ class AppColors extends ColorScheme { ), onSecondary: Colors.black, background: const MaterialColor( - 0x0B0C0D, + 0xFF0B0C0D, { 100: Color.fromRGBO(163, 170, 177, 1), 200: Color.fromRGBO(138, 145, 153, 1), @@ -118,7 +118,7 @@ class AppColors extends ColorScheme { surface: Colors.black, onSurface: Colors.white, textColor: const MaterialColor( - 0xFFFFFF, + 0xFFFFFFFF, { 100: Color.fromRGBO(255, 255, 255, 1), 200: Color.fromRGBO(194, 194, 204, 1), @@ -128,7 +128,7 @@ class AppColors extends ColorScheme { }, ), success: const MaterialColor( - 0x10B981, + 0xFF10B981, { 100: Color.fromRGBO(231, 248, 242, 1), 200: Color.fromRGBO(159, 227, 205, 1), @@ -138,7 +138,7 @@ class AppColors extends ColorScheme { }, ), info: const MaterialColor( - 0x1169F7, + 0xFF1169F7, { 100: Color.fromRGBO(207, 232, 254, 1), 200: Color.fromRGBO(111, 176, 252, 1), @@ -148,7 +148,7 @@ class AppColors extends ColorScheme { }, ), warning: const MaterialColor( - 0xF59E0B, + 0xFFF59E0B, { 100: Color.fromRGBO(254, 245, 231, 1), 200: Color.fromRGBO(251, 216, 157, 1), @@ -158,7 +158,7 @@ class AppColors extends ColorScheme { }, ), error: const MaterialColor( - 0xF4444, + 0xFF0E9F6F, { 100: Color.fromRGBO(253, 236, 236, 1), 200: Color.fromRGBO(249, 180, 180, 1), diff --git a/supabase-ws-flutter-mobile/lib/ui/widgets/design_system/buttons/base_button.dart b/supabase-ws-flutter-mobile/lib/ui/widgets/design_system/buttons/base_button.dart new file mode 100644 index 0000000..8426f20 --- /dev/null +++ b/supabase-ws-flutter-mobile/lib/ui/widgets/design_system/buttons/base_button.dart @@ -0,0 +1,91 @@ +import 'package:flutter_template/ui/extensions/context_extensions.dart'; +import 'package:flutter_template/ui/theme/app_theme.dart'; +import 'package:flutter_template/ui/theme/text_styles.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class AppBaseButton extends StatelessWidget { + final String text; + final VoidCallback? onPressed; + final Color backgroundColor; + final Color textColor; + final Color focusColor; + final Color disabledTextColor; + final Color pressedColor; + final Color disabledColor; + final Color hoveredColor; + final Color? borderSideColor; + final Icon? iconLeft; + final Icon? iconRight; + final double verticalPadding; + + const AppBaseButton({ + required this.text, + required this.onPressed, + required this.backgroundColor, + required this.textColor, + required this.disabledColor, + required this.focusColor, + required this.pressedColor, + required this.disabledTextColor, + required this.hoveredColor, + this.borderSideColor, + this.iconLeft, + this.iconRight, + this.verticalPadding = 12, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) => MaterialButton( + minWidth: 100.w, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28.r), + side: BorderSide( + width: 2, + color: borderSideColor ?? Colors.transparent, + ), + ), + elevation: 0.0, + color: backgroundColor, + disabledColor: disabledColor, + disabledTextColor: disabledTextColor, + highlightColor: pressedColor, + textColor: textColor, + onPressed: onPressed, + hoverColor: hoveredColor, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (iconLeft != null) + Container( + margin: EdgeInsets.only(right: 16.w), + child: iconLeft, + ), + Padding( + padding: EdgeInsets.symmetric( + vertical: verticalPadding.h, + ), + child: Text( + text, + style: context.theme.textStyles.bodySmall + ?.semibold() + .copyWith(color: textColor), + ), + ), + if (iconRight != null) + Container( + margin: EdgeInsets.only(left: 16.w), + child: iconRight, + ), + ], + ), + ); +} + +enum StyleButton { + filled, + stroke, + ghost, +} diff --git a/supabase-ws-flutter-mobile/lib/ui/widgets/design_system/buttons/primary_button.dart b/supabase-ws-flutter-mobile/lib/ui/widgets/design_system/buttons/primary_button.dart new file mode 100644 index 0000000..cbb0c25 --- /dev/null +++ b/supabase-ws-flutter-mobile/lib/ui/widgets/design_system/buttons/primary_button.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_template/ui/extensions/context_extensions.dart'; +import 'package:flutter_template/ui/theme/app_theme.dart'; +import 'package:flutter_template/ui/widgets/design_system/buttons/base_button.dart'; + +class AppPrimaryButton extends StatelessWidget { + final String text; + final VoidCallback? onPressed; + final Icon? iconLeft; + final Icon? iconRight; + final StyleButton style; + + const AppPrimaryButton({ + required this.text, + required this.onPressed, + required this.style, + this.iconLeft, + this.iconRight, + Key? key, + }) : super(key: key); + + factory AppPrimaryButton.stroke( + String text, + VoidCallback? onPressed, { + Icon? iconLeft, + Icon? iconRight, + }) => + AppPrimaryButton( + text: text, + onPressed: onPressed, + style: StyleButton.stroke, + iconRight: iconRight, + iconLeft: iconLeft, + ); + + factory AppPrimaryButton.filled( + String text, + VoidCallback? onPressed, { + Icon? iconLeft, + Icon? iconRight, + }) => + AppPrimaryButton( + text: text, + onPressed: onPressed, + style: StyleButton.filled, + iconRight: iconRight, + iconLeft: iconLeft, + ); + + factory AppPrimaryButton.ghost( + String text, + VoidCallback? onPressed, { + Icon? iconLeft, + Icon? iconRight, + }) => + AppPrimaryButton( + text: text, + onPressed: onPressed, + style: StyleButton.ghost, + iconRight: iconRight, + iconLeft: iconLeft, + ); + + @override + Widget build(BuildContext context) { + switch (style) { + case StyleButton.filled: + return _primaryFilledButton(context); + case StyleButton.stroke: + return _primaryStrokeButton(context); + case StyleButton.ghost: + return _primaryGhostButton(context); + } + } + + AppBaseButton _primaryFilledButton(BuildContext context) => AppBaseButton( + text: text, + onPressed: onPressed, + iconLeft: iconLeft, + iconRight: iconRight, + backgroundColor: context.theme.colors.primary.shade500, + textColor: context.theme.colors.textColor.shade100, + disabledColor: context.theme.colors.background.shade400, + disabledTextColor: context.theme.colors.textColor.shade300, + hoveredColor: context.theme.colors.primary.shade400, + focusColor: context.theme.colors.primary.shade500, + pressedColor: context.theme.colors.primary.shade600, + ); + + AppBaseButton _primaryGhostButton(BuildContext context) => AppBaseButton( + text: text, + onPressed: onPressed, + iconLeft: iconLeft, + iconRight: iconRight, + backgroundColor: Colors.transparent, + textColor: context.theme.colors.primary.shade500, + disabledColor: Colors.transparent, + disabledTextColor: context.theme.colors.textColor.shade300, + focusColor: Colors.transparent, + pressedColor: Colors.transparent, + hoveredColor: Colors.transparent, + ); + + AppBaseButton _primaryStrokeButton(BuildContext context) => AppBaseButton( + text: text, + onPressed: onPressed, + iconLeft: iconLeft, + iconRight: iconRight, + backgroundColor: Colors.transparent, + textColor: context.theme.colors.primary.shade500, + disabledColor: Colors.transparent, + focusColor: Colors.transparent, + pressedColor: Colors.transparent, + hoveredColor: Colors.transparent, + disabledTextColor: context.theme.colors.textColor.shade300, + borderSideColor: context.theme.colors.primary.shade900, + ); +} diff --git a/supabase-ws-flutter-mobile/lib/ui/widgets/design_system/text_fields/input_text.dart b/supabase-ws-flutter-mobile/lib/ui/widgets/design_system/text_fields/input_text.dart new file mode 100644 index 0000000..4999cd2 --- /dev/null +++ b/supabase-ws-flutter-mobile/lib/ui/widgets/design_system/text_fields/input_text.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_template/ui/extensions/context_extensions.dart'; +import 'package:flutter_template/ui/theme/app_theme.dart'; + +class AppTextInputField extends StatelessWidget { + final TextEditingController? controller; + final IconData? leadingIcon; + final IconData? trailingIcon; + final String? hintText; + final String? labelText; + final void Function()? actionSecondIcon; + final Color? backgroundColor; + final String? error; + final Function(String)? onChanged; + final bool? obscureText; + + const AppTextInputField({ + Key? key, + this.controller, + this.leadingIcon, + this.trailingIcon, + this.hintText, + this.labelText, + this.actionSecondIcon, + this.backgroundColor, + this.error, + this.onChanged, + this.obscureText, + }) : super(key: key); + + @override + Widget build(BuildContext context) => TextField( + onChanged: onChanged, + controller: controller, + decoration: InputDecoration( + focusedBorder: OutlineInputBorder( + borderSide: error == null + ? BorderSide(color: context.theme.colors.primary.shade900) + : BorderSide(color: context.theme.colors.error.shade400), + borderRadius: BorderRadius.circular(4.r), + ), + enabledBorder: OutlineInputBorder( + borderSide: + BorderSide(color: context.theme.colors.primary.shade900), + borderRadius: BorderRadius.circular(4.r), + ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: context.theme.colors.error.shade400), + borderRadius: BorderRadius.circular(4.r), + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide(color: context.theme.colors.error.shade400), + borderRadius: BorderRadius.circular(4.r), + ), + prefixIcon: leadingIcon != null ? Icon(leadingIcon) : null, + labelText: labelText, + labelStyle: context.theme.textStyles.bodySmall?.copyWith( + color: context.theme.colors.textColor.shade100, + ), + hintText: hintText, + hintStyle: context.theme.textStyles.bodyMedium?.copyWith( + color: context.theme.colors.textColor.shade300, + ), + errorText: error, + errorStyle: context.theme.textStyles.bodySmall?.copyWith( + color: context.theme.colors.error.shade300, + ), + ), + style: context.theme.textStyles.bodyMedium?.copyWith( + color: context.theme.colors.textColor.shade200, + ), + obscureText: obscureText ?? false, + ); +}