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? onChanged; /// 제출(Enter) 시 호출되는 콜백. final ValueChanged? 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 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, ), ), ), ], ); } }