// Form input widgets import 'package:flutter/material.dart'; import 'package:inou_app/design/inou_theme.dart'; import 'package:inou_app/design/inou_text.dart'; /// Text input field with validation support class InouTextField extends StatelessWidget { final String? label; final String? placeholder; final TextEditingController? controller; final bool obscureText; final TextInputType? keyboardType; final int? maxLength; final int? maxLines; final bool isCode; final ValueChanged? onChanged; final FormFieldValidator? validator; final Widget? suffixIcon; final Iterable? autofillHints; final bool enabled; const InouTextField({ super.key, this.label, this.placeholder, this.controller, this.obscureText = false, this.keyboardType, this.maxLength, this.maxLines = 1, this.isCode = false, this.onChanged, this.validator, this.suffixIcon, this.autofillHints, this.enabled = true, }); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (label != null) ...[ Text( label!, style: InouText.label, ), const SizedBox(height: 4), ], TextFormField( controller: controller, obscureText: obscureText, keyboardType: keyboardType, maxLength: maxLength, maxLines: maxLines, enabled: enabled, textAlign: isCode ? TextAlign.center : TextAlign.start, onChanged: onChanged, validator: validator, autofillHints: autofillHints, style: isCode ? const TextStyle( fontSize: 22, fontWeight: FontWeight.w500, letterSpacing: 8, fontFamily: 'SF Mono', ) : InouText.body, decoration: InputDecoration( hintText: placeholder, counterText: '', suffixIcon: suffixIcon, filled: true, fillColor: InouTheme.bgCard, border: OutlineInputBorder( borderRadius: InouTheme.borderRadiusMd, borderSide: BorderSide(color: InouTheme.border), ), enabledBorder: OutlineInputBorder( borderRadius: InouTheme.borderRadiusMd, borderSide: BorderSide(color: InouTheme.border), ), focusedBorder: OutlineInputBorder( borderRadius: InouTheme.borderRadiusMd, borderSide: BorderSide(color: InouTheme.accent, width: 1), // 1px per styleguide ), errorBorder: OutlineInputBorder( borderRadius: InouTheme.borderRadiusMd, borderSide: BorderSide(color: InouTheme.danger), ), focusedErrorBorder: OutlineInputBorder( borderRadius: InouTheme.borderRadiusMd, borderSide: BorderSide(color: InouTheme.danger, width: 1), // 1px per styleguide ), contentPadding: EdgeInsets.symmetric( horizontal: 12, vertical: maxLines != null && maxLines! > 1 ? 12 : 14, ), ), ), ], ); } } /// Dropdown select class InouSelect extends StatelessWidget { final String? label; final T? value; final List> options; final ValueChanged? onChanged; const InouSelect({ super.key, this.label, this.value, required this.options, this.onChanged, }); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (label != null) ...[ Text(label!, style: InouText.label), const SizedBox(height: 4), ], Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: InouTheme.bgCard, border: Border.all(color: InouTheme.border), borderRadius: InouTheme.borderRadiusMd, ), child: DropdownButtonHideUnderline( child: DropdownButton( value: value, isExpanded: true, items: options .map((o) => DropdownMenuItem( value: o.value, child: Text(o.label), )) .toList(), onChanged: onChanged, ), ), ), ], ); } } class InouSelectOption { final T value; final String label; const InouSelectOption({required this.value, required this.label}); } /// Radio group class InouRadioGroup extends StatelessWidget { final String? label; final String? hint; final T? value; final List> options; final ValueChanged? onChanged; final Axis direction; const InouRadioGroup({ super.key, this.label, this.hint, this.value, required this.options, this.onChanged, this.direction = Axis.horizontal, }); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (label != null) ...[ Text(label!, style: InouText.label), if (hint != null) ...[ const SizedBox(height: 2), Text( hint!, style: InouText.bodySmall.copyWith(color: InouTheme.textMuted), ), ], const SizedBox(height: 8), ], direction == Axis.horizontal ? Row( children: _buildOptions(), ) : Column( crossAxisAlignment: CrossAxisAlignment.start, children: _buildOptions(), ), ], ); } List _buildOptions() { return options.map((option) { return Padding( padding: EdgeInsets.only( right: direction == Axis.horizontal ? 16 : 0, bottom: direction == Axis.vertical ? 8 : 0, ), child: InkWell( onTap: () => onChanged?.call(option.value), borderRadius: BorderRadius.circular(4), child: Row( mainAxisSize: MainAxisSize.min, children: [ Radio( value: option.value, groupValue: value, onChanged: onChanged, activeColor: InouTheme.accent, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), Text(option.label, style: InouText.body), ], ), ), ); }).toList(); } } class InouRadioOption { final T value; final String label; const InouRadioOption({required this.value, required this.label}); } /// Checkbox with optional custom child class InouCheckbox extends StatelessWidget { final bool value; final String? label; final Widget? child; final ValueChanged? onChanged; const InouCheckbox({ super.key, required this.value, this.label, this.child, this.onChanged, }) : assert(label != null || child != null, 'Provide either label or child'); @override Widget build(BuildContext context) { return InkWell( onTap: () => onChanged?.call(!value), borderRadius: BorderRadius.circular(4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 24, height: 24, child: Checkbox( value: value, onChanged: onChanged, activeColor: InouTheme.accent, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), const SizedBox(width: 8), Expanded( child: child ?? Text( label!, style: InouText.body.copyWith(color: InouTheme.textMuted), ), ), ], ), ); } }