From e25965a00627c93ae0b272893f4e849d04a9d3ad Mon Sep 17 00:00:00 2001 From: Adonis Date: Thu, 20 Aug 2026 04:41:34 +0100 Subject: [PATCH] feat(dievas): add copyWith and warning state to text input theme --- .../text_area/dievas_text_area.dart | 86 +++++++++++++------ .../text_input/dievas_text_input.dart | 86 +++++++++++++------ .../src/theme/color/sets/input_colours.dart | 17 +++- .../dievas_text_input_theme_data.dart | 52 +++++++++++ .../src/themes/dievas_dark_theme_data.dart | 1 + .../src/themes/dievas_light_theme_data.dart | 1 + .../src/themes/themes.component.mapper.dart | 1 + .../form/dievas_text_area_test.dart | 43 ++++++++++ .../form/dievas_text_input_test.dart | 43 ++++++++++ .../lib/components/form/dievas_text_area.dart | 11 +++ .../components/form/dievas_text_input.dart | 54 ++++++++++++ .../src/semantic/colour_semantic_dark.dart | 3 + .../src/semantic/colour_semantic_light.dart | 3 + 13 files changed, 344 insertions(+), 57 deletions(-) diff --git a/packages/dievas/lib/src/components/text_area/dievas_text_area.dart b/packages/dievas/lib/src/components/text_area/dievas_text_area.dart index 00e4021..5ee4875 100644 --- a/packages/dievas/lib/src/components/text_area/dievas_text_area.dart +++ b/packages/dievas/lib/src/components/text_area/dievas_text_area.dart @@ -31,6 +31,7 @@ class DievasTextArea extends StatelessWidget { this.hint, this.helperText, this.errorText, + this.warningText, this.minLines = 3, this.maxLines = 6, this.enabled = true, @@ -60,6 +61,11 @@ class DievasTextArea extends StatelessWidget { /// Error message. Overrides [helperText] and switches the border to error colour. final String? errorText; + /// Non-blocking validation feedback. Overrides [helperText] and switches the + /// border to the warning colour. Takes precedence over the normal state but + /// yields to [errorText] when both are set. + final String? warningText; + /// Minimum visible lines. Defaults to 3. final int minLines; @@ -80,12 +86,22 @@ class DievasTextArea extends StatelessWidget { @override Widget build(BuildContext context) { final theme = DievasTheme.componentsOf(context).textInput; - final hasError = errorText != null && errorText!.isNotEmpty; + final hasError = switch (errorText) { + final error? => error.isNotEmpty, + _ => false, + }; + final hasWarning = switch (warningText) { + final warning? when !hasError => warning.isNotEmpty, + _ => false, + }; // TextArea always uses md tokens - final borderColour = hasError ? theme.borderColourError : theme.borderColour; - final focusedBorderColour = hasError ? theme.borderColourError : theme.borderColourFocused; + final (borderColour, focusedBorderColour) = switch ((hasError, hasWarning)) { + (true, _) => (theme.borderColourError, theme.borderColourError), + (false, true) => (theme.borderColourWarning, theme.borderColourWarning), + _ => (theme.borderColour, theme.borderColourFocused), + }; final border = OutlineInputBorder( borderRadius: theme.borderRadius, @@ -105,9 +121,11 @@ class DievasTextArea extends StatelessWidget { final content = _DievasTextFieldContent( theme: theme, hasError: hasError, + hasWarning: hasWarning, label: label, helperText: helperText, errorText: errorText, + warningText: warningText, field: TextField( controller: controller, focusNode: focusNode, @@ -143,11 +161,10 @@ class DievasTextArea extends StatelessWidget { ), ); - if (!enabled) { - return Opacity(opacity: theme.disabledOpacity, child: content); - } - - return content; + return switch (enabled) { + true => content, + _ => Opacity(opacity: theme.disabledOpacity, child: content), + }; } } @@ -155,40 +172,53 @@ class _DievasTextFieldContent extends StatelessWidget { const _DievasTextFieldContent({ required this.theme, required this.hasError, + required this.hasWarning, required this.field, this.label, this.helperText, this.errorText, + this.warningText, }); final DievasTextInputThemeData theme; final bool hasError; + final bool hasWarning; final Widget field; final String? label; final String? helperText; final String? errorText; + final String? warningText; @override - Widget build(BuildContext context) => Column( - mainAxisSize: .min, - crossAxisAlignment: .start, - children: [ - if (label case final text?) ...[ - DefaultTextStyle(style: theme.labelStyle, child: Text(text)), - SizedBox(height: theme.labelSpacing), - ], - field, - if (errorText case final errorText? when hasError) ...[ - SizedBox(height: theme.helperSpacing), - DefaultTextStyle( - style: theme.errorStyle.copyWith(color: theme.borderColourError), - child: Text(errorText), - ), - ] else if (helperText case final helperText?) ...[ - SizedBox(height: theme.helperSpacing), - DefaultTextStyle(style: theme.helperStyle, child: Text(helperText)), + Widget build(BuildContext context) { + final feedback = switch ((errorText, hasError, warningText, hasWarning, helperText)) { + (final error?, true, _, _, _) => ( + message: error, + style: theme.errorStyle.copyWith(color: theme.borderColourError), + ), + (_, _, final warning?, true, _) => ( + message: warning, + style: theme.errorStyle.copyWith(color: theme.borderColourWarning), + ), + (_, _, _, _, final helper?) => (message: helper, style: theme.helperStyle), + _ => null, + }; + + return Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + if (label case final text?) ...[ + DefaultTextStyle(style: theme.labelStyle, child: Text(text)), + SizedBox(height: theme.labelSpacing), + ], + field, + if (feedback case (message: final message, style: final style)?) ...[ + SizedBox(height: theme.helperSpacing), + DefaultTextStyle(style: style, child: Text(message)), + ], ], - ], - ); + ); + } } diff --git a/packages/dievas/lib/src/components/text_input/dievas_text_input.dart b/packages/dievas/lib/src/components/text_input/dievas_text_input.dart index a9ebcff..f9f0015 100644 --- a/packages/dievas/lib/src/components/text_input/dievas_text_input.dart +++ b/packages/dievas/lib/src/components/text_input/dievas_text_input.dart @@ -42,6 +42,7 @@ class DievasTextInput extends StatefulWidget { this.hint, this.helperText, this.errorText, + this.warningText, this.leadingIcon, this.trailingIcon, this.size = DievasTextInputSize.md, @@ -74,6 +75,11 @@ class DievasTextInput extends StatefulWidget { /// error colour. final String? errorText; + /// Non-blocking validation feedback. Overrides [helperText] and switches the + /// border to the warning colour. Takes precedence over the normal state but + /// yields to [errorText] when both are set. + final String? warningText; + /// Leading widget inside the input container (icon, prefix, etc.). final Widget? leadingIcon; @@ -104,7 +110,14 @@ class _DievasTextInputState extends State { @override Widget build(BuildContext context) { final theme = DievasTheme.componentsOf(context).textInput; - final hasError = widget.errorText != null && widget.errorText!.isNotEmpty; + final hasError = switch (widget.errorText) { + final error? => error.isNotEmpty, + _ => false, + }; + final hasWarning = switch (widget.warningText) { + final warning? when !hasError => warning.isNotEmpty, + _ => false, + }; final height = switch (widget.size) { .sm => theme.height.sm, @@ -130,8 +143,11 @@ class _DievasTextInputState extends State { .lg => theme.placeholderStyle.lg, }; - final borderColour = hasError ? theme.borderColourError : theme.borderColour; - final focusedBorderColour = hasError ? theme.borderColourError : theme.borderColourFocused; + final (borderColour, focusedBorderColour) = switch ((hasError, hasWarning)) { + (true, _) => (theme.borderColourError, theme.borderColourError), + (false, true) => (theme.borderColourWarning, theme.borderColourWarning), + _ => (theme.borderColour, theme.borderColourFocused), + }; final border = OutlineInputBorder( borderRadius: theme.borderRadius, @@ -151,9 +167,11 @@ class _DievasTextInputState extends State { final content = _DievasTextInputFieldContent( theme: theme, hasError: hasError, + hasWarning: hasWarning, label: widget.label, helperText: widget.helperText, errorText: widget.errorText, + warningText: widget.warningText, field: SizedBox( height: height, child: TextField( @@ -221,11 +239,10 @@ class _DievasTextInputState extends State { ), ); - if (!widget.enabled) { - return Opacity(opacity: theme.disabledOpacity, child: content); - } - - return content; + return switch (widget.enabled) { + true => content, + _ => Opacity(opacity: theme.disabledOpacity, child: content), + }; } } @@ -233,40 +250,53 @@ class _DievasTextInputFieldContent extends StatelessWidget { const _DievasTextInputFieldContent({ required this.theme, required this.hasError, + required this.hasWarning, required this.field, this.label, this.helperText, this.errorText, + this.warningText, }); final DievasTextInputThemeData theme; final bool hasError; + final bool hasWarning; final Widget field; final String? label; final String? helperText; final String? errorText; + final String? warningText; @override - Widget build(BuildContext context) => Column( - mainAxisSize: .min, - crossAxisAlignment: .start, - children: [ - if (label case final labelText?) ...[ - DefaultTextStyle(style: theme.labelStyle, child: Text(labelText)), - SizedBox(height: theme.labelSpacing), - ], - field, - if (errorText case final errorText? when hasError) ...[ - SizedBox(height: theme.helperSpacing), - DefaultTextStyle( - style: theme.errorStyle.copyWith(color: theme.borderColourError), - child: Text(errorText), - ), - ] else if (helperText case final helperText?) ...[ - SizedBox(height: theme.helperSpacing), - DefaultTextStyle(style: theme.helperStyle, child: Text(helperText)), + Widget build(BuildContext context) { + final feedback = switch ((errorText, hasError, warningText, hasWarning, helperText)) { + (final error?, true, _, _, _) => ( + message: error, + style: theme.errorStyle.copyWith(color: theme.borderColourError), + ), + (_, _, final warning?, true, _) => ( + message: warning, + style: theme.errorStyle.copyWith(color: theme.borderColourWarning), + ), + (_, _, _, _, final helper?) => (message: helper, style: theme.helperStyle), + _ => null, + }; + + return Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + if (label case final labelText?) ...[ + DefaultTextStyle(style: theme.labelStyle, child: Text(labelText)), + SizedBox(height: theme.labelSpacing), + ], + field, + if (feedback case (message: final message, style: final style)?) ...[ + SizedBox(height: theme.helperSpacing), + DefaultTextStyle(style: style, child: Text(message)), + ], ], - ], - ); + ); + } } diff --git a/packages/dievas/lib/src/theme/color/sets/input_colours.dart b/packages/dievas/lib/src/theme/color/sets/input_colours.dart index 6ab41ff..1d7d286 100644 --- a/packages/dievas/lib/src/theme/color/sets/input_colours.dart +++ b/packages/dievas/lib/src/theme/color/sets/input_colours.dart @@ -8,6 +8,7 @@ final class InputColours extends Equatable { required this.inputBorder, required this.inputBorderFocus, required this.inputBorderError, + required this.inputBorderWarning, required this.inputText, required this.inputPlaceholder, }); @@ -24,6 +25,9 @@ final class InputColours extends Equatable { /// Input border in error state. final Color inputBorderError; + /// Input border in warning state. + final Color inputBorderWarning; + /// Input text colour (typed value). final Color inputText; @@ -35,6 +39,7 @@ final class InputColours extends Equatable { Color? inputBorder, Color? inputBorderFocus, Color? inputBorderError, + Color? inputBorderWarning, Color? inputText, Color? inputPlaceholder, }) => InputColours( @@ -42,6 +47,7 @@ final class InputColours extends Equatable { inputBorder: inputBorder ?? this.inputBorder, inputBorderFocus: inputBorderFocus ?? this.inputBorderFocus, inputBorderError: inputBorderError ?? this.inputBorderError, + inputBorderWarning: inputBorderWarning ?? this.inputBorderWarning, inputText: inputText ?? this.inputText, inputPlaceholder: inputPlaceholder ?? this.inputPlaceholder, ); @@ -51,10 +57,19 @@ final class InputColours extends Equatable { inputBorder: Color.lerp(a.inputBorder, b.inputBorder, t)!, inputBorderFocus: Color.lerp(a.inputBorderFocus, b.inputBorderFocus, t)!, inputBorderError: Color.lerp(a.inputBorderError, b.inputBorderError, t)!, + inputBorderWarning: Color.lerp(a.inputBorderWarning, b.inputBorderWarning, t)!, inputText: Color.lerp(a.inputText, b.inputText, t)!, inputPlaceholder: Color.lerp(a.inputPlaceholder, b.inputPlaceholder, t)!, ); @override - List get props => [inputBg, inputBorder, inputBorderFocus, inputBorderError, inputText, inputPlaceholder]; + List get props => [ + inputBg, + inputBorder, + inputBorderFocus, + inputBorderError, + inputBorderWarning, + inputText, + inputPlaceholder, + ]; } diff --git a/packages/dievas/lib/src/theme/component/text_input/dievas_text_input_theme_data.dart b/packages/dievas/lib/src/theme/component/text_input/dievas_text_input_theme_data.dart index 75f5fda..54c6fdb 100644 --- a/packages/dievas/lib/src/theme/component/text_input/dievas_text_input_theme_data.dart +++ b/packages/dievas/lib/src/theme/component/text_input/dievas_text_input_theme_data.dart @@ -24,6 +24,7 @@ final class DievasTextInputThemeData extends Equatable { required this.borderColour, required this.borderColourFocused, required this.borderColourError, + required this.borderColourWarning, required this.iconColour, required this.iconSize, required this.iconSpacing, @@ -74,6 +75,9 @@ final class DievasTextInputThemeData extends Equatable { /// Border colour in error state. final Color borderColourError; + /// Border colour in warning state. + final Color borderColourWarning; + /// Leading / trailing icon colour. final Color iconColour; @@ -92,6 +96,53 @@ final class DievasTextInputThemeData extends Equatable { /// Opacity multiplier applied when disabled. final double disabledOpacity; + /// Creates a copy of this [DievasTextInputThemeData] with the given fields replaced. + DievasTextInputThemeData copyWith({ + DievasInputThemeValue? inputStyle, + TextStyle? labelStyle, + TextStyle? helperStyle, + TextStyle? errorStyle, + DievasInputThemeValue? placeholderStyle, + DievasInputThemeValue? height, + DievasInputThemeValue? contentPadding, + BorderRadius? borderRadius, + double? strokeWidth, + double? strokeWidthFocused, + Color? bgColour, + Color? borderColour, + Color? borderColourFocused, + Color? borderColourError, + Color? borderColourWarning, + Color? iconColour, + double? iconSize, + double? iconSpacing, + double? labelSpacing, + double? helperSpacing, + double? disabledOpacity, + }) => DievasTextInputThemeData( + inputStyle: inputStyle ?? this.inputStyle, + labelStyle: labelStyle ?? this.labelStyle, + helperStyle: helperStyle ?? this.helperStyle, + errorStyle: errorStyle ?? this.errorStyle, + placeholderStyle: placeholderStyle ?? this.placeholderStyle, + height: height ?? this.height, + contentPadding: contentPadding ?? this.contentPadding, + borderRadius: borderRadius ?? this.borderRadius, + strokeWidth: strokeWidth ?? this.strokeWidth, + strokeWidthFocused: strokeWidthFocused ?? this.strokeWidthFocused, + bgColour: bgColour ?? this.bgColour, + borderColour: borderColour ?? this.borderColour, + borderColourFocused: borderColourFocused ?? this.borderColourFocused, + borderColourError: borderColourError ?? this.borderColourError, + borderColourWarning: borderColourWarning ?? this.borderColourWarning, + iconColour: iconColour ?? this.iconColour, + iconSize: iconSize ?? this.iconSize, + iconSpacing: iconSpacing ?? this.iconSpacing, + labelSpacing: labelSpacing ?? this.labelSpacing, + helperSpacing: helperSpacing ?? this.helperSpacing, + disabledOpacity: disabledOpacity ?? this.disabledOpacity, + ); + @override List get props => [ inputStyle, @@ -108,6 +159,7 @@ final class DievasTextInputThemeData extends Equatable { borderColour, borderColourFocused, borderColourError, + borderColourWarning, iconColour, iconSize, iconSpacing, diff --git a/packages/dievas/lib/src/themes/dievas_dark_theme_data.dart b/packages/dievas/lib/src/themes/dievas_dark_theme_data.dart index 13ee4c8..cedeb0e 100644 --- a/packages/dievas/lib/src/themes/dievas_dark_theme_data.dart +++ b/packages/dievas/lib/src/themes/dievas_dark_theme_data.dart @@ -84,6 +84,7 @@ final class DievasDarkThemeData extends DievasGlobalThemeData { inputBorder: Color(DievasColourSemanticDark.inputBorder), inputBorderFocus: Color(DievasColourSemanticDark.inputBorderFocus), inputBorderError: Color(DievasColourSemanticDark.inputBorderError), + inputBorderWarning: Color(DievasColourSemanticDark.inputBorderWarning), inputText: Color(DievasColourSemanticDark.inputText), inputPlaceholder: Color(DievasColourSemanticDark.inputPlaceholder), ), diff --git a/packages/dievas/lib/src/themes/dievas_light_theme_data.dart b/packages/dievas/lib/src/themes/dievas_light_theme_data.dart index b0561a0..6d5fcd6 100644 --- a/packages/dievas/lib/src/themes/dievas_light_theme_data.dart +++ b/packages/dievas/lib/src/themes/dievas_light_theme_data.dart @@ -84,6 +84,7 @@ final class DievasLightThemeData extends DievasGlobalThemeData { inputBorder: Color(DievasColourSemanticLight.inputBorder), inputBorderFocus: Color(DievasColourSemanticLight.inputBorderFocus), inputBorderError: Color(DievasColourSemanticLight.inputBorderError), + inputBorderWarning: Color(DievasColourSemanticLight.inputBorderWarning), inputText: Color(DievasColourSemanticLight.inputText), inputPlaceholder: Color(DievasColourSemanticLight.inputPlaceholder), ), diff --git a/packages/dievas/lib/src/themes/themes.component.mapper.dart b/packages/dievas/lib/src/themes/themes.component.mapper.dart index c75b3b6..d76370e 100644 --- a/packages/dievas/lib/src/themes/themes.component.mapper.dart +++ b/packages/dievas/lib/src/themes/themes.component.mapper.dart @@ -487,6 +487,7 @@ DievasTextInputThemeData _createTextInputTheme( borderColour: colours.input.inputBorder, borderColourFocused: colours.input.inputBorderFocus, borderColourError: colours.input.inputBorderError, + borderColourWarning: colours.input.inputBorderWarning, iconColour: colours.icon.iconSecondary, iconSize: DievasSizingPrimitives.s20, iconSpacing: spacing.sm, diff --git a/packages/dievas/test/components/form/dievas_text_area_test.dart b/packages/dievas/test/components/form/dievas_text_area_test.dart index 5d48d53..483f408 100644 --- a/packages/dievas/test/components/form/dievas_text_area_test.dart +++ b/packages/dievas/test/components/form/dievas_text_area_test.dart @@ -34,6 +34,49 @@ void main() { expect(find.text('Required'), findsOneWidget); }); + testWidgets('renders warning text', (tester) async { + await tester.pumpWidget(Harness(child: const DievasTextArea(warningText: 'Bio is very short'))); + expect(tester.takeException(), isNull); + expect(find.text('Bio is very short'), findsOneWidget); + }); + + testWidgets('warning takes precedence over helper text', (tester) async { + await tester.pumpWidget( + Harness( + child: const DievasTextArea(helperText: 'Optional', warningText: 'Bio is very short'), + ), + ); + expect(find.text('Bio is very short'), findsOneWidget); + expect(find.text('Optional'), findsNothing); + }); + + testWidgets('error takes precedence over warning', (tester) async { + await tester.pumpWidget( + Harness( + child: const DievasTextArea(errorText: 'Required', warningText: 'Bio is very short'), + ), + ); + expect(find.text('Required'), findsOneWidget); + expect(find.text('Bio is very short'), findsNothing); + }); + + testWidgets('uses warning border colour when warningText is set', (tester) async { + const warningColour = Color(0xFF00FF00); + await tester.pumpWidget( + Harness( + child: const DievasTextArea(warningText: 'Watch out'), + themeOverrides: (theme) => theme.copyWith( + components: theme.components.copyWith( + textInput: theme.components.textInput.copyWith(borderColourWarning: warningColour), + ), + ), + ), + ); + final decoration = tester.widget(find.byType(TextField)).decoration!; + final border = decoration.enabledBorder! as OutlineInputBorder; + expect(border.borderSide.color, warningColour); + }); + testWidgets('renders with custom min/max lines', (tester) async { await tester.pumpWidget(Harness(child: const DievasTextArea(minLines: 2, maxLines: 8))); expect(tester.takeException(), isNull); diff --git a/packages/dievas/test/components/form/dievas_text_input_test.dart b/packages/dievas/test/components/form/dievas_text_input_test.dart index 5509a26..cd24d54 100644 --- a/packages/dievas/test/components/form/dievas_text_input_test.dart +++ b/packages/dievas/test/components/form/dievas_text_input_test.dart @@ -41,6 +41,49 @@ void main() { expect(find.text('Invalid email'), findsOneWidget); }); + testWidgets('renders warning text', (tester) async { + await tester.pumpWidget(Harness(child: const DievasTextInput(warningText: 'Username is taken'))); + expect(tester.takeException(), isNull); + expect(find.text('Username is taken'), findsOneWidget); + }); + + testWidgets('warning takes precedence over helper text', (tester) async { + await tester.pumpWidget( + Harness( + child: const DievasTextInput(helperText: 'Optional', warningText: 'Username is taken'), + ), + ); + expect(find.text('Username is taken'), findsOneWidget); + expect(find.text('Optional'), findsNothing); + }); + + testWidgets('error takes precedence over warning', (tester) async { + await tester.pumpWidget( + Harness( + child: const DievasTextInput(errorText: 'Invalid email', warningText: 'Username is taken'), + ), + ); + expect(find.text('Invalid email'), findsOneWidget); + expect(find.text('Username is taken'), findsNothing); + }); + + testWidgets('uses warning border colour when warningText is set', (tester) async { + const warningColour = Color(0xFF00FF00); + await tester.pumpWidget( + Harness( + child: const DievasTextInput(warningText: 'Watch out'), + themeOverrides: (theme) => theme.copyWith( + components: theme.components.copyWith( + textInput: theme.components.textInput.copyWith(borderColourWarning: warningColour), + ), + ), + ), + ); + final decoration = tester.widget(find.byType(TextField)).decoration!; + final border = decoration.enabledBorder! as OutlineInputBorder; + expect(border.borderSide.color, warningColour); + }); + testWidgets('renders with leading icon', (tester) async { await tester.pumpWidget(Harness(child: const DievasTextInput(leadingIcon: Icon(Icons.email_outlined)))); expect(tester.takeException(), isNull); diff --git a/packages/dievas_gallery/lib/components/form/dievas_text_area.dart b/packages/dievas_gallery/lib/components/form/dievas_text_area.dart index 8a5340d..f1da5e9 100644 --- a/packages/dievas_gallery/lib/components/form/dievas_text_area.dart +++ b/packages/dievas_gallery/lib/components/form/dievas_text_area.dart @@ -19,6 +19,7 @@ class _Playground extends StatelessWidget { final hint = context.knobs.string(label: 'Hint', initialValue: 'Tell us about yourself...'); final helperText = context.knobs.string(label: 'Helper', initialValue: 'Max 300 characters.'); final errorText = context.knobs.string(label: 'Error', initialValue: ''); + final warningText = context.knobs.string(label: 'Warning', initialValue: ''); final minLines = context.knobs.double.slider(label: 'Min Lines', initialValue: 3, min: 1, max: 8).round(); final maxLines = context.knobs.double.slider(label: 'Max Lines', initialValue: 6, min: 2, max: 12).round(); @@ -33,6 +34,7 @@ class _Playground extends StatelessWidget { hint: hint.isEmpty ? null : hint, helperText: helperText.isEmpty ? null : helperText, errorText: errorText.isEmpty ? null : errorText, + warningText: warningText.isEmpty ? null : warningText, minLines: minLines.clamp(1, maxLines), maxLines: maxLines, enabled: !disabled, @@ -72,6 +74,15 @@ class _AllStates extends StatelessWidget { ), ), SizedBox(height: context.spacing.mdPlus), + _StateBlock( + 'Warning', + child: const DievasTextArea( + label: 'Bio', + hint: 'Tell us about yourself...', + warningText: 'Bio is very short — add a little more detail.', + ), + ), + SizedBox(height: context.spacing.mdPlus), _StateBlock( 'Disabled', child: const DievasTextArea(label: 'Notes', hint: 'No notes available.', enabled: false), diff --git a/packages/dievas_gallery/lib/components/form/dievas_text_input.dart b/packages/dievas_gallery/lib/components/form/dievas_text_input.dart index 6bbd49e..3e5fd26 100644 --- a/packages/dievas_gallery/lib/components/form/dievas_text_input.dart +++ b/packages/dievas_gallery/lib/components/form/dievas_text_input.dart @@ -10,6 +10,7 @@ final textInputComponent = WidgetbookComponent( WidgetbookUseCase(name: 'Playground', builder: (ctx) => _Playground()), WidgetbookUseCase(name: 'All Sizes', builder: (ctx) => _AllSizes()), WidgetbookUseCase(name: 'All States', builder: (ctx) => _AllStates()), + WidgetbookUseCase(name: 'Theme Override', builder: (ctx) => _ThemeOverride()), ], ); @@ -27,6 +28,7 @@ class _Playground extends StatelessWidget { final hint = context.knobs.string(label: 'Hint', initialValue: 'you@example.com'); final helperText = context.knobs.string(label: 'Helper', initialValue: "We'll never share your email."); final errorText = context.knobs.string(label: 'Error', initialValue: ''); + final warningText = context.knobs.string(label: 'Warning', initialValue: ''); final showLeadingIcon = context.knobs.boolean(label: 'Leading Icon'); final showTrailingIcon = context.knobs.boolean(label: 'Trailing Icon'); final disabled = context.knobs.boolean(label: 'Disabled'); @@ -41,6 +43,7 @@ class _Playground extends StatelessWidget { hint: hint.isEmpty ? null : hint, helperText: helperText.isEmpty ? null : helperText, errorText: errorText.isEmpty ? null : errorText, + warningText: warningText.isEmpty ? null : warningText, enabled: !disabled, leadingIcon: showLeadingIcon ? const Icon(Icons.email_outlined) : null, trailingIcon: showTrailingIcon ? const Icon(Icons.visibility_outlined) : null, @@ -109,6 +112,15 @@ class _AllStates extends StatelessWidget { ), ), SizedBox(height: context.spacing.mdPlus), + _StateBlock( + 'Warning', + child: DievasTextInput( + label: 'Email', + hint: 'you@serticodeinc.com', + warningText: 'This email is close to a registered one.', + ), + ), + SizedBox(height: context.spacing.mdPlus), _StateBlock( 'With Icons', child: DievasTextInput( @@ -128,6 +140,48 @@ class _AllStates extends StatelessWidget { ); } +class _ThemeOverride extends StatelessWidget { + @override + Widget build(BuildContext context) { + final base = DievasLightThemeData(); + final overridden = base.copyWith( + components: base.components.copyWith( + textInput: base.components.textInput.copyWith( + borderColourFocused: const Color(0xFF0D9488), + borderColourWarning: const Color(0xFFF59E0B), + ), + ), + ); + + return Center( + child: Padding( + padding: .symmetric(horizontal: context.spacing.xl, vertical: context.spacing.lg), + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Text( + 'Focus + warning colours overridden per instance via copyWith', + style: context.typography.labelXs.copyWith(color: context.colours.text.textTertiary), + ), + SizedBox(height: context.spacing.md), + ComponentBoundary( + child: DievasTheme( + data: overridden, + child: DievasTextInput( + label: 'Email', + hint: 'you@example.com', + warningText: 'Focus ring and warning border are custom colours.', + ), + ), + ), + ], + ), + ), + ); + } +} + class _StateBlock extends StatelessWidget { const _StateBlock(this.name, {required this.child}); final String name; diff --git a/packages/dievas_tokens/lib/src/semantic/colour_semantic_dark.dart b/packages/dievas_tokens/lib/src/semantic/colour_semantic_dark.dart index fa627b6..0fd6ec8 100644 --- a/packages/dievas_tokens/lib/src/semantic/colour_semantic_dark.dart +++ b/packages/dievas_tokens/lib/src/semantic/colour_semantic_dark.dart @@ -212,6 +212,9 @@ abstract final class DievasColourSemanticDark { /// Input border in error state. static const int inputBorderError = DievasColourPrimitives.red500; + /// Input border in warning state. + static const int inputBorderWarning = DievasColourPrimitives.amber500; + /// Input text colour (typed value). static const int inputText = DievasColourPrimitives.slate50; diff --git a/packages/dievas_tokens/lib/src/semantic/colour_semantic_light.dart b/packages/dievas_tokens/lib/src/semantic/colour_semantic_light.dart index a6798d9..359bd89 100644 --- a/packages/dievas_tokens/lib/src/semantic/colour_semantic_light.dart +++ b/packages/dievas_tokens/lib/src/semantic/colour_semantic_light.dart @@ -227,6 +227,9 @@ abstract final class DievasColourSemanticLight { /// Input border in error state. static const int inputBorderError = DievasColourPrimitives.red500; + /// Input border in warning state. + static const int inputBorderWarning = DievasColourPrimitives.amber500; + /// Input text colour (typed value). static const int inputText = DievasColourPrimitives.slate900;