import 'package:flutter/material.dart'; import 'package:superport/screens/common/theme_shadcn.dart'; /// shadcn/ui 스타일 기본 컴포넌트들 // 카드 컴포넌트 class ShadcnCard extends StatelessWidget { final Widget child; final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? margin; final double? width; final double? height; final VoidCallback? onTap; const ShadcnCard({ Key? key, required this.child, this.padding, this.margin, this.width, this.height, this.onTap, }) : super(key: key); @override Widget build(BuildContext context) { final cardContent = Container( width: width, height: height, padding: padding ?? const EdgeInsets.all(ShadcnTheme.spacing6), margin: margin, decoration: BoxDecoration( color: ShadcnTheme.card, borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg), border: Border.all(color: ShadcnTheme.border), boxShadow: ShadcnTheme.cardShadow, ), child: child, ); if (onTap != null) { return GestureDetector(onTap: onTap, child: cardContent); } return cardContent; } } // 버튼 컴포넌트 class ShadcnButton extends StatelessWidget { final String text; final VoidCallback? onPressed; final ShadcnButtonVariant variant; final ShadcnButtonSize size; final Widget? icon; final bool fullWidth; final bool loading; final Color? backgroundColor; final Color? textColor; const ShadcnButton({ Key? key, required this.text, this.onPressed, this.variant = ShadcnButtonVariant.primary, this.size = ShadcnButtonSize.medium, this.icon, this.fullWidth = false, this.loading = false, this.backgroundColor, this.textColor, }) : super(key: key); @override Widget build(BuildContext context) { final ButtonStyle style = _getButtonStyle(); final EdgeInsetsGeometry padding = _getPadding(); Widget buttonChild = Row( mainAxisSize: fullWidth ? MainAxisSize.max : MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ if (loading) SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( textColor ?? _getDefaultTextColor(), ), ), ) else if (icon != null) icon!, if ((loading || icon != null) && text.isNotEmpty) const SizedBox(width: ShadcnTheme.spacing2), if (text.isNotEmpty) Text(text, style: _getTextStyle()), ], ); if (variant == ShadcnButtonVariant.primary) { return SizedBox( width: fullWidth ? double.infinity : null, child: ElevatedButton( onPressed: loading ? null : onPressed, style: style.copyWith(padding: WidgetStateProperty.all(padding)), child: buttonChild, ), ); } else if (variant == ShadcnButtonVariant.secondary) { return SizedBox( width: fullWidth ? double.infinity : null, child: OutlinedButton( onPressed: loading ? null : onPressed, style: style.copyWith(padding: WidgetStateProperty.all(padding)), child: buttonChild, ), ); } else { return SizedBox( width: fullWidth ? double.infinity : null, child: TextButton( onPressed: loading ? null : onPressed, style: style.copyWith(padding: WidgetStateProperty.all(padding)), child: buttonChild, ), ); } } ButtonStyle _getButtonStyle() { switch (variant) { case ShadcnButtonVariant.primary: return ElevatedButton.styleFrom( backgroundColor: backgroundColor ?? ShadcnTheme.primary, foregroundColor: textColor ?? ShadcnTheme.primaryForeground, elevation: 0, shadowColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), ); case ShadcnButtonVariant.secondary: return OutlinedButton.styleFrom( backgroundColor: backgroundColor ?? ShadcnTheme.secondary, foregroundColor: textColor ?? ShadcnTheme.secondaryForeground, side: const BorderSide(color: ShadcnTheme.border), elevation: 0, shadowColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), ); case ShadcnButtonVariant.destructive: return ElevatedButton.styleFrom( backgroundColor: backgroundColor ?? ShadcnTheme.destructive, foregroundColor: textColor ?? ShadcnTheme.destructiveForeground, elevation: 0, shadowColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), ); case ShadcnButtonVariant.ghost: return TextButton.styleFrom( backgroundColor: backgroundColor ?? Colors.transparent, foregroundColor: textColor ?? ShadcnTheme.foreground, elevation: 0, shadowColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), ); } } EdgeInsetsGeometry _getPadding() { switch (size) { case ShadcnButtonSize.small: return const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing3, vertical: ShadcnTheme.spacing1, ); case ShadcnButtonSize.medium: return const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing4, vertical: ShadcnTheme.spacing2, ); case ShadcnButtonSize.large: return const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing8, vertical: ShadcnTheme.spacing3, ); } } TextStyle _getTextStyle() { TextStyle baseStyle; switch (size) { case ShadcnButtonSize.small: baseStyle = ShadcnTheme.labelSmall; break; case ShadcnButtonSize.medium: baseStyle = ShadcnTheme.labelMedium; break; case ShadcnButtonSize.large: baseStyle = ShadcnTheme.labelLarge; break; } return textColor != null ? baseStyle.copyWith(color: textColor) : baseStyle; } Color _getDefaultTextColor() { switch (variant) { case ShadcnButtonVariant.primary: return ShadcnTheme.primaryForeground; case ShadcnButtonVariant.secondary: return ShadcnTheme.secondaryForeground; case ShadcnButtonVariant.destructive: return ShadcnTheme.destructiveForeground; case ShadcnButtonVariant.ghost: return ShadcnTheme.foreground; } } } // 버튼 variants enum ShadcnButtonVariant { primary, secondary, destructive, ghost } // 버튼 사이즈 enum ShadcnButtonSize { small, medium, large } // 입력 필드 컴포넌트 class ShadcnInput extends StatelessWidget { final String? label; final String? placeholder; final String? errorText; final TextEditingController? controller; final bool obscureText; final TextInputType? keyboardType; final ValueChanged? onChanged; final VoidCallback? onTap; final Widget? prefixIcon; final Widget? suffixIcon; final bool readOnly; final bool enabled; final int? maxLines; const ShadcnInput({ Key? key, this.label, this.placeholder, this.errorText, this.controller, this.obscureText = false, this.keyboardType, this.onChanged, this.onTap, this.prefixIcon, this.suffixIcon, this.readOnly = false, this.enabled = true, this.maxLines = 1, }) : super(key: key); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (label != null) ...[ Text(label!, style: ShadcnTheme.labelMedium), const SizedBox(height: ShadcnTheme.spacing1), ], TextFormField( controller: controller, obscureText: obscureText, keyboardType: keyboardType, onChanged: onChanged, onTap: onTap, readOnly: readOnly, enabled: enabled, maxLines: maxLines, decoration: InputDecoration( hintText: placeholder, prefixIcon: prefixIcon, suffixIcon: suffixIcon, errorText: errorText, filled: true, fillColor: ShadcnTheme.background, contentPadding: const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing3, vertical: ShadcnTheme.spacing2, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), borderSide: const BorderSide(color: ShadcnTheme.input), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), borderSide: const BorderSide(color: ShadcnTheme.input), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), borderSide: const BorderSide(color: ShadcnTheme.ring, width: 2), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), borderSide: const BorderSide(color: ShadcnTheme.destructive), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), borderSide: const BorderSide( color: ShadcnTheme.destructive, width: 2, ), ), hintStyle: ShadcnTheme.bodyMedium.copyWith( color: ShadcnTheme.mutedForeground, ), ), ), ], ); } } // 배지 컴포넌트 class ShadcnBadge extends StatelessWidget { final String text; final ShadcnBadgeVariant variant; final ShadcnBadgeSize size; const ShadcnBadge({ Key? key, required this.text, this.variant = ShadcnBadgeVariant.primary, this.size = ShadcnBadgeSize.medium, }) : super(key: key); @override Widget build(BuildContext context) { return Container( padding: _getPadding(), decoration: BoxDecoration( color: _getBackgroundColor(), borderRadius: BorderRadius.circular(ShadcnTheme.radiusXl), border: Border.all(color: _getBorderColor()), ), child: Text(text, style: _getTextStyle()), ); } EdgeInsetsGeometry _getPadding() { switch (size) { case ShadcnBadgeSize.small: return const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing1, vertical: ShadcnTheme.spacing1 / 2, ); case ShadcnBadgeSize.medium: return const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing2, vertical: ShadcnTheme.spacing1, ); case ShadcnBadgeSize.large: return const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing3, vertical: ShadcnTheme.spacing1, ); } } Color _getBackgroundColor() { switch (variant) { case ShadcnBadgeVariant.primary: return ShadcnTheme.primary; case ShadcnBadgeVariant.secondary: return ShadcnTheme.secondary; case ShadcnBadgeVariant.destructive: return ShadcnTheme.destructive; case ShadcnBadgeVariant.success: return ShadcnTheme.success; case ShadcnBadgeVariant.warning: return ShadcnTheme.warning; case ShadcnBadgeVariant.outline: return Colors.transparent; } } Color _getBorderColor() { switch (variant) { case ShadcnBadgeVariant.outline: return ShadcnTheme.border; default: return Colors.transparent; } } TextStyle _getTextStyle() { final Color textColor = variant == ShadcnBadgeVariant.outline ? ShadcnTheme.foreground : variant == ShadcnBadgeVariant.secondary ? ShadcnTheme.secondaryForeground : ShadcnTheme.primaryForeground; switch (size) { case ShadcnBadgeSize.small: return ShadcnTheme.labelSmall.copyWith(color: textColor); case ShadcnBadgeSize.medium: return ShadcnTheme.labelMedium.copyWith(color: textColor); case ShadcnBadgeSize.large: return ShadcnTheme.labelLarge.copyWith(color: textColor); } } } // 배지 variants enum ShadcnBadgeVariant { primary, secondary, destructive, success, warning, outline, } // 배지 사이즈 enum ShadcnBadgeSize { small, medium, large } // 구분선 컴포넌트 class ShadcnSeparator extends StatelessWidget { final Axis direction; final double thickness; final Color? color; const ShadcnSeparator({ Key? key, this.direction = Axis.horizontal, this.thickness = 1.0, this.color, }) : super(key: key); @override Widget build(BuildContext context) { return Container( width: direction == Axis.horizontal ? double.infinity : thickness, height: direction == Axis.vertical ? double.infinity : thickness, color: color ?? ShadcnTheme.border, ); } } // 아바타 컴포넌트 class ShadcnAvatar extends StatelessWidget { final String? imageUrl; final String? initials; final double size; final Color? backgroundColor; const ShadcnAvatar({ Key? key, this.imageUrl, this.initials, this.size = 40, this.backgroundColor, }) : super(key: key); @override Widget build(BuildContext context) { return Container( width: size, height: size, decoration: BoxDecoration( color: backgroundColor ?? ShadcnTheme.muted, shape: BoxShape.circle, border: Border.all(color: ShadcnTheme.border), ), child: ClipOval( child: imageUrl != null ? Image.network( imageUrl!, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => _buildFallback(), ) : _buildFallback(), ), ); } Widget _buildFallback() { return Container( color: backgroundColor ?? ShadcnTheme.muted, child: Center( child: Text( initials ?? '?', style: ShadcnTheme.labelMedium.copyWith( color: ShadcnTheme.mutedForeground, fontSize: size * 0.4, ), ), ), ); } }