- 모든 *_redesign.dart 파일을 기본 화면 파일로 통합 - 백업용 컨트롤러 파일들 제거 (*_controller.backup.dart) - 사용하지 않는 예제 및 테스트 파일 제거 - Clean Architecture 적용 후 남은 정리 작업 완료 - 테스트 코드 정리 및 구조 개선 준비 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
871 lines
26 KiB
Dart
871 lines
26 KiB
Dart
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<ShadcnCard> createState() => _ShadcnCardState();
|
|
}
|
|
|
|
class _ShadcnCardState extends State<ShadcnCard> {
|
|
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<ShadcnButton> createState() => _ShadcnButtonState();
|
|
}
|
|
|
|
class _ShadcnButtonState extends State<ShadcnButton> {
|
|
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<Color>(
|
|
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<String>? 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<ShadcnInput> createState() => _ShadcnInputState();
|
|
}
|
|
|
|
class _ShadcnInputState extends State<ShadcnInput> {
|
|
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: 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,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
} |