import 'package:flutter/material.dart'; import 'package:superport/screens/common/theme_shadcn.dart'; /// ERP 시스템에 최적화된 UI 컴포넌트들 // ============= 카드 컴포넌트 ============= class ShadcnCard extends StatefulWidget { final Widget child; final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? margin; final double? width; final double? height; final VoidCallback? onTap; final bool hoverable; final bool elevated; const ShadcnCard({ Key? key, required this.child, this.padding, this.margin, this.width, this.height, this.onTap, this.hoverable = true, this.elevated = false, }) : super(key: key); @override State createState() => _ShadcnCardState(); } class _ShadcnCardState extends State { bool _isHovered = false; @override Widget build(BuildContext context) { final cardContent = MouseRegion( onEnter: widget.hoverable ? (_) => setState(() => _isHovered = true) : null, onExit: widget.hoverable ? (_) => setState(() => _isHovered = false) : null, child: AnimatedContainer( duration: const Duration(milliseconds: 200), width: widget.width, height: widget.height, padding: widget.padding ?? const EdgeInsets.all(ShadcnTheme.spacing6), margin: widget.margin, decoration: BoxDecoration( color: _isHovered && widget.hoverable ? ShadcnTheme.cardHover : ShadcnTheme.card, borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg), border: Border.all( color: _isHovered && widget.hoverable ? ShadcnTheme.borderStrong : ShadcnTheme.border, width: 1, ), boxShadow: widget.elevated ? ShadcnTheme.shadowLg : _isHovered && widget.hoverable ? ShadcnTheme.shadowMd : ShadcnTheme.shadowSm, ), child: widget.child, ), ); if (widget.onTap != null) { return GestureDetector( onTap: widget.onTap, child: cardContent, ); } return cardContent; } } // ============= 버튼 컴포넌트 ============= class ShadcnButton extends StatefulWidget { 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 State createState() => _ShadcnButtonState(); } class _ShadcnButtonState extends State { bool _isHovered = false; bool _isPressed = false; @override Widget build(BuildContext context) { final ButtonStyle style = _getButtonStyle(); final EdgeInsetsGeometry padding = _getPadding(); Widget buttonChild = Row( mainAxisSize: widget.fullWidth ? MainAxisSize.max : MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ if (widget.loading) SizedBox( width: _getIconSize(), height: _getIconSize(), child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( widget.textColor ?? _getDefaultTextColor(), ), ), ) else if (widget.icon != null) widget.icon!, if ((widget.loading || widget.icon != null) && widget.text.isNotEmpty) const SizedBox(width: ShadcnTheme.spacing2), if (widget.text.isNotEmpty) Text(widget.text, style: _getTextStyle()), ], ); Widget button; if (widget.variant == ShadcnButtonVariant.primary || widget.variant == ShadcnButtonVariant.destructive) { button = ElevatedButton( onPressed: widget.loading ? null : widget.onPressed, style: style.copyWith(padding: WidgetStateProperty.all(padding)), child: buttonChild, ); } else if (widget.variant == ShadcnButtonVariant.secondary) { button = OutlinedButton( onPressed: widget.loading ? null : widget.onPressed, style: style.copyWith(padding: WidgetStateProperty.all(padding)), child: buttonChild, ); } else { button = TextButton( onPressed: widget.loading ? null : widget.onPressed, style: style.copyWith(padding: WidgetStateProperty.all(padding)), child: buttonChild, ); } return MouseRegion( onEnter: (_) => setState(() => _isHovered = true), onExit: (_) => setState(() => _isHovered = false), child: GestureDetector( onTapDown: (_) => setState(() => _isPressed = true), onTapUp: (_) => setState(() => _isPressed = false), onTapCancel: () => setState(() => _isPressed = false), child: AnimatedScale( scale: _isPressed ? 0.98 : 1.0, duration: const Duration(milliseconds: 100), child: SizedBox( width: widget.fullWidth ? double.infinity : null, height: _getHeight(), child: button, ), ), ), ); } double _getHeight() { switch (widget.size) { case ShadcnButtonSize.small: return 32; case ShadcnButtonSize.medium: return 40; case ShadcnButtonSize.large: return 48; } } double _getIconSize() { switch (widget.size) { case ShadcnButtonSize.small: return 14; case ShadcnButtonSize.medium: return 16; case ShadcnButtonSize.large: return 20; } } ButtonStyle _getButtonStyle() { switch (widget.variant) { case ShadcnButtonVariant.primary: return ElevatedButton.styleFrom( backgroundColor: widget.backgroundColor ?? (_isHovered ? ShadcnTheme.primaryDark : ShadcnTheme.primary), foregroundColor: widget.textColor ?? ShadcnTheme.primaryForeground, elevation: 0, shadowColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), ).copyWith( overlayColor: WidgetStateProperty.all( ShadcnTheme.primaryDark.withValues(alpha: 0.1), ), ); case ShadcnButtonVariant.secondary: return OutlinedButton.styleFrom( backgroundColor: widget.backgroundColor ?? (_isHovered ? ShadcnTheme.backgroundSecondary : Colors.transparent), foregroundColor: widget.textColor ?? ShadcnTheme.foreground, side: BorderSide( color: _isHovered ? ShadcnTheme.borderStrong : ShadcnTheme.border, width: 1, ), elevation: 0, shadowColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), ); case ShadcnButtonVariant.destructive: return ElevatedButton.styleFrom( backgroundColor: widget.backgroundColor ?? (_isHovered ? Color(0xFFB91C1C) : ShadcnTheme.error), foregroundColor: widget.textColor ?? ShadcnTheme.errorForeground, elevation: 0, shadowColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), ); case ShadcnButtonVariant.ghost: return TextButton.styleFrom( backgroundColor: widget.backgroundColor ?? (_isHovered ? ShadcnTheme.backgroundSecondary : Colors.transparent), foregroundColor: widget.textColor ?? ShadcnTheme.foreground, elevation: 0, shadowColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), ); } } EdgeInsetsGeometry _getPadding() { switch (widget.size) { case ShadcnButtonSize.small: return const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing3, vertical: 0, ); case ShadcnButtonSize.medium: return const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing4, vertical: 0, ); case ShadcnButtonSize.large: return const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing6, vertical: 0, ); } } TextStyle _getTextStyle() { TextStyle baseStyle; switch (widget.size) { case ShadcnButtonSize.small: baseStyle = ShadcnTheme.labelSmall; break; case ShadcnButtonSize.medium: baseStyle = ShadcnTheme.labelMedium; break; case ShadcnButtonSize.large: baseStyle = ShadcnTheme.labelLarge; break; } return widget.textColor != null ? baseStyle.copyWith(color: widget.textColor, fontWeight: FontWeight.w500) : baseStyle.copyWith(fontWeight: FontWeight.w500); } Color _getDefaultTextColor() { switch (widget.variant) { case ShadcnButtonVariant.primary: return ShadcnTheme.primaryForeground; case ShadcnButtonVariant.secondary: return ShadcnTheme.foreground; case ShadcnButtonVariant.destructive: return ShadcnTheme.errorForeground; case ShadcnButtonVariant.ghost: return ShadcnTheme.foreground; } } } // 버튼 variants enum ShadcnButtonVariant { primary, secondary, destructive, ghost } // 버튼 사이즈 enum ShadcnButtonSize { small, medium, large } // ============= 입력 필드 컴포넌트 ============= class ShadcnInput extends StatefulWidget { final String? label; final String? placeholder; final String? errorText; final String? helperText; 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; final bool required; const ShadcnInput({ Key? key, this.label, this.placeholder, this.errorText, this.helperText, this.controller, this.obscureText = false, this.keyboardType, this.onChanged, this.onTap, this.prefixIcon, this.suffixIcon, this.readOnly = false, this.enabled = true, this.maxLines = 1, this.required = false, }) : super(key: key); @override State createState() => _ShadcnInputState(); } class _ShadcnInputState extends State { bool _isFocused = false; bool _isHovered = false; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.label != null) ...[ Row( children: [ Text(widget.label!, style: ShadcnTheme.labelMedium), if (widget.required) ...[ const SizedBox(width: 2), Text('*', style: ShadcnTheme.labelMedium.copyWith( color: ShadcnTheme.error, )), ], ], ), const SizedBox(height: ShadcnTheme.spacing1), ], MouseRegion( onEnter: (_) => setState(() => _isHovered = true), onExit: (_) => setState(() => _isHovered = false), child: Focus( onFocusChange: (focused) => setState(() => _isFocused = focused), child: TextFormField( controller: widget.controller, obscureText: widget.obscureText, keyboardType: widget.keyboardType, onChanged: widget.onChanged, onTap: widget.onTap, readOnly: widget.readOnly, enabled: widget.enabled, maxLines: widget.maxLines, style: ShadcnTheme.bodyMedium, decoration: InputDecoration( hintText: widget.placeholder, prefixIcon: widget.prefixIcon, suffixIcon: widget.suffixIcon, errorText: widget.errorText, helperText: widget.helperText, filled: true, fillColor: !widget.enabled ? ShadcnTheme.backgroundSecondary : _isHovered ? ShadcnTheme.inputHover : ShadcnTheme.input, contentPadding: EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing3, vertical: widget.maxLines! > 1 ? ShadcnTheme.spacing3 : ShadcnTheme.spacing2, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), borderSide: BorderSide( color: widget.errorText != null ? ShadcnTheme.error : ShadcnTheme.inputBorder, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), borderSide: BorderSide( color: widget.errorText != null ? ShadcnTheme.error : _isHovered ? ShadcnTheme.borderStrong : ShadcnTheme.inputBorder, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), borderSide: BorderSide( color: widget.errorText != null ? ShadcnTheme.error : ShadcnTheme.inputFocus, width: 2, ), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), borderSide: const BorderSide(color: ShadcnTheme.error), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), borderSide: const BorderSide( color: ShadcnTheme.error, width: 2, ), ), disabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), borderSide: BorderSide( color: ShadcnTheme.border.withValues(alpha: 0.5), ), ), hintStyle: ShadcnTheme.bodyMedium.copyWith( color: ShadcnTheme.foregroundSubtle, ), errorStyle: ShadcnTheme.bodySmall.copyWith( color: ShadcnTheme.error, ), helperStyle: ShadcnTheme.bodySmall.copyWith( color: ShadcnTheme.foregroundMuted, ), ), ), ), ), ], ); } } // ============= 배지 컴포넌트 ============= class ShadcnBadge extends StatelessWidget { final String text; final ShadcnBadgeVariant variant; final ShadcnBadgeSize size; final Widget? icon; const ShadcnBadge({ Key? key, required this.text, this.variant = ShadcnBadgeVariant.primary, this.size = ShadcnBadgeSize.medium, this.icon, }) : super(key: key); @override Widget build(BuildContext context) { return Container( padding: _getPadding(), decoration: BoxDecoration( color: _getBackgroundColor(), borderRadius: BorderRadius.circular(ShadcnTheme.radiusFull), border: Border.all( color: _getBorderColor(), width: variant == ShadcnBadgeVariant.outline ? 1 : 0, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (icon != null) ...[ icon!, const SizedBox(width: ShadcnTheme.spacing1), ], Text(text, style: _getTextStyle()), ], ), ); } EdgeInsetsGeometry _getPadding() { switch (size) { case ShadcnBadgeSize.small: return const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing2, vertical: 2, ); case ShadcnBadgeSize.medium: return const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing2 + 2, vertical: 4, ); case ShadcnBadgeSize.large: return const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing3, vertical: 6, ); } } Color _getBackgroundColor() { switch (variant) { case ShadcnBadgeVariant.primary: return ShadcnTheme.primaryLight; case ShadcnBadgeVariant.secondary: return ShadcnTheme.secondaryLight; case ShadcnBadgeVariant.destructive: return ShadcnTheme.errorLight; case ShadcnBadgeVariant.success: return ShadcnTheme.successLight; case ShadcnBadgeVariant.warning: return ShadcnTheme.warningLight; case ShadcnBadgeVariant.info: return ShadcnTheme.infoLight; case ShadcnBadgeVariant.outline: return Colors.transparent; // 비즈니스 상태 배지 case ShadcnBadgeVariant.equipmentIn: return ShadcnTheme.equipmentIn.withValues(alpha: 0.1); case ShadcnBadgeVariant.equipmentOut: return ShadcnTheme.equipmentOut.withValues(alpha: 0.1); case ShadcnBadgeVariant.equipmentRent: return ShadcnTheme.equipmentRent.withValues(alpha: 0.1); case ShadcnBadgeVariant.companyHeadquarters: return ShadcnTheme.companyHeadquarters.withValues(alpha: 0.1); case ShadcnBadgeVariant.companyBranch: return ShadcnTheme.companyBranch.withValues(alpha: 0.1); case ShadcnBadgeVariant.companyPartner: return ShadcnTheme.companyPartner.withValues(alpha: 0.1); case ShadcnBadgeVariant.companyCustomer: return ShadcnTheme.companyCustomer.withValues(alpha: 0.1); } } Color _getBorderColor() { switch (variant) { case ShadcnBadgeVariant.outline: return ShadcnTheme.border; default: return Colors.transparent; } } Color _getTextColor() { switch (variant) { case ShadcnBadgeVariant.primary: return ShadcnTheme.primary; case ShadcnBadgeVariant.secondary: return ShadcnTheme.secondaryDark; case ShadcnBadgeVariant.destructive: return ShadcnTheme.error; case ShadcnBadgeVariant.success: return ShadcnTheme.success; case ShadcnBadgeVariant.warning: return ShadcnTheme.warning; case ShadcnBadgeVariant.info: return ShadcnTheme.info; case ShadcnBadgeVariant.outline: return ShadcnTheme.foreground; // 비즈니스 상태 텍스트 색상 case ShadcnBadgeVariant.equipmentIn: return ShadcnTheme.equipmentIn; case ShadcnBadgeVariant.equipmentOut: return ShadcnTheme.equipmentOut; case ShadcnBadgeVariant.equipmentRent: return ShadcnTheme.equipmentRent; case ShadcnBadgeVariant.companyHeadquarters: return ShadcnTheme.companyHeadquarters; case ShadcnBadgeVariant.companyBranch: return ShadcnTheme.companyBranch; case ShadcnBadgeVariant.companyPartner: return ShadcnTheme.companyPartner; case ShadcnBadgeVariant.companyCustomer: return ShadcnTheme.companyCustomer; } } TextStyle _getTextStyle() { final Color textColor = _getTextColor(); switch (size) { case ShadcnBadgeSize.small: return ShadcnTheme.caption.copyWith( color: textColor, fontWeight: FontWeight.w500, ); case ShadcnBadgeSize.medium: return ShadcnTheme.labelSmall.copyWith( color: textColor, fontWeight: FontWeight.w500, ); case ShadcnBadgeSize.large: return ShadcnTheme.labelMedium.copyWith( color: textColor, fontWeight: FontWeight.w500, ); } } } // 배지 variants (비즈니스 상태 추가) enum ShadcnBadgeVariant { primary, secondary, destructive, success, warning, info, outline, // 장비 상태 equipmentIn, equipmentOut, equipmentRent, // 회사 타입 companyHeadquarters, companyBranch, companyPartner, companyCustomer, } // 배지 사이즈 enum ShadcnBadgeSize { small, medium, large } // ============= 구분선 컴포넌트 ============= class ShadcnSeparator extends StatelessWidget { final Axis direction; final double thickness; final Color? color; final EdgeInsetsGeometry? margin; const ShadcnSeparator({ Key? key, this.direction = Axis.horizontal, this.thickness = 1.0, this.color, this.margin, }) : super(key: key); @override Widget build(BuildContext context) { return Container( margin: margin, width: direction == Axis.horizontal ? double.infinity : thickness, height: direction == Axis.vertical ? double.infinity : thickness, color: color ?? ShadcnTheme.divider, ); } } // ============= 아바타 컴포넌트 ============= class ShadcnAvatar extends StatelessWidget { final String? imageUrl; final String? initials; final double size; final Color? backgroundColor; final Color? textColor; final bool showBorder; const ShadcnAvatar({ Key? key, this.imageUrl, this.initials, this.size = 40, this.backgroundColor, this.textColor, this.showBorder = true, }) : super(key: key); @override Widget build(BuildContext context) { return Container( width: size, height: size, decoration: BoxDecoration( color: backgroundColor ?? ShadcnTheme.backgroundSecondary, shape: BoxShape.circle, border: showBorder ? Border.all(color: ShadcnTheme.border, width: 1) : null, ), child: ClipOval( child: imageUrl != null ? Image.network( imageUrl!, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => _buildFallback(), ) : _buildFallback(), ), ); } Widget _buildFallback() { final displayText = initials?.toUpperCase() ?? '?'; return Container( color: backgroundColor ?? ShadcnTheme.backgroundSecondary, child: Center( child: Text( displayText, style: TextStyle( color: textColor ?? ShadcnTheme.foregroundSecondary, fontSize: size * 0.4, fontWeight: FontWeight.w500, ), ), ), ); } } // ============= 칩 컴포넌트 ============= class ShadcnChip extends StatelessWidget { final String label; final Color? backgroundColor; final Color? textColor; final VoidCallback? onDeleted; final Widget? avatar; final bool selected; const ShadcnChip({ Key? key, required this.label, this.backgroundColor, this.textColor, this.onDeleted, this.avatar, this.selected = false, }) : super(key: key); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing3, vertical: ShadcnTheme.spacing1, ), decoration: BoxDecoration( color: selected ? ShadcnTheme.primaryLight : backgroundColor ?? ShadcnTheme.backgroundSecondary, borderRadius: BorderRadius.circular(ShadcnTheme.radiusFull), border: Border.all( color: selected ? ShadcnTheme.primary : ShadcnTheme.border, width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (avatar != null) ...[ avatar!, const SizedBox(width: ShadcnTheme.spacing1), ], Text( label, style: ShadcnTheme.labelSmall.copyWith( color: selected ? ShadcnTheme.primary : textColor ?? ShadcnTheme.foreground, ), ), if (onDeleted != null) ...[ const SizedBox(width: ShadcnTheme.spacing1), GestureDetector( onTap: onDeleted, child: Icon( Icons.close, size: 14, color: selected ? ShadcnTheme.primary : ShadcnTheme.foregroundMuted, ), ), ], ], ), ); } } // ============= 프로그레스 바 ============= class ShadcnProgress extends StatelessWidget { final double value; // 0.0 ~ 1.0 final double height; final Color? backgroundColor; final Color? valueColor; final bool showLabel; const ShadcnProgress({ Key? key, required this.value, this.height = 8, this.backgroundColor, this.valueColor, this.showLabel = false, }) : super(key: key); @override Widget build(BuildContext context) { final percentage = (value * 100).toInt(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (showLabel) ...[ Text( '$percentage%', style: ShadcnTheme.labelSmall, ), const SizedBox(height: ShadcnTheme.spacing1), ], Container( height: height, decoration: BoxDecoration( color: backgroundColor ?? ShadcnTheme.backgroundSecondary, borderRadius: BorderRadius.circular(height / 2), ), child: ClipRRect( borderRadius: BorderRadius.circular(height / 2), child: Stack( children: [ FractionallySizedBox( widthFactor: value.clamp(0.0, 1.0), child: Container( decoration: BoxDecoration( color: valueColor ?? ShadcnTheme.primary, ), ), ), ], ), ), ), ], ); } }