From c6d7ea61bdc4c912369e90719aef4d56cb383726 Mon Sep 17 00:00:00 2001 From: Brazol Date: Thu, 19 Mar 2026 09:27:43 +0100 Subject: [PATCH 1/3] loading spinner component --- .../lib/app/gallery_app.directories.g.dart | 19 + .../common/stream_loading_spinner.dart | 370 ++++++++++++++++++ .../lib/src/components.dart | 1 + .../common/stream_loading_spinner.dart | 196 ++++++++++ .../src/factory/stream_component_factory.dart | 8 + .../stream_component_factory.g.theme.dart | 10 +- .../stream_message_theme.g.theme.dart | 6 + 7 files changed, 608 insertions(+), 2 deletions(-) create mode 100644 apps/design_system_gallery/lib/components/common/stream_loading_spinner.dart create mode 100644 packages/stream_core_flutter/lib/src/components/common/stream_loading_spinner.dart diff --git a/apps/design_system_gallery/lib/app/gallery_app.directories.g.dart b/apps/design_system_gallery/lib/app/gallery_app.directories.g.dart index b4a6e01e..90334856 100644 --- a/apps/design_system_gallery/lib/app/gallery_app.directories.g.dart +++ b/apps/design_system_gallery/lib/app/gallery_app.directories.g.dart @@ -36,6 +36,8 @@ import 'package:design_system_gallery/components/common/stream_checkbox.dart' as _design_system_gallery_components_common_stream_checkbox; import 'package:design_system_gallery/components/common/stream_flex.dart' as _design_system_gallery_components_common_stream_flex; +import 'package:design_system_gallery/components/common/stream_loading_spinner.dart' + as _design_system_gallery_components_common_stream_loading_spinner; import 'package:design_system_gallery/components/common/stream_progress_bar.dart' as _design_system_gallery_components_common_stream_progress_bar; import 'package:design_system_gallery/components/context_menu/stream_context_menu.dart' @@ -421,6 +423,23 @@ final directories = <_widgetbook.WidgetbookNode>[ ), ], ), + _widgetbook.WidgetbookComponent( + name: 'StreamLoadingSpinner', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'Playground', + builder: + _design_system_gallery_components_common_stream_loading_spinner + .buildStreamLoadingSpinnerPlayground, + ), + _widgetbook.WidgetbookUseCase( + name: 'Showcase', + builder: + _design_system_gallery_components_common_stream_loading_spinner + .buildStreamLoadingSpinnerShowcase, + ), + ], + ), _widgetbook.WidgetbookComponent( name: 'StreamProgressBar', useCases: [ diff --git a/apps/design_system_gallery/lib/components/common/stream_loading_spinner.dart b/apps/design_system_gallery/lib/components/common/stream_loading_spinner.dart new file mode 100644 index 00000000..88a56911 --- /dev/null +++ b/apps/design_system_gallery/lib/components/common/stream_loading_spinner.dart @@ -0,0 +1,370 @@ +import 'package:flutter/material.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +// ============================================================================= +// Playground +// ============================================================================= + +@widgetbook.UseCase( + name: 'Playground', + type: StreamLoadingSpinner, + path: '[Components]/Common', +) +Widget buildStreamLoadingSpinnerPlayground(BuildContext context) { + final size = context.knobs.double.slider( + label: 'Size', + initialValue: 20, + min: 12, + max: 64, + description: 'The diameter of the spinner.', + ); + + final strokeWidth = context.knobs.double.slider( + label: 'Stroke Width', + initialValue: 2, + min: 1, + max: 8, + description: 'The width of the track and arc.', + ); + + return Center( + child: StreamLoadingSpinner( + size: size, + strokeWidth: strokeWidth, + ), + ); +} + +// ============================================================================= +// Showcase +// ============================================================================= + +@widgetbook.UseCase( + name: 'Showcase', + type: StreamLoadingSpinner, + path: '[Components]/Common', +) +Widget buildStreamLoadingSpinnerShowcase(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return DefaultTextStyle( + style: textTheme.bodyDefault.copyWith(color: colorScheme.textPrimary), + child: SingleChildScrollView( + padding: EdgeInsets.all(spacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SizeVariantsSection(), + SizedBox(height: spacing.xl), + const _StrokeVariantsSection(), + SizedBox(height: spacing.xl), + const _ColorVariantsSection(), + ], + ), + ), + ); +} + +// ============================================================================= +// Size Variants Section +// ============================================================================= + +class _SizeVariantsSection extends StatelessWidget { + const _SizeVariantsSection(); + + @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: 'SIZE 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( + 'Spinners at different sizes', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + SizedBox(height: spacing.md), + Row( + children: [ + for (final (label, size) in [ + ('16px', 16.0), + ('20px', 20.0), + ('32px', 32.0), + ('48px', 48.0), + ]) ...[ + _SpinnerDemo(label: label, size: size), + if (size != 48.0) SizedBox(width: spacing.xl), + ], + ], + ), + ], + ), + ), + ], + ); + } +} + +// ============================================================================= +// Stroke Width Variants Section +// ============================================================================= + +class _StrokeVariantsSection extends StatelessWidget { + const _StrokeVariantsSection(); + + @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: 'STROKE WIDTH 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( + 'Different stroke widths at 32px size', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + SizedBox(height: spacing.md), + Row( + children: [ + for (final (label, strokeWidth) in [ + ('1px', 1.0), + ('2px', 2.0), + ('4px', 4.0), + ('6px', 6.0), + ]) ...[ + _SpinnerDemo(label: label, size: 32, strokeWidth: strokeWidth), + if (strokeWidth != 6.0) SizedBox(width: spacing.xl), + ], + ], + ), + ], + ), + ), + ], + ); + } +} + +// ============================================================================= +// Color Variants Section +// ============================================================================= + +class _ColorVariantsSection extends StatelessWidget { + const _ColorVariantsSection(); + + @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: 'COLOR 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( + 'Custom arc and track colors', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + SizedBox(height: spacing.md), + Row( + children: [ + _ColorDemo(label: 'Default', size: 32), + SizedBox(width: spacing.xl), + _ColorDemo(label: 'Success', size: 32, color: colorScheme.accentSuccess), + SizedBox(width: spacing.xl), + _ColorDemo(label: 'Warning', size: 32, color: colorScheme.accentWarning), + SizedBox(width: spacing.xl), + _ColorDemo(label: 'Error', size: 32, color: colorScheme.accentError), + ], + ), + ], + ), + ), + ], + ); + } +} + +// ============================================================================= +// Shared Widgets +// ============================================================================= + +class _SpinnerDemo extends StatelessWidget { + const _SpinnerDemo({required this.label, required this.size, this.strokeWidth}); + + final String label; + final double size; + final double? strokeWidth; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return Column( + children: [ + SizedBox( + width: 56, + height: 56, + child: Center( + child: StreamLoadingSpinner( + size: size, + strokeWidth: strokeWidth, + ), + ), + ), + SizedBox(height: spacing.sm), + Text( + label, + style: textTheme.metadataEmphasis.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + ], + ); + } +} + +class _ColorDemo extends StatelessWidget { + const _ColorDemo({required this.label, required this.size, this.color}); + + final String label; + final double size; + final Color? color; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return Column( + children: [ + SizedBox( + width: 56, + height: 56, + child: Center( + child: StreamLoadingSpinner( + size: size, + color: color, + ), + ), + ), + SizedBox(height: spacing.sm), + Text( + label, + style: textTheme.metadataEmphasis.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + ], + ); + } +} + +class _SectionLabel extends StatelessWidget { + const _SectionLabel({required this.label}); + + final String label; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final radius = context.streamRadius; + final spacing = context.streamSpacing; + + return Container( + padding: EdgeInsets.symmetric(horizontal: spacing.sm, vertical: spacing.xs), + decoration: BoxDecoration( + color: colorScheme.accentPrimary, + borderRadius: BorderRadius.all(radius.xs), + ), + child: Text( + label, + style: textTheme.metadataEmphasis.copyWith( + color: colorScheme.textOnAccent, + letterSpacing: 1, + fontSize: 9, + ), + ), + ); + } +} diff --git a/packages/stream_core_flutter/lib/src/components.dart b/packages/stream_core_flutter/lib/src/components.dart index 6a669def..30d04061 100644 --- a/packages/stream_core_flutter/lib/src/components.dart +++ b/packages/stream_core_flutter/lib/src/components.dart @@ -12,6 +12,7 @@ export 'components/buttons/stream_button.dart' hide DefaultStreamButton; export 'components/buttons/stream_emoji_button.dart' hide DefaultStreamEmojiButton; export 'components/common/stream_checkbox.dart' hide DefaultStreamCheckbox; export 'components/common/stream_flex.dart'; +export 'components/common/stream_loading_spinner.dart' hide DefaultStreamLoadingSpinner; export 'components/common/stream_progress_bar.dart' hide DefaultStreamProgressBar; export 'components/context_menu/stream_context_menu.dart'; export 'components/context_menu/stream_context_menu_action.dart' hide DefaultStreamContextMenuAction; diff --git a/packages/stream_core_flutter/lib/src/components/common/stream_loading_spinner.dart b/packages/stream_core_flutter/lib/src/components/common/stream_loading_spinner.dart new file mode 100644 index 00000000..dc551886 --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/common/stream_loading_spinner.dart @@ -0,0 +1,196 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +import '../../factory/stream_component_factory.dart'; +import '../../theme/semantics/stream_color_scheme.dart'; +import '../../theme/stream_theme_extensions.dart'; + +/// A circular loading spinner component. +/// +/// [StreamLoadingSpinner] displays a circular loading spinner that rotates +/// continuously. It supports customizing the size, stroke width, and colors. +class StreamLoadingSpinner extends StatelessWidget { + /// Creates a [StreamLoadingSpinner]. + StreamLoadingSpinner({ + super.key, + double? size, + double? strokeWidth, + Color? color, + Color? trackColor, + }) : props = StreamLoadingSpinnerProps( + size: size, + strokeWidth: strokeWidth, + color: color, + trackColor: trackColor, + ); + + /// The props controlling the appearance of this spinner. + final StreamLoadingSpinnerProps props; + + @override + Widget build(BuildContext context) { + final builder = StreamComponentFactory.of(context).loadingSpinner; + if (builder != null) return builder(context, props); + return DefaultStreamLoadingSpinner(props: props); + } +} + +/// Properties for configuring a [StreamLoadingSpinner]. +/// +/// This class holds all the configuration options for a loading spinner, +/// allowing them to be passed through the [StreamComponentFactory]. +/// +/// See also: +/// +/// * [StreamLoadingSpinner], which uses these properties. +/// * [DefaultStreamLoadingSpinner], the default implementation. +class StreamLoadingSpinnerProps { + /// Creates properties for a loading spinner. + const StreamLoadingSpinnerProps({ + this.size, + this.strokeWidth, + this.color, + this.trackColor, + }); + + /// The diameter of the spinner. + /// + /// If null, defaults to 20. + final double? size; + + /// The width of both the track and the animated arc. + /// + /// If null, defaults to 2. + final double? strokeWidth; + + /// The color of the animated arc. + /// + /// If null, uses [StreamColorScheme.accentPrimary]. + final Color? color; + + /// The color of the background track circle. + /// + /// If null, uses [StreamColorScheme.borderDefault]. + final Color? trackColor; +} + +/// Default implementation of [StreamLoadingSpinner]. +/// +/// Renders a circular spinner with a background track and a rotating arc +/// using [CustomPaint]. Styling is resolved from widget props and the +/// current [StreamColorScheme]. +/// +/// See also: +/// +/// * [StreamLoadingSpinner], the public API widget. +class DefaultStreamLoadingSpinner extends StatefulWidget { + /// Creates a default Stream loading spinner. + const DefaultStreamLoadingSpinner({super.key, required this.props}); + + /// The props controlling the appearance of this spinner. + final StreamLoadingSpinnerProps props; + + @override + State createState() => _DefaultStreamLoadingSpinnerState(); +} + +class _DefaultStreamLoadingSpinnerState extends State with SingleTickerProviderStateMixin { + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1000), + )..repeat(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final effectiveSize = widget.props.size ?? 20; + final effectiveStrokeWidth = widget.props.strokeWidth ?? 2; + final effectiveColor = widget.props.color ?? colorScheme.accentPrimary; + final effectiveTrackColor = widget.props.trackColor ?? colorScheme.borderDefault; + + return SizedBox.square( + dimension: effectiveSize, + child: AnimatedBuilder( + animation: _controller, + builder: (context, _) { + return CustomPaint( + painter: _SpinnerPainter( + progress: _controller.value, + color: effectiveColor, + trackColor: effectiveTrackColor, + strokeWidth: effectiveStrokeWidth, + ), + ); + }, + ), + ); + } +} + +class _SpinnerPainter extends CustomPainter { + _SpinnerPainter({ + required this.progress, + required this.color, + required this.trackColor, + required this.strokeWidth, + }); + + final double progress; + final Color color; + final Color trackColor; + final double strokeWidth; + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final radius = (size.width - strokeWidth) / 2; + + // Draw the background track. + final trackPaint = Paint() + ..color = trackColor + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth + ..strokeCap = StrokeCap.round; + + canvas.drawCircle(center, radius, trackPaint); + + // Draw the animated arc. + final arcPaint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth + ..strokeCap = StrokeCap.round; + + const sweepAngle = math.pi / 3; // 60 degrees + final startAngle = 2 * math.pi * progress - math.pi / 2; + + canvas.drawArc( + Rect.fromCircle(center: center, radius: radius), + startAngle, + sweepAngle, + false, + arcPaint, + ); + } + + @override + bool shouldRepaint(_SpinnerPainter oldDelegate) { + return oldDelegate.progress != progress || + oldDelegate.color != color || + oldDelegate.trackColor != trackColor || + oldDelegate.strokeWidth != strokeWidth; + } +} diff --git a/packages/stream_core_flutter/lib/src/factory/stream_component_factory.dart b/packages/stream_core_flutter/lib/src/factory/stream_component_factory.dart index 5caa76d3..bace8caf 100644 --- a/packages/stream_core_flutter/lib/src/factory/stream_component_factory.dart +++ b/packages/stream_core_flutter/lib/src/factory/stream_component_factory.dart @@ -146,6 +146,7 @@ class StreamComponentBuilders with _$StreamComponentBuilders { StreamComponentBuilder? emojiChipBar, StreamComponentBuilder? fileTypeIcon, StreamComponentBuilder? listTile, + StreamComponentBuilder? loadingSpinner, StreamComponentBuilder? onlineIndicator, StreamComponentBuilder? progressBar, StreamComponentBuilder? reactions, @@ -169,6 +170,7 @@ class StreamComponentBuilders with _$StreamComponentBuilders { emojiChipBar: emojiChipBar, fileTypeIcon: fileTypeIcon, listTile: listTile, + loadingSpinner: loadingSpinner, onlineIndicator: onlineIndicator, progressBar: progressBar, reactions: reactions, @@ -193,6 +195,7 @@ class StreamComponentBuilders with _$StreamComponentBuilders { required this.emojiChipBar, required this.fileTypeIcon, required this.listTile, + required this.loadingSpinner, required this.onlineIndicator, required this.progressBar, required this.reactions, @@ -294,6 +297,11 @@ class StreamComponentBuilders with _$StreamComponentBuilders { /// When null, [StreamListTile] uses [DefaultStreamListTile]. final StreamComponentBuilder? listTile; + /// Custom builder for loading spinner widgets. + /// + /// When null, [StreamLoadingSpinner] uses [DefaultStreamLoadingSpinner]. + final StreamComponentBuilder? loadingSpinner; + /// Custom builder for online indicator widgets. /// /// When null, [StreamOnlineIndicator] uses [DefaultStreamOnlineIndicator]. diff --git a/packages/stream_core_flutter/lib/src/factory/stream_component_factory.g.theme.dart b/packages/stream_core_flutter/lib/src/factory/stream_component_factory.g.theme.dart index 8e8e9732..bcf8c3e4 100644 --- a/packages/stream_core_flutter/lib/src/factory/stream_component_factory.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/factory/stream_component_factory.g.theme.dart @@ -46,6 +46,7 @@ mixin _$StreamComponentBuilders { emojiChipBar: t < 0.5 ? a.emojiChipBar : b.emojiChipBar, fileTypeIcon: t < 0.5 ? a.fileTypeIcon : b.fileTypeIcon, listTile: t < 0.5 ? a.listTile : b.listTile, + loadingSpinner: t < 0.5 ? a.loadingSpinner : b.loadingSpinner, onlineIndicator: t < 0.5 ? a.onlineIndicator : b.onlineIndicator, progressBar: t < 0.5 ? a.progressBar : b.progressBar, reactions: t < 0.5 ? a.reactions : b.reactions, @@ -72,6 +73,7 @@ mixin _$StreamComponentBuilders { emojiChipBar, Widget Function(BuildContext, StreamFileTypeIconProps)? fileTypeIcon, Widget Function(BuildContext, StreamListTileProps)? listTile, + Widget Function(BuildContext, StreamLoadingSpinnerProps)? loadingSpinner, Widget Function(BuildContext, StreamOnlineIndicatorProps)? onlineIndicator, Widget Function(BuildContext, StreamProgressBarProps)? progressBar, Widget Function(BuildContext, StreamReactionsProps)? reactions, @@ -95,6 +97,7 @@ mixin _$StreamComponentBuilders { emojiChipBar: emojiChipBar ?? _this.emojiChipBar, fileTypeIcon: fileTypeIcon ?? _this.fileTypeIcon, listTile: listTile ?? _this.listTile, + loadingSpinner: loadingSpinner ?? _this.loadingSpinner, onlineIndicator: onlineIndicator ?? _this.onlineIndicator, progressBar: progressBar ?? _this.progressBar, reactions: reactions ?? _this.reactions, @@ -129,6 +132,7 @@ mixin _$StreamComponentBuilders { emojiChipBar: other.emojiChipBar, fileTypeIcon: other.fileTypeIcon, listTile: other.listTile, + loadingSpinner: other.loadingSpinner, onlineIndicator: other.onlineIndicator, progressBar: other.progressBar, reactions: other.reactions, @@ -164,6 +168,7 @@ mixin _$StreamComponentBuilders { _other.emojiChipBar == _this.emojiChipBar && _other.fileTypeIcon == _this.fileTypeIcon && _other.listTile == _this.listTile && + _other.loadingSpinner == _this.loadingSpinner && _other.onlineIndicator == _this.onlineIndicator && _other.progressBar == _this.progressBar && _other.reactions == _this.reactions; @@ -173,7 +178,7 @@ mixin _$StreamComponentBuilders { int get hashCode { final _this = (this as StreamComponentBuilders); - return Object.hash( + return Object.hashAll([ runtimeType, _this.extensions, _this.avatar, @@ -191,9 +196,10 @@ mixin _$StreamComponentBuilders { _this.emojiChipBar, _this.fileTypeIcon, _this.listTile, + _this.loadingSpinner, _this.onlineIndicator, _this.progressBar, _this.reactions, - ); + ]); } } diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_message_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_message_theme.g.theme.dart index 28b89d4e..94bb2fad 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_message_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_message_theme.g.theme.dart @@ -140,6 +140,7 @@ mixin _$StreamMessageStyle { t, ), textSystemColor: Color.lerp(a.textSystemColor, b.textSystemColor, t), + textReadColor: Color.lerp(a.textReadColor, b.textReadColor, t), borderColor: Color.lerp(a.borderColor, b.borderColor, t), borderOnChatColor: Color.lerp( a.borderOnChatColor, @@ -186,6 +187,7 @@ mixin _$StreamMessageStyle { Color? textLinkColor, Color? textReactionColor, Color? textSystemColor, + Color? textReadColor, Color? borderColor, Color? borderOnChatColor, Color? threadConnectorColor, @@ -211,6 +213,7 @@ mixin _$StreamMessageStyle { textLinkColor: textLinkColor ?? _this.textLinkColor, textReactionColor: textReactionColor ?? _this.textReactionColor, textSystemColor: textSystemColor ?? _this.textSystemColor, + textReadColor: textReadColor ?? _this.textReadColor, borderColor: borderColor ?? _this.borderColor, borderOnChatColor: borderOnChatColor ?? _this.borderOnChatColor, threadConnectorColor: threadConnectorColor ?? _this.threadConnectorColor, @@ -245,6 +248,7 @@ mixin _$StreamMessageStyle { textLinkColor: other.textLinkColor, textReactionColor: other.textReactionColor, textSystemColor: other.textSystemColor, + textReadColor: other.textReadColor, borderColor: other.borderColor, borderOnChatColor: other.borderOnChatColor, threadConnectorColor: other.threadConnectorColor, @@ -280,6 +284,7 @@ mixin _$StreamMessageStyle { _other.textLinkColor == _this.textLinkColor && _other.textReactionColor == _this.textReactionColor && _other.textSystemColor == _this.textSystemColor && + _other.textReadColor == _this.textReadColor && _other.borderColor == _this.borderColor && _other.borderOnChatColor == _this.borderOnChatColor && _other.threadConnectorColor == _this.threadConnectorColor && @@ -306,6 +311,7 @@ mixin _$StreamMessageStyle { _this.textLinkColor, _this.textReactionColor, _this.textSystemColor, + _this.textReadColor, _this.borderColor, _this.borderOnChatColor, _this.threadConnectorColor, From 97ab9b378f036e22c18f9d2adb9d41b4a4c21529 Mon Sep 17 00:00:00 2001 From: Brazol Date: Thu, 19 Mar 2026 10:25:42 +0100 Subject: [PATCH 2/3] fix --- .../lib/components/common/stream_loading_spinner.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/design_system_gallery/lib/components/common/stream_loading_spinner.dart b/apps/design_system_gallery/lib/components/common/stream_loading_spinner.dart index 88a56911..cfda77a7 100644 --- a/apps/design_system_gallery/lib/components/common/stream_loading_spinner.dart +++ b/apps/design_system_gallery/lib/components/common/stream_loading_spinner.dart @@ -242,7 +242,7 @@ class _ColorVariantsSection extends StatelessWidget { SizedBox(height: spacing.md), Row( children: [ - _ColorDemo(label: 'Default', size: 32), + const _ColorDemo(label: 'Default', size: 32), SizedBox(width: spacing.xl), _ColorDemo(label: 'Success', size: 32, color: colorScheme.accentSuccess), SizedBox(width: spacing.xl), From 0142061e3ca351b9130a58225430a677388cdc2f Mon Sep 17 00:00:00 2001 From: Brazol Date: Thu, 19 Mar 2026 10:30:57 +0100 Subject: [PATCH 3/3] fix --- .../lib/src/factory/stream_component_factory.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_core_flutter/lib/src/factory/stream_component_factory.dart b/packages/stream_core_flutter/lib/src/factory/stream_component_factory.dart index 10810af8..42381e62 100644 --- a/packages/stream_core_flutter/lib/src/factory/stream_component_factory.dart +++ b/packages/stream_core_flutter/lib/src/factory/stream_component_factory.dart @@ -322,7 +322,7 @@ class StreamComponentBuilders with _$StreamComponentBuilders { /// /// When null, [StreamLoadingSpinner] uses [DefaultStreamLoadingSpinner]. final StreamComponentBuilder? loadingSpinner; - + /// Custom builder for message annotation widgets. /// /// When null, [StreamMessageAnnotation] uses [DefaultStreamMessageAnnotation].