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

File diff suppressed because it is too large Load Diff

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,
),
),
),
],
),
),
),
],
);
}
}

View File

@@ -1,84 +1,170 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
/// shadcn/ui 스타일 테마 시스템
/// ERP 시스템에 최적화된 색채 심리학 기반 테마 시스템
class ShadcnTheme {
// Teal 색상 시스템
// ============= 색상 팔레트 =============
// 배경 및 표면 색상
static const Color background = Color(0xFFFFFFFF);
static const Color foreground = Color(0xFF0F172A);
static const Color card = Color(0xFFFFFFFF);
static const Color cardForeground = Color(0xFF0F172A);
static const Color popover = Color(0xFFFFFFFF);
static const Color popoverForeground = Color(0xFF0F172A);
static const Color primary = Color(0xFF0D9488); // teal-600
static const Color backgroundSecondary = Color(0xFFF9FAFB); // 보조 배경
static const Color surface = Color(0xFFFFFFFF);
static const Color surfaceHover = Color(0xFFF3F4F6); // 호버 상태
// 텍스트 색상
static const Color foreground = Color(0xFF111827); // 주요 텍스트 (진한 검정)
static const Color foregroundSecondary = Color(0xFF374151); // 보조 텍스트
static const Color foregroundMuted = Color(0xFF6B7280); // 비활성 텍스트
static const Color foregroundSubtle = Color(0xFF9CA3AF); // 희미한 텍스트
// Primary 색상 (신뢰감 있는 블루)
static const Color primary = Color(0xFF2563EB); // blue-600
static const Color primaryDark = Color(0xFF1E40AF); // blue-800
static const Color primaryLight = Color(0xFFDBEAFE); // blue-100
static const Color primaryForeground = Color(0xFFFFFFFF);
static const Color secondary = Color(0xFFF0FDFA); // teal-50
static const Color secondaryForeground = Color(0xFF134E4A); // teal-900
static const Color muted = Color(0xFFF1F5F9); // slate-100
static const Color mutedForeground = Color(0xFF64748B); // slate-500
static const Color accent = Color(0xFF14B8A6); // teal-500
static const Color accentForeground = Color(0xFFFFFFFF);
static const Color destructive = Color(0xFFEF4444); // red-500
static const Color destructiveForeground = Color(0xFFFFFFFF);
static const Color border = Color(0xFFE5E7EB); // gray-200 (기본 border는 연한 회색)
static const Color input = Color(0xFFE5E7EB); // gray-200
static const Color ring = Color(0xFF14B8A6); // teal-500
// Secondary 색상 (중립 그레이)
static const Color secondary = Color(0xFF6B7280); // gray-500
static const Color secondaryDark = Color(0xFF374151); // gray-700
static const Color secondaryLight = Color(0xFFF9FAFB); // gray-50
static const Color secondaryForeground = Color(0xFF111827);
// ============= 시맨틱 색상 =============
static const Color success = Color(0xFF059669); // emerald-600
static const Color successLight = Color(0xFFD1FAE5); // emerald-100
static const Color successForeground = Color(0xFFFFFFFF);
static const Color warning = Color(0xFFD97706); // amber-600
static const Color warningLight = Color(0xFFFEF3C7); // amber-100
static const Color warningForeground = Color(0xFFFFFFFF);
static const Color error = Color(0xFFDC2626); // red-600
static const Color errorLight = Color(0xFFFEE2E2); // red-100
static const Color errorForeground = Color(0xFFFFFFFF);
static const Color info = Color(0xFF0891B2); // cyan-600
static const Color infoLight = Color(0xFFCFFAFE); // cyan-100
static const Color infoForeground = Color(0xFFFFFFFF);
// ============= 비즈니스 상태 색상 =============
// 회사 구분 색상
static const Color companyHeadquarters = Color(0xFF2563EB); // 본사 - Primary Blue (권위)
static const Color companyBranch = Color(0xFF7C3AED); // 지점 - Purple (연결성)
static const Color companyPartner = Color(0xFF059669); // 파트너사 - Green (협력)
static const Color companyCustomer = Color(0xFFEA580C); // 고객사 - Orange (활력)
// 장비 상태 색상
static const Color equipmentIn = Color(0xFF059669); // 입고 - Green (진입/추가)
static const Color equipmentOut = Color(0xFF0891B2); // 출고 - Cyan (이동/프로세스)
static const Color equipmentRent = Color(0xFF7C3AED); // 대여 - Purple (임시 상태)
static const Color equipmentDisposal = Color(0xFF6B7280); // 폐기 - Gray (비활성)
static const Color equipmentRepair = Color(0xFFD97706); // 수리중 - Amber (주의 필요)
static const Color equipmentUnknown = Color(0xFF9CA3AF); // 알수없음 - Light Gray
// ============= UI 요소 색상 =============
static const Color border = Color(0xFFE5E7EB); // gray-200
static const Color borderStrong = Color(0xFFD1D5DB); // gray-300
static const Color borderFocus = Color(0xFF2563EB); // primary
static const Color divider = Color(0xFFF3F4F6); // gray-100
static const Color card = Color(0xFFFFFFFF);
static const Color cardForeground = Color(0xFF111827);
static const Color cardHover = Color(0xFFF9FAFB);
static const Color input = Color(0xFFFFFFFF);
static const Color inputBorder = Color(0xFFD1D5DB); // gray-300
static const Color inputHover = Color(0xFFF9FAFB);
static const Color inputFocus = Color(0xFF2563EB);
// 기존 호환성을 위한 별칭
static const Color destructive = error;
static const Color destructiveForeground = errorForeground;
static const Color muted = backgroundSecondary;
static const Color mutedForeground = foregroundMuted;
static const Color accent = primary;
static const Color accentForeground = primaryForeground;
static const Color ring = primaryDark;
static const Color popover = card;
static const Color popoverForeground = cardForeground;
// Teal 그라데이션 색상 (기존 호환)
static const Color gradient1 = primary;
static const Color gradient2 = primaryDark;
static const Color gradient3 = Color(0xFF1D4ED8); // blue-700
// 추가 색상 (기존 호환)
static const Color blue = primary;
static const Color purple = companyBranch;
static const Color green = companyPartner;
static const Color radius = Color(0xFF000000); // 사용하지 않음
// Teal 그라데이션 색상
static const Color gradient1 = Color(0xFF14B8A6); // teal-500
static const Color gradient2 = Color(0xFF0D9488); // teal-600
static const Color gradient3 = Color(0xFF0F766E); // teal-700
// 상태 색상
static const Color success = Color(0xFF10B981); // emerald-500
static const Color warning = Color(0xFFF59E0B); // amber-500
static const Color error = Color(0xFFEF4444); // red-500
static const Color info = Color(0xFF0891B2); // cyan-600
// 추가 색상 (회사 구분용)
static const Color blue = Color(0xFF3B82F6); // blue-500
static const Color purple = Color(0xFF8B5CF6); // purple-500
static const Color green = Color(0xFF22C55E); // green-500
// 그림자 설정
static List<BoxShadow> get cardShadow => [
// ============= 그림자 시스템 =============
static List<BoxShadow> get shadowXs => [
BoxShadow(
color: primary.withValues(alpha: 0.08),
blurRadius: 8,
offset: const Offset(0, 2),
),
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 16,
offset: const Offset(0, 8),
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
];
static List<BoxShadow> get buttonShadow => [
static List<BoxShadow> get shadowSm => [
BoxShadow(
color: primary.withValues(alpha: 0.2),
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
];
// 간격 시스템
static List<BoxShadow> get shadowMd => [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 8,
offset: const Offset(0, 4),
),
];
static List<BoxShadow> get shadowLg => [
BoxShadow(
color: Colors.black.withValues(alpha: 0.10),
blurRadius: 16,
offset: const Offset(0, 8),
),
];
static List<BoxShadow> get shadowXl => [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 24,
offset: const Offset(0, 12),
),
];
// 카드 및 버튼 그림자 (기존 호환)
static List<BoxShadow> get cardShadow => shadowMd;
static List<BoxShadow> get buttonShadow => shadowSm;
// ============= 간격 시스템 (8px 기반) =============
static const double spacing0 = 0.0;
static const double spacing1 = 4.0;
static const double spacing2 = 8.0;
static const double spacing3 = 12.0;
static const double spacing4 = 16.0;
static const double spacing5 = 20.0;
static const double spacing6 = 24.0;
static const double spacing7 = 28.0;
static const double spacing8 = 32.0;
static const double spacing9 = 36.0;
static const double spacing10 = 40.0;
static const double spacing12 = 48.0;
static const double spacing14 = 56.0;
static const double spacing16 = 64.0;
static const double spacing20 = 80.0;
static const double spacing24 = 96.0;
// 라운드 설정
// ============= 라운드 설정 =============
static const double radiusNone = 0.0;
static const double radiusSm = 2.0;
static const double radiusXs = 2.0;
static const double radiusSm = 4.0;
static const double radiusMd = 6.0;
static const double radiusLg = 8.0;
static const double radiusXl = 12.0;
@@ -86,40 +172,63 @@ class ShadcnTheme {
static const double radius3xl = 24.0;
static const double radiusFull = 9999.0;
// 타이포그래피 시스템 (통일된 크기)
// ============= 타이포그래피 시스템 =============
// 헤딩 스타일
static TextStyle get headingH1 => GoogleFonts.inter(
fontSize: 32,
fontSize: 36,
fontWeight: FontWeight.w700,
color: foreground,
letterSpacing: -0.02,
height: 1.2,
);
static TextStyle get headingH2 => GoogleFonts.inter(
fontSize: 28,
fontWeight: FontWeight.w600,
color: foreground,
letterSpacing: -0.01,
height: 1.3,
);
static TextStyle get headingH3 => GoogleFonts.inter(
fontSize: 24,
fontWeight: FontWeight.w600,
color: foreground,
letterSpacing: -0.01,
);
static TextStyle get headingH3 => GoogleFonts.inter(
fontSize: 20,
fontWeight: FontWeight.w500,
color: foreground,
letterSpacing: -0.01,
height: 1.35,
);
static TextStyle get headingH4 => GoogleFonts.inter(
fontSize: 20,
fontWeight: FontWeight.w500,
color: foreground,
letterSpacing: 0,
height: 1.4,
);
static TextStyle get headingH5 => GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w500,
color: foreground,
letterSpacing: 0,
height: 1.4,
);
static TextStyle get headingH6 => GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w500,
color: foreground,
letterSpacing: 0,
height: 1.5,
);
// 본문 스타일
static TextStyle get bodyLarge => GoogleFonts.inter(
fontSize: 14,
fontSize: 16,
fontWeight: FontWeight.w400,
color: foreground,
letterSpacing: 0,
height: 1.6,
);
static TextStyle get bodyMedium => GoogleFonts.inter(
@@ -127,66 +236,106 @@ class ShadcnTheme {
fontWeight: FontWeight.w400,
color: foreground,
letterSpacing: 0,
height: 1.6,
);
static TextStyle get bodySmall => GoogleFonts.inter(
fontSize: 12,
fontSize: 13,
fontWeight: FontWeight.w400,
color: mutedForeground,
color: foregroundSecondary,
letterSpacing: 0,
height: 1.5,
);
static TextStyle get bodyXs => GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w400,
color: foregroundMuted,
letterSpacing: 0,
height: 1.5,
);
// 기타 스타일
static TextStyle get bodyMuted => GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w400,
color: mutedForeground,
color: foregroundMuted,
letterSpacing: 0,
height: 1.6,
);
static TextStyle get labelLarge => GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: foreground,
letterSpacing: 0,
letterSpacing: 0.02,
height: 1.4,
);
static TextStyle get labelMedium => GoogleFonts.inter(
fontSize: 12,
fontSize: 13,
fontWeight: FontWeight.w500,
color: foreground,
letterSpacing: 0,
letterSpacing: 0.02,
height: 1.4,
);
static TextStyle get labelSmall => GoogleFonts.inter(
fontSize: 10,
fontSize: 12,
fontWeight: FontWeight.w500,
color: mutedForeground,
letterSpacing: 0,
color: foregroundSecondary,
letterSpacing: 0.02,
height: 1.4,
);
// Flutter 테마 데이터
static TextStyle get caption => GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w400,
color: foregroundMuted,
letterSpacing: 0.02,
height: 1.4,
);
// 코드/모노스페이스
static TextStyle get code => GoogleFonts.jetBrainsMono(
fontSize: 13,
fontWeight: FontWeight.w400,
color: foreground,
letterSpacing: 0,
height: 1.5,
);
// ============= Flutter 테마 데이터 =============
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
colorScheme: const ColorScheme.light(
primary: primary,
primaryContainer: primaryLight,
secondary: secondary,
secondaryContainer: secondaryLight,
surface: background,
surfaceContainerHighest: card,
onSurface: foreground,
onPrimary: primaryForeground,
onSecondary: secondaryForeground,
error: destructive,
onError: destructiveForeground,
error: error,
errorContainer: errorLight,
onError: errorForeground,
outline: border,
outlineVariant: input,
outlineVariant: divider,
),
scaffoldBackgroundColor: background,
textTheme: TextTheme(
headlineLarge: headingH1,
headlineMedium: headingH2,
headlineSmall: headingH3,
titleLarge: headingH4,
displayLarge: headingH1,
displayMedium: headingH2,
displaySmall: headingH3,
headlineLarge: headingH3,
headlineMedium: headingH4,
headlineSmall: headingH5,
titleLarge: headingH6,
titleMedium: labelLarge,
titleSmall: labelMedium,
bodyLarge: bodyLarge,
bodyMedium: bodyMedium,
bodySmall: bodySmall,
@@ -198,11 +347,16 @@ class ShadcnTheme {
backgroundColor: background,
foregroundColor: foreground,
elevation: 0,
scrolledUnderElevation: 1,
shadowColor: Colors.black.withValues(alpha: 0.1),
scrolledUnderElevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
titleTextStyle: headingH4,
iconTheme: const IconThemeData(color: foreground),
centerTitle: false,
titleTextStyle: headingH5,
toolbarHeight: 64,
iconTheme: const IconThemeData(
color: foregroundSecondary,
size: 20,
),
),
cardTheme: CardThemeData(
color: card,
@@ -211,86 +365,234 @@ class ShadcnTheme {
borderRadius: BorderRadius.circular(radiusLg),
side: const BorderSide(color: border, width: 1),
),
shadowColor: Colors.black.withValues(alpha: 0.05),
shadowColor: Colors.transparent,
margin: EdgeInsets.zero,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primary,
foregroundColor: primaryForeground,
disabledBackgroundColor: backgroundSecondary,
disabledForegroundColor: foregroundMuted,
elevation: 0,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusMd),
),
minimumSize: const Size(64, 40),
padding: const EdgeInsets.symmetric(
horizontal: spacing4,
horizontal: spacing6,
vertical: spacing2,
),
textStyle: labelMedium,
textStyle: labelMedium.copyWith(fontWeight: FontWeight.w500),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: foreground,
side: const BorderSide(color: border),
disabledForegroundColor: foregroundMuted,
side: const BorderSide(color: border, width: 1),
elevation: 0,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusMd),
),
minimumSize: const Size(64, 40),
padding: const EdgeInsets.symmetric(
horizontal: spacing4,
horizontal: spacing6,
vertical: spacing2,
),
textStyle: labelMedium,
textStyle: labelMedium.copyWith(fontWeight: FontWeight.w500),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: foreground,
foregroundColor: primary,
disabledForegroundColor: foregroundMuted,
elevation: 0,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusMd),
),
minimumSize: const Size(64, 40),
padding: const EdgeInsets.symmetric(
horizontal: spacing4,
vertical: spacing2,
),
textStyle: labelMedium,
textStyle: labelMedium.copyWith(fontWeight: FontWeight.w500),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: background,
fillColor: input,
hoverColor: inputHover,
contentPadding: const EdgeInsets.symmetric(
horizontal: spacing3,
vertical: spacing2,
vertical: spacing3,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusMd),
borderSide: const BorderSide(color: input),
borderSide: const BorderSide(color: inputBorder, width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusMd),
borderSide: const BorderSide(color: input),
borderSide: const BorderSide(color: inputBorder, width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusMd),
borderSide: const BorderSide(color: ring, width: 2),
borderSide: const BorderSide(color: inputFocus, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusMd),
borderSide: const BorderSide(color: destructive),
borderSide: const BorderSide(color: error, width: 1),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusMd),
borderSide: const BorderSide(color: destructive, width: 2),
borderSide: const BorderSide(color: error, width: 2),
),
hintStyle: bodyMedium.copyWith(color: mutedForeground),
labelStyle: labelMedium,
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusMd),
borderSide: BorderSide(color: border.withValues(alpha: 0.5), width: 1),
),
hintStyle: bodyMedium.copyWith(color: foregroundSubtle),
labelStyle: labelMedium.copyWith(color: foregroundSecondary),
helperStyle: bodySmall.copyWith(color: foregroundMuted),
errorStyle: bodySmall.copyWith(color: error),
prefixIconColor: foregroundMuted,
suffixIconColor: foregroundMuted,
),
dividerTheme: const DividerThemeData(
color: divider,
thickness: 1,
space: 1,
),
chipTheme: ChipThemeData(
backgroundColor: backgroundSecondary,
disabledColor: backgroundSecondary.withValues(alpha: 0.5),
selectedColor: primaryLight,
secondarySelectedColor: primaryLight,
labelStyle: labelSmall,
secondaryLabelStyle: labelSmall,
padding: const EdgeInsets.symmetric(horizontal: spacing2, vertical: spacing1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusFull),
side: const BorderSide(color: Colors.transparent),
),
),
dialogTheme: DialogThemeData(
backgroundColor: card,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusLg),
),
titleTextStyle: headingH5,
contentTextStyle: bodyMedium,
),
tooltipTheme: TooltipThemeData(
decoration: BoxDecoration(
color: foreground,
borderRadius: BorderRadius.circular(radiusMd),
),
textStyle: bodySmall.copyWith(color: background),
padding: const EdgeInsets.symmetric(
horizontal: spacing3,
vertical: spacing2,
),
),
snackBarTheme: SnackBarThemeData(
backgroundColor: foreground,
contentTextStyle: bodyMedium.copyWith(color: background),
actionTextColor: primaryLight,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusMd),
),
),
dataTableTheme: DataTableThemeData(
headingRowColor: WidgetStateProperty.all(backgroundSecondary),
headingTextStyle: labelMedium.copyWith(
color: foreground,
fontWeight: FontWeight.w600,
),
dataTextStyle: bodyMedium,
dividerThickness: 1,
horizontalMargin: spacing4,
columnSpacing: spacing6,
headingRowHeight: 48,
dataRowMinHeight: 56,
dataRowMaxHeight: 56,
),
dividerTheme: const DividerThemeData(color: border, thickness: 1),
);
}
// ============= 유틸리티 메서드 =============
/// 회사 타입에 따른 색상 반환
static Color getCompanyColor(String type) {
switch (type.toLowerCase()) {
case '본사':
case 'headquarters':
return companyHeadquarters;
case '지점':
case 'branch':
return companyBranch;
case '파트너사':
case 'partner':
return companyPartner;
case '고객사':
case 'customer':
return companyCustomer;
default:
return secondary;
}
}
/// 장비 상태에 따른 색상 반환
static Color getEquipmentStatusColor(String status) {
switch (status.toLowerCase()) {
case '입고':
case 'in':
return equipmentIn;
case '출고':
case 'out':
return equipmentOut;
case '대여':
case 'rent':
return equipmentRent;
case '폐기':
case 'disposal':
return equipmentDisposal;
case '수리중':
case 'repair':
return equipmentRepair;
case '알수없음':
case 'unknown':
return equipmentUnknown;
default:
return secondary;
}
}
/// 상태별 배경색 반환 (연한 버전)
static Color getStatusBackgroundColor(String status) {
switch (status.toLowerCase()) {
case 'success':
case '성공':
case '완료':
return successLight;
case 'warning':
case '경고':
case '주의':
return warningLight;
case 'error':
case '오류':
case '실패':
return errorLight;
case 'info':
case '정보':
case '알림':
return infoLight;
default:
return backgroundSecondary;
}
}
}