From 4a9d3a19a2efe19cc65939e282ee453b5f3df22d Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 8 Apr 2026 01:34:04 +0800 Subject: [PATCH] feat: add base widgets with PageStandards (Button, TextField, Card, ListTile) --- lib/src/widgets/base/standard_button.dart | 105 ++++++++++++++ lib/src/widgets/base/standard_card.dart | 62 +++++++++ lib/src/widgets/base/standard_list_tile.dart | 93 +++++++++++++ lib/src/widgets/base/standard_text_field.dart | 129 ++++++++++++++++++ 4 files changed, 389 insertions(+) create mode 100644 lib/src/widgets/base/standard_button.dart create mode 100644 lib/src/widgets/base/standard_card.dart create mode 100644 lib/src/widgets/base/standard_list_tile.dart create mode 100644 lib/src/widgets/base/standard_text_field.dart diff --git a/lib/src/widgets/base/standard_button.dart b/lib/src/widgets/base/standard_button.dart new file mode 100644 index 0000000..554be14 --- /dev/null +++ b/lib/src/widgets/base/standard_button.dart @@ -0,0 +1,105 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; + +enum StandardButtonType { primary, secondary, outline, text } + +class StandardButton extends StatelessWidget { + final String text; + final VoidCallback? onPressed; + final StandardButtonType type; + final bool isLoading; + final IconData? icon; + final bool isFullWidth; + final double? width; + final double? height; + + const StandardButton({ + super.key, + required this.text, + this.onPressed, + this.type = StandardButtonType.primary, + this.isLoading = false, + this.icon, + this.isFullWidth = false, + this.width, + this.height, + }); + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + + return GestureDetector( + onTap: isLoading ? null : onPressed, + child: Container( + width: isFullWidth ? double.infinity : width, + height: height ?? standards.scaledHeight(48), + padding: standards.scaledPadding( + EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), + decoration: _buildDecoration(standards), + child: _buildChild(standards), + ), + ); + } + + BoxDecoration _buildDecoration(PageStandards standards) { + switch (type) { + case StandardButtonType.primary: + return BoxDecoration( + color: standards.primaryColor, + borderRadius: BorderRadius.circular(standards.scaledRadius(12)), + ); + case StandardButtonType.secondary: + return BoxDecoration( + color: standards.secondaryColor, + borderRadius: BorderRadius.circular(standards.scaledRadius(12)), + ); + case StandardButtonType.outline: + return BoxDecoration( + border: Border.all(color: standards.primaryColor, width: 1.5), + borderRadius: BorderRadius.circular(standards.scaledRadius(12)), + ); + case StandardButtonType.text: + return const BoxDecoration(); + } + } + + Widget _buildChild(PageStandards standards) { + if (isLoading) { + return Center( + child: CupertinoActivityIndicator(color: _getTextColor(standards)), + ); + } + + return Row( + mainAxisSize: isFullWidth ? MainAxisSize.max : MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) ...[ + Icon(icon, color: _getTextColor(standards), size: standards.fontSize + 4), + SizedBox(width: standards.scaledWidth(8)), + ], + Text( + text, + style: standards.textStyle.copyWith( + color: _getTextColor(standards), + fontWeight: FontWeight.w600, + ), + ), + ], + ); + } + + Color _getTextColor(PageStandards standards) { + switch (type) { + case StandardButtonType.primary: + case StandardButtonType.secondary: + return CupertinoColors.white; + case StandardButtonType.outline: + case StandardButtonType.text: + return standards.primaryColor; + } + } +} diff --git a/lib/src/widgets/base/standard_card.dart b/lib/src/widgets/base/standard_card.dart new file mode 100644 index 0000000..ed5a9d5 --- /dev/null +++ b/lib/src/widgets/base/standard_card.dart @@ -0,0 +1,62 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; + +class StandardCard extends StatelessWidget { + final Widget child; + final VoidCallback? onTap; + final EdgeInsetsGeometry? padding; + final EdgeInsetsGeometry? margin; + final Color? backgroundColor; + final double? borderRadius; + final bool showShadow; + final bool showBorder; + final Color? borderColor; + + const StandardCard({ + super.key, + required this.child, + this.onTap, + this.padding, + this.margin, + this.backgroundColor, + this.borderRadius, + this.showShadow = true, + this.showBorder = false, + this.borderColor, + }); + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + + return GestureDetector( + onTap: onTap, + child: Container( + margin: margin ?? standards.scaledPadding(EdgeInsets.all(8)), + padding: padding ?? standards.scaledPadding(EdgeInsets.all(16)), + decoration: BoxDecoration( + color: backgroundColor ?? standards.backgroundColor, + borderRadius: BorderRadius.circular( + borderRadius ?? standards.scaledRadius(16), + ), + boxShadow: showShadow + ? [ + BoxShadow( + color: standards.textColor.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ] + : null, + border: showBorder + ? Border.all( + color: borderColor ?? standards.textColor.withOpacity(0.1), + ) + : null, + ), + child: child, + ), + ); + } +} diff --git a/lib/src/widgets/base/standard_list_tile.dart b/lib/src/widgets/base/standard_list_tile.dart new file mode 100644 index 0000000..9e2c692 --- /dev/null +++ b/lib/src/widgets/base/standard_list_tile.dart @@ -0,0 +1,93 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; + +class StandardListTile extends StatelessWidget { + final IconData? leadingIcon; + final Widget? leading; + final String title; + final String? subtitle; + final Widget? trailing; + final VoidCallback? onTap; + final bool showChevron; + final EdgeInsetsGeometry? padding; + + const StandardListTile({ + super.key, + this.leadingIcon, + this.leading, + required this.title, + this.subtitle, + this.trailing, + this.onTap, + this.showChevron = false, + this.padding, + }); + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Container( + padding: padding ?? + standards.scaledPadding( + EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + child: Row( + children: [ + if (leading != null) leading!, + if (leadingIcon != null) + Container( + width: standards.scaledWidth(40), + height: standards.scaledHeight(40), + decoration: BoxDecoration( + color: standards.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(standards.scaledRadius(10)), + ), + child: Icon( + leadingIcon, + color: standards.primaryColor, + size: standards.fontSize + 8, + ), + ), + if (leadingIcon != null || leading != null) + SizedBox(width: standards.scaledWidth(12)), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: standards.textStyle.copyWith( + fontWeight: FontWeight.w500, + ), + ), + if (subtitle != null) ...[ + SizedBox(height: standards.scaledHeight(2)), + Text( + subtitle!, + style: standards.textStyle.copyWith( + color: standards.textColor.withOpacity(0.6), + fontSize: standards.fontSize - 2, + ), + ), + ], + ], + ), + ), + if (trailing != null) trailing!, + if (showChevron) + Icon( + CupertinoIcons.chevron_right, + color: standards.textColor.withOpacity(0.4), + size: standards.fontSize + 4, + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/widgets/base/standard_text_field.dart b/lib/src/widgets/base/standard_text_field.dart new file mode 100644 index 0000000..5d78ab5 --- /dev/null +++ b/lib/src/widgets/base/standard_text_field.dart @@ -0,0 +1,129 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; + +class StandardTextField extends StatelessWidget { + final String? placeholder; + final String? initialValue; + final ValueChanged? onChanged; + final VoidCallback? onEditingComplete; + final ValueChanged? onSubmitted; + final bool obscureText; + final TextInputType? keyboardType; + final TextEditingController? controller; + final FocusNode? focusNode; + final IconData? prefixIcon; + final IconData? suffixIcon; + final VoidCallback? onSuffixIconPressed; + final String? errorText; + final int? maxLines; + final bool enabled; + final bool autofocus; + + const StandardTextField({ + super.key, + this.placeholder, + this.initialValue, + this.onChanged, + this.onEditingComplete, + this.onSubmitted, + this.obscureText = false, + this.keyboardType, + this.controller, + this.focusNode, + this.prefixIcon, + this.suffixIcon, + this.onSuffixIconPressed, + this.errorText, + this.maxLines = 1, + this.enabled = true, + this.autofocus = false, + }); + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + color: standards.backgroundColor, + borderRadius: BorderRadius.circular(standards.scaledRadius(12)), + border: Border.all( + color: errorText != null + ? standards.secondaryColor + : standards.textColor.withOpacity(0.2), + ), + ), + child: Row( + children: [ + if (prefixIcon != null) + Padding( + padding: EdgeInsets.only(left: standards.scaledWidth(12)), + child: Icon( + prefixIcon, + color: standards.textColor.withOpacity(0.5), + size: standards.fontSize + 4, + ), + ), + Expanded( + child: CupertinoTextField( + controller: controller, + focusNode: focusNode, + placeholder: placeholder, + placeholderStyle: TextStyle( + color: standards.textColor.withOpacity(0.5), + fontSize: standards.fontSize, + ), + style: TextStyle( + color: standards.textColor, + fontSize: standards.fontSize, + ), + onChanged: onChanged, + onEditingComplete: onEditingComplete, + onSubmitted: onSubmitted, + obscureText: obscureText, + keyboardType: keyboardType, + maxLines: maxLines, + enabled: enabled, + autofocus: autofocus, + decoration: const BoxDecoration(), + padding: standards.scaledPadding( + EdgeInsets.symmetric(horizontal: 12, vertical: 12), + ), + ), + ), + if (suffixIcon != null) + GestureDetector( + onTap: onSuffixIconPressed, + child: Padding( + padding: EdgeInsets.only(right: standards.scaledWidth(12)), + child: Icon( + suffixIcon, + color: standards.textColor.withOpacity(0.5), + size: standards.fontSize + 4, + ), + ), + ), + ], + ), + ), + if (errorText != null) ...[ + SizedBox(height: standards.scaledHeight(4)), + Padding( + padding: EdgeInsets.only(left: standards.scaledWidth(12)), + child: Text( + errorText!, + style: TextStyle( + color: standards.secondaryColor, + fontSize: standards.fontSize - 2, + ), + ), + ), + ], + ], + ); + } +}