Files
superport/lib/screens/common/components/shadcn_components.dart
JiWoong Sul 1e6da44917
Some checks failed
Flutter Test & Quality Check / Build APK (push) Has been cancelled
Flutter Test & Quality Check / Test on macos-latest (push) Has been cancelled
Flutter Test & Quality Check / Test on ubuntu-latest (push) Has been cancelled
refactor: UI 화면 통합 및 불필요한 파일 정리
- 모든 *_redesign.dart 파일을 기본 화면 파일로 통합
- 백업용 컨트롤러 파일들 제거 (*_controller.backup.dart)
- 사용하지 않는 예제 및 테스트 파일 제거
- Clean Architecture 적용 후 남은 정리 작업 완료
- 테스트 코드 정리 및 구조 개선 준비

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-11 14:00:44 +09:00

871 lines
26 KiB
Dart

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