197 lines
5.6 KiB
Dart
197 lines
5.6 KiB
Dart
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;
|
|
/// 필수 여부. true면 라벨 옆에 `*` 표시를 추가한다.
|
|
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;
|
|
/// 제출(Enter) 시 호출되는 콜백.
|
|
final ValueChanged<String>? onSubmitted;
|
|
/// 키보드 타입. 숫자/이메일 등으로 지정 가능.
|
|
final TextInputType? keyboardType;
|
|
/// 입력 활성 여부.
|
|
final bool enabled;
|
|
/// 읽기 전용 여부. true면 수정 불가.
|
|
final bool readOnly;
|
|
/// 최대 줄 수. 1보다 크면 멀티라인 입력을 지원한다.
|
|
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,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|