From 6b5ca4e964e189ac7282e2af09a4165a1092f1e8 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 9 Sep 2026 16:52:07 +0200 Subject: [PATCH 1/4] feat(ui): add style and border options to StreamErrorBadge The call control button needs the badge in a warning treatment with no border: on a red call control button the error style disappears into the button, and over video the border loses its edge in either mode. Adds `StreamErrorBadgeStyle` (`.error`, the unchanged default, and `.warning`) and `showBorder`, defaulting to true. The border already painted with `strokeAlignOutside`, so dropping it leaves the badge's layout size untouched. The warning background resolves to `StreamColorScheme.accentWarning`, which is what the Figma `video/control/call-control-error-badge/bg` variable aliases. That token is being retuned from orange to yellow in design-system-tokens#73; the badge picks the new value up for free once the mirrored tokens land here. The icon is pinned to black rather than a mode-aware text color, because the warning background does not invert between light and dark. Also gives the badge the component theme it was missing, matching the sibling badges: `StreamErrorBadgeThemeData` carries a shared `size` and `border` plus an `errorStyle` and `warningStyle`, each a `StreamErrorBadgeThemeStyle` of `backgroundColor`/`foregroundColor`. Resolution merges the theme's partial override onto the defaults, so overriding one color leaves the other resolving normally. Both enums move into the theme file, where every themed component here keeps them; they are still exported from `core.dart`. Co-Authored-By: Claude Opus 5 --- .../components/badge/stream_error_badge.dart | 125 +++++++++- packages/stream_core_flutter/CHANGELOG.md | 20 ++ packages/stream_core_flutter/lib/core.dart | 1 + .../components/badge/stream_error_badge.dart | 136 ++++++++--- .../components/stream_error_badge_theme.dart | 201 ++++++++++++++++ .../stream_error_badge_theme.g.theme.dart | 199 ++++++++++++++++ .../lib/src/theme/stream_theme.dart | 9 + .../lib/src/theme/stream_theme.g.theme.dart | 9 + .../src/theme/stream_theme_extensions.dart | 4 + .../badge/stream_error_badge_golden_test.dart | 102 ++++++++ .../badge/stream_error_badge_test.dart | 220 ++++++++++++++++++ 11 files changed, 985 insertions(+), 41 deletions(-) create mode 100644 packages/stream_core_flutter/lib/src/theme/components/stream_error_badge_theme.dart create mode 100644 packages/stream_core_flutter/lib/src/theme/components/stream_error_badge_theme.g.theme.dart create mode 100644 packages/stream_core_flutter/test/components/badge/stream_error_badge_golden_test.dart create mode 100644 packages/stream_core_flutter/test/components/badge/stream_error_badge_test.dart diff --git a/apps/design_system_gallery/lib/components/badge/stream_error_badge.dart b/apps/design_system_gallery/lib/components/badge/stream_error_badge.dart index e4ea39fa..bf566b20 100644 --- a/apps/design_system_gallery/lib/components/badge/stream_error_badge.dart +++ b/apps/design_system_gallery/lib/components/badge/stream_error_badge.dart @@ -21,8 +21,22 @@ Widget buildStreamErrorBadgePlayground(BuildContext context) { description: 'The diameter of the badge.', ); + final style = context.knobs.object.dropdown( + label: 'Style', + options: StreamErrorBadgeStyle.values, + initialOption: StreamErrorBadgeStyle.error, + labelBuilder: (option) => option.name.toUpperCase(), + description: 'The severity the badge conveys.', + ); + + final showBorder = context.knobs.boolean( + label: 'Show Border', + initialValue: true, + description: 'Whether to show a border around the badge.', + ); + return Center( - child: StreamErrorBadge(size: size), + child: StreamErrorBadge(size: size, style: style, showBorder: showBorder), ); } @@ -44,10 +58,12 @@ Widget buildStreamErrorBadgeShowcase(BuildContext context) { style: textTheme.bodyDefault.copyWith(color: colorScheme.textPrimary), child: SingleChildScrollView( padding: EdgeInsets.all(spacing.lg), - child: const Column( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _SizeVariantsSection(), + const _SizeVariantsSection(), + SizedBox(height: spacing.xl), + const _StyleVariantsSection(), ], ), ), @@ -154,6 +170,109 @@ class _SizeDemo extends StatelessWidget { } } +// ============================================================================= +// Style Variants Section +// ============================================================================= + +class _StyleVariantsSection extends StatelessWidget { + const _StyleVariantsSection(); + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final boxShadow = context.streamBoxShadow; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'STYLE VARIANTS'), + SizedBox(height: spacing.md), + Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + padding: EdgeInsets.all(spacing.md), + decoration: BoxDecoration( + color: colorScheme.backgroundSurface, + borderRadius: BorderRadius.all(radius.lg), + boxShadow: boxShadow.elevation1, + ), + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.all(radius.lg), + border: Border.all(color: colorScheme.borderSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Error and warning, each with the border on and off', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + SizedBox(height: spacing.md), + Row( + children: [ + for (final style in StreamErrorBadgeStyle.values) + for (final showBorder in [true, false]) + Padding( + padding: EdgeInsetsDirectional.only(end: spacing.xl), + child: _StyleDemo(style: style, showBorder: showBorder), + ), + ], + ), + ], + ), + ), + ], + ); + } +} + +class _StyleDemo extends StatelessWidget { + const _StyleDemo({required this.style, required this.showBorder}); + + final StreamErrorBadgeStyle style; + final bool showBorder; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return Column( + children: [ + SizedBox( + width: 48, + height: 48, + child: Center( + child: StreamErrorBadge(style: style, showBorder: showBorder), + ), + ), + SizedBox(height: spacing.sm), + Text( + style.name.toUpperCase(), + style: textTheme.metadataEmphasis.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + Text( + showBorder ? 'border' : 'no border', + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, + fontFamily: 'monospace', + fontSize: 10, + ), + ), + ], + ); + } +} + // ============================================================================= // Shared Widgets // ============================================================================= diff --git a/packages/stream_core_flutter/CHANGELOG.md b/packages/stream_core_flutter/CHANGELOG.md index f0fcf3f7..cd77a817 100644 --- a/packages/stream_core_flutter/CHANGELOG.md +++ b/packages/stream_core_flutter/CHANGELOG.md @@ -3,6 +3,26 @@ ### ✨ Features - Added the `lowBandwidthFill` icon. +- Added `StreamErrorBadge.style`, taking a `StreamErrorBadgeStyle` — `.error` + (the default, unchanged) or `.warning`, a warning-colored background with a + black icon. The icon is pinned to black rather than to a mode-aware text + color, because the warning background does not invert between light and dark. +- Added `StreamErrorBadge.showBorder`, defaulting to true. Set it to false for + the borderless badge the call control button uses, where the border loses its + edge over video. +- Added `StreamErrorBadgeTheme` and `StreamErrorBadgeThemeData`, reachable as + `StreamTheme.errorBadgeTheme` and `BuildContext.streamErrorBadgeTheme`. It + carries a shared `size` and `border` plus an `errorStyle` and a + `warningStyle`, each a `StreamErrorBadgeThemeStyle` of `backgroundColor` and + `foregroundColor`, so the two styles can be themed independently. Use + `styleOf` to look up the entry for a given `StreamErrorBadgeStyle`. + +### 🔄 Changed + +- `StreamErrorBadgeSize` moved from `stream_error_badge.dart` to + `stream_error_badge_theme.dart`, alongside the new `StreamErrorBadgeStyle`. + Both are still exported from `core.dart`, so imports through the barrel are + unaffected. ## 0.5.1 diff --git a/packages/stream_core_flutter/lib/core.dart b/packages/stream_core_flutter/lib/core.dart index fa8e0dee..f47fb479 100644 --- a/packages/stream_core_flutter/lib/core.dart +++ b/packages/stream_core_flutter/lib/core.dart @@ -77,6 +77,7 @@ export 'src/theme/components/stream_context_menu_action_theme.dart'; export 'src/theme/components/stream_context_menu_theme.dart'; export 'src/theme/components/stream_emoji_button_theme.dart'; export 'src/theme/components/stream_emoji_chip_theme.dart'; +export 'src/theme/components/stream_error_badge_theme.dart'; export 'src/theme/components/stream_list_tile_theme.dart'; export 'src/theme/components/stream_media_viewer_theme.dart'; export 'src/theme/components/stream_online_indicator_theme.dart'; diff --git a/packages/stream_core_flutter/lib/src/components/badge/stream_error_badge.dart b/packages/stream_core_flutter/lib/src/components/badge/stream_error_badge.dart index b3088fc9..2294420f 100644 --- a/packages/stream_core_flutter/lib/src/components/badge/stream_error_badge.dart +++ b/packages/stream_core_flutter/lib/src/components/badge/stream_error_badge.dart @@ -1,37 +1,18 @@ import 'package:flutter/material.dart'; import '../../factory/stream_component_factory.dart'; +import '../../theme/components/stream_error_badge_theme.dart'; +import '../../theme/primitives/stream_colors.dart'; import '../../theme/primitives/stream_icons.dart'; import '../../theme/semantics/stream_color_scheme.dart'; import '../../theme/stream_theme_extensions.dart'; -/// Predefined sizes for [StreamErrorBadge]. -/// -/// Each size corresponds to a specific diameter and icon size in logical pixels. -enum StreamErrorBadgeSize { - /// Medium badge (24px diameter, 20px icon). - md(24, 20), - - /// Small badge (20px diameter, 16px icon). - sm(20, 16), - - /// Extra-small badge (16px diameter, 12px icon). - xs(16, 12); - - const StreamErrorBadgeSize(this.value, this.iconSize); - - /// The diameter of the badge in logical pixels. - final double value; - - /// The icon size for this badge size. - final double iconSize; -} - -/// A circular error badge that displays an exclamation mark icon. +/// A circular badge that displays an exclamation mark icon. /// /// [StreamErrorBadge] is used to indicate a failed operation, such as a /// message that could not be sent. It renders as a fixed-size circle with -/// an error-colored background and an exclamation mark icon. +/// a background colored by [StreamErrorBadgeStyle] and an exclamation mark +/// icon. /// /// {@tool snippet} /// @@ -51,9 +32,29 @@ enum StreamErrorBadgeSize { /// ``` /// {@end-tool} /// +/// {@tool snippet} +/// +/// Warning variant without a border, as a call control button uses it: +/// +/// ```dart +/// StreamErrorBadge( +/// style: StreamErrorBadgeStyle.warning, +/// showBorder: false, +/// ) +/// ``` +/// {@end-tool} +/// +/// ## Theming +/// +/// [StreamErrorBadge] uses [StreamErrorBadgeThemeData] for default styling. +/// Colors are determined by the current [StreamColorScheme]. +/// /// See also: /// /// * [StreamErrorBadgeSize], the available size variants. +/// * [StreamErrorBadgeStyle], the available style variants. +/// * [StreamErrorBadgeThemeData], for customizing appearance. +/// * [StreamErrorBadgeTheme], for overriding theme in a subtree. /// * [StreamRetryBadge], a badge for indicating retryable actions. /// * [StreamBadgeNotification], a badge for displaying notification counts. class StreamErrorBadge extends StatelessWidget { @@ -61,7 +62,9 @@ class StreamErrorBadge extends StatelessWidget { StreamErrorBadge({ super.key, StreamErrorBadgeSize? size, - }) : props = .new(size: size); + StreamErrorBadgeStyle? style, + bool showBorder = true, + }) : props = .new(size: size, style: style, showBorder: showBorder); /// The properties that configure this error badge. final StreamErrorBadgeProps props; @@ -85,18 +88,36 @@ class StreamErrorBadge extends StatelessWidget { /// * [DefaultStreamErrorBadge], the default implementation. class StreamErrorBadgeProps { /// Creates properties for an error badge. - const StreamErrorBadgeProps({this.size}); + const StreamErrorBadgeProps({ + this.size, + this.style, + this.showBorder = true, + }); /// The size of the badge. /// - /// If null, defaults to [StreamErrorBadgeSize.sm]. + /// If null, uses [StreamErrorBadgeThemeData.size], or falls back to + /// [StreamErrorBadgeSize.sm]. final StreamErrorBadgeSize? size; + + /// The severity the badge conveys. + /// + /// If null, defaults to [StreamErrorBadgeStyle.error]. + final StreamErrorBadgeStyle? style; + + /// Whether a border is drawn around the badge. + /// + /// The border is drawn outside the badge's [size], so it separates the + /// badge from whatever it overlaps without changing the badge's layout + /// size. Defaults to true. + final bool showBorder; } /// The default implementation of [StreamErrorBadge]. /// /// Renders a circular badge with an exclamation mark icon. Styling is -/// resolved from the current [StreamColorScheme] and [StreamIcons]. +/// resolved from [StreamErrorBadgeThemeData], falling back to the current +/// [StreamColorScheme] and [StreamIcons]. /// /// See also: /// @@ -112,27 +133,66 @@ class DefaultStreamErrorBadge extends StatelessWidget { @override Widget build(BuildContext context) { final icons = context.streamIcons; - final colorScheme = context.streamColorScheme; - final effectiveSize = props.size ?? StreamErrorBadgeSize.sm; + final theme = context.streamErrorBadgeTheme; + final defaults = _StreamErrorBadgeThemeDefaults(context); - final border = Border.all( - width: 2, - color: colorScheme.borderOnInverse, - strokeAlign: BorderSide.strokeAlignOutside, - ); + final effectiveSize = props.size ?? theme.size ?? defaults.size; + final effectiveStyle = props.style ?? StreamErrorBadgeStyle.error; + final effectiveBorder = props.showBorder ? theme.border ?? defaults.border : null; + + // Defaults first, theme overrides layered on top, so every color resolves. + final style = defaults.styleOf(effectiveStyle).merge(theme.styleOf(effectiveStyle)); return AnimatedContainer( width: effectiveSize.value, height: effectiveSize.value, clipBehavior: Clip.antiAlias, duration: kThemeChangeDuration, - decoration: BoxDecoration(shape: BoxShape.circle, color: colorScheme.accentError), - foregroundDecoration: BoxDecoration(shape: BoxShape.circle, border: border), + decoration: BoxDecoration(shape: BoxShape.circle, color: style.backgroundColor), + foregroundDecoration: BoxDecoration(shape: BoxShape.circle, border: effectiveBorder), child: IconTheme( - data: .new(size: effectiveSize.iconSize, color: colorScheme.textOnAccent), + data: .new(size: effectiveSize.iconSize, color: style.foregroundColor), child: Center(child: Icon(icons.exclamationMarkFill)), ), ); } } + +class _StreamErrorBadgeThemeDefaults extends StreamErrorBadgeThemeData { + _StreamErrorBadgeThemeDefaults(this._context); + + final BuildContext _context; + + late final _colorScheme = _context.streamColorScheme; + + @override + StreamErrorBadgeSize get size => .sm; + + @override + StreamErrorBadgeThemeStyle get errorStyle => .new( + backgroundColor: _colorScheme.accentError, + foregroundColor: _colorScheme.textOnAccent, + ); + + @override + StreamErrorBadgeThemeStyle get warningStyle => .new( + backgroundColor: _colorScheme.accentWarning, + foregroundColor: StreamColors.black, + ); + + @override + BoxBorder get border => Border.all( + width: 2, + color: _colorScheme.borderOnInverse, + strokeAlign: BorderSide.strokeAlignOutside, + ); + + // Narrowed to non-nullable: every style has a default, so callers can merge + // the theme's partial override straight onto the result. + @override + StreamErrorBadgeThemeStyle styleOf(StreamErrorBadgeStyle style) => switch (style) { + StreamErrorBadgeStyle.error => errorStyle, + StreamErrorBadgeStyle.warning => warningStyle, + }; +} diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_error_badge_theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_error_badge_theme.dart new file mode 100644 index 00000000..4567dabc --- /dev/null +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_error_badge_theme.dart @@ -0,0 +1,201 @@ +import 'package:flutter/widgets.dart'; +import 'package:theme_extensions_builder_annotation/theme_extensions_builder_annotation.dart'; + +import '../stream_theme.dart'; + +part 'stream_error_badge_theme.g.theme.dart'; + +/// Predefined sizes for [StreamErrorBadge]. +/// +/// Each size corresponds to a specific diameter and icon size in logical +/// pixels. +/// +/// See also: +/// +/// * [StreamErrorBadge], which uses these size variants. +/// * [StreamErrorBadgeThemeData.size], for setting a global default. +enum StreamErrorBadgeSize { + /// Medium badge (24px diameter, 20px icon). + md(24, 20), + + /// Small badge (20px diameter, 16px icon). + sm(20, 16), + + /// Extra-small badge (16px diameter, 12px icon). + xs(16, 12); + + const StreamErrorBadgeSize(this.value, this.iconSize); + + /// The diameter of the badge in logical pixels. + final double value; + + /// The icon size for this badge size. + final double iconSize; +} + +/// The severity a [StreamErrorBadge] conveys. +/// +/// Determines which background and icon color the badge applies. +/// +/// See also: +/// +/// * [StreamErrorBadge], which uses these style variants. +enum StreamErrorBadgeStyle { + /// Error style — an error-colored background with an on-accent icon. + /// + /// The default. Reads as a failure that needs the user's attention. + error, + + /// Warning style — a warning-colored background with a black icon. + /// + /// For a cautionary state rather than an outright failure, and for + /// surfaces where the error style would not separate from what sits + /// underneath — a call control button, or arbitrary video. + /// + /// The icon is pinned to black rather than to a mode-aware text color, + /// because the warning background does not invert between light and dark. + warning, +} + +/// Applies an error badge theme to descendant widgets. +/// +/// Wrap a subtree with [StreamErrorBadgeTheme] to override error badge +/// styling. Access the merged theme using [BuildContext.streamErrorBadgeTheme]. +/// +/// See also: +/// +/// * [StreamErrorBadgeThemeData], which describes the theme. +/// * [StreamErrorBadge], the widget affected by this theme. +class StreamErrorBadgeTheme extends InheritedTheme { + /// Creates an error badge theme. + const StreamErrorBadgeTheme({ + super.key, + required this.data, + required super.child, + }); + + /// The error badge theme data for descendant widgets. + final StreamErrorBadgeThemeData data; + + /// Returns the merged [StreamErrorBadgeThemeData] from local and global + /// themes. + static StreamErrorBadgeThemeData of(BuildContext context) { + final localTheme = context.dependOnInheritedWidgetOfExactType(); + return StreamTheme.of(context).errorBadgeTheme.merge(localTheme?.data); + } + + @override + Widget wrap(BuildContext context, Widget child) { + return StreamErrorBadgeTheme(data: data, child: child); + } + + @override + bool updateShouldNotify(StreamErrorBadgeTheme oldWidget) => data != oldWidget.data; +} + +/// Theme data for customizing [StreamErrorBadge] widgets. +/// +/// Organizes badge colors by [StreamErrorBadgeStyle], so the warning style can +/// be styled without touching the error one. +/// +/// {@tool snippet} +/// +/// Customize badge appearance globally via [StreamTheme]: +/// +/// ```dart +/// StreamTheme( +/// errorBadgeTheme: StreamErrorBadgeThemeData( +/// warningStyle: StreamErrorBadgeThemeStyle( +/// backgroundColor: Colors.amber, +/// foregroundColor: Colors.black, +/// ), +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamErrorBadge], the widget that uses this theme data. +/// * [StreamErrorBadgeTheme], for overriding theme in a widget subtree. +/// * [StreamErrorBadgeThemeStyle], the per-style properties this groups. +@themeGen +@immutable +class StreamErrorBadgeThemeData with _$StreamErrorBadgeThemeData { + /// Creates an error badge theme with optional style overrides per + /// [StreamErrorBadgeStyle]. + const StreamErrorBadgeThemeData({ + this.size, + this.errorStyle, + this.warningStyle, + this.border, + }); + + /// The default size for error badges. + /// + /// Falls back to [StreamErrorBadgeSize.sm]. + final StreamErrorBadgeSize? size; + + /// Styling for badges of the [StreamErrorBadgeStyle.error] style. + final StreamErrorBadgeThemeStyle? errorStyle; + + /// Styling for badges of the [StreamErrorBadgeStyle.warning] style. + final StreamErrorBadgeThemeStyle? warningStyle; + + /// The border drawn around the badge. + /// + /// Applied when [StreamErrorBadge.showBorder] is true. Allows customization + /// of both border color and width. Shared by both styles. + final BoxBorder? border; + + /// The styling for badges of the given [style]. + StreamErrorBadgeThemeStyle? styleOf(StreamErrorBadgeStyle style) => switch (style) { + StreamErrorBadgeStyle.error => errorStyle, + StreamErrorBadgeStyle.warning => warningStyle, + }; + + /// Linearly interpolate between two [StreamErrorBadgeThemeData]. + static StreamErrorBadgeThemeData? lerp( + StreamErrorBadgeThemeData? a, + StreamErrorBadgeThemeData? b, + double t, + ) => _$StreamErrorBadgeThemeData.lerp(a, b, t); +} + +/// Visual styling properties for a single [StreamErrorBadgeStyle]. +/// +/// See also: +/// +/// * [StreamErrorBadgeThemeData], which groups one of these per style. +/// * [StreamErrorBadge], which uses this styling. +@themeGen +@immutable +class StreamErrorBadgeThemeStyle with _$StreamErrorBadgeThemeStyle { + /// Creates error badge style properties. + const StreamErrorBadgeThemeStyle({ + this.backgroundColor, + this.foregroundColor, + }); + + /// The fill color of the badge circle. + /// + /// Defaults to [StreamColorScheme.accentError] on + /// [StreamErrorBadgeStyle.error] and [StreamColorScheme.accentWarning] on + /// [StreamErrorBadgeStyle.warning]. + final Color? backgroundColor; + + /// The color of the exclamation mark icon. + /// + /// Defaults to [StreamColorScheme.textOnAccent] on + /// [StreamErrorBadgeStyle.error]. On [StreamErrorBadgeStyle.warning] it + /// falls back to black rather than to a mode-aware text color, because the + /// warning background does not invert between light and dark. + final Color? foregroundColor; + + /// Linearly interpolate between two [StreamErrorBadgeThemeStyle]. + static StreamErrorBadgeThemeStyle? lerp( + StreamErrorBadgeThemeStyle? a, + StreamErrorBadgeThemeStyle? b, + double t, + ) => _$StreamErrorBadgeThemeStyle.lerp(a, b, t); +} diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_error_badge_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_error_badge_theme.g.theme.dart new file mode 100644 index 00000000..c709b0bd --- /dev/null +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_error_badge_theme.g.theme.dart @@ -0,0 +1,199 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_element + +part of 'stream_error_badge_theme.dart'; + +// ************************************************************************** +// ThemeGenGenerator +// ************************************************************************** + +mixin _$StreamErrorBadgeThemeData { + bool get canMerge => true; + + static StreamErrorBadgeThemeData? lerp( + StreamErrorBadgeThemeData? a, + StreamErrorBadgeThemeData? b, + double t, + ) { + if (identical(a, b)) { + return a; + } + + if (a == null) { + return t == 1.0 ? b : null; + } + + if (b == null) { + return t == 0.0 ? a : null; + } + + return StreamErrorBadgeThemeData( + size: t < 0.5 ? a.size : b.size, + errorStyle: StreamErrorBadgeThemeStyle.lerp( + a.errorStyle, + b.errorStyle, + t, + ), + warningStyle: StreamErrorBadgeThemeStyle.lerp( + a.warningStyle, + b.warningStyle, + t, + ), + border: BoxBorder.lerp(a.border, b.border, t), + ); + } + + StreamErrorBadgeThemeData copyWith({ + StreamErrorBadgeSize? size, + StreamErrorBadgeThemeStyle? errorStyle, + StreamErrorBadgeThemeStyle? warningStyle, + BoxBorder? border, + }) { + final _this = (this as StreamErrorBadgeThemeData); + + return StreamErrorBadgeThemeData( + size: size ?? _this.size, + errorStyle: errorStyle ?? _this.errorStyle, + warningStyle: warningStyle ?? _this.warningStyle, + border: border ?? _this.border, + ); + } + + StreamErrorBadgeThemeData merge(StreamErrorBadgeThemeData? other) { + final _this = (this as StreamErrorBadgeThemeData); + + if (other == null || identical(_this, other)) { + return _this; + } + + if (!other.canMerge) { + return other; + } + + return copyWith( + size: other.size, + errorStyle: _this.errorStyle?.merge(other.errorStyle) ?? other.errorStyle, + warningStyle: + _this.warningStyle?.merge(other.warningStyle) ?? other.warningStyle, + border: other.border, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + final _this = (this as StreamErrorBadgeThemeData); + final _other = (other as StreamErrorBadgeThemeData); + + return _other.size == _this.size && + _other.errorStyle == _this.errorStyle && + _other.warningStyle == _this.warningStyle && + _other.border == _this.border; + } + + @override + int get hashCode { + final _this = (this as StreamErrorBadgeThemeData); + + return Object.hash( + runtimeType, + _this.size, + _this.errorStyle, + _this.warningStyle, + _this.border, + ); + } +} + +mixin _$StreamErrorBadgeThemeStyle { + bool get canMerge => true; + + static StreamErrorBadgeThemeStyle? lerp( + StreamErrorBadgeThemeStyle? a, + StreamErrorBadgeThemeStyle? b, + double t, + ) { + if (identical(a, b)) { + return a; + } + + if (a == null) { + return t == 1.0 ? b : null; + } + + if (b == null) { + return t == 0.0 ? a : null; + } + + return StreamErrorBadgeThemeStyle( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + foregroundColor: Color.lerp(a.foregroundColor, b.foregroundColor, t), + ); + } + + StreamErrorBadgeThemeStyle copyWith({ + Color? backgroundColor, + Color? foregroundColor, + }) { + final _this = (this as StreamErrorBadgeThemeStyle); + + return StreamErrorBadgeThemeStyle( + backgroundColor: backgroundColor ?? _this.backgroundColor, + foregroundColor: foregroundColor ?? _this.foregroundColor, + ); + } + + StreamErrorBadgeThemeStyle merge(StreamErrorBadgeThemeStyle? other) { + final _this = (this as StreamErrorBadgeThemeStyle); + + if (other == null || identical(_this, other)) { + return _this; + } + + if (!other.canMerge) { + return other; + } + + return copyWith( + backgroundColor: other.backgroundColor, + foregroundColor: other.foregroundColor, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + final _this = (this as StreamErrorBadgeThemeStyle); + final _other = (other as StreamErrorBadgeThemeStyle); + + return _other.backgroundColor == _this.backgroundColor && + _other.foregroundColor == _this.foregroundColor; + } + + @override + int get hashCode { + final _this = (this as StreamErrorBadgeThemeStyle); + + return Object.hash( + runtimeType, + _this.backgroundColor, + _this.foregroundColor, + ); + } +} diff --git a/packages/stream_core_flutter/lib/src/theme/stream_theme.dart b/packages/stream_core_flutter/lib/src/theme/stream_theme.dart index ae3b1757..6fbbea2a 100644 --- a/packages/stream_core_flutter/lib/src/theme/stream_theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/stream_theme.dart @@ -16,6 +16,7 @@ import 'components/stream_context_menu_action_theme.dart'; import 'components/stream_context_menu_theme.dart'; import 'components/stream_emoji_button_theme.dart'; import 'components/stream_emoji_chip_theme.dart'; +import 'components/stream_error_badge_theme.dart'; import 'components/stream_jump_to_unread_button_theme.dart'; import 'components/stream_list_tile_theme.dart'; import 'components/stream_media_viewer_theme.dart'; @@ -133,6 +134,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { StreamContextMenuActionThemeData? contextMenuActionTheme, StreamEmojiButtonThemeData? emojiButtonTheme, StreamEmojiChipThemeData? emojiChipTheme, + StreamErrorBadgeThemeData? errorBadgeTheme, StreamJumpToUnreadButtonThemeData? jumpToUnreadButtonTheme, StreamListTileThemeData? listTileTheme, StreamMediaViewerThemeData? mediaViewerTheme, @@ -195,6 +197,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { contextMenuActionTheme ??= const StreamContextMenuActionThemeData(); emojiButtonTheme ??= const StreamEmojiButtonThemeData(); emojiChipTheme ??= const StreamEmojiChipThemeData(); + errorBadgeTheme ??= const StreamErrorBadgeThemeData(); jumpToUnreadButtonTheme ??= const StreamJumpToUnreadButtonThemeData(); listTileTheme ??= const StreamListTileThemeData(); mediaViewerTheme ??= const StreamMediaViewerThemeData(); @@ -245,6 +248,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { contextMenuActionTheme: contextMenuActionTheme, emojiButtonTheme: emojiButtonTheme, emojiChipTheme: emojiChipTheme, + errorBadgeTheme: errorBadgeTheme, jumpToUnreadButtonTheme: jumpToUnreadButtonTheme, listTileTheme: listTileTheme, mediaViewerTheme: mediaViewerTheme, @@ -309,6 +313,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { required this.contextMenuActionTheme, required this.emojiButtonTheme, required this.emojiChipTheme, + required this.errorBadgeTheme, required this.jumpToUnreadButtonTheme, required this.listTileTheme, required this.mediaViewerTheme, @@ -448,6 +453,9 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { /// The emoji chip theme for this theme. final StreamEmojiChipThemeData emojiChipTheme; + /// The error badge theme for this theme. + final StreamErrorBadgeThemeData errorBadgeTheme; + /// The jump-to-unread button theme for this theme. final StreamJumpToUnreadButtonThemeData jumpToUnreadButtonTheme; @@ -567,6 +575,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { contextMenuActionTheme: contextMenuActionTheme, emojiButtonTheme: emojiButtonTheme, emojiChipTheme: emojiChipTheme, + errorBadgeTheme: errorBadgeTheme, jumpToUnreadButtonTheme: jumpToUnreadButtonTheme, listTileTheme: listTileTheme, mediaViewerTheme: mediaViewerTheme, diff --git a/packages/stream_core_flutter/lib/src/theme/stream_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/stream_theme.g.theme.dart index 41dbcae2..fccde8d6 100644 --- a/packages/stream_core_flutter/lib/src/theme/stream_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/stream_theme.g.theme.dart @@ -36,6 +36,7 @@ mixin _$StreamTheme on ThemeExtension { StreamContextMenuActionThemeData? contextMenuActionTheme, StreamEmojiButtonThemeData? emojiButtonTheme, StreamEmojiChipThemeData? emojiChipTheme, + StreamErrorBadgeThemeData? errorBadgeTheme, StreamJumpToUnreadButtonThemeData? jumpToUnreadButtonTheme, StreamListTileThemeData? listTileTheme, StreamMediaViewerThemeData? mediaViewerTheme, @@ -96,6 +97,7 @@ mixin _$StreamTheme on ThemeExtension { contextMenuActionTheme ?? _this.contextMenuActionTheme, emojiButtonTheme: emojiButtonTheme ?? _this.emojiButtonTheme, emojiChipTheme: emojiChipTheme ?? _this.emojiChipTheme, + errorBadgeTheme: errorBadgeTheme ?? _this.errorBadgeTheme, jumpToUnreadButtonTheme: jumpToUnreadButtonTheme ?? _this.jumpToUnreadButtonTheme, listTileTheme: listTileTheme ?? _this.listTileTheme, @@ -232,6 +234,11 @@ mixin _$StreamTheme on ThemeExtension { other.emojiChipTheme, t, )!, + errorBadgeTheme: StreamErrorBadgeThemeData.lerp( + _this.errorBadgeTheme, + other.errorBadgeTheme, + t, + )!, jumpToUnreadButtonTheme: StreamJumpToUnreadButtonThemeData.lerp( _this.jumpToUnreadButtonTheme, other.jumpToUnreadButtonTheme, @@ -399,6 +406,7 @@ mixin _$StreamTheme on ThemeExtension { _other.contextMenuActionTheme == _this.contextMenuActionTheme && _other.emojiButtonTheme == _this.emojiButtonTheme && _other.emojiChipTheme == _this.emojiChipTheme && + _other.errorBadgeTheme == _this.errorBadgeTheme && _other.jumpToUnreadButtonTheme == _this.jumpToUnreadButtonTheme && _other.listTileTheme == _this.listTileTheme && _other.mediaViewerTheme == _this.mediaViewerTheme && @@ -462,6 +470,7 @@ mixin _$StreamTheme on ThemeExtension { _this.contextMenuActionTheme, _this.emojiButtonTheme, _this.emojiChipTheme, + _this.errorBadgeTheme, _this.jumpToUnreadButtonTheme, _this.listTileTheme, _this.mediaViewerTheme, diff --git a/packages/stream_core_flutter/lib/src/theme/stream_theme_extensions.dart b/packages/stream_core_flutter/lib/src/theme/stream_theme_extensions.dart index d75227dd..106d735c 100644 --- a/packages/stream_core_flutter/lib/src/theme/stream_theme_extensions.dart +++ b/packages/stream_core_flutter/lib/src/theme/stream_theme_extensions.dart @@ -14,6 +14,7 @@ import 'components/stream_context_menu_action_theme.dart'; import 'components/stream_context_menu_theme.dart'; import 'components/stream_emoji_button_theme.dart'; import 'components/stream_emoji_chip_theme.dart'; +import 'components/stream_error_badge_theme.dart'; import 'components/stream_jump_to_unread_button_theme.dart'; import 'components/stream_list_tile_theme.dart'; import 'components/stream_media_viewer_theme.dart'; @@ -142,6 +143,9 @@ extension StreamThemeExtension on BuildContext { /// Returns the [StreamEmojiChipThemeData] from the nearest ancestor. StreamEmojiChipThemeData get streamEmojiChipTheme => StreamEmojiChipTheme.of(this); + /// Returns the [StreamErrorBadgeThemeData] from the nearest ancestor. + StreamErrorBadgeThemeData get streamErrorBadgeTheme => StreamErrorBadgeTheme.of(this); + /// Returns the [StreamJumpToUnreadButtonThemeData] from the nearest ancestor. StreamJumpToUnreadButtonThemeData get streamJumpToUnreadButtonTheme => StreamJumpToUnreadButtonTheme.of(this); diff --git a/packages/stream_core_flutter/test/components/badge/stream_error_badge_golden_test.dart b/packages/stream_core_flutter/test/components/badge/stream_error_badge_golden_test.dart new file mode 100644 index 00000000..9871da75 --- /dev/null +++ b/packages/stream_core_flutter/test/components/badge/stream_error_badge_golden_test.dart @@ -0,0 +1,102 @@ +import 'package:alchemist/alchemist.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_core_flutter/core.dart'; + +void main() { + group('StreamErrorBadge Golden Tests', () { + goldenTest( + 'renders light theme style and size matrix', + fileName: 'stream_error_badge_light_matrix', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 100), + children: [ + for (final style in StreamErrorBadgeStyle.values) + for (final size in StreamErrorBadgeSize.values) + GoldenTestScenario( + name: '${style.name}_${size.name}', + child: _buildInTheme( + StreamErrorBadge(style: style, size: size), + ), + ), + ], + ), + ); + + goldenTest( + 'renders dark theme style and size matrix', + fileName: 'stream_error_badge_dark_matrix', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 100), + children: [ + for (final style in StreamErrorBadgeStyle.values) + for (final size in StreamErrorBadgeSize.values) + GoldenTestScenario( + name: '${style.name}_${size.name}', + child: _buildInTheme( + StreamErrorBadge(style: style, size: size), + brightness: Brightness.dark, + ), + ), + ], + ), + ); + + // The border's job is to separate the badge from what it overlaps, and its + // color tracks the app background — so it is only visible over something + // else. These scenarios overlay a contrasting swatch to show it. + goldenTest( + 'renders the border toggle over a contrasting surface', + fileName: 'stream_error_badge_border_toggle', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 100), + children: [ + for (final brightness in Brightness.values) + for (final style in StreamErrorBadgeStyle.values) + for (final showBorder in [true, false]) + GoldenTestScenario( + name: + '${brightness.name}_${style.name}_' + '${showBorder ? 'border' : 'no_border'}', + child: _buildInTheme( + StreamErrorBadge(style: style, showBorder: showBorder), + brightness: brightness, + overContrastingSurface: true, + ), + ), + ], + ), + ); + }); +} + +Widget _buildInTheme( + Widget child, { + Brightness brightness = Brightness.light, + bool overContrastingSurface = false, +}) { + final streamTheme = StreamTheme(brightness: brightness); + return Theme( + data: ThemeData( + brightness: brightness, + extensions: [streamTheme], + ), + child: Builder( + builder: (context) => Material( + color: StreamTheme.of(context).colorScheme.backgroundApp, + child: Padding( + padding: const EdgeInsets.all(8), + child: Center( + child: switch (overContrastingSurface) { + true => ColoredBox( + color: StreamTheme.of(context).colorScheme.accentNeutral, + child: Padding(padding: const EdgeInsets.all(6), child: child), + ), + false => child, + }, + ), + ), + ), + ), + ); +} diff --git a/packages/stream_core_flutter/test/components/badge/stream_error_badge_test.dart b/packages/stream_core_flutter/test/components/badge/stream_error_badge_test.dart new file mode 100644 index 00000000..e4819b4f --- /dev/null +++ b/packages/stream_core_flutter/test/components/badge/stream_error_badge_test.dart @@ -0,0 +1,220 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_core_flutter/core.dart'; + +void main() { + testWidgets('StreamErrorBadge draws a border by default', (tester) async { + await tester.pumpWidget(_wrap(StreamErrorBadge())); + + final border = _borderOf(tester)!; + expect(border.top.color, _colorSchemeOf(tester).borderOnInverse); + expect(border.top.width, 2); + expect(border.top.strokeAlign, BorderSide.strokeAlignOutside); + }); + + testWidgets('StreamErrorBadge draws no border when showBorder is false', (tester) async { + await tester.pumpWidget(_wrap(StreamErrorBadge(showBorder: false))); + + expect(_borderOf(tester), isNull); + }); + + testWidgets('StreamErrorBadge keeps its layout size when the border is dropped', (tester) async { + const size = StreamErrorBadgeSize.sm; + + await tester.pumpWidget(_wrap(StreamErrorBadge(size: size))); + final withBorder = tester.getSize(find.byType(StreamErrorBadge)); + + await tester.pumpWidget(_wrap(StreamErrorBadge(size: size, showBorder: false))); + final withoutBorder = tester.getSize(find.byType(StreamErrorBadge)); + + expect(withBorder, Size.square(size.value)); + expect(withoutBorder, withBorder); + }); + + testWidgets('StreamErrorBadge uses the error colors by default', (tester) async { + await tester.pumpWidget(_wrap(StreamErrorBadge())); + + final colorScheme = _colorSchemeOf(tester); + expect(_backgroundColorOf(tester), colorScheme.accentError); + expect(_iconColorOf(tester), colorScheme.textOnAccent); + }); + + testWidgets('StreamErrorBadge uses the warning colors for the warning style', (tester) async { + await tester.pumpWidget(_wrap(StreamErrorBadge(style: StreamErrorBadgeStyle.warning))); + + expect(_backgroundColorOf(tester), _colorSchemeOf(tester).accentWarning); + expect(_iconColorOf(tester), StreamColors.black); + }); + + testWidgets('StreamErrorBadgeTheme overrides the resolved style colors', (tester) async { + const themeData = StreamErrorBadgeThemeData( + errorStyle: StreamErrorBadgeThemeStyle( + backgroundColor: Color(0xFF111111), + foregroundColor: Color(0xFF222222), + ), + warningStyle: StreamErrorBadgeThemeStyle( + backgroundColor: Color(0xFF333333), + foregroundColor: Color(0xFF444444), + ), + ); + + await tester.pumpWidget( + _wrap( + StreamErrorBadgeTheme(data: themeData, child: StreamErrorBadge()), + ), + ); + + expect(_backgroundColorOf(tester), themeData.errorStyle!.backgroundColor); + expect(_iconColorOf(tester), themeData.errorStyle!.foregroundColor); + + await tester.pumpWidget( + _wrap( + StreamErrorBadgeTheme( + data: themeData, + child: StreamErrorBadge(style: StreamErrorBadgeStyle.warning), + ), + ), + ); + + expect(_backgroundColorOf(tester), themeData.warningStyle!.backgroundColor); + expect(_iconColorOf(tester), themeData.warningStyle!.foregroundColor); + }); + + testWidgets('StreamErrorBadgeTheme leaves the other style untouched', (tester) async { + const themeData = StreamErrorBadgeThemeData( + warningStyle: StreamErrorBadgeThemeStyle(backgroundColor: Color(0xFF333333)), + ); + + await tester.pumpWidget( + _wrap( + StreamErrorBadgeTheme(data: themeData, child: StreamErrorBadge()), + ), + ); + + final colorScheme = _colorSchemeOf(tester); + expect(_backgroundColorOf(tester), colorScheme.accentError); + expect(_iconColorOf(tester), colorScheme.textOnAccent); + }); + + testWidgets('StreamErrorBadgeThemeStyle merges onto the defaults per property', (tester) async { + // Only the background is overridden — the icon color must still resolve. + const themeData = StreamErrorBadgeThemeData( + warningStyle: StreamErrorBadgeThemeStyle(backgroundColor: Color(0xFF333333)), + ); + + await tester.pumpWidget( + _wrap( + StreamErrorBadgeTheme( + data: themeData, + child: StreamErrorBadge(style: StreamErrorBadgeStyle.warning), + ), + ), + ); + + expect(_backgroundColorOf(tester), const Color(0xFF333333)); + expect(_iconColorOf(tester), StreamColors.black); + }); + + testWidgets('StreamErrorBadgeThemeData.styleOf maps each style to its own entry', (tester) async { + const errorStyle = StreamErrorBadgeThemeStyle(backgroundColor: Color(0xFF111111)); + const warningStyle = StreamErrorBadgeThemeStyle(backgroundColor: Color(0xFF333333)); + const themeData = StreamErrorBadgeThemeData(errorStyle: errorStyle, warningStyle: warningStyle); + + expect(themeData.styleOf(StreamErrorBadgeStyle.error), errorStyle); + expect(themeData.styleOf(StreamErrorBadgeStyle.warning), warningStyle); + expect(const StreamErrorBadgeThemeData().styleOf(StreamErrorBadgeStyle.error), isNull); + }); + + testWidgets('StreamErrorBadgeTheme overrides the border and default size', (tester) async { + final themeData = StreamErrorBadgeThemeData( + size: StreamErrorBadgeSize.md, + border: Border.all(width: 4, color: const Color(0xFF555555)), + ); + + await tester.pumpWidget( + _wrap( + StreamErrorBadgeTheme(data: themeData, child: StreamErrorBadge()), + ), + ); + + expect(tester.getSize(find.byType(StreamErrorBadge)), Size.square(StreamErrorBadgeSize.md.value)); + expect(_borderOf(tester)!.top.width, 4); + expect(_borderOf(tester)!.top.color, const Color(0xFF555555)); + }); + + testWidgets('StreamErrorBadge.size wins over the theme default', (tester) async { + const themeData = StreamErrorBadgeThemeData(size: StreamErrorBadgeSize.md); + + await tester.pumpWidget( + _wrap( + StreamErrorBadgeTheme( + data: themeData, + child: StreamErrorBadge(size: StreamErrorBadgeSize.xs), + ), + ), + ); + + expect(tester.getSize(find.byType(StreamErrorBadge)), Size.square(StreamErrorBadgeSize.xs.value)); + }); + + testWidgets('StreamErrorBadgeTheme border is ignored when showBorder is false', (tester) async { + final themeData = StreamErrorBadgeThemeData( + border: Border.all(width: 4, color: const Color(0xFF555555)), + ); + + await tester.pumpWidget( + _wrap( + StreamErrorBadgeTheme( + data: themeData, + child: StreamErrorBadge(showBorder: false), + ), + ), + ); + + expect(_borderOf(tester), isNull); + }); + + testWidgets('StreamErrorBadge pins the warning icon to black in both brightnesses', (tester) async { + for (final brightness in Brightness.values) { + await tester.pumpWidget( + _wrap( + StreamErrorBadge(style: StreamErrorBadgeStyle.warning), + brightness: brightness, + ), + ); + + expect(_iconColorOf(tester), StreamColors.black, reason: 'brightness: ${brightness.name}'); + } + }); +} + +Widget _wrap(Widget child, {Brightness brightness = Brightness.light}) { + return MaterialApp( + theme: ThemeData( + brightness: brightness, + extensions: [StreamTheme(brightness: brightness)], + ), + home: Scaffold(body: Center(child: child)), + ); +} + +StreamColorScheme _colorSchemeOf(WidgetTester tester) { + final context = tester.element(find.byType(StreamErrorBadge)); + return StreamTheme.of(context).colorScheme; +} + +// The badge paints its border into `foregroundDecoration` with +// `strokeAlignOutside`, so it separates the badge without growing it. +BoxBorder? _borderOf(WidgetTester tester) { + final container = tester.widget(find.byType(AnimatedContainer)); + return (container.foregroundDecoration! as BoxDecoration).border; +} + +Color? _backgroundColorOf(WidgetTester tester) { + final container = tester.widget(find.byType(AnimatedContainer)); + return (container.decoration! as BoxDecoration).color; +} + +Color? _iconColorOf(WidgetTester tester) { + return tester.widget(find.byType(Icon)).color ?? IconTheme.of(tester.element(find.byType(Icon))).color; +} From da6a1b7ef66f30fbaf85ffeabda6b50d2dc9aba4 Mon Sep 17 00:00:00 2001 From: renefloor <15101411+renefloor@users.noreply.github.com> Date: Wed, 9 Sep 2026 14:53:57 +0000 Subject: [PATCH 2/4] chore: Update Goldens --- .../ci/stream_error_badge_border_toggle.png | Bin 0 -> 8690 bytes .../ci/stream_error_badge_dark_matrix.png | Bin 0 -> 3904 bytes .../ci/stream_error_badge_light_matrix.png | Bin 0 -> 3883 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/stream_core_flutter/test/components/badge/goldens/ci/stream_error_badge_border_toggle.png create mode 100644 packages/stream_core_flutter/test/components/badge/goldens/ci/stream_error_badge_dark_matrix.png create mode 100644 packages/stream_core_flutter/test/components/badge/goldens/ci/stream_error_badge_light_matrix.png diff --git a/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_error_badge_border_toggle.png b/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_error_badge_border_toggle.png new file mode 100644 index 0000000000000000000000000000000000000000..208799175df3a0ba61bf42debbf4d779a955cc05 GIT binary patch literal 8690 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 literal 0 HcmV?d00001 diff --git a/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_error_badge_dark_matrix.png b/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_error_badge_dark_matrix.png new file mode 100644 index 0000000000000000000000000000000000000000..687cd4cd2570e62028f3a2491f34a73cc51d92b7 GIT binary patch literal 3904 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 literal 0 HcmV?d00001 diff --git a/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_error_badge_light_matrix.png b/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_error_badge_light_matrix.png new file mode 100644 index 0000000000000000000000000000000000000000..52dda52143c56bf8646d30c886333a386c215081 GIT binary patch literal 3883 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 literal 0 HcmV?d00001 From a59d4b1cd009f6dd05020818c0e03325f11b8f68 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 9 Sep 2026 16:59:50 +0200 Subject: [PATCH 3/4] docs(ui): trim the StreamErrorBadge changelog entries Co-Authored-By: Claude Opus 5 --- packages/stream_core_flutter/CHANGELOG.md | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/packages/stream_core_flutter/CHANGELOG.md b/packages/stream_core_flutter/CHANGELOG.md index cd77a817..73218196 100644 --- a/packages/stream_core_flutter/CHANGELOG.md +++ b/packages/stream_core_flutter/CHANGELOG.md @@ -4,25 +4,9 @@ - Added the `lowBandwidthFill` icon. - Added `StreamErrorBadge.style`, taking a `StreamErrorBadgeStyle` — `.error` - (the default, unchanged) or `.warning`, a warning-colored background with a - black icon. The icon is pinned to black rather than to a mode-aware text - color, because the warning background does not invert between light and dark. -- Added `StreamErrorBadge.showBorder`, defaulting to true. Set it to false for - the borderless badge the call control button uses, where the border loses its - edge over video. -- Added `StreamErrorBadgeTheme` and `StreamErrorBadgeThemeData`, reachable as - `StreamTheme.errorBadgeTheme` and `BuildContext.streamErrorBadgeTheme`. It - carries a shared `size` and `border` plus an `errorStyle` and a - `warningStyle`, each a `StreamErrorBadgeThemeStyle` of `backgroundColor` and - `foregroundColor`, so the two styles can be themed independently. Use - `styleOf` to look up the entry for a given `StreamErrorBadgeStyle`. - -### 🔄 Changed - -- `StreamErrorBadgeSize` moved from `stream_error_badge.dart` to - `stream_error_badge_theme.dart`, alongside the new `StreamErrorBadgeStyle`. - Both are still exported from `core.dart`, so imports through the barrel are - unaffected. + (the default) or `.warning` — and `StreamErrorBadge.showBorder`. +- Added `StreamErrorBadgeTheme` and `StreamErrorBadgeThemeData`, carrying a + `StreamErrorBadgeThemeStyle` per style. ## 0.5.1 From fd972b9d4b164006c61a60474b1374a655a535c3 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 10 Sep 2026 08:42:39 +0200 Subject: [PATCH 4/4] refactor(ui): flatten the StreamErrorBadge theme colors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review. `StreamErrorBadgeThemeData` now carries `errorBackgroundColor`, `errorForegroundColor`, `warningBackgroundColor` and `warningForegroundColor` directly, resolved by private per-property switches in the widget — the same shape as `StreamBadgeNotificationThemeData`, the closest sibling, and as `FloatingActionButtonThemeData` upstream. `StreamErrorBadgeThemeStyle` and `styleOf` are gone, and with them the covariant-narrowing `styleOf` override in the defaults class that only existed to let the nested object merge without a `!`. A public class plus a generated merge/lerp/copyWith was a lot of surface for two colors; nesting pays for itself on `StreamButtonThemeData`, but that is nine combinations of fourteen properties. The switches stay exhaustive, so a third style still breaks the build. Also points `showBorder` at `StreamErrorBadgeThemeData.border` for the border treatment, mirroring `StreamAvatar.showBorder`. Co-Authored-By: Claude Opus 5 --- packages/stream_core_flutter/CHANGELOG.md | 2 +- .../components/badge/stream_error_badge.dart | 57 ++++--- .../components/stream_error_badge_theme.dart | 83 ++++------- .../stream_error_badge_theme.g.theme.dart | 139 +++++------------- .../badge/stream_error_badge_test.dart | 40 ++--- 5 files changed, 110 insertions(+), 211 deletions(-) diff --git a/packages/stream_core_flutter/CHANGELOG.md b/packages/stream_core_flutter/CHANGELOG.md index 73218196..082c92e7 100644 --- a/packages/stream_core_flutter/CHANGELOG.md +++ b/packages/stream_core_flutter/CHANGELOG.md @@ -6,7 +6,7 @@ - Added `StreamErrorBadge.style`, taking a `StreamErrorBadgeStyle` — `.error` (the default) or `.warning` — and `StreamErrorBadge.showBorder`. - Added `StreamErrorBadgeTheme` and `StreamErrorBadgeThemeData`, carrying a - `StreamErrorBadgeThemeStyle` per style. + background and foreground color per style. ## 0.5.1 diff --git a/packages/stream_core_flutter/lib/src/components/badge/stream_error_badge.dart b/packages/stream_core_flutter/lib/src/components/badge/stream_error_badge.dart index 2294420f..6d114479 100644 --- a/packages/stream_core_flutter/lib/src/components/badge/stream_error_badge.dart +++ b/packages/stream_core_flutter/lib/src/components/badge/stream_error_badge.dart @@ -107,9 +107,10 @@ class StreamErrorBadgeProps { /// Whether a border is drawn around the badge. /// - /// The border is drawn outside the badge's [size], so it separates the - /// badge from whatever it overlaps without changing the badge's layout - /// size. Defaults to true. + /// The border style is determined by [StreamErrorBadgeThemeData.border]. It + /// is drawn outside the badge's [size], so it separates the badge from + /// whatever it overlaps without changing the badge's layout size. Defaults + /// to true. final bool showBorder; } @@ -141,22 +142,40 @@ class DefaultStreamErrorBadge extends StatelessWidget { final effectiveStyle = props.style ?? StreamErrorBadgeStyle.error; final effectiveBorder = props.showBorder ? theme.border ?? defaults.border : null; - // Defaults first, theme overrides layered on top, so every color resolves. - final style = defaults.styleOf(effectiveStyle).merge(theme.styleOf(effectiveStyle)); + final effectiveBackgroundColor = _resolveBackgroundColor(effectiveStyle, theme, defaults); + final effectiveForegroundColor = _resolveForegroundColor(effectiveStyle, theme, defaults); return AnimatedContainer( width: effectiveSize.value, height: effectiveSize.value, clipBehavior: Clip.antiAlias, duration: kThemeChangeDuration, - decoration: BoxDecoration(shape: BoxShape.circle, color: style.backgroundColor), + decoration: BoxDecoration(shape: BoxShape.circle, color: effectiveBackgroundColor), foregroundDecoration: BoxDecoration(shape: BoxShape.circle, border: effectiveBorder), child: IconTheme( - data: .new(size: effectiveSize.iconSize, color: style.foregroundColor), + data: .new(size: effectiveSize.iconSize, color: effectiveForegroundColor), child: Center(child: Icon(icons.exclamationMarkFill)), ), ); } + + Color _resolveBackgroundColor( + StreamErrorBadgeStyle style, + StreamErrorBadgeThemeData theme, + _StreamErrorBadgeThemeDefaults defaults, + ) => switch (style) { + .error => theme.errorBackgroundColor ?? defaults.errorBackgroundColor, + .warning => theme.warningBackgroundColor ?? defaults.warningBackgroundColor, + }; + + Color _resolveForegroundColor( + StreamErrorBadgeStyle style, + StreamErrorBadgeThemeData theme, + _StreamErrorBadgeThemeDefaults defaults, + ) => switch (style) { + .error => theme.errorForegroundColor ?? defaults.errorForegroundColor, + .warning => theme.warningForegroundColor ?? defaults.warningForegroundColor, + }; } class _StreamErrorBadgeThemeDefaults extends StreamErrorBadgeThemeData { @@ -170,16 +189,16 @@ class _StreamErrorBadgeThemeDefaults extends StreamErrorBadgeThemeData { StreamErrorBadgeSize get size => .sm; @override - StreamErrorBadgeThemeStyle get errorStyle => .new( - backgroundColor: _colorScheme.accentError, - foregroundColor: _colorScheme.textOnAccent, - ); + Color get errorBackgroundColor => _colorScheme.accentError; @override - StreamErrorBadgeThemeStyle get warningStyle => .new( - backgroundColor: _colorScheme.accentWarning, - foregroundColor: StreamColors.black, - ); + Color get errorForegroundColor => _colorScheme.textOnAccent; + + @override + Color get warningBackgroundColor => _colorScheme.accentWarning; + + @override + Color get warningForegroundColor => StreamColors.black; @override BoxBorder get border => Border.all( @@ -187,12 +206,4 @@ class _StreamErrorBadgeThemeDefaults extends StreamErrorBadgeThemeData { color: _colorScheme.borderOnInverse, strokeAlign: BorderSide.strokeAlignOutside, ); - - // Narrowed to non-nullable: every style has a default, so callers can merge - // the theme's partial override straight onto the result. - @override - StreamErrorBadgeThemeStyle styleOf(StreamErrorBadgeStyle style) => switch (style) { - StreamErrorBadgeStyle.error => errorStyle, - StreamErrorBadgeStyle.warning => warningStyle, - }; } diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_error_badge_theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_error_badge_theme.dart index 4567dabc..bfa0421c 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_error_badge_theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_error_badge_theme.dart @@ -105,10 +105,8 @@ class StreamErrorBadgeTheme extends InheritedTheme { /// ```dart /// StreamTheme( /// errorBadgeTheme: StreamErrorBadgeThemeData( -/// warningStyle: StreamErrorBadgeThemeStyle( -/// backgroundColor: Colors.amber, -/// foregroundColor: Colors.black, -/// ), +/// warningBackgroundColor: Colors.amber, +/// warningForegroundColor: Colors.black, /// ), /// ) /// ``` @@ -118,16 +116,16 @@ class StreamErrorBadgeTheme extends InheritedTheme { /// /// * [StreamErrorBadge], the widget that uses this theme data. /// * [StreamErrorBadgeTheme], for overriding theme in a widget subtree. -/// * [StreamErrorBadgeThemeStyle], the per-style properties this groups. @themeGen @immutable class StreamErrorBadgeThemeData with _$StreamErrorBadgeThemeData { - /// Creates an error badge theme with optional style overrides per - /// [StreamErrorBadgeStyle]. + /// Creates an error badge theme with optional style overrides. const StreamErrorBadgeThemeData({ this.size, - this.errorStyle, - this.warningStyle, + this.errorBackgroundColor, + this.errorForegroundColor, + this.warningBackgroundColor, + this.warningForegroundColor, this.border, }); @@ -136,11 +134,26 @@ class StreamErrorBadgeThemeData with _$StreamErrorBadgeThemeData { /// Falls back to [StreamErrorBadgeSize.sm]. final StreamErrorBadgeSize? size; - /// Styling for badges of the [StreamErrorBadgeStyle.error] style. - final StreamErrorBadgeThemeStyle? errorStyle; + /// The fill color of badges of the [StreamErrorBadgeStyle.error] style. + /// + /// Defaults to [StreamColorScheme.accentError]. + final Color? errorBackgroundColor; + + /// The icon color of badges of the [StreamErrorBadgeStyle.error] style. + /// + /// Defaults to [StreamColorScheme.textOnAccent]. + final Color? errorForegroundColor; - /// Styling for badges of the [StreamErrorBadgeStyle.warning] style. - final StreamErrorBadgeThemeStyle? warningStyle; + /// The fill color of badges of the [StreamErrorBadgeStyle.warning] style. + /// + /// Defaults to [StreamColorScheme.accentWarning]. + final Color? warningBackgroundColor; + + /// The icon color of badges of the [StreamErrorBadgeStyle.warning] style. + /// + /// Defaults to black rather than to a mode-aware text color, because the + /// warning background does not invert between light and dark. + final Color? warningForegroundColor; /// The border drawn around the badge. /// @@ -148,12 +161,6 @@ class StreamErrorBadgeThemeData with _$StreamErrorBadgeThemeData { /// of both border color and width. Shared by both styles. final BoxBorder? border; - /// The styling for badges of the given [style]. - StreamErrorBadgeThemeStyle? styleOf(StreamErrorBadgeStyle style) => switch (style) { - StreamErrorBadgeStyle.error => errorStyle, - StreamErrorBadgeStyle.warning => warningStyle, - }; - /// Linearly interpolate between two [StreamErrorBadgeThemeData]. static StreamErrorBadgeThemeData? lerp( StreamErrorBadgeThemeData? a, @@ -161,41 +168,3 @@ class StreamErrorBadgeThemeData with _$StreamErrorBadgeThemeData { double t, ) => _$StreamErrorBadgeThemeData.lerp(a, b, t); } - -/// Visual styling properties for a single [StreamErrorBadgeStyle]. -/// -/// See also: -/// -/// * [StreamErrorBadgeThemeData], which groups one of these per style. -/// * [StreamErrorBadge], which uses this styling. -@themeGen -@immutable -class StreamErrorBadgeThemeStyle with _$StreamErrorBadgeThemeStyle { - /// Creates error badge style properties. - const StreamErrorBadgeThemeStyle({ - this.backgroundColor, - this.foregroundColor, - }); - - /// The fill color of the badge circle. - /// - /// Defaults to [StreamColorScheme.accentError] on - /// [StreamErrorBadgeStyle.error] and [StreamColorScheme.accentWarning] on - /// [StreamErrorBadgeStyle.warning]. - final Color? backgroundColor; - - /// The color of the exclamation mark icon. - /// - /// Defaults to [StreamColorScheme.textOnAccent] on - /// [StreamErrorBadgeStyle.error]. On [StreamErrorBadgeStyle.warning] it - /// falls back to black rather than to a mode-aware text color, because the - /// warning background does not invert between light and dark. - final Color? foregroundColor; - - /// Linearly interpolate between two [StreamErrorBadgeThemeStyle]. - static StreamErrorBadgeThemeStyle? lerp( - StreamErrorBadgeThemeStyle? a, - StreamErrorBadgeThemeStyle? b, - double t, - ) => _$StreamErrorBadgeThemeStyle.lerp(a, b, t); -} diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_error_badge_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_error_badge_theme.g.theme.dart index c709b0bd..60c1bfa4 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_error_badge_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_error_badge_theme.g.theme.dart @@ -31,14 +31,24 @@ mixin _$StreamErrorBadgeThemeData { return StreamErrorBadgeThemeData( size: t < 0.5 ? a.size : b.size, - errorStyle: StreamErrorBadgeThemeStyle.lerp( - a.errorStyle, - b.errorStyle, + errorBackgroundColor: Color.lerp( + a.errorBackgroundColor, + b.errorBackgroundColor, t, ), - warningStyle: StreamErrorBadgeThemeStyle.lerp( - a.warningStyle, - b.warningStyle, + errorForegroundColor: Color.lerp( + a.errorForegroundColor, + b.errorForegroundColor, + t, + ), + warningBackgroundColor: Color.lerp( + a.warningBackgroundColor, + b.warningBackgroundColor, + t, + ), + warningForegroundColor: Color.lerp( + a.warningForegroundColor, + b.warningForegroundColor, t, ), border: BoxBorder.lerp(a.border, b.border, t), @@ -47,16 +57,22 @@ mixin _$StreamErrorBadgeThemeData { StreamErrorBadgeThemeData copyWith({ StreamErrorBadgeSize? size, - StreamErrorBadgeThemeStyle? errorStyle, - StreamErrorBadgeThemeStyle? warningStyle, + Color? errorBackgroundColor, + Color? errorForegroundColor, + Color? warningBackgroundColor, + Color? warningForegroundColor, BoxBorder? border, }) { final _this = (this as StreamErrorBadgeThemeData); return StreamErrorBadgeThemeData( size: size ?? _this.size, - errorStyle: errorStyle ?? _this.errorStyle, - warningStyle: warningStyle ?? _this.warningStyle, + errorBackgroundColor: errorBackgroundColor ?? _this.errorBackgroundColor, + errorForegroundColor: errorForegroundColor ?? _this.errorForegroundColor, + warningBackgroundColor: + warningBackgroundColor ?? _this.warningBackgroundColor, + warningForegroundColor: + warningForegroundColor ?? _this.warningForegroundColor, border: border ?? _this.border, ); } @@ -74,9 +90,10 @@ mixin _$StreamErrorBadgeThemeData { return copyWith( size: other.size, - errorStyle: _this.errorStyle?.merge(other.errorStyle) ?? other.errorStyle, - warningStyle: - _this.warningStyle?.merge(other.warningStyle) ?? other.warningStyle, + errorBackgroundColor: other.errorBackgroundColor, + errorForegroundColor: other.errorForegroundColor, + warningBackgroundColor: other.warningBackgroundColor, + warningForegroundColor: other.warningForegroundColor, border: other.border, ); } @@ -95,8 +112,10 @@ mixin _$StreamErrorBadgeThemeData { final _other = (other as StreamErrorBadgeThemeData); return _other.size == _this.size && - _other.errorStyle == _this.errorStyle && - _other.warningStyle == _this.warningStyle && + _other.errorBackgroundColor == _this.errorBackgroundColor && + _other.errorForegroundColor == _this.errorForegroundColor && + _other.warningBackgroundColor == _this.warningBackgroundColor && + _other.warningForegroundColor == _this.warningForegroundColor && _other.border == _this.border; } @@ -107,93 +126,11 @@ mixin _$StreamErrorBadgeThemeData { return Object.hash( runtimeType, _this.size, - _this.errorStyle, - _this.warningStyle, + _this.errorBackgroundColor, + _this.errorForegroundColor, + _this.warningBackgroundColor, + _this.warningForegroundColor, _this.border, ); } } - -mixin _$StreamErrorBadgeThemeStyle { - bool get canMerge => true; - - static StreamErrorBadgeThemeStyle? lerp( - StreamErrorBadgeThemeStyle? a, - StreamErrorBadgeThemeStyle? b, - double t, - ) { - if (identical(a, b)) { - return a; - } - - if (a == null) { - return t == 1.0 ? b : null; - } - - if (b == null) { - return t == 0.0 ? a : null; - } - - return StreamErrorBadgeThemeStyle( - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - foregroundColor: Color.lerp(a.foregroundColor, b.foregroundColor, t), - ); - } - - StreamErrorBadgeThemeStyle copyWith({ - Color? backgroundColor, - Color? foregroundColor, - }) { - final _this = (this as StreamErrorBadgeThemeStyle); - - return StreamErrorBadgeThemeStyle( - backgroundColor: backgroundColor ?? _this.backgroundColor, - foregroundColor: foregroundColor ?? _this.foregroundColor, - ); - } - - StreamErrorBadgeThemeStyle merge(StreamErrorBadgeThemeStyle? other) { - final _this = (this as StreamErrorBadgeThemeStyle); - - if (other == null || identical(_this, other)) { - return _this; - } - - if (!other.canMerge) { - return other; - } - - return copyWith( - backgroundColor: other.backgroundColor, - foregroundColor: other.foregroundColor, - ); - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - - if (other.runtimeType != runtimeType) { - return false; - } - - final _this = (this as StreamErrorBadgeThemeStyle); - final _other = (other as StreamErrorBadgeThemeStyle); - - return _other.backgroundColor == _this.backgroundColor && - _other.foregroundColor == _this.foregroundColor; - } - - @override - int get hashCode { - final _this = (this as StreamErrorBadgeThemeStyle); - - return Object.hash( - runtimeType, - _this.backgroundColor, - _this.foregroundColor, - ); - } -} diff --git a/packages/stream_core_flutter/test/components/badge/stream_error_badge_test.dart b/packages/stream_core_flutter/test/components/badge/stream_error_badge_test.dart index e4819b4f..5e262e23 100644 --- a/packages/stream_core_flutter/test/components/badge/stream_error_badge_test.dart +++ b/packages/stream_core_flutter/test/components/badge/stream_error_badge_test.dart @@ -48,14 +48,10 @@ void main() { testWidgets('StreamErrorBadgeTheme overrides the resolved style colors', (tester) async { const themeData = StreamErrorBadgeThemeData( - errorStyle: StreamErrorBadgeThemeStyle( - backgroundColor: Color(0xFF111111), - foregroundColor: Color(0xFF222222), - ), - warningStyle: StreamErrorBadgeThemeStyle( - backgroundColor: Color(0xFF333333), - foregroundColor: Color(0xFF444444), - ), + errorBackgroundColor: Color(0xFF111111), + errorForegroundColor: Color(0xFF222222), + warningBackgroundColor: Color(0xFF333333), + warningForegroundColor: Color(0xFF444444), ); await tester.pumpWidget( @@ -64,8 +60,8 @@ void main() { ), ); - expect(_backgroundColorOf(tester), themeData.errorStyle!.backgroundColor); - expect(_iconColorOf(tester), themeData.errorStyle!.foregroundColor); + expect(_backgroundColorOf(tester), themeData.errorBackgroundColor); + expect(_iconColorOf(tester), themeData.errorForegroundColor); await tester.pumpWidget( _wrap( @@ -76,14 +72,12 @@ void main() { ), ); - expect(_backgroundColorOf(tester), themeData.warningStyle!.backgroundColor); - expect(_iconColorOf(tester), themeData.warningStyle!.foregroundColor); + expect(_backgroundColorOf(tester), themeData.warningBackgroundColor); + expect(_iconColorOf(tester), themeData.warningForegroundColor); }); testWidgets('StreamErrorBadgeTheme leaves the other style untouched', (tester) async { - const themeData = StreamErrorBadgeThemeData( - warningStyle: StreamErrorBadgeThemeStyle(backgroundColor: Color(0xFF333333)), - ); + const themeData = StreamErrorBadgeThemeData(warningBackgroundColor: Color(0xFF333333)); await tester.pumpWidget( _wrap( @@ -96,11 +90,9 @@ void main() { expect(_iconColorOf(tester), colorScheme.textOnAccent); }); - testWidgets('StreamErrorBadgeThemeStyle merges onto the defaults per property', (tester) async { + testWidgets('StreamErrorBadgeTheme falls back per property', (tester) async { // Only the background is overridden — the icon color must still resolve. - const themeData = StreamErrorBadgeThemeData( - warningStyle: StreamErrorBadgeThemeStyle(backgroundColor: Color(0xFF333333)), - ); + const themeData = StreamErrorBadgeThemeData(warningBackgroundColor: Color(0xFF333333)); await tester.pumpWidget( _wrap( @@ -115,16 +107,6 @@ void main() { expect(_iconColorOf(tester), StreamColors.black); }); - testWidgets('StreamErrorBadgeThemeData.styleOf maps each style to its own entry', (tester) async { - const errorStyle = StreamErrorBadgeThemeStyle(backgroundColor: Color(0xFF111111)); - const warningStyle = StreamErrorBadgeThemeStyle(backgroundColor: Color(0xFF333333)); - const themeData = StreamErrorBadgeThemeData(errorStyle: errorStyle, warningStyle: warningStyle); - - expect(themeData.styleOf(StreamErrorBadgeStyle.error), errorStyle); - expect(themeData.styleOf(StreamErrorBadgeStyle.warning), warningStyle); - expect(const StreamErrorBadgeThemeData().styleOf(StreamErrorBadgeStyle.error), isNull); - }); - testWidgets('StreamErrorBadgeTheme overrides the border and default size', (tester) async { final themeData = StreamErrorBadgeThemeData( size: StreamErrorBadgeSize.md,