feat: ERP 시스템 UI/UX 전면 재설계

색채 심리학 기반 컬러 시스템 구축:
- Primary Color를 신뢰감 있는 블루(#2563EB)로 변경
- 비즈니스 상태별 의미 있는 색상 체계 도입 (본사/지점/파트너사/고객사)
- 장비 상태별 색상 정의 (입고/출고/대여/폐기/수리중)
- 정보 계층을 명확히 하는 그레이 스케일 시스템

F-Pattern 레이아웃 적용:
- 1차 시선: 상단 헤더 (로고, 검색, 알림, 프로필)
- 2차 시선: 페이지 헤더 (제목, 브레드크럼, 주요 액션)
- 주요 작업 영역: 중앙 콘텐츠

1920x1080 해상도 최적화:
- 최대 콘텐츠 너비 1440px 제한
- 12컬럼 그리드 시스템 적용
- 수평 스크롤 제거

컴포넌트 시스템 개선:
- 호버 효과 및 마이크로 애니메이션 추가
- 비즈니스 상태별 배지 컴포넌트
- 향상된 입력 필드 인터랙션
- 프로그레스바, 칩 컴포넌트 추가

사이드바 UX 개선:
- 부드러운 접기/펼치기 애니메이션
- 활성 상태 아이콘 변화
- 알림 배지 표시 기능

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
JiWoong Sul
2025-08-09 02:16:38 +09:00
parent b8f10dd588
commit 8302ff37cc
3 changed files with 1669 additions and 594 deletions

View File

@@ -1,16 +1,18 @@
import 'package:flutter/material.dart';
import 'package:superport/screens/common/theme_shadcn.dart';
/// shadcn/ui 스타일 기본 컴포넌트들
/// ERP 시스템에 최적화된 UI 컴포넌트들
// 카드 컴포넌트
class ShadcnCard extends StatelessWidget {
// ============= 카드 컴포넌트 =============
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,
@@ -20,34 +22,62 @@ class ShadcnCard extends StatelessWidget {
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 = 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: Colors.black),
boxShadow: ShadcnTheme.cardShadow,
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,
),
child: child,
);
if (onTap != null) {
return GestureDetector(onTap: onTap, child: cardContent);
if (widget.onTap != null) {
return GestureDetector(
onTap: widget.onTap,
child: cardContent,
);
}
return cardContent;
}
}
// 버튼 컴포넌트
class ShadcnButton extends StatelessWidget {
// ============= 버튼 컴포넌트 =============
class ShadcnButton extends StatefulWidget {
final String text;
final VoidCallback? onPressed;
final ShadcnButtonVariant variant;
@@ -71,81 +101,133 @@ class ShadcnButton extends StatelessWidget {
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: fullWidth ? MainAxisSize.max : MainAxisSize.min,
mainAxisSize: widget.fullWidth ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (loading)
if (widget.loading)
SizedBox(
width: 16,
height: 16,
width: _getIconSize(),
height: _getIconSize(),
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
textColor ?? _getDefaultTextColor(),
widget.textColor ?? _getDefaultTextColor(),
),
),
)
else if (icon != null)
icon!,
if ((loading || icon != null) && text.isNotEmpty)
else if (widget.icon != null)
widget.icon!,
if ((widget.loading || widget.icon != null) && widget.text.isNotEmpty)
const SizedBox(width: ShadcnTheme.spacing2),
if (text.isNotEmpty) Text(text, style: _getTextStyle()),
if (widget.text.isNotEmpty)
Text(widget.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,
),
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 (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 if (widget.variant == ShadcnButtonVariant.secondary) {
button = OutlinedButton(
onPressed: widget.loading ? null : widget.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,
),
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 (variant) {
switch (widget.variant) {
case ShadcnButtonVariant.primary:
return ElevatedButton.styleFrom(
backgroundColor: backgroundColor ?? ShadcnTheme.primary,
foregroundColor: textColor ?? ShadcnTheme.primaryForeground,
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: backgroundColor ?? ShadcnTheme.secondary,
foregroundColor: textColor ?? ShadcnTheme.secondaryForeground,
side: const BorderSide(color: Colors.black),
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(
@@ -154,8 +236,9 @@ class ShadcnButton extends StatelessWidget {
);
case ShadcnButtonVariant.destructive:
return ElevatedButton.styleFrom(
backgroundColor: backgroundColor ?? ShadcnTheme.destructive,
foregroundColor: textColor ?? ShadcnTheme.destructiveForeground,
backgroundColor: widget.backgroundColor ??
(_isHovered ? Color(0xFFB91C1C) : ShadcnTheme.error),
foregroundColor: widget.textColor ?? ShadcnTheme.errorForeground,
elevation: 0,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
@@ -164,8 +247,9 @@ class ShadcnButton extends StatelessWidget {
);
case ShadcnButtonVariant.ghost:
return TextButton.styleFrom(
backgroundColor: backgroundColor ?? Colors.transparent,
foregroundColor: textColor ?? ShadcnTheme.foreground,
backgroundColor: widget.backgroundColor ??
(_isHovered ? ShadcnTheme.backgroundSecondary : Colors.transparent),
foregroundColor: widget.textColor ?? ShadcnTheme.foreground,
elevation: 0,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
@@ -176,28 +260,28 @@ class ShadcnButton extends StatelessWidget {
}
EdgeInsetsGeometry _getPadding() {
switch (size) {
switch (widget.size) {
case ShadcnButtonSize.small:
return const EdgeInsets.symmetric(
horizontal: ShadcnTheme.spacing3,
vertical: 6,
vertical: 0,
);
case ShadcnButtonSize.medium:
return const EdgeInsets.symmetric(
horizontal: ShadcnTheme.spacing4,
vertical: 10,
vertical: 0,
);
case ShadcnButtonSize.large:
return const EdgeInsets.symmetric(
horizontal: ShadcnTheme.spacing8,
vertical: ShadcnTheme.spacing3,
horizontal: ShadcnTheme.spacing6,
vertical: 0,
);
}
}
TextStyle _getTextStyle() {
TextStyle baseStyle;
switch (size) {
switch (widget.size) {
case ShadcnButtonSize.small:
baseStyle = ShadcnTheme.labelSmall;
break;
@@ -208,17 +292,19 @@ class ShadcnButton extends StatelessWidget {
baseStyle = ShadcnTheme.labelLarge;
break;
}
return textColor != null ? baseStyle.copyWith(color: textColor) : baseStyle;
return widget.textColor != null
? baseStyle.copyWith(color: widget.textColor, fontWeight: FontWeight.w500)
: baseStyle.copyWith(fontWeight: FontWeight.w500);
}
Color _getDefaultTextColor() {
switch (variant) {
switch (widget.variant) {
case ShadcnButtonVariant.primary:
return ShadcnTheme.primaryForeground;
case ShadcnButtonVariant.secondary:
return ShadcnTheme.secondaryForeground;
return ShadcnTheme.foreground;
case ShadcnButtonVariant.destructive:
return ShadcnTheme.destructiveForeground;
return ShadcnTheme.errorForeground;
case ShadcnButtonVariant.ghost:
return ShadcnTheme.foreground;
}
@@ -231,11 +317,12 @@ enum ShadcnButtonVariant { primary, secondary, destructive, ghost }
// 버튼 사이즈
enum ShadcnButtonSize { small, medium, large }
// 입력 필드 컴포넌트
class ShadcnInput extends StatelessWidget {
// ============= 입력 필드 컴포넌트 =============
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;
@@ -246,12 +333,14 @@ class ShadcnInput extends StatelessWidget {
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,
@@ -262,63 +351,124 @@ class ShadcnInput extends StatelessWidget {
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 _isFocused = false;
bool _isHovered = false;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (label != null) ...[
Text(label!, style: ShadcnTheme.labelMedium),
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),
],
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: 10,
),
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,
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,
),
),
),
hintStyle: ShadcnTheme.bodyMedium.copyWith(
color: ShadcnTheme.mutedForeground.withValues(alpha: 0.8),
),
),
),
],
@@ -326,17 +476,19 @@ class ShadcnInput extends StatelessWidget {
}
}
// 배지 컴포넌트
// ============= 배지 컴포넌트 =============
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
@@ -345,10 +497,22 @@ class ShadcnBadge extends StatelessWidget {
padding: _getPadding(),
decoration: BoxDecoration(
color: _getBackgroundColor(),
borderRadius: BorderRadius.circular(ShadcnTheme.radiusXl),
border: Border.all(color: _getBorderColor()),
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()),
],
),
child: Text(text, style: _getTextStyle()),
);
}
@@ -356,18 +520,18 @@ class ShadcnBadge extends StatelessWidget {
switch (size) {
case ShadcnBadgeSize.small:
return const EdgeInsets.symmetric(
horizontal: ShadcnTheme.spacing1,
vertical: ShadcnTheme.spacing1 / 2,
horizontal: ShadcnTheme.spacing2,
vertical: 2,
);
case ShadcnBadgeSize.medium:
return const EdgeInsets.symmetric(
horizontal: ShadcnTheme.spacing2,
vertical: ShadcnTheme.spacing1,
horizontal: ShadcnTheme.spacing2 + 2,
vertical: 4,
);
case ShadcnBadgeSize.large:
return const EdgeInsets.symmetric(
horizontal: ShadcnTheme.spacing3,
vertical: ShadcnTheme.spacing1,
vertical: 6,
);
}
}
@@ -375,90 +539,160 @@ class ShadcnBadge extends StatelessWidget {
Color _getBackgroundColor() {
switch (variant) {
case ShadcnBadgeVariant.primary:
return ShadcnTheme.primary;
return ShadcnTheme.primaryLight;
case ShadcnBadgeVariant.secondary:
return ShadcnTheme.secondary;
return ShadcnTheme.secondaryLight;
case ShadcnBadgeVariant.destructive:
return ShadcnTheme.destructive;
return ShadcnTheme.errorLight;
case ShadcnBadgeVariant.success:
return ShadcnTheme.success;
return ShadcnTheme.successLight;
case ShadcnBadgeVariant.warning:
return ShadcnTheme.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 Colors.black;
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 =
variant == ShadcnBadgeVariant.outline
? ShadcnTheme.foreground
: variant == ShadcnBadgeVariant.secondary
? ShadcnTheme.secondaryForeground
: ShadcnTheme.primaryForeground;
final Color textColor = _getTextColor();
switch (size) {
case ShadcnBadgeSize.small:
return ShadcnTheme.labelSmall.copyWith(color: textColor);
return ShadcnTheme.caption.copyWith(
color: textColor,
fontWeight: FontWeight.w500,
);
case ShadcnBadgeSize.medium:
return ShadcnTheme.labelMedium.copyWith(color: textColor);
return ShadcnTheme.labelSmall.copyWith(
color: textColor,
fontWeight: FontWeight.w500,
);
case ShadcnBadgeSize.large:
return ShadcnTheme.labelLarge.copyWith(color: textColor);
return ShadcnTheme.labelMedium.copyWith(
color: textColor,
fontWeight: FontWeight.w500,
);
}
}
}
// 배지 variants
// 배지 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 ?? Colors.black,
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,
@@ -466,6 +700,8 @@ class ShadcnAvatar extends StatelessWidget {
this.initials,
this.size = 40,
this.backgroundColor,
this.textColor,
this.showBorder = true,
}) : super(key: key);
@override
@@ -474,36 +710,166 @@ class ShadcnAvatar extends StatelessWidget {
width: size,
height: size,
decoration: BoxDecoration(
color: backgroundColor ?? ShadcnTheme.muted,
color: backgroundColor ?? ShadcnTheme.backgroundSecondary,
shape: BoxShape.circle,
border: Border.all(color: Colors.black),
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(),
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.muted,
color: backgroundColor ?? ShadcnTheme.backgroundSecondary,
child: Center(
child: Text(
initials ?? '?',
style: ShadcnTheme.labelMedium.copyWith(
color: ShadcnTheme.mutedForeground,
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,
),
),
),
],
),
),
),
],
);
}
}