전역 구조 리팩터링 및 테스트 확장
This commit is contained in:
176
lib/widgets/components/form_field.dart
Normal file
176
lib/widgets/components/form_field.dart
Normal file
@@ -0,0 +1,176 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
const double _kFieldSpacing = 8;
|
||||
const double _kFieldCaptionSpacing = 6;
|
||||
|
||||
/// 폼 필드 라벨과 본문을 일관되게 배치하기 위한 위젯.
|
||||
class SuperportFormField extends StatelessWidget {
|
||||
const SuperportFormField({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.child,
|
||||
this.required = false,
|
||||
this.caption,
|
||||
this.errorText,
|
||||
this.trailing,
|
||||
this.spacing = _kFieldSpacing,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final Widget child;
|
||||
final bool required;
|
||||
final String? caption;
|
||||
final String? errorText;
|
||||
final Widget? trailing;
|
||||
final double spacing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
final captionStyle = theme.textTheme.muted.copyWith(fontSize: 12);
|
||||
final errorStyle = theme.textTheme.small.copyWith(
|
||||
fontSize: 12,
|
||||
color: theme.colorScheme.destructive,
|
||||
);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _FieldLabel(label: label, required: required),
|
||||
),
|
||||
if (trailing != null) trailing!,
|
||||
],
|
||||
),
|
||||
SizedBox(height: spacing),
|
||||
child,
|
||||
if (errorText != null && errorText!.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: _kFieldCaptionSpacing),
|
||||
child: Text(errorText!, style: errorStyle),
|
||||
)
|
||||
else if (caption != null && caption!.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: _kFieldCaptionSpacing),
|
||||
child: Text(caption!, style: captionStyle),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `ShadInput`을 Superport 스타일에 맞게 설정한 텍스트 필드.
|
||||
class SuperportTextInput extends StatelessWidget {
|
||||
const SuperportTextInput({
|
||||
super.key,
|
||||
this.controller,
|
||||
this.placeholder,
|
||||
this.onChanged,
|
||||
this.onSubmitted,
|
||||
this.keyboardType,
|
||||
this.enabled = true,
|
||||
this.readOnly = false,
|
||||
this.maxLines = 1,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
});
|
||||
|
||||
final TextEditingController? controller;
|
||||
final Widget? placeholder;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final ValueChanged<String>? onSubmitted;
|
||||
final TextInputType? keyboardType;
|
||||
final bool enabled;
|
||||
final bool readOnly;
|
||||
final int maxLines;
|
||||
final Widget? leading;
|
||||
final Widget? trailing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ShadInput(
|
||||
controller: controller,
|
||||
placeholder: placeholder,
|
||||
enabled: enabled,
|
||||
readOnly: readOnly,
|
||||
keyboardType: keyboardType,
|
||||
maxLines: maxLines,
|
||||
leading: leading,
|
||||
trailing: trailing,
|
||||
onChanged: onChanged,
|
||||
onSubmitted: onSubmitted,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `ShadSwitch`를 라벨과 함께 사용하기 위한 헬퍼.
|
||||
class SuperportSwitchField extends StatelessWidget {
|
||||
const SuperportSwitchField({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
this.label,
|
||||
this.caption,
|
||||
});
|
||||
|
||||
final bool value;
|
||||
final ValueChanged<bool> onChanged;
|
||||
final String? label;
|
||||
final String? caption;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (label != null) Text(label!, style: theme.textTheme.small),
|
||||
const SizedBox(height: 8),
|
||||
ShadSwitch(value: value, onChanged: onChanged),
|
||||
if (caption != null && caption!.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: _kFieldCaptionSpacing),
|
||||
child: Text(caption!, style: theme.textTheme.muted),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FieldLabel extends StatelessWidget {
|
||||
const _FieldLabel({required this.label, required this.required});
|
||||
|
||||
final String label;
|
||||
final bool required;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
final textStyle = theme.textTheme.small.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(label, style: textStyle),
|
||||
if (required)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Text(
|
||||
'*',
|
||||
style: theme.textTheme.small.copyWith(
|
||||
color: theme.colorScheme.destructive,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user