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:
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,18 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:superport/screens/common/theme_shadcn.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 Widget child;
|
||||||
final EdgeInsetsGeometry? padding;
|
final EdgeInsetsGeometry? padding;
|
||||||
final EdgeInsetsGeometry? margin;
|
final EdgeInsetsGeometry? margin;
|
||||||
final double? width;
|
final double? width;
|
||||||
final double? height;
|
final double? height;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
final bool hoverable;
|
||||||
|
final bool elevated;
|
||||||
|
|
||||||
const ShadcnCard({
|
const ShadcnCard({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -20,34 +22,62 @@ class ShadcnCard extends StatelessWidget {
|
|||||||
this.width,
|
this.width,
|
||||||
this.height,
|
this.height,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
|
this.hoverable = true,
|
||||||
|
this.elevated = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ShadcnCard> createState() => _ShadcnCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShadcnCardState extends State<ShadcnCard> {
|
||||||
|
bool _isHovered = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cardContent = Container(
|
final cardContent = MouseRegion(
|
||||||
width: width,
|
onEnter: widget.hoverable ? (_) => setState(() => _isHovered = true) : null,
|
||||||
height: height,
|
onExit: widget.hoverable ? (_) => setState(() => _isHovered = false) : null,
|
||||||
padding: padding ?? const EdgeInsets.all(ShadcnTheme.spacing6),
|
child: AnimatedContainer(
|
||||||
margin: margin,
|
duration: const Duration(milliseconds: 200),
|
||||||
decoration: BoxDecoration(
|
width: widget.width,
|
||||||
color: ShadcnTheme.card,
|
height: widget.height,
|
||||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
|
padding: widget.padding ?? const EdgeInsets.all(ShadcnTheme.spacing6),
|
||||||
border: Border.all(color: Colors.black),
|
margin: widget.margin,
|
||||||
boxShadow: ShadcnTheme.cardShadow,
|
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) {
|
if (widget.onTap != null) {
|
||||||
return GestureDetector(onTap: onTap, child: cardContent);
|
return GestureDetector(
|
||||||
|
onTap: widget.onTap,
|
||||||
|
child: cardContent,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cardContent;
|
return cardContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 버튼 컴포넌트
|
// ============= 버튼 컴포넌트 =============
|
||||||
class ShadcnButton extends StatelessWidget {
|
class ShadcnButton extends StatefulWidget {
|
||||||
final String text;
|
final String text;
|
||||||
final VoidCallback? onPressed;
|
final VoidCallback? onPressed;
|
||||||
final ShadcnButtonVariant variant;
|
final ShadcnButtonVariant variant;
|
||||||
@@ -71,81 +101,133 @@ class ShadcnButton extends StatelessWidget {
|
|||||||
this.textColor,
|
this.textColor,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ShadcnButton> createState() => _ShadcnButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShadcnButtonState extends State<ShadcnButton> {
|
||||||
|
bool _isHovered = false;
|
||||||
|
bool _isPressed = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ButtonStyle style = _getButtonStyle();
|
final ButtonStyle style = _getButtonStyle();
|
||||||
final EdgeInsetsGeometry padding = _getPadding();
|
final EdgeInsetsGeometry padding = _getPadding();
|
||||||
|
|
||||||
Widget buttonChild = Row(
|
Widget buttonChild = Row(
|
||||||
mainAxisSize: fullWidth ? MainAxisSize.max : MainAxisSize.min,
|
mainAxisSize: widget.fullWidth ? MainAxisSize.max : MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (loading)
|
if (widget.loading)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 16,
|
width: _getIconSize(),
|
||||||
height: 16,
|
height: _getIconSize(),
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
textColor ?? _getDefaultTextColor(),
|
widget.textColor ?? _getDefaultTextColor(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else if (icon != null)
|
else if (widget.icon != null)
|
||||||
icon!,
|
widget.icon!,
|
||||||
if ((loading || icon != null) && text.isNotEmpty)
|
if ((widget.loading || widget.icon != null) && widget.text.isNotEmpty)
|
||||||
const SizedBox(width: ShadcnTheme.spacing2),
|
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) {
|
Widget button;
|
||||||
return SizedBox(
|
if (widget.variant == ShadcnButtonVariant.primary ||
|
||||||
width: fullWidth ? double.infinity : null,
|
widget.variant == ShadcnButtonVariant.destructive) {
|
||||||
child: ElevatedButton(
|
button = ElevatedButton(
|
||||||
onPressed: loading ? null : onPressed,
|
onPressed: widget.loading ? null : widget.onPressed,
|
||||||
style: style.copyWith(padding: WidgetStateProperty.all(padding)),
|
style: style.copyWith(padding: WidgetStateProperty.all(padding)),
|
||||||
child: buttonChild,
|
child: buttonChild,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} else if (variant == ShadcnButtonVariant.secondary) {
|
} else if (widget.variant == ShadcnButtonVariant.secondary) {
|
||||||
return SizedBox(
|
button = OutlinedButton(
|
||||||
width: fullWidth ? double.infinity : null,
|
onPressed: widget.loading ? null : widget.onPressed,
|
||||||
child: OutlinedButton(
|
style: style.copyWith(padding: WidgetStateProperty.all(padding)),
|
||||||
onPressed: loading ? null : onPressed,
|
child: buttonChild,
|
||||||
style: style.copyWith(padding: WidgetStateProperty.all(padding)),
|
|
||||||
child: buttonChild,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return SizedBox(
|
button = TextButton(
|
||||||
width: fullWidth ? double.infinity : null,
|
onPressed: widget.loading ? null : widget.onPressed,
|
||||||
child: TextButton(
|
style: style.copyWith(padding: WidgetStateProperty.all(padding)),
|
||||||
onPressed: loading ? null : onPressed,
|
child: buttonChild,
|
||||||
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() {
|
ButtonStyle _getButtonStyle() {
|
||||||
switch (variant) {
|
switch (widget.variant) {
|
||||||
case ShadcnButtonVariant.primary:
|
case ShadcnButtonVariant.primary:
|
||||||
return ElevatedButton.styleFrom(
|
return ElevatedButton.styleFrom(
|
||||||
backgroundColor: backgroundColor ?? ShadcnTheme.primary,
|
backgroundColor: widget.backgroundColor ??
|
||||||
foregroundColor: textColor ?? ShadcnTheme.primaryForeground,
|
(_isHovered ? ShadcnTheme.primaryDark : ShadcnTheme.primary),
|
||||||
|
foregroundColor: widget.textColor ?? ShadcnTheme.primaryForeground,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shadowColor: Colors.transparent,
|
shadowColor: Colors.transparent,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||||
),
|
),
|
||||||
|
).copyWith(
|
||||||
|
overlayColor: WidgetStateProperty.all(
|
||||||
|
ShadcnTheme.primaryDark.withValues(alpha: 0.1),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
case ShadcnButtonVariant.secondary:
|
case ShadcnButtonVariant.secondary:
|
||||||
return OutlinedButton.styleFrom(
|
return OutlinedButton.styleFrom(
|
||||||
backgroundColor: backgroundColor ?? ShadcnTheme.secondary,
|
backgroundColor: widget.backgroundColor ??
|
||||||
foregroundColor: textColor ?? ShadcnTheme.secondaryForeground,
|
(_isHovered ? ShadcnTheme.backgroundSecondary : Colors.transparent),
|
||||||
side: const BorderSide(color: Colors.black),
|
foregroundColor: widget.textColor ?? ShadcnTheme.foreground,
|
||||||
|
side: BorderSide(
|
||||||
|
color: _isHovered ? ShadcnTheme.borderStrong : ShadcnTheme.border,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shadowColor: Colors.transparent,
|
shadowColor: Colors.transparent,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -154,8 +236,9 @@ class ShadcnButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
case ShadcnButtonVariant.destructive:
|
case ShadcnButtonVariant.destructive:
|
||||||
return ElevatedButton.styleFrom(
|
return ElevatedButton.styleFrom(
|
||||||
backgroundColor: backgroundColor ?? ShadcnTheme.destructive,
|
backgroundColor: widget.backgroundColor ??
|
||||||
foregroundColor: textColor ?? ShadcnTheme.destructiveForeground,
|
(_isHovered ? Color(0xFFB91C1C) : ShadcnTheme.error),
|
||||||
|
foregroundColor: widget.textColor ?? ShadcnTheme.errorForeground,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shadowColor: Colors.transparent,
|
shadowColor: Colors.transparent,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -164,8 +247,9 @@ class ShadcnButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
case ShadcnButtonVariant.ghost:
|
case ShadcnButtonVariant.ghost:
|
||||||
return TextButton.styleFrom(
|
return TextButton.styleFrom(
|
||||||
backgroundColor: backgroundColor ?? Colors.transparent,
|
backgroundColor: widget.backgroundColor ??
|
||||||
foregroundColor: textColor ?? ShadcnTheme.foreground,
|
(_isHovered ? ShadcnTheme.backgroundSecondary : Colors.transparent),
|
||||||
|
foregroundColor: widget.textColor ?? ShadcnTheme.foreground,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shadowColor: Colors.transparent,
|
shadowColor: Colors.transparent,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -176,28 +260,28 @@ class ShadcnButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EdgeInsetsGeometry _getPadding() {
|
EdgeInsetsGeometry _getPadding() {
|
||||||
switch (size) {
|
switch (widget.size) {
|
||||||
case ShadcnButtonSize.small:
|
case ShadcnButtonSize.small:
|
||||||
return const EdgeInsets.symmetric(
|
return const EdgeInsets.symmetric(
|
||||||
horizontal: ShadcnTheme.spacing3,
|
horizontal: ShadcnTheme.spacing3,
|
||||||
vertical: 6,
|
vertical: 0,
|
||||||
);
|
);
|
||||||
case ShadcnButtonSize.medium:
|
case ShadcnButtonSize.medium:
|
||||||
return const EdgeInsets.symmetric(
|
return const EdgeInsets.symmetric(
|
||||||
horizontal: ShadcnTheme.spacing4,
|
horizontal: ShadcnTheme.spacing4,
|
||||||
vertical: 10,
|
vertical: 0,
|
||||||
);
|
);
|
||||||
case ShadcnButtonSize.large:
|
case ShadcnButtonSize.large:
|
||||||
return const EdgeInsets.symmetric(
|
return const EdgeInsets.symmetric(
|
||||||
horizontal: ShadcnTheme.spacing8,
|
horizontal: ShadcnTheme.spacing6,
|
||||||
vertical: ShadcnTheme.spacing3,
|
vertical: 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextStyle _getTextStyle() {
|
TextStyle _getTextStyle() {
|
||||||
TextStyle baseStyle;
|
TextStyle baseStyle;
|
||||||
switch (size) {
|
switch (widget.size) {
|
||||||
case ShadcnButtonSize.small:
|
case ShadcnButtonSize.small:
|
||||||
baseStyle = ShadcnTheme.labelSmall;
|
baseStyle = ShadcnTheme.labelSmall;
|
||||||
break;
|
break;
|
||||||
@@ -208,17 +292,19 @@ class ShadcnButton extends StatelessWidget {
|
|||||||
baseStyle = ShadcnTheme.labelLarge;
|
baseStyle = ShadcnTheme.labelLarge;
|
||||||
break;
|
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() {
|
Color _getDefaultTextColor() {
|
||||||
switch (variant) {
|
switch (widget.variant) {
|
||||||
case ShadcnButtonVariant.primary:
|
case ShadcnButtonVariant.primary:
|
||||||
return ShadcnTheme.primaryForeground;
|
return ShadcnTheme.primaryForeground;
|
||||||
case ShadcnButtonVariant.secondary:
|
case ShadcnButtonVariant.secondary:
|
||||||
return ShadcnTheme.secondaryForeground;
|
return ShadcnTheme.foreground;
|
||||||
case ShadcnButtonVariant.destructive:
|
case ShadcnButtonVariant.destructive:
|
||||||
return ShadcnTheme.destructiveForeground;
|
return ShadcnTheme.errorForeground;
|
||||||
case ShadcnButtonVariant.ghost:
|
case ShadcnButtonVariant.ghost:
|
||||||
return ShadcnTheme.foreground;
|
return ShadcnTheme.foreground;
|
||||||
}
|
}
|
||||||
@@ -231,11 +317,12 @@ enum ShadcnButtonVariant { primary, secondary, destructive, ghost }
|
|||||||
// 버튼 사이즈
|
// 버튼 사이즈
|
||||||
enum ShadcnButtonSize { small, medium, large }
|
enum ShadcnButtonSize { small, medium, large }
|
||||||
|
|
||||||
// 입력 필드 컴포넌트
|
// ============= 입력 필드 컴포넌트 =============
|
||||||
class ShadcnInput extends StatelessWidget {
|
class ShadcnInput extends StatefulWidget {
|
||||||
final String? label;
|
final String? label;
|
||||||
final String? placeholder;
|
final String? placeholder;
|
||||||
final String? errorText;
|
final String? errorText;
|
||||||
|
final String? helperText;
|
||||||
final TextEditingController? controller;
|
final TextEditingController? controller;
|
||||||
final bool obscureText;
|
final bool obscureText;
|
||||||
final TextInputType? keyboardType;
|
final TextInputType? keyboardType;
|
||||||
@@ -246,12 +333,14 @@ class ShadcnInput extends StatelessWidget {
|
|||||||
final bool readOnly;
|
final bool readOnly;
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
final int? maxLines;
|
final int? maxLines;
|
||||||
|
final bool required;
|
||||||
|
|
||||||
const ShadcnInput({
|
const ShadcnInput({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.label,
|
this.label,
|
||||||
this.placeholder,
|
this.placeholder,
|
||||||
this.errorText,
|
this.errorText,
|
||||||
|
this.helperText,
|
||||||
this.controller,
|
this.controller,
|
||||||
this.obscureText = false,
|
this.obscureText = false,
|
||||||
this.keyboardType,
|
this.keyboardType,
|
||||||
@@ -262,63 +351,124 @@ class ShadcnInput extends StatelessWidget {
|
|||||||
this.readOnly = false,
|
this.readOnly = false,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
this.maxLines = 1,
|
this.maxLines = 1,
|
||||||
|
this.required = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ShadcnInput> createState() => _ShadcnInputState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShadcnInputState extends State<ShadcnInput> {
|
||||||
|
bool _isFocused = false;
|
||||||
|
bool _isHovered = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (label != null) ...[
|
if (widget.label != null) ...[
|
||||||
Text(label!, style: ShadcnTheme.labelMedium),
|
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),
|
const SizedBox(height: ShadcnTheme.spacing1),
|
||||||
],
|
],
|
||||||
TextFormField(
|
MouseRegion(
|
||||||
controller: controller,
|
onEnter: (_) => setState(() => _isHovered = true),
|
||||||
obscureText: obscureText,
|
onExit: (_) => setState(() => _isHovered = false),
|
||||||
keyboardType: keyboardType,
|
child: Focus(
|
||||||
onChanged: onChanged,
|
onFocusChange: (focused) => setState(() => _isFocused = focused),
|
||||||
onTap: onTap,
|
child: TextFormField(
|
||||||
readOnly: readOnly,
|
controller: widget.controller,
|
||||||
enabled: enabled,
|
obscureText: widget.obscureText,
|
||||||
maxLines: maxLines,
|
keyboardType: widget.keyboardType,
|
||||||
decoration: InputDecoration(
|
onChanged: widget.onChanged,
|
||||||
hintText: placeholder,
|
onTap: widget.onTap,
|
||||||
prefixIcon: prefixIcon,
|
readOnly: widget.readOnly,
|
||||||
suffixIcon: suffixIcon,
|
enabled: widget.enabled,
|
||||||
errorText: errorText,
|
maxLines: widget.maxLines,
|
||||||
filled: true,
|
style: ShadcnTheme.bodyMedium,
|
||||||
fillColor: ShadcnTheme.background,
|
decoration: InputDecoration(
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
hintText: widget.placeholder,
|
||||||
horizontal: ShadcnTheme.spacing3,
|
prefixIcon: widget.prefixIcon,
|
||||||
vertical: 10,
|
suffixIcon: widget.suffixIcon,
|
||||||
),
|
errorText: widget.errorText,
|
||||||
border: OutlineInputBorder(
|
helperText: widget.helperText,
|
||||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
filled: true,
|
||||||
borderSide: const BorderSide(color: ShadcnTheme.input),
|
fillColor: !widget.enabled
|
||||||
),
|
? ShadcnTheme.backgroundSecondary
|
||||||
enabledBorder: OutlineInputBorder(
|
: _isHovered
|
||||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
? ShadcnTheme.inputHover
|
||||||
borderSide: const BorderSide(color: ShadcnTheme.input),
|
: ShadcnTheme.input,
|
||||||
),
|
contentPadding: EdgeInsets.symmetric(
|
||||||
focusedBorder: OutlineInputBorder(
|
horizontal: ShadcnTheme.spacing3,
|
||||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
vertical: widget.maxLines! > 1
|
||||||
borderSide: const BorderSide(color: ShadcnTheme.ring, width: 2),
|
? ShadcnTheme.spacing3
|
||||||
),
|
: ShadcnTheme.spacing2,
|
||||||
errorBorder: OutlineInputBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
border: OutlineInputBorder(
|
||||||
borderSide: const BorderSide(color: ShadcnTheme.destructive),
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||||
),
|
borderSide: BorderSide(
|
||||||
focusedErrorBorder: OutlineInputBorder(
|
color: widget.errorText != null
|
||||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
? ShadcnTheme.error
|
||||||
borderSide: const BorderSide(
|
: ShadcnTheme.inputBorder,
|
||||||
color: ShadcnTheme.destructive,
|
),
|
||||||
width: 2,
|
),
|
||||||
|
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 {
|
class ShadcnBadge extends StatelessWidget {
|
||||||
final String text;
|
final String text;
|
||||||
final ShadcnBadgeVariant variant;
|
final ShadcnBadgeVariant variant;
|
||||||
final ShadcnBadgeSize size;
|
final ShadcnBadgeSize size;
|
||||||
|
final Widget? icon;
|
||||||
|
|
||||||
const ShadcnBadge({
|
const ShadcnBadge({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.text,
|
required this.text,
|
||||||
this.variant = ShadcnBadgeVariant.primary,
|
this.variant = ShadcnBadgeVariant.primary,
|
||||||
this.size = ShadcnBadgeSize.medium,
|
this.size = ShadcnBadgeSize.medium,
|
||||||
|
this.icon,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -345,10 +497,22 @@ class ShadcnBadge extends StatelessWidget {
|
|||||||
padding: _getPadding(),
|
padding: _getPadding(),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _getBackgroundColor(),
|
color: _getBackgroundColor(),
|
||||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusXl),
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusFull),
|
||||||
border: Border.all(color: _getBorderColor()),
|
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) {
|
switch (size) {
|
||||||
case ShadcnBadgeSize.small:
|
case ShadcnBadgeSize.small:
|
||||||
return const EdgeInsets.symmetric(
|
return const EdgeInsets.symmetric(
|
||||||
horizontal: ShadcnTheme.spacing1,
|
horizontal: ShadcnTheme.spacing2,
|
||||||
vertical: ShadcnTheme.spacing1 / 2,
|
vertical: 2,
|
||||||
);
|
);
|
||||||
case ShadcnBadgeSize.medium:
|
case ShadcnBadgeSize.medium:
|
||||||
return const EdgeInsets.symmetric(
|
return const EdgeInsets.symmetric(
|
||||||
horizontal: ShadcnTheme.spacing2,
|
horizontal: ShadcnTheme.spacing2 + 2,
|
||||||
vertical: ShadcnTheme.spacing1,
|
vertical: 4,
|
||||||
);
|
);
|
||||||
case ShadcnBadgeSize.large:
|
case ShadcnBadgeSize.large:
|
||||||
return const EdgeInsets.symmetric(
|
return const EdgeInsets.symmetric(
|
||||||
horizontal: ShadcnTheme.spacing3,
|
horizontal: ShadcnTheme.spacing3,
|
||||||
vertical: ShadcnTheme.spacing1,
|
vertical: 6,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -375,90 +539,160 @@ class ShadcnBadge extends StatelessWidget {
|
|||||||
Color _getBackgroundColor() {
|
Color _getBackgroundColor() {
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case ShadcnBadgeVariant.primary:
|
case ShadcnBadgeVariant.primary:
|
||||||
return ShadcnTheme.primary;
|
return ShadcnTheme.primaryLight;
|
||||||
case ShadcnBadgeVariant.secondary:
|
case ShadcnBadgeVariant.secondary:
|
||||||
return ShadcnTheme.secondary;
|
return ShadcnTheme.secondaryLight;
|
||||||
case ShadcnBadgeVariant.destructive:
|
case ShadcnBadgeVariant.destructive:
|
||||||
return ShadcnTheme.destructive;
|
return ShadcnTheme.errorLight;
|
||||||
case ShadcnBadgeVariant.success:
|
case ShadcnBadgeVariant.success:
|
||||||
return ShadcnTheme.success;
|
return ShadcnTheme.successLight;
|
||||||
case ShadcnBadgeVariant.warning:
|
case ShadcnBadgeVariant.warning:
|
||||||
return ShadcnTheme.warning;
|
return ShadcnTheme.warningLight;
|
||||||
|
case ShadcnBadgeVariant.info:
|
||||||
|
return ShadcnTheme.infoLight;
|
||||||
case ShadcnBadgeVariant.outline:
|
case ShadcnBadgeVariant.outline:
|
||||||
return Colors.transparent;
|
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() {
|
Color _getBorderColor() {
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case ShadcnBadgeVariant.outline:
|
case ShadcnBadgeVariant.outline:
|
||||||
return Colors.black;
|
return ShadcnTheme.border;
|
||||||
default:
|
default:
|
||||||
return Colors.transparent;
|
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() {
|
TextStyle _getTextStyle() {
|
||||||
final Color textColor =
|
final Color textColor = _getTextColor();
|
||||||
variant == ShadcnBadgeVariant.outline
|
|
||||||
? ShadcnTheme.foreground
|
|
||||||
: variant == ShadcnBadgeVariant.secondary
|
|
||||||
? ShadcnTheme.secondaryForeground
|
|
||||||
: ShadcnTheme.primaryForeground;
|
|
||||||
|
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case ShadcnBadgeSize.small:
|
case ShadcnBadgeSize.small:
|
||||||
return ShadcnTheme.labelSmall.copyWith(color: textColor);
|
return ShadcnTheme.caption.copyWith(
|
||||||
|
color: textColor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
);
|
||||||
case ShadcnBadgeSize.medium:
|
case ShadcnBadgeSize.medium:
|
||||||
return ShadcnTheme.labelMedium.copyWith(color: textColor);
|
return ShadcnTheme.labelSmall.copyWith(
|
||||||
|
color: textColor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
);
|
||||||
case ShadcnBadgeSize.large:
|
case ShadcnBadgeSize.large:
|
||||||
return ShadcnTheme.labelLarge.copyWith(color: textColor);
|
return ShadcnTheme.labelMedium.copyWith(
|
||||||
|
color: textColor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 배지 variants
|
// 배지 variants (비즈니스 상태 추가)
|
||||||
enum ShadcnBadgeVariant {
|
enum ShadcnBadgeVariant {
|
||||||
primary,
|
primary,
|
||||||
secondary,
|
secondary,
|
||||||
destructive,
|
destructive,
|
||||||
success,
|
success,
|
||||||
warning,
|
warning,
|
||||||
|
info,
|
||||||
outline,
|
outline,
|
||||||
|
// 장비 상태
|
||||||
|
equipmentIn,
|
||||||
|
equipmentOut,
|
||||||
|
equipmentRent,
|
||||||
|
// 회사 타입
|
||||||
|
companyHeadquarters,
|
||||||
|
companyBranch,
|
||||||
|
companyPartner,
|
||||||
|
companyCustomer,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 배지 사이즈
|
// 배지 사이즈
|
||||||
enum ShadcnBadgeSize { small, medium, large }
|
enum ShadcnBadgeSize { small, medium, large }
|
||||||
|
|
||||||
// 구분선 컴포넌트
|
// ============= 구분선 컴포넌트 =============
|
||||||
class ShadcnSeparator extends StatelessWidget {
|
class ShadcnSeparator extends StatelessWidget {
|
||||||
final Axis direction;
|
final Axis direction;
|
||||||
final double thickness;
|
final double thickness;
|
||||||
final Color? color;
|
final Color? color;
|
||||||
|
final EdgeInsetsGeometry? margin;
|
||||||
|
|
||||||
const ShadcnSeparator({
|
const ShadcnSeparator({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.direction = Axis.horizontal,
|
this.direction = Axis.horizontal,
|
||||||
this.thickness = 1.0,
|
this.thickness = 1.0,
|
||||||
this.color,
|
this.color,
|
||||||
|
this.margin,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
|
margin: margin,
|
||||||
width: direction == Axis.horizontal ? double.infinity : thickness,
|
width: direction == Axis.horizontal ? double.infinity : thickness,
|
||||||
height: direction == Axis.vertical ? double.infinity : thickness,
|
height: direction == Axis.vertical ? double.infinity : thickness,
|
||||||
color: color ?? Colors.black,
|
color: color ?? ShadcnTheme.divider,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 아바타 컴포넌트
|
// ============= 아바타 컴포넌트 =============
|
||||||
class ShadcnAvatar extends StatelessWidget {
|
class ShadcnAvatar extends StatelessWidget {
|
||||||
final String? imageUrl;
|
final String? imageUrl;
|
||||||
final String? initials;
|
final String? initials;
|
||||||
final double size;
|
final double size;
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
|
final Color? textColor;
|
||||||
|
final bool showBorder;
|
||||||
|
|
||||||
const ShadcnAvatar({
|
const ShadcnAvatar({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -466,6 +700,8 @@ class ShadcnAvatar extends StatelessWidget {
|
|||||||
this.initials,
|
this.initials,
|
||||||
this.size = 40,
|
this.size = 40,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
|
this.textColor,
|
||||||
|
this.showBorder = true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -474,36 +710,166 @@ class ShadcnAvatar extends StatelessWidget {
|
|||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor ?? ShadcnTheme.muted,
|
color: backgroundColor ?? ShadcnTheme.backgroundSecondary,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(color: Colors.black),
|
border: showBorder
|
||||||
|
? Border.all(color: ShadcnTheme.border, width: 1)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
child: ClipOval(
|
child: ClipOval(
|
||||||
child:
|
child: imageUrl != null
|
||||||
imageUrl != null
|
? Image.network(
|
||||||
? Image.network(
|
imageUrl!,
|
||||||
imageUrl!,
|
fit: BoxFit.cover,
|
||||||
fit: BoxFit.cover,
|
errorBuilder: (context, error, stackTrace) => _buildFallback(),
|
||||||
errorBuilder:
|
)
|
||||||
(context, error, stackTrace) => _buildFallback(),
|
: _buildFallback(),
|
||||||
)
|
|
||||||
: _buildFallback(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFallback() {
|
Widget _buildFallback() {
|
||||||
|
final displayText = initials?.toUpperCase() ?? '?';
|
||||||
return Container(
|
return Container(
|
||||||
color: backgroundColor ?? ShadcnTheme.muted,
|
color: backgroundColor ?? ShadcnTheme.backgroundSecondary,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
initials ?? '?',
|
displayText,
|
||||||
style: ShadcnTheme.labelMedium.copyWith(
|
style: TextStyle(
|
||||||
color: ShadcnTheme.mutedForeground,
|
color: textColor ?? ShadcnTheme.foregroundSecondary,
|
||||||
fontSize: size * 0.4,
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,84 +1,170 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
/// shadcn/ui 스타일 테마 시스템
|
/// ERP 시스템에 최적화된 색채 심리학 기반 테마 시스템
|
||||||
class ShadcnTheme {
|
class ShadcnTheme {
|
||||||
// Teal 기반 색상 시스템
|
// ============= 기본 색상 팔레트 =============
|
||||||
|
// 배경 및 표면 색상
|
||||||
static const Color background = Color(0xFFFFFFFF);
|
static const Color background = Color(0xFFFFFFFF);
|
||||||
static const Color foreground = Color(0xFF0F172A);
|
static const Color backgroundSecondary = Color(0xFFF9FAFB); // 보조 배경
|
||||||
static const Color card = Color(0xFFFFFFFF);
|
static const Color surface = Color(0xFFFFFFFF);
|
||||||
static const Color cardForeground = Color(0xFF0F172A);
|
static const Color surfaceHover = Color(0xFFF3F4F6); // 호버 상태
|
||||||
static const Color popover = Color(0xFFFFFFFF);
|
|
||||||
static const Color popoverForeground = Color(0xFF0F172A);
|
// 텍스트 색상
|
||||||
static const Color primary = Color(0xFF0D9488); // teal-600
|
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 primaryForeground = Color(0xFFFFFFFF);
|
||||||
static const Color secondary = Color(0xFFF0FDFA); // teal-50
|
|
||||||
static const Color secondaryForeground = Color(0xFF134E4A); // teal-900
|
// Secondary 색상 (중립 그레이)
|
||||||
static const Color muted = Color(0xFFF1F5F9); // slate-100
|
static const Color secondary = Color(0xFF6B7280); // gray-500
|
||||||
static const Color mutedForeground = Color(0xFF64748B); // slate-500
|
static const Color secondaryDark = Color(0xFF374151); // gray-700
|
||||||
static const Color accent = Color(0xFF14B8A6); // teal-500
|
static const Color secondaryLight = Color(0xFFF9FAFB); // gray-50
|
||||||
static const Color accentForeground = Color(0xFFFFFFFF);
|
static const Color secondaryForeground = Color(0xFF111827);
|
||||||
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 success = Color(0xFF059669); // emerald-600
|
||||||
static const Color input = Color(0xFFE5E7EB); // gray-200
|
static const Color successLight = Color(0xFFD1FAE5); // emerald-100
|
||||||
static const Color ring = Color(0xFF14B8A6); // teal-500
|
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); // 사용하지 않음
|
static const Color radius = Color(0xFF000000); // 사용하지 않음
|
||||||
|
|
||||||
// Teal 그라데이션 색상
|
// ============= 그림자 시스템 =============
|
||||||
static const Color gradient1 = Color(0xFF14B8A6); // teal-500
|
static List<BoxShadow> get shadowXs => [
|
||||||
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 => [
|
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: primary.withValues(alpha: 0.08),
|
color: Colors.black.withValues(alpha: 0.05),
|
||||||
blurRadius: 8,
|
blurRadius: 2,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 1),
|
||||||
),
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withValues(alpha: 0.04),
|
|
||||||
blurRadius: 16,
|
|
||||||
offset: const Offset(0, 8),
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
static List<BoxShadow> get buttonShadow => [
|
static List<BoxShadow> get shadowSm => [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: primary.withValues(alpha: 0.2),
|
color: Colors.black.withValues(alpha: 0.05),
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
offset: const Offset(0, 2),
|
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 spacing1 = 4.0;
|
||||||
static const double spacing2 = 8.0;
|
static const double spacing2 = 8.0;
|
||||||
static const double spacing3 = 12.0;
|
static const double spacing3 = 12.0;
|
||||||
static const double spacing4 = 16.0;
|
static const double spacing4 = 16.0;
|
||||||
static const double spacing5 = 20.0;
|
static const double spacing5 = 20.0;
|
||||||
static const double spacing6 = 24.0;
|
static const double spacing6 = 24.0;
|
||||||
|
static const double spacing7 = 28.0;
|
||||||
static const double spacing8 = 32.0;
|
static const double spacing8 = 32.0;
|
||||||
|
static const double spacing9 = 36.0;
|
||||||
static const double spacing10 = 40.0;
|
static const double spacing10 = 40.0;
|
||||||
static const double spacing12 = 48.0;
|
static const double spacing12 = 48.0;
|
||||||
|
static const double spacing14 = 56.0;
|
||||||
static const double spacing16 = 64.0;
|
static const double spacing16 = 64.0;
|
||||||
static const double spacing20 = 80.0;
|
static const double spacing20 = 80.0;
|
||||||
|
static const double spacing24 = 96.0;
|
||||||
|
|
||||||
// 라운드 설정
|
// ============= 라운드 설정 =============
|
||||||
static const double radiusNone = 0.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 radiusMd = 6.0;
|
||||||
static const double radiusLg = 8.0;
|
static const double radiusLg = 8.0;
|
||||||
static const double radiusXl = 12.0;
|
static const double radiusXl = 12.0;
|
||||||
@@ -86,40 +172,63 @@ class ShadcnTheme {
|
|||||||
static const double radius3xl = 24.0;
|
static const double radius3xl = 24.0;
|
||||||
static const double radiusFull = 9999.0;
|
static const double radiusFull = 9999.0;
|
||||||
|
|
||||||
// 타이포그래피 시스템 (통일된 크기)
|
// ============= 타이포그래피 시스템 =============
|
||||||
|
// 헤딩 스타일
|
||||||
static TextStyle get headingH1 => GoogleFonts.inter(
|
static TextStyle get headingH1 => GoogleFonts.inter(
|
||||||
fontSize: 32,
|
fontSize: 36,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: foreground,
|
color: foreground,
|
||||||
letterSpacing: -0.02,
|
letterSpacing: -0.02,
|
||||||
|
height: 1.2,
|
||||||
);
|
);
|
||||||
|
|
||||||
static TextStyle get headingH2 => GoogleFonts.inter(
|
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,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: foreground,
|
color: foreground,
|
||||||
letterSpacing: -0.01,
|
letterSpacing: -0.01,
|
||||||
);
|
height: 1.35,
|
||||||
|
|
||||||
static TextStyle get headingH3 => GoogleFonts.inter(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: foreground,
|
|
||||||
letterSpacing: -0.01,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
static TextStyle get headingH4 => GoogleFonts.inter(
|
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,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: foreground,
|
color: foreground,
|
||||||
letterSpacing: 0,
|
letterSpacing: 0,
|
||||||
|
height: 1.5,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 본문 스타일
|
||||||
static TextStyle get bodyLarge => GoogleFonts.inter(
|
static TextStyle get bodyLarge => GoogleFonts.inter(
|
||||||
fontSize: 14,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
color: foreground,
|
color: foreground,
|
||||||
letterSpacing: 0,
|
letterSpacing: 0,
|
||||||
|
height: 1.6,
|
||||||
);
|
);
|
||||||
|
|
||||||
static TextStyle get bodyMedium => GoogleFonts.inter(
|
static TextStyle get bodyMedium => GoogleFonts.inter(
|
||||||
@@ -127,66 +236,106 @@ class ShadcnTheme {
|
|||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
color: foreground,
|
color: foreground,
|
||||||
letterSpacing: 0,
|
letterSpacing: 0,
|
||||||
|
height: 1.6,
|
||||||
);
|
);
|
||||||
|
|
||||||
static TextStyle get bodySmall => GoogleFonts.inter(
|
static TextStyle get bodySmall => GoogleFonts.inter(
|
||||||
fontSize: 12,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
color: mutedForeground,
|
color: foregroundSecondary,
|
||||||
letterSpacing: 0,
|
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(
|
static TextStyle get bodyMuted => GoogleFonts.inter(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
color: mutedForeground,
|
color: foregroundMuted,
|
||||||
letterSpacing: 0,
|
letterSpacing: 0,
|
||||||
|
height: 1.6,
|
||||||
);
|
);
|
||||||
|
|
||||||
static TextStyle get labelLarge => GoogleFonts.inter(
|
static TextStyle get labelLarge => GoogleFonts.inter(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: foreground,
|
color: foreground,
|
||||||
letterSpacing: 0,
|
letterSpacing: 0.02,
|
||||||
|
height: 1.4,
|
||||||
);
|
);
|
||||||
|
|
||||||
static TextStyle get labelMedium => GoogleFonts.inter(
|
static TextStyle get labelMedium => GoogleFonts.inter(
|
||||||
fontSize: 12,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: foreground,
|
color: foreground,
|
||||||
letterSpacing: 0,
|
letterSpacing: 0.02,
|
||||||
|
height: 1.4,
|
||||||
);
|
);
|
||||||
|
|
||||||
static TextStyle get labelSmall => GoogleFonts.inter(
|
static TextStyle get labelSmall => GoogleFonts.inter(
|
||||||
fontSize: 10,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: mutedForeground,
|
color: foregroundSecondary,
|
||||||
letterSpacing: 0,
|
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 {
|
static ThemeData get lightTheme {
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
colorScheme: const ColorScheme.light(
|
colorScheme: const ColorScheme.light(
|
||||||
primary: primary,
|
primary: primary,
|
||||||
|
primaryContainer: primaryLight,
|
||||||
secondary: secondary,
|
secondary: secondary,
|
||||||
|
secondaryContainer: secondaryLight,
|
||||||
surface: background,
|
surface: background,
|
||||||
surfaceContainerHighest: card,
|
surfaceContainerHighest: card,
|
||||||
onSurface: foreground,
|
onSurface: foreground,
|
||||||
onPrimary: primaryForeground,
|
onPrimary: primaryForeground,
|
||||||
onSecondary: secondaryForeground,
|
onSecondary: secondaryForeground,
|
||||||
error: destructive,
|
error: error,
|
||||||
onError: destructiveForeground,
|
errorContainer: errorLight,
|
||||||
|
onError: errorForeground,
|
||||||
outline: border,
|
outline: border,
|
||||||
outlineVariant: input,
|
outlineVariant: divider,
|
||||||
),
|
),
|
||||||
scaffoldBackgroundColor: background,
|
scaffoldBackgroundColor: background,
|
||||||
textTheme: TextTheme(
|
textTheme: TextTheme(
|
||||||
headlineLarge: headingH1,
|
displayLarge: headingH1,
|
||||||
headlineMedium: headingH2,
|
displayMedium: headingH2,
|
||||||
headlineSmall: headingH3,
|
displaySmall: headingH3,
|
||||||
titleLarge: headingH4,
|
headlineLarge: headingH3,
|
||||||
|
headlineMedium: headingH4,
|
||||||
|
headlineSmall: headingH5,
|
||||||
|
titleLarge: headingH6,
|
||||||
|
titleMedium: labelLarge,
|
||||||
|
titleSmall: labelMedium,
|
||||||
bodyLarge: bodyLarge,
|
bodyLarge: bodyLarge,
|
||||||
bodyMedium: bodyMedium,
|
bodyMedium: bodyMedium,
|
||||||
bodySmall: bodySmall,
|
bodySmall: bodySmall,
|
||||||
@@ -198,11 +347,16 @@ class ShadcnTheme {
|
|||||||
backgroundColor: background,
|
backgroundColor: background,
|
||||||
foregroundColor: foreground,
|
foregroundColor: foreground,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
scrolledUnderElevation: 1,
|
scrolledUnderElevation: 0,
|
||||||
shadowColor: Colors.black.withValues(alpha: 0.1),
|
shadowColor: Colors.transparent,
|
||||||
surfaceTintColor: Colors.transparent,
|
surfaceTintColor: Colors.transparent,
|
||||||
titleTextStyle: headingH4,
|
centerTitle: false,
|
||||||
iconTheme: const IconThemeData(color: foreground),
|
titleTextStyle: headingH5,
|
||||||
|
toolbarHeight: 64,
|
||||||
|
iconTheme: const IconThemeData(
|
||||||
|
color: foregroundSecondary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
cardTheme: CardThemeData(
|
cardTheme: CardThemeData(
|
||||||
color: card,
|
color: card,
|
||||||
@@ -211,86 +365,234 @@ class ShadcnTheme {
|
|||||||
borderRadius: BorderRadius.circular(radiusLg),
|
borderRadius: BorderRadius.circular(radiusLg),
|
||||||
side: const BorderSide(color: border, width: 1),
|
side: const BorderSide(color: border, width: 1),
|
||||||
),
|
),
|
||||||
shadowColor: Colors.black.withValues(alpha: 0.05),
|
shadowColor: Colors.transparent,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: primary,
|
backgroundColor: primary,
|
||||||
foregroundColor: primaryForeground,
|
foregroundColor: primaryForeground,
|
||||||
|
disabledBackgroundColor: backgroundSecondary,
|
||||||
|
disabledForegroundColor: foregroundMuted,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shadowColor: Colors.transparent,
|
shadowColor: Colors.transparent,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(radiusMd),
|
borderRadius: BorderRadius.circular(radiusMd),
|
||||||
),
|
),
|
||||||
|
minimumSize: const Size(64, 40),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: spacing4,
|
horizontal: spacing6,
|
||||||
vertical: spacing2,
|
vertical: spacing2,
|
||||||
),
|
),
|
||||||
textStyle: labelMedium,
|
textStyle: labelMedium.copyWith(fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: foreground,
|
foregroundColor: foreground,
|
||||||
side: const BorderSide(color: border),
|
disabledForegroundColor: foregroundMuted,
|
||||||
|
side: const BorderSide(color: border, width: 1),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shadowColor: Colors.transparent,
|
shadowColor: Colors.transparent,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(radiusMd),
|
borderRadius: BorderRadius.circular(radiusMd),
|
||||||
),
|
),
|
||||||
|
minimumSize: const Size(64, 40),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: spacing4,
|
horizontal: spacing6,
|
||||||
vertical: spacing2,
|
vertical: spacing2,
|
||||||
),
|
),
|
||||||
textStyle: labelMedium,
|
textStyle: labelMedium.copyWith(fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
textButtonTheme: TextButtonThemeData(
|
textButtonTheme: TextButtonThemeData(
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: foreground,
|
foregroundColor: primary,
|
||||||
|
disabledForegroundColor: foregroundMuted,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shadowColor: Colors.transparent,
|
shadowColor: Colors.transparent,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(radiusMd),
|
borderRadius: BorderRadius.circular(radiusMd),
|
||||||
),
|
),
|
||||||
|
minimumSize: const Size(64, 40),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: spacing4,
|
horizontal: spacing4,
|
||||||
vertical: spacing2,
|
vertical: spacing2,
|
||||||
),
|
),
|
||||||
textStyle: labelMedium,
|
textStyle: labelMedium.copyWith(fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
inputDecorationTheme: InputDecorationTheme(
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: background,
|
fillColor: input,
|
||||||
|
hoverColor: inputHover,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
horizontal: spacing3,
|
horizontal: spacing3,
|
||||||
vertical: spacing2,
|
vertical: spacing3,
|
||||||
),
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(radiusMd),
|
borderRadius: BorderRadius.circular(radiusMd),
|
||||||
borderSide: const BorderSide(color: input),
|
borderSide: const BorderSide(color: inputBorder, width: 1),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(radiusMd),
|
borderRadius: BorderRadius.circular(radiusMd),
|
||||||
borderSide: const BorderSide(color: input),
|
borderSide: const BorderSide(color: inputBorder, width: 1),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(radiusMd),
|
borderRadius: BorderRadius.circular(radiusMd),
|
||||||
borderSide: const BorderSide(color: ring, width: 2),
|
borderSide: const BorderSide(color: inputFocus, width: 2),
|
||||||
),
|
),
|
||||||
errorBorder: OutlineInputBorder(
|
errorBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(radiusMd),
|
borderRadius: BorderRadius.circular(radiusMd),
|
||||||
borderSide: const BorderSide(color: destructive),
|
borderSide: const BorderSide(color: error, width: 1),
|
||||||
),
|
),
|
||||||
focusedErrorBorder: OutlineInputBorder(
|
focusedErrorBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(radiusMd),
|
borderRadius: BorderRadius.circular(radiusMd),
|
||||||
borderSide: const BorderSide(color: destructive, width: 2),
|
borderSide: const BorderSide(color: error, width: 2),
|
||||||
),
|
),
|
||||||
hintStyle: bodyMedium.copyWith(color: mutedForeground),
|
disabledBorder: OutlineInputBorder(
|
||||||
labelStyle: labelMedium,
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user