사용하지 않는 파일 정리 전 백업 (Phase 10 완료 후 상태)
This commit is contained in:
368
lib/widgets/shadcn/shad_date_picker.dart
Normal file
368
lib/widgets/shadcn/shad_date_picker.dart
Normal file
@@ -0,0 +1,368 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
class SuperportShadDatePicker extends StatefulWidget {
|
||||
final String label;
|
||||
final DateTime? value;
|
||||
final ValueChanged<DateTime?>? onChanged;
|
||||
final DateTime? firstDate;
|
||||
final DateTime? lastDate;
|
||||
final String dateFormat;
|
||||
final String? placeholder;
|
||||
final bool enabled;
|
||||
final bool required;
|
||||
final String? errorText;
|
||||
final String? helperText;
|
||||
final bool allowClear;
|
||||
final DatePickerMode mode;
|
||||
|
||||
const SuperportShadDatePicker({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.value,
|
||||
this.onChanged,
|
||||
this.firstDate,
|
||||
this.lastDate,
|
||||
this.dateFormat = 'yyyy-MM-dd',
|
||||
this.placeholder,
|
||||
this.enabled = true,
|
||||
this.required = false,
|
||||
this.errorText,
|
||||
this.helperText,
|
||||
this.allowClear = true,
|
||||
this.mode = DatePickerMode.day,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SuperportShadDatePicker> createState() => _SuperportShadDatePickerState();
|
||||
}
|
||||
|
||||
class _SuperportShadDatePickerState extends State<SuperportShadDatePicker> {
|
||||
late TextEditingController _controller;
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(
|
||||
text: widget.value != null
|
||||
? DateFormat(widget.dateFormat).format(widget.value!)
|
||||
: '',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(SuperportShadDatePicker oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.value != widget.value) {
|
||||
_controller.text = widget.value != null
|
||||
? DateFormat(widget.dateFormat).format(widget.value!)
|
||||
: '';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _selectDate() async {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: widget.value ?? DateTime.now(),
|
||||
firstDate: widget.firstDate ?? DateTime(1900),
|
||||
lastDate: widget.lastDate ?? DateTime(2100),
|
||||
initialDatePickerMode: widget.mode,
|
||||
locale: const Locale('ko', 'KR'),
|
||||
builder: (context, child) {
|
||||
return Theme(
|
||||
data: ThemeData.light().copyWith(
|
||||
primaryColor: theme.colorScheme.primary,
|
||||
colorScheme: ColorScheme.light(
|
||||
primary: theme.colorScheme.primary,
|
||||
onPrimary: theme.colorScheme.primaryForeground,
|
||||
surface: theme.colorScheme.card,
|
||||
onSurface: theme.colorScheme.cardForeground,
|
||||
),
|
||||
),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
widget.onChanged?.call(picked);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.label,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
if (widget.required)
|
||||
Text(
|
||||
' *',
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: theme.colorScheme.destructive,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
onTap: widget.enabled ? _selectDate : null,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: IgnorePointer(
|
||||
child: ShadInput(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
placeholder: Text(widget.placeholder ?? 'YYYY-MM-DD'),
|
||||
enabled: widget.enabled,
|
||||
readOnly: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.errorText != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.errorText!,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: theme.colorScheme.destructive,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.helperText != null && widget.errorText == null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.helperText!,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: theme.colorScheme.mutedForeground,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SuperportShadDateRangePicker extends StatefulWidget {
|
||||
final String label;
|
||||
final DateTimeRange? value;
|
||||
final ValueChanged<DateTimeRange?>? onChanged;
|
||||
final DateTime? firstDate;
|
||||
final DateTime? lastDate;
|
||||
final String dateFormat;
|
||||
final String? placeholder;
|
||||
final bool enabled;
|
||||
final bool required;
|
||||
final String? errorText;
|
||||
final String? helperText;
|
||||
final bool allowClear;
|
||||
|
||||
const SuperportShadDateRangePicker({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.value,
|
||||
this.onChanged,
|
||||
this.firstDate,
|
||||
this.lastDate,
|
||||
this.dateFormat = 'yyyy-MM-dd',
|
||||
this.placeholder,
|
||||
this.enabled = true,
|
||||
this.required = false,
|
||||
this.errorText,
|
||||
this.helperText,
|
||||
this.allowClear = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SuperportShadDateRangePicker> createState() => _SuperportShadDateRangePickerState();
|
||||
}
|
||||
|
||||
class _SuperportShadDateRangePickerState extends State<SuperportShadDateRangePicker> {
|
||||
late TextEditingController _controller;
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateControllerText();
|
||||
}
|
||||
|
||||
void _updateControllerText() {
|
||||
if (widget.value != null) {
|
||||
final startText = DateFormat(widget.dateFormat).format(widget.value!.start);
|
||||
final endText = DateFormat(widget.dateFormat).format(widget.value!.end);
|
||||
_controller = TextEditingController(text: '$startText ~ $endText');
|
||||
} else {
|
||||
_controller = TextEditingController();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(SuperportShadDateRangePicker oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.value != widget.value) {
|
||||
_updateControllerText();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _selectDateRange() async {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
final DateTimeRange? picked = await showDateRangePicker(
|
||||
context: context,
|
||||
initialDateRange: widget.value,
|
||||
firstDate: widget.firstDate ?? DateTime(1900),
|
||||
lastDate: widget.lastDate ?? DateTime(2100),
|
||||
locale: const Locale('ko', 'KR'),
|
||||
builder: (context, child) {
|
||||
return Theme(
|
||||
data: ThemeData.light().copyWith(
|
||||
primaryColor: theme.colorScheme.primary,
|
||||
colorScheme: ColorScheme.light(
|
||||
primary: theme.colorScheme.primary,
|
||||
onPrimary: theme.colorScheme.primaryForeground,
|
||||
surface: theme.colorScheme.card,
|
||||
onSurface: theme.colorScheme.cardForeground,
|
||||
),
|
||||
),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
widget.onChanged?.call(picked);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.label,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
if (widget.required)
|
||||
Text(
|
||||
' *',
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: theme.colorScheme.destructive,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
onTap: widget.enabled ? _selectDateRange : null,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: IgnorePointer(
|
||||
child: ShadInput(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
placeholder: Text(widget.placeholder ?? 'YYYY-MM-DD ~ YYYY-MM-DD'),
|
||||
enabled: widget.enabled,
|
||||
readOnly: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.errorText != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.errorText!,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: theme.colorScheme.destructive,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.helperText != null && widget.errorText == null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.helperText!,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: theme.colorScheme.mutedForeground,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class KoreanDateTimeFormatter {
|
||||
static String format(DateTime dateTime, {String pattern = 'yyyy년 MM월 dd일 (E)'}) {
|
||||
final formatter = DateFormat(pattern, 'ko_KR');
|
||||
return formatter.format(dateTime);
|
||||
}
|
||||
|
||||
static String formatRange(DateTimeRange range, {String pattern = 'yyyy.MM.dd'}) {
|
||||
final formatter = DateFormat(pattern, 'ko_KR');
|
||||
return '${formatter.format(range.start)} ~ ${formatter.format(range.end)}';
|
||||
}
|
||||
|
||||
static String formatRelative(DateTime dateTime) {
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(dateTime);
|
||||
|
||||
if (difference.inDays == 0) {
|
||||
if (difference.inHours == 0) {
|
||||
if (difference.inMinutes == 0) {
|
||||
return '방금 전';
|
||||
}
|
||||
return '${difference.inMinutes}분 전';
|
||||
}
|
||||
return '${difference.inHours}시간 전';
|
||||
} else if (difference.inDays == 1) {
|
||||
return '어제';
|
||||
} else if (difference.inDays == 2) {
|
||||
return '그저께';
|
||||
} else if (difference.inDays < 7) {
|
||||
return '${difference.inDays}일 전';
|
||||
} else if (difference.inDays < 30) {
|
||||
return '${(difference.inDays / 7).round()}주 전';
|
||||
} else if (difference.inDays < 365) {
|
||||
return '${(difference.inDays / 30).round()}개월 전';
|
||||
} else {
|
||||
return '${(difference.inDays / 365).round()}년 전';
|
||||
}
|
||||
}
|
||||
}
|
||||
398
lib/widgets/shadcn/shad_dialog.dart
Normal file
398
lib/widgets/shadcn/shad_dialog.dart
Normal file
@@ -0,0 +1,398 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
enum DialogType {
|
||||
info,
|
||||
warning,
|
||||
error,
|
||||
success,
|
||||
confirm,
|
||||
custom,
|
||||
}
|
||||
|
||||
class SuperportShadDialog extends StatelessWidget {
|
||||
final String title;
|
||||
final String? description;
|
||||
final Widget? content;
|
||||
final List<Widget>? actions;
|
||||
final DialogType type;
|
||||
final bool dismissible;
|
||||
final double? width;
|
||||
final double? maxHeight;
|
||||
final VoidCallback? onClose;
|
||||
final bool showCloseButton;
|
||||
final bool loading;
|
||||
final String? loadingMessage;
|
||||
|
||||
const SuperportShadDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.description,
|
||||
this.content,
|
||||
this.actions,
|
||||
this.type = DialogType.custom,
|
||||
this.dismissible = true,
|
||||
this.width,
|
||||
this.maxHeight,
|
||||
this.onClose,
|
||||
this.showCloseButton = true,
|
||||
this.loading = false,
|
||||
this.loadingMessage,
|
||||
});
|
||||
|
||||
static Future<T?> show<T>({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
String? description,
|
||||
Widget? content,
|
||||
List<Widget>? actions,
|
||||
DialogType type = DialogType.custom,
|
||||
bool dismissible = true,
|
||||
double? width,
|
||||
double? maxHeight,
|
||||
bool showCloseButton = true,
|
||||
}) {
|
||||
return showDialog<T>(
|
||||
context: context,
|
||||
barrierDismissible: dismissible,
|
||||
builder: (context) => SuperportShadDialog(
|
||||
title: title,
|
||||
description: description,
|
||||
content: content,
|
||||
actions: actions,
|
||||
type: type,
|
||||
dismissible: dismissible,
|
||||
width: width,
|
||||
maxHeight: maxHeight,
|
||||
showCloseButton: showCloseButton,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<bool?> confirm({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required String message,
|
||||
String confirmText = '확인',
|
||||
String cancelText = '취소',
|
||||
DialogType type = DialogType.confirm,
|
||||
}) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => SuperportShadDialog(
|
||||
title: title,
|
||||
description: message,
|
||||
type: type,
|
||||
dismissible: false,
|
||||
showCloseButton: false,
|
||||
actions: [
|
||||
ShadButton.outline(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(cancelText),
|
||||
),
|
||||
ShadButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text(confirmText),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> alert({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required String message,
|
||||
String confirmText = '확인',
|
||||
DialogType type = DialogType.info,
|
||||
}) {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => SuperportShadDialog(
|
||||
title: title,
|
||||
description: message,
|
||||
type: type,
|
||||
dismissible: false,
|
||||
showCloseButton: false,
|
||||
actions: [
|
||||
ShadButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(confirmText),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<T?> form<T>({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required Widget form,
|
||||
required Future<T?> Function() onSubmit,
|
||||
String submitText = '저장',
|
||||
String cancelText = '취소',
|
||||
double? width,
|
||||
double? maxHeight,
|
||||
}) async {
|
||||
bool isLoading = false;
|
||||
String? errorMessage;
|
||||
|
||||
return await showDialog<T>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return SuperportShadDialog(
|
||||
title: title,
|
||||
width: width ?? 500,
|
||||
maxHeight: maxHeight,
|
||||
dismissible: false,
|
||||
showCloseButton: !isLoading,
|
||||
loading: isLoading,
|
||||
loadingMessage: '처리 중...',
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (errorMessage != null) ...[
|
||||
ShadAlert.destructive(
|
||||
title: const Text('오류'),
|
||||
description: Text(errorMessage!),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
form,
|
||||
],
|
||||
),
|
||||
actions: isLoading
|
||||
? null
|
||||
: [
|
||||
ShadButton.outline(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: Text(cancelText),
|
||||
),
|
||||
ShadButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
errorMessage = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final result = await onSubmit();
|
||||
if (dialogContext.mounted) {
|
||||
Navigator.of(dialogContext).pop(result);
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
errorMessage = e.toString();
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Text(submitText),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getIconForType() {
|
||||
switch (type) {
|
||||
case DialogType.info:
|
||||
return Icons.info_outline;
|
||||
case DialogType.warning:
|
||||
return Icons.warning_amber_outlined;
|
||||
case DialogType.error:
|
||||
return Icons.error_outline;
|
||||
case DialogType.success:
|
||||
return Icons.check_circle_outline;
|
||||
case DialogType.confirm:
|
||||
return Icons.help_outline;
|
||||
case DialogType.custom:
|
||||
default:
|
||||
return Icons.message_outlined;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getColorForType(ShadColorScheme colorScheme) {
|
||||
switch (type) {
|
||||
case DialogType.info:
|
||||
return colorScheme.primary;
|
||||
case DialogType.warning:
|
||||
return const Color(0xFFFFC107);
|
||||
case DialogType.error:
|
||||
return colorScheme.destructive;
|
||||
case DialogType.success:
|
||||
return const Color(0xFF2E8B57);
|
||||
case DialogType.confirm:
|
||||
return colorScheme.primary;
|
||||
case DialogType.custom:
|
||||
default:
|
||||
return colorScheme.foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
final typeColor = _getColorForType(theme.colorScheme);
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Container(
|
||||
width: width ?? 480,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: width ?? 480,
|
||||
maxHeight: maxHeight ?? MediaQuery.of(context).size.height * 0.8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.background,
|
||||
borderRadius: theme.radius,
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.border,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: theme.colorScheme.border,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (type != DialogType.custom) ...[
|
||||
Icon(
|
||||
_getIconForType(),
|
||||
color: typeColor,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.h4.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (description != null && content == null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
description!,
|
||||
style: theme.textTheme.p.copyWith(
|
||||
color: theme.colorScheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (showCloseButton && !loading) ...[
|
||||
const SizedBox(width: 12),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
onPressed: onClose ?? () => Navigator.of(context).pop(),
|
||||
style: IconButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (loading) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
if (loadingMessage != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
loadingMessage!,
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
if (content != null) ...[
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: content!,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (actions != null && actions!.isNotEmpty) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: theme.colorScheme.border,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
for (int i = 0; i < actions!.length; i++) ...[
|
||||
if (i > 0) const SizedBox(width: 8),
|
||||
actions![i],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FormDialog extends StatefulWidget {
|
||||
final Widget child;
|
||||
final GlobalKey<FormState> formKey;
|
||||
|
||||
const FormDialog({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.formKey,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FormDialog> createState() => _FormDialogState();
|
||||
}
|
||||
|
||||
class _FormDialogState extends State<FormDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Form(
|
||||
key: widget.formKey,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
314
lib/widgets/shadcn/shad_select.dart
Normal file
314
lib/widgets/shadcn/shad_select.dart
Normal file
@@ -0,0 +1,314 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
class SuperportShadSelect<T> extends StatefulWidget {
|
||||
final String label;
|
||||
final String? placeholder;
|
||||
final List<T> items;
|
||||
final T? value;
|
||||
final String Function(T) itemLabel;
|
||||
final ValueChanged<T?>? onChanged;
|
||||
final bool enabled;
|
||||
final bool searchable;
|
||||
final String? errorText;
|
||||
final String? helperText;
|
||||
final Widget? prefixIcon;
|
||||
final VoidCallback? onClear;
|
||||
final Future<List<T>> Function(String)? onSearch;
|
||||
final bool required;
|
||||
|
||||
const SuperportShadSelect({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.items,
|
||||
required this.itemLabel,
|
||||
this.placeholder,
|
||||
this.value,
|
||||
this.onChanged,
|
||||
this.enabled = true,
|
||||
this.searchable = false,
|
||||
this.errorText,
|
||||
this.helperText,
|
||||
this.prefixIcon,
|
||||
this.onClear,
|
||||
this.onSearch,
|
||||
this.required = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SuperportShadSelect<T>> createState() => _SuperportShadSelectState<T>();
|
||||
}
|
||||
|
||||
class _SuperportShadSelectState<T> extends State<SuperportShadSelect<T>> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
List<T> _filteredItems = [];
|
||||
bool _isSearching = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_filteredItems = widget.items;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(SuperportShadSelect<T> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.items != widget.items) {
|
||||
_filteredItems = widget.items;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleSearch(String query) async {
|
||||
if (query.isEmpty) {
|
||||
setState(() {
|
||||
_filteredItems = widget.items;
|
||||
_isSearching = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isSearching = true;
|
||||
});
|
||||
|
||||
if (widget.onSearch != null) {
|
||||
final results = await widget.onSearch!(query);
|
||||
setState(() {
|
||||
_filteredItems = results;
|
||||
_isSearching = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_filteredItems = widget.items.where((item) {
|
||||
return widget.itemLabel(item)
|
||||
.toLowerCase()
|
||||
.contains(query.toLowerCase());
|
||||
}).toList();
|
||||
_isSearching = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.label,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
if (widget.required)
|
||||
Text(
|
||||
' *',
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: theme.colorScheme.destructive,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ShadSelect<T>(
|
||||
placeholder: widget.placeholder != null
|
||||
? Text(widget.placeholder!)
|
||||
: const Text('선택하세요'),
|
||||
selectedOptionBuilder: (context, value) {
|
||||
if (value == null) return const Text('선택하세요');
|
||||
return Text(widget.itemLabel(value));
|
||||
},
|
||||
enabled: widget.enabled,
|
||||
options: _filteredItems.map((item) {
|
||||
return ShadOption(
|
||||
value: item,
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.value == item)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 8),
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.itemLabel(item),
|
||||
style: theme.textTheme.small.copyWith(
|
||||
fontWeight: widget.value == item
|
||||
? FontWeight.w600
|
||||
: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: widget.onChanged,
|
||||
),
|
||||
if (widget.errorText != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.errorText!,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: theme.colorScheme.destructive,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.helperText != null && widget.errorText == null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.helperText!,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: theme.colorScheme.mutedForeground,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CascadeSelect<T, U> extends StatefulWidget {
|
||||
final String parentLabel;
|
||||
final String childLabel;
|
||||
final List<T> parentItems;
|
||||
final String Function(T) parentItemLabel;
|
||||
final Future<List<U>> Function(T) getChildItems;
|
||||
final String Function(U) childItemLabel;
|
||||
final T? parentValue;
|
||||
final U? childValue;
|
||||
final ValueChanged<T?>? onParentChanged;
|
||||
final ValueChanged<U?>? onChildChanged;
|
||||
final bool enabled;
|
||||
final bool searchable;
|
||||
final bool required;
|
||||
|
||||
const CascadeSelect({
|
||||
super.key,
|
||||
required this.parentLabel,
|
||||
required this.childLabel,
|
||||
required this.parentItems,
|
||||
required this.parentItemLabel,
|
||||
required this.getChildItems,
|
||||
required this.childItemLabel,
|
||||
this.parentValue,
|
||||
this.childValue,
|
||||
this.onParentChanged,
|
||||
this.onChildChanged,
|
||||
this.enabled = true,
|
||||
this.searchable = true,
|
||||
this.required = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CascadeSelect<T, U>> createState() => _CascadeSelectState<T, U>();
|
||||
}
|
||||
|
||||
class _CascadeSelectState<T, U> extends State<CascadeSelect<T, U>> {
|
||||
List<U> _childItems = [];
|
||||
bool _isLoadingChildren = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.parentValue != null) {
|
||||
_loadChildItems(widget.parentValue as T);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CascadeSelect<T, U> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.parentValue != widget.parentValue &&
|
||||
widget.parentValue != null) {
|
||||
_loadChildItems(widget.parentValue as T);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadChildItems(T parentValue) async {
|
||||
setState(() {
|
||||
_isLoadingChildren = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final items = await widget.getChildItems(parentValue);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_childItems = items;
|
||||
_isLoadingChildren = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_childItems = [];
|
||||
_isLoadingChildren = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
SuperportShadSelect<T>(
|
||||
label: widget.parentLabel,
|
||||
items: widget.parentItems,
|
||||
itemLabel: widget.parentItemLabel,
|
||||
value: widget.parentValue,
|
||||
onChanged: (value) {
|
||||
widget.onParentChanged?.call(value);
|
||||
widget.onChildChanged?.call(null);
|
||||
if (value != null) {
|
||||
_loadChildItems(value);
|
||||
} else {
|
||||
setState(() {
|
||||
_childItems = [];
|
||||
});
|
||||
}
|
||||
},
|
||||
enabled: widget.enabled,
|
||||
searchable: widget.searchable,
|
||||
required: widget.required,
|
||||
placeholder: '${widget.parentLabel}을(를) 선택하세요',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SuperportShadSelect<U>(
|
||||
label: widget.childLabel,
|
||||
items: _childItems,
|
||||
itemLabel: widget.childItemLabel,
|
||||
value: widget.childValue,
|
||||
onChanged: widget.onChildChanged,
|
||||
enabled: widget.enabled &&
|
||||
widget.parentValue != null &&
|
||||
!_isLoadingChildren,
|
||||
searchable: widget.searchable,
|
||||
required: widget.required,
|
||||
placeholder: _isLoadingChildren
|
||||
? '로딩 중...'
|
||||
: widget.parentValue == null
|
||||
? '먼저 ${widget.parentLabel}을(를) 선택하세요'
|
||||
: '${widget.childLabel}을(를) 선택하세요',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
386
lib/widgets/shadcn/shad_table.dart
Normal file
386
lib/widgets/shadcn/shad_table.dart
Normal file
@@ -0,0 +1,386 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
class SuperportShadTable<T> extends StatefulWidget {
|
||||
final List<T> data;
|
||||
final List<ShadTableColumn<T>> columns;
|
||||
final Function(T)? onRowTap;
|
||||
final Function(T)? onRowDoubleTap;
|
||||
final bool showCheckbox;
|
||||
final Set<T>? selectedRows;
|
||||
final Function(Set<T>)? onSelectionChanged;
|
||||
final bool sortable;
|
||||
final int rowsPerPage;
|
||||
final String? emptyMessage;
|
||||
final Widget? header;
|
||||
final Widget? footer;
|
||||
final bool loading;
|
||||
final bool striped;
|
||||
final bool hoverable;
|
||||
final ScrollController? scrollController;
|
||||
|
||||
const SuperportShadTable({
|
||||
super.key,
|
||||
required this.data,
|
||||
required this.columns,
|
||||
this.onRowTap,
|
||||
this.onRowDoubleTap,
|
||||
this.showCheckbox = false,
|
||||
this.selectedRows,
|
||||
this.onSelectionChanged,
|
||||
this.sortable = true,
|
||||
this.rowsPerPage = 10,
|
||||
this.emptyMessage,
|
||||
this.header,
|
||||
this.footer,
|
||||
this.loading = false,
|
||||
this.striped = true,
|
||||
this.hoverable = true,
|
||||
this.scrollController,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SuperportShadTable<T>> createState() => _SuperportShadTableState<T>();
|
||||
}
|
||||
|
||||
class _SuperportShadTableState<T> extends State<SuperportShadTable<T>> {
|
||||
late List<T> _sortedData;
|
||||
String? _sortColumn;
|
||||
bool _sortAscending = true;
|
||||
int _currentPage = 1;
|
||||
late Set<T> _selectedRows;
|
||||
final ScrollController _defaultScrollController = ScrollController();
|
||||
|
||||
ScrollController get _scrollController =>
|
||||
widget.scrollController ?? _defaultScrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_sortedData = List.from(widget.data);
|
||||
_selectedRows = widget.selectedRows ?? {};
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(SuperportShadTable<T> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.data != widget.data) {
|
||||
_sortedData = List.from(widget.data);
|
||||
_applySort();
|
||||
}
|
||||
if (oldWidget.selectedRows != widget.selectedRows) {
|
||||
_selectedRows = widget.selectedRows ?? {};
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_defaultScrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _sort(String columnKey) {
|
||||
if (!widget.sortable) return;
|
||||
|
||||
setState(() {
|
||||
if (_sortColumn == columnKey) {
|
||||
_sortAscending = !_sortAscending;
|
||||
} else {
|
||||
_sortColumn = columnKey;
|
||||
_sortAscending = true;
|
||||
}
|
||||
_applySort();
|
||||
});
|
||||
}
|
||||
|
||||
void _applySort() {
|
||||
if (_sortColumn == null) return;
|
||||
|
||||
final column = widget.columns.firstWhere(
|
||||
(col) => col.key == _sortColumn,
|
||||
orElse: () => widget.columns.first,
|
||||
);
|
||||
|
||||
if (column.sorter != null) {
|
||||
_sortedData.sort((a, b) {
|
||||
final result = column.sorter!(a, b);
|
||||
return _sortAscending ? result : -result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _toggleRowSelection(T row) {
|
||||
setState(() {
|
||||
if (_selectedRows.contains(row)) {
|
||||
_selectedRows.remove(row);
|
||||
} else {
|
||||
_selectedRows.add(row);
|
||||
}
|
||||
widget.onSelectionChanged?.call(_selectedRows);
|
||||
});
|
||||
}
|
||||
|
||||
void _toggleAllSelection() {
|
||||
setState(() {
|
||||
if (_selectedRows.length == _pageData.length) {
|
||||
_selectedRows.clear();
|
||||
} else {
|
||||
_selectedRows = Set.from(_pageData);
|
||||
}
|
||||
widget.onSelectionChanged?.call(_selectedRows);
|
||||
});
|
||||
}
|
||||
|
||||
List<T> get _pageData {
|
||||
final startIndex = (_currentPage - 1) * widget.rowsPerPage;
|
||||
final endIndex = startIndex + widget.rowsPerPage;
|
||||
return _sortedData.sublist(
|
||||
startIndex,
|
||||
endIndex.clamp(0, _sortedData.length),
|
||||
);
|
||||
}
|
||||
|
||||
int get _totalPages {
|
||||
return (_sortedData.length / widget.rowsPerPage).ceil();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
if (widget.loading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (_sortedData.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inbox_outlined,
|
||||
size: 64,
|
||||
color: theme.colorScheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
widget.emptyMessage ?? '데이터가 없습니다',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (widget.header != null) ...[
|
||||
widget.header!,
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
ShadCard(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DataTable(
|
||||
columns: [
|
||||
if (widget.showCheckbox)
|
||||
DataColumn(
|
||||
label: Checkbox(
|
||||
value: _selectedRows.length == _pageData.length &&
|
||||
_pageData.isNotEmpty,
|
||||
onChanged: (_) => _toggleAllSelection(),
|
||||
),
|
||||
),
|
||||
...widget.columns.map((column) {
|
||||
return DataColumn(
|
||||
label: InkWell(
|
||||
onTap: column.sorter != null && widget.sortable
|
||||
? () => _sort(column.key)
|
||||
: null,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
column.label,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (column.sorter != null && widget.sortable) ...[
|
||||
const SizedBox(width: 4),
|
||||
if (_sortColumn == column.key)
|
||||
Icon(
|
||||
_sortAscending
|
||||
? Icons.arrow_upward
|
||||
: Icons.arrow_downward,
|
||||
size: 16,
|
||||
)
|
||||
else
|
||||
const Icon(
|
||||
Icons.unfold_more,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
rows: _pageData.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final row = entry.value;
|
||||
final isSelected = _selectedRows.contains(row);
|
||||
final isStriped = widget.striped && index.isOdd;
|
||||
|
||||
return DataRow(
|
||||
selected: isSelected,
|
||||
color: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return theme.colorScheme.accent.withValues(alpha: 0.1);
|
||||
}
|
||||
if (widget.hoverable &&
|
||||
states.contains(WidgetState.hovered)) {
|
||||
return theme.colorScheme.muted.withValues(alpha: 0.5);
|
||||
}
|
||||
if (isStriped) {
|
||||
return theme.colorScheme.muted.withValues(alpha: 0.3);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
onSelectChanged: widget.showCheckbox
|
||||
? (_) => _toggleRowSelection(row)
|
||||
: null,
|
||||
cells: [
|
||||
if (widget.showCheckbox)
|
||||
DataCell(
|
||||
Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (_) => _toggleRowSelection(row),
|
||||
),
|
||||
),
|
||||
...widget.columns.map((column) {
|
||||
final cellValue = column.accessor(row);
|
||||
Widget cellWidget;
|
||||
|
||||
if (column.render != null) {
|
||||
cellWidget = column.render!(row, cellValue);
|
||||
} else if (cellValue is DateTime) {
|
||||
cellWidget = Text(
|
||||
DateFormat('yyyy-MM-dd HH:mm').format(cellValue),
|
||||
style: theme.textTheme.small,
|
||||
);
|
||||
} else if (cellValue is num) {
|
||||
cellWidget = Text(
|
||||
NumberFormat('#,###').format(cellValue),
|
||||
style: theme.textTheme.small,
|
||||
);
|
||||
} else {
|
||||
cellWidget = Text(
|
||||
cellValue?.toString() ?? '-',
|
||||
style: theme.textTheme.small,
|
||||
);
|
||||
}
|
||||
|
||||
return DataCell(
|
||||
cellWidget,
|
||||
onTap: widget.onRowTap != null
|
||||
? () => widget.onRowTap!(row)
|
||||
: null,
|
||||
onDoubleTap: widget.onRowDoubleTap != null
|
||||
? () => widget.onRowDoubleTap!(row)
|
||||
: null,
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
if (_totalPages > 1) ...[
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'총 ${_sortedData.length}개 중 '
|
||||
'${((_currentPage - 1) * widget.rowsPerPage) + 1}-'
|
||||
'${((_currentPage - 1) * widget.rowsPerPage) + _pageData.length}',
|
||||
style: theme.textTheme.muted,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
enabled: _currentPage > 1,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_currentPage--;
|
||||
});
|
||||
},
|
||||
child: const Icon(Icons.chevron_left, size: 16),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'$_currentPage / $_totalPages',
|
||||
style: theme.textTheme.small,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
enabled: _currentPage < _totalPages,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_currentPage++;
|
||||
});
|
||||
},
|
||||
child: const Icon(Icons.chevron_right, size: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.footer != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
widget.footer!,
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShadTableColumn<T> {
|
||||
final String key;
|
||||
final String label;
|
||||
final dynamic Function(T) accessor;
|
||||
final Widget Function(T, dynamic)? render;
|
||||
final int Function(T, T)? sorter;
|
||||
final double? width;
|
||||
final bool sortable;
|
||||
|
||||
const ShadTableColumn({
|
||||
required this.key,
|
||||
required this.label,
|
||||
required this.accessor,
|
||||
this.render,
|
||||
this.sorter,
|
||||
this.width,
|
||||
this.sortable = true,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user