From 6cf5347154f5e4bbe4108e5e59693c54314e7317 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 26 Feb 2026 09:53:57 +0100 Subject: [PATCH 01/15] add channel list item --- .../lib/app/gallery_app.directories.g.dart | 24 + .../stream_channel_list_item.dart | 409 ++++++++++++++++++ .../lib/src/components.dart | 1 + .../lib/src/components/channel_list.dart | 1 + .../stream_channel_list_item.dart | 372 ++++++++++++++++ .../src/factory/stream_component_factory.dart | 6 + .../stream_component_factory.g.theme.dart | 6 + .../stream_core_flutter/lib/src/theme.dart | 1 + .../stream_channel_list_item_theme.dart | 140 ++++++ ...tream_channel_list_item_theme.g.theme.dart | 128 ++++++ .../lib/src/theme/stream_theme.dart | 9 + .../lib/src/theme/stream_theme.g.theme.dart | 13 +- .../src/theme/stream_theme_extensions.dart | 4 + 13 files changed, 1112 insertions(+), 2 deletions(-) create mode 100644 apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart create mode 100644 packages/stream_core_flutter/lib/src/components/channel_list.dart create mode 100644 packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart create mode 100644 packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.dart create mode 100644 packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.g.theme.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 acaa5b8..123abfb 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 @@ -28,6 +28,8 @@ import 'package:design_system_gallery/components/buttons/button.dart' as _design_system_gallery_components_buttons_button; import 'package:design_system_gallery/components/buttons/stream_emoji_button.dart' as _design_system_gallery_components_buttons_stream_emoji_button; +import 'package:design_system_gallery/components/channel_list/stream_channel_list_item.dart' + as _design_system_gallery_components_channel_list_stream_channel_list_item; 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_progress_bar.dart' @@ -336,6 +338,28 @@ final directories = <_widgetbook.WidgetbookNode>[ ), ], ), + _widgetbook.WidgetbookFolder( + name: 'Channel List', + children: [ + _widgetbook.WidgetbookComponent( + name: 'StreamChannelListItem', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'Playground', + builder: + _design_system_gallery_components_channel_list_stream_channel_list_item + .buildStreamChannelListItemPlayground, + ), + _widgetbook.WidgetbookUseCase( + name: 'Showcase', + builder: + _design_system_gallery_components_channel_list_stream_channel_list_item + .buildStreamChannelListItemShowcase, + ), + ], + ), + ], + ), _widgetbook.WidgetbookFolder( name: 'Common', children: [ diff --git a/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart b/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart new file mode 100644 index 0000000..7deba98 --- /dev/null +++ b/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart @@ -0,0 +1,409 @@ +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; + +const _sampleImageUrl = 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200'; + +// ============================================================================= +// Playground +// ============================================================================= + +@widgetbook.UseCase( + name: 'Playground', + type: StreamChannelListItem, + path: '[Components]/Channel List', +) +Widget buildStreamChannelListItemPlayground(BuildContext context) { + final title = context.knobs.string( + label: 'Title', + initialValue: 'Design Team', + description: 'The channel name.', + ); + + final subtitle = context.knobs.stringOrNull( + label: 'Subtitle', + initialValue: 'New mockups ready for review', + description: 'The message preview text.', + ); + + final timestamp = context.knobs.stringOrNull( + label: 'Timestamp', + initialValue: '9:41', + description: 'The formatted timestamp.', + ); + + final unreadCount = context.knobs.int.slider( + label: 'Unread Count', + initialValue: 3, + max: 99, + description: 'Number of unread messages. Shows a badge when > 0.', + ); + + final showMuteIcon = context.knobs.boolean( + label: 'Show Mute Icon', + description: 'Display a mute icon after the title.', + ); + + final colorScheme = context.streamColorScheme; + final icons = context.streamIcons; + + return ColoredBox( + color: colorScheme.backgroundApp, + child: Center( + child: StreamChannelListItem( + avatar: StreamOnlineIndicator( + isOnline: true, + child: StreamAvatar( + imageUrl: _sampleImageUrl, + size: StreamAvatarSize.xl, + placeholder: (context) => const Text('DT'), + ), + ), + title: title, + titleTrailing: showMuteIcon ? Icon(icons.mute, size: 16, color: colorScheme.textTertiary) : null, + subtitle: subtitle, + timestamp: timestamp, + unreadCount: unreadCount, + ), + ), + ); +} + +// ============================================================================= +// Showcase +// ============================================================================= + +@widgetbook.UseCase( + name: 'Showcase', + type: StreamChannelListItem, + path: '[Components]/Channel List', +) +Widget buildStreamChannelListItemShowcase(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return DefaultTextStyle( + style: textTheme.bodyDefault.copyWith(color: colorScheme.textPrimary), + child: ColoredBox( + color: colorScheme.backgroundApp, + child: SingleChildScrollView( + padding: EdgeInsets.all(spacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _DirectMessageSection(), + SizedBox(height: spacing.xl), + const _GroupChannelSection(), + SizedBox(height: spacing.xl), + const _EdgeCasesSection(), + ], + ), + ), + ), + ); +} + +// ============================================================================= +// Direct Message Section +// ============================================================================= + +class _DirectMessageSection extends StatelessWidget { + const _DirectMessageSection(); + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final spacing = context.streamSpacing; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'DIRECT MESSAGES'), + SizedBox(height: spacing.md), + _ShowcaseCard( + description: 'One-on-one conversations with online indicator', + child: Column( + children: [ + StreamChannelListItem( + avatar: StreamOnlineIndicator( + isOnline: true, + child: StreamAvatar( + imageUrl: _sampleImageUrl, + size: StreamAvatarSize.xl, + placeholder: (context) => const Text('JD'), + ), + ), + title: 'Jane Doe', + subtitle: 'Hey! Are you free for a call?', + timestamp: '9:41', + unreadCount: 3, + ), + StreamChannelListItem( + avatar: StreamOnlineIndicator( + isOnline: false, + child: StreamAvatar( + size: StreamAvatarSize.xl, + placeholder: (context) => const Text('BS'), + ), + ), + title: 'Bob Smith', + subtitle: 'Thanks for the update!', + timestamp: 'Yesterday', + ), + StreamChannelListItem( + avatar: StreamOnlineIndicator( + isOnline: true, + child: StreamAvatar( + size: StreamAvatarSize.xl, + backgroundColor: colorScheme.avatarPalette[2].backgroundColor, + foregroundColor: colorScheme.avatarPalette[2].foregroundColor, + placeholder: (context) => const Text('CW'), + ), + ), + title: 'Carol White', + subtitle: 'See you tomorrow!', + timestamp: 'Saturday', + ), + ], + ), + ), + ], + ); + } +} + +// ============================================================================= +// Group Channel Section +// ============================================================================= + +class _GroupChannelSection extends StatelessWidget { + const _GroupChannelSection(); + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final icons = context.streamIcons; + final spacing = context.streamSpacing; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'GROUP CHANNELS'), + SizedBox(height: spacing.md), + _ShowcaseCard( + description: 'Group conversations with sender prefix and mute icon', + child: Column( + children: [ + StreamChannelListItem( + avatar: StreamAvatarGroup( + size: StreamAvatarGroupSize.xl, + children: [ + StreamAvatar(placeholder: (context) => const Text('JD')), + StreamAvatar( + imageUrl: _sampleImageUrl, + placeholder: (context) => const Text('AB'), + ), + StreamAvatar( + backgroundColor: colorScheme.avatarPalette[1].backgroundColor, + foregroundColor: colorScheme.avatarPalette[1].foregroundColor, + placeholder: (context) => const Text('CW'), + ), + ], + ), + title: 'Design Team', + subtitle: 'Alice: New mockups ready for review', + timestamp: '9:41', + unreadCount: 5, + ), + StreamChannelListItem( + avatar: StreamAvatarGroup( + size: StreamAvatarGroupSize.xl, + children: [ + StreamAvatar(placeholder: (context) => const Text('EF')), + StreamAvatar(placeholder: (context) => const Text('GH')), + ], + ), + title: 'Engineering', + subtitle: 'Bob: PR merged successfully', + timestamp: '10:15', + unreadCount: 12, + ), + StreamChannelListItem( + avatar: StreamAvatarGroup( + size: StreamAvatarGroupSize.xl, + children: [ + StreamAvatar(placeholder: (context) => const Text('IJ')), + StreamAvatar(placeholder: (context) => const Text('KL')), + StreamAvatar(placeholder: (context) => const Text('MN')), + ], + ), + title: 'Muted Group', + titleTrailing: Icon( + icons.mute, + size: 16, + color: colorScheme.textTertiary, + ), + subtitle: 'Carol: Meeting notes attached', + timestamp: 'Yesterday', + ), + ], + ), + ), + ], + ); + } +} + +// ============================================================================= +// Edge Cases Section +// ============================================================================= + +class _EdgeCasesSection extends StatelessWidget { + const _EdgeCasesSection(); + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final spacing = context.streamSpacing; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'EDGE CASES'), + SizedBox(height: spacing.md), + _ShowcaseCard( + description: 'Long text, no unread, and minimal content', + child: Column( + children: [ + StreamChannelListItem( + avatar: StreamAvatar( + size: StreamAvatarSize.xl, + placeholder: (context) => const Text('LT'), + ), + title: 'Very Long Channel Name That Should Be Truncated Properly', + subtitle: + 'This is a very long message preview that should be truncated with an ellipsis when it overflows', + timestamp: '01/15/2026', + unreadCount: 99, + ), + StreamChannelListItem( + avatar: StreamAvatar( + size: StreamAvatarSize.xl, + backgroundColor: colorScheme.avatarPalette[3].backgroundColor, + foregroundColor: colorScheme.avatarPalette[3].foregroundColor, + placeholder: (context) => const Text('NR'), + ), + title: 'No Unread', + subtitle: 'All caught up!', + timestamp: '3:00', + ), + StreamChannelListItem( + avatar: StreamAvatar( + size: StreamAvatarSize.xl, + placeholder: (context) => const Text('MN'), + ), + title: 'Minimal', + ), + ], + ), + ), + ], + ); + } +} + +// ============================================================================= +// Shared Widgets +// ============================================================================= + +class _ShowcaseCard extends StatelessWidget { + const _ShowcaseCard({ + required this.description, + required this.child, + }); + + final String description; + final Widget child; + + @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 Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + 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: [ + Padding( + padding: EdgeInsets.fromLTRB( + spacing.md, + spacing.sm, + spacing.md, + spacing.sm, + ), + child: Text( + description, + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + ), + Divider(height: 1, color: colorScheme.borderSubtle), + ColoredBox( + color: colorScheme.backgroundApp, + child: child, + ), + ], + ), + ); + } +} + +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 13fda19..66f60d7 100644 --- a/packages/stream_core_flutter/lib/src/components.dart +++ b/packages/stream_core_flutter/lib/src/components.dart @@ -8,6 +8,7 @@ export 'components/badge/stream_media_badge.dart'; export 'components/badge/stream_online_indicator.dart' hide DefaultStreamOnlineIndicator; export 'components/buttons/stream_button.dart' hide DefaultStreamButton; export 'components/buttons/stream_emoji_button.dart' hide DefaultStreamEmojiButton; +export 'components/channel_list.dart' hide DefaultStreamChannelListItem; export 'components/common/stream_checkbox.dart' hide DefaultStreamCheckbox; export 'components/common/stream_progress_bar.dart' hide DefaultStreamProgressBar; export 'components/context_menu/stream_context_menu.dart'; diff --git a/packages/stream_core_flutter/lib/src/components/channel_list.dart b/packages/stream_core_flutter/lib/src/components/channel_list.dart new file mode 100644 index 0000000..2913701 --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/channel_list.dart @@ -0,0 +1 @@ +export 'channel_list/stream_channel_list_item.dart'; diff --git a/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart b/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart new file mode 100644 index 0000000..fb7944d --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart @@ -0,0 +1,372 @@ +import 'package:flutter/material.dart'; + +import '../../factory/stream_component_factory.dart'; +import '../../theme/components/stream_channel_list_item_theme.dart'; +import '../../theme/primitives/stream_spacing.dart'; +import '../../theme/semantics/stream_color_scheme.dart'; +import '../../theme/semantics/stream_text_theme.dart'; +import '../../theme/stream_theme_extensions.dart'; +import '../badge/stream_badge_count.dart'; + +/// A list item for displaying a channel in a channel list. +/// +/// [StreamChannelListItem] displays a channel's avatar, title, message preview, +/// timestamp, and unread count in a standard list item layout. +/// +/// The [avatar] is passed as a widget, allowing full customization of +/// the avatar appearance (e.g., single user avatar, group avatar, or +/// avatar with online indicator). +/// +/// {@tool snippet} +/// +/// Basic usage: +/// +/// ```dart +/// StreamChannelListItem( +/// avatar: StreamAvatar(placeholder: (context) => Text('AB')), +/// title: 'General', +/// subtitle: 'Hello, how are you?', +/// timestamp: '9:41', +/// unreadCount: 3, +/// ) +/// ``` +/// {@end-tool} +/// +/// {@tool snippet} +/// +/// With a mute icon after the title: +/// +/// ```dart +/// StreamChannelListItem( +/// avatar: StreamAvatar(placeholder: (context) => Text('AB')), +/// title: 'Muted Channel', +/// titleTrailing: Icon(Icons.volume_off, size: 16), +/// subtitle: 'Last message...', +/// timestamp: 'Yesterday', +/// ) +/// ``` +/// {@end-tool} +/// +/// ## Theming +/// +/// [StreamChannelListItem] uses [StreamChannelListItemThemeData] for default +/// styling. Colors and text styles are determined by the current +/// [StreamColorScheme] and [StreamTextTheme]. +/// +/// See also: +/// +/// * [StreamChannelListItemThemeData], for customizing appearance. +/// * [StreamChannelListItemTheme], for overriding theme in a widget subtree. +/// * [StreamBadgeCount], which displays the unread count badge. +class StreamChannelListItem extends StatelessWidget { + /// Creates a channel list item. + StreamChannelListItem({ + super.key, + required Widget avatar, + required String title, + Widget? titleTrailing, + String? subtitle, + Widget? subtitleTrailing, + String? timestamp, + int unreadCount = 0, + VoidCallback? onTap, + VoidCallback? onLongPress, + }) : props = StreamChannelListItemProps( + avatar: avatar, + title: title, + titleTrailing: titleTrailing, + subtitle: subtitle, + subtitleTrailing: subtitleTrailing, + timestamp: timestamp, + unreadCount: unreadCount, + onTap: onTap, + onLongPress: onLongPress, + ); + + /// The properties that configure this channel list item. + final StreamChannelListItemProps props; + + @override + Widget build(BuildContext context) { + final builder = StreamComponentFactory.maybeOf(context)?.channelListItem; + if (builder != null) return builder(context, props); + return DefaultStreamChannelListItem(props: props); + } +} + +/// Properties for configuring a [StreamChannelListItem]. +/// +/// This class holds all the configuration options for a channel list item, +/// allowing them to be passed through the [StreamComponentFactory]. +/// +/// See also: +/// +/// * [StreamChannelListItem], which uses these properties. +/// * [DefaultStreamChannelListItem], the default implementation. +class StreamChannelListItemProps { + /// Creates properties for a channel list item. + const StreamChannelListItemProps({ + required this.avatar, + required this.title, + this.titleTrailing, + this.subtitle, + this.subtitleTrailing, + this.timestamp, + this.unreadCount = 0, + this.onTap, + this.onLongPress, + }); + + /// The avatar widget displayed at the leading edge. + /// + /// Typically a [StreamAvatar], [StreamAvatarGroup], or an avatar wrapped + /// in a [StreamOnlineIndicator]. + final Widget avatar; + + /// The channel title text. + final String title; + + /// An optional widget displayed after the title text. + /// + /// Typically used for a mute icon or similar indicator. + final Widget? titleTrailing; + + /// The message preview text displayed below the title. + final String? subtitle; + + /// An optional trailing widget in the subtitle row. + /// + /// Typically used for a mute icon or similar indicator. + final Widget? subtitleTrailing; + + /// The formatted timestamp string. + /// + /// Displayed in the trailing section of the title row. + final String? timestamp; + + /// The number of unread messages. + /// + /// When greater than zero, a [StreamBadgeCount] is displayed. + final int unreadCount; + + /// Called when the list item is tapped. + final VoidCallback? onTap; + + /// Called when the list item is long-pressed. + final VoidCallback? onLongPress; +} + +/// The default implementation of [StreamChannelListItem]. +/// +/// This widget renders the channel list item with theming support. +/// It's used as the default factory implementation in [StreamComponentFactory]. +/// +/// See also: +/// +/// * [StreamChannelListItem], the public API widget. +/// * [StreamChannelListItemProps], which configures this widget. +class DefaultStreamChannelListItem extends StatefulWidget { + /// Creates a default channel list item with the given [props]. + const DefaultStreamChannelListItem({super.key, required this.props}); + + /// The properties that configure this channel list item. + final StreamChannelListItemProps props; + + @override + State createState() => _DefaultStreamChannelListItemState(); +} + +class _DefaultStreamChannelListItemState extends State { + var _isHovered = false; + var _isPressed = false; + + StreamChannelListItemProps get props => widget.props; + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + final channelListItemTheme = context.streamChannelListItemTheme; + final defaults = _StreamChannelListItemThemeDefaults(context); + + final effectiveBackgroundColor = channelListItemTheme.backgroundColor ?? defaults.backgroundColor; + final effectiveHoverColor = channelListItemTheme.hoverColor ?? defaults.hoverColor; + final effectivePressedColor = channelListItemTheme.pressedColor ?? defaults.pressedColor; + final effectiveBorderColor = channelListItemTheme.borderColor ?? defaults.borderColor; + final effectiveTitleStyle = channelListItemTheme.titleStyle ?? defaults.titleStyle; + final effectiveSubtitleStyle = channelListItemTheme.subtitleStyle ?? defaults.subtitleStyle; + final effectiveTimestampStyle = channelListItemTheme.timestampStyle ?? defaults.timestampStyle; + + final background = _isPressed + ? effectivePressedColor + : _isHovered + ? effectiveHoverColor + : effectiveBackgroundColor; + + return MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: GestureDetector( + onTap: props.onTap, + onLongPress: props.onLongPress, + onTapDown: (_) => setState(() => _isPressed = true), + onTapUp: (_) => setState(() => _isPressed = false), + onTapCancel: () => setState(() => _isPressed = false), + behavior: HitTestBehavior.opaque, + child: Container( + decoration: BoxDecoration( + color: background, + borderRadius: BorderRadius.circular(spacing.lg), + ), + padding: EdgeInsets.all(spacing.md - 4), + margin: const EdgeInsets.all(4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: spacing.md, + children: [ + props.avatar, + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(vertical: spacing.xxxs), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: spacing.xxs, + children: [ + _TitleRow( + title: props.title, + titleTrailing: props.titleTrailing, + timestamp: props.timestamp, + unreadCount: props.unreadCount, + titleStyle: effectiveTitleStyle, + timestampStyle: effectiveTimestampStyle, + spacing: spacing, + ), + if (props.subtitle != null) + _SubtitleRow( + subtitle: props.subtitle!, + subtitleTrailing: props.subtitleTrailing, + subtitleStyle: effectiveSubtitleStyle, + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _TitleRow extends StatelessWidget { + const _TitleRow({ + required this.title, + this.titleTrailing, + this.timestamp, + required this.unreadCount, + required this.titleStyle, + required this.timestampStyle, + required this.spacing, + }); + + final String title; + final Widget? titleTrailing; + final String? timestamp; + final int unreadCount; + final TextStyle titleStyle; + final TextStyle timestampStyle; + final StreamSpacing spacing; + + @override + Widget build(BuildContext context) { + return Row( + spacing: spacing.md, + children: [ + Expanded( + child: Row( + spacing: spacing.xxs, + children: [ + Flexible( + child: Text( + title, + style: titleStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ?titleTrailing, + ], + ), + ), + if (timestamp != null || unreadCount > 0) + Row( + mainAxisSize: MainAxisSize.min, + spacing: spacing.xs, + children: [ + if (timestamp != null) Text(timestamp!, style: timestampStyle), + if (unreadCount > 0) StreamBadgeCount(label: '$unreadCount'), + ], + ), + ], + ); + } +} + +class _SubtitleRow extends StatelessWidget { + const _SubtitleRow({ + required this.subtitle, + this.subtitleTrailing, + required this.subtitleStyle, + }); + + final String subtitle; + final Widget? subtitleTrailing; + final TextStyle subtitleStyle; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Text( + subtitle, + style: subtitleStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ?subtitleTrailing, + ], + ); + } +} + +class _StreamChannelListItemThemeDefaults extends StreamChannelListItemThemeData { + _StreamChannelListItemThemeDefaults(this._context); + + final BuildContext _context; + + late final _colorScheme = _context.streamColorScheme; + late final _textTheme = _context.streamTextTheme; + + @override + TextStyle get titleStyle => _textTheme.headingSm.copyWith(color: _colorScheme.textPrimary); + + @override + TextStyle get subtitleStyle => _textTheme.captionDefault.copyWith(color: _colorScheme.textSecondary); + + @override + TextStyle get timestampStyle => _textTheme.captionDefault.copyWith(color: _colorScheme.textTertiary); + + @override + Color get backgroundColor => _colorScheme.backgroundApp; + + @override + Color get hoverColor => _colorScheme.stateHover; + + @override + Color get pressedColor => _colorScheme.statePressed; + + @override + Color get borderColor => _colorScheme.borderSubtle; +} 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 0bde159..589d206 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 @@ -155,6 +155,7 @@ class StreamComponentBuilders with _$StreamComponentBuilders { this.avatarStack, this.badgeCount, this.button, + this.channelListItem, this.checkbox, this.contextMenuAction, this.emoji, @@ -189,6 +190,11 @@ class StreamComponentBuilders with _$StreamComponentBuilders { /// When null, [StreamButton] uses [DefaultStreamButton]. final StreamComponentBuilder? button; + /// Custom builder for channel list item widgets. + /// + /// When null, [StreamChannelListItem] uses [DefaultStreamChannelListItem]. + final StreamComponentBuilder? channelListItem; + /// Custom builder for checkbox widgets. /// /// When null, [StreamCheckbox] uses [DefaultStreamCheckbox]. 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 d9ea8d1..9466048 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 @@ -35,6 +35,7 @@ mixin _$StreamComponentBuilders { avatarStack: t < 0.5 ? a.avatarStack : b.avatarStack, badgeCount: t < 0.5 ? a.badgeCount : b.badgeCount, button: t < 0.5 ? a.button : b.button, + channelListItem: t < 0.5 ? a.channelListItem : b.channelListItem, checkbox: t < 0.5 ? a.checkbox : b.checkbox, contextMenuAction: t < 0.5 ? a.contextMenuAction : b.contextMenuAction, emoji: t < 0.5 ? a.emoji : b.emoji, @@ -51,6 +52,7 @@ mixin _$StreamComponentBuilders { Widget Function(BuildContext, StreamAvatarStackProps)? avatarStack, Widget Function(BuildContext, StreamBadgeCountProps)? badgeCount, Widget Function(BuildContext, StreamButtonProps)? button, + Widget Function(BuildContext, StreamChannelListItemProps)? channelListItem, Widget Function(BuildContext, StreamCheckboxProps)? checkbox, Widget Function(BuildContext, StreamContextMenuActionProps)? contextMenuAction, @@ -68,6 +70,7 @@ mixin _$StreamComponentBuilders { avatarStack: avatarStack ?? _this.avatarStack, badgeCount: badgeCount ?? _this.badgeCount, button: button ?? _this.button, + channelListItem: channelListItem ?? _this.channelListItem, checkbox: checkbox ?? _this.checkbox, contextMenuAction: contextMenuAction ?? _this.contextMenuAction, emoji: emoji ?? _this.emoji, @@ -95,6 +98,7 @@ mixin _$StreamComponentBuilders { avatarStack: other.avatarStack, badgeCount: other.badgeCount, button: other.button, + channelListItem: other.channelListItem, checkbox: other.checkbox, contextMenuAction: other.contextMenuAction, emoji: other.emoji, @@ -123,6 +127,7 @@ mixin _$StreamComponentBuilders { _other.avatarStack == _this.avatarStack && _other.badgeCount == _this.badgeCount && _other.button == _this.button && + _other.channelListItem == _this.channelListItem && _other.checkbox == _this.checkbox && _other.contextMenuAction == _this.contextMenuAction && _other.emoji == _this.emoji && @@ -143,6 +148,7 @@ mixin _$StreamComponentBuilders { _this.avatarStack, _this.badgeCount, _this.button, + _this.channelListItem, _this.checkbox, _this.contextMenuAction, _this.emoji, diff --git a/packages/stream_core_flutter/lib/src/theme.dart b/packages/stream_core_flutter/lib/src/theme.dart index 78452e7..e08166d 100644 --- a/packages/stream_core_flutter/lib/src/theme.dart +++ b/packages/stream_core_flutter/lib/src/theme.dart @@ -3,6 +3,7 @@ export 'factory/stream_component_factory.dart'; export 'theme/components/stream_avatar_theme.dart'; export 'theme/components/stream_badge_count_theme.dart'; export 'theme/components/stream_button_theme.dart'; +export 'theme/components/stream_channel_list_item_theme.dart'; export 'theme/components/stream_checkbox_theme.dart'; export 'theme/components/stream_context_menu_action_theme.dart'; export 'theme/components/stream_context_menu_theme.dart'; diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.dart new file mode 100644 index 0000000..234f651 --- /dev/null +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.dart @@ -0,0 +1,140 @@ +import 'package:flutter/widgets.dart'; +import 'package:theme_extensions_builder_annotation/theme_extensions_builder_annotation.dart'; + +import '../stream_theme.dart'; + +part 'stream_channel_list_item_theme.g.theme.dart'; + +/// Applies a channel list item theme to descendant +/// [StreamChannelListItem] widgets. +/// +/// Wrap a subtree with [StreamChannelListItemTheme] to override styling. +/// Access the merged theme using [BuildContext.streamChannelListItemTheme]. +/// +/// {@tool snippet} +/// +/// Override channel list item colors for a specific section: +/// +/// ```dart +/// StreamChannelListItemTheme( +/// data: StreamChannelListItemThemeData( +/// backgroundColor: Colors.grey.shade50, +/// ), +/// child: StreamChannelListItem( +/// avatar: StreamAvatar(...), +/// title: 'General', +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamChannelListItemThemeData], which describes the theme. +/// * [StreamChannelListItem], the widget affected by this theme. +class StreamChannelListItemTheme extends InheritedTheme { + /// Creates a channel list item theme that controls descendant widgets. + const StreamChannelListItemTheme({ + super.key, + required this.data, + required super.child, + }); + + /// The channel list item theme data for descendant widgets. + final StreamChannelListItemThemeData data; + + /// Returns the [StreamChannelListItemThemeData] merged from local and + /// global themes. + /// + /// Local values from the nearest [StreamChannelListItemTheme] ancestor + /// take precedence over global values from [StreamTheme.of]. + static StreamChannelListItemThemeData of(BuildContext context) { + final localTheme = context.dependOnInheritedWidgetOfExactType(); + return StreamTheme.of(context).channelListItemTheme.merge(localTheme?.data); + } + + @override + Widget wrap(BuildContext context, Widget child) { + return StreamChannelListItemTheme(data: data, child: child); + } + + @override + bool updateShouldNotify(StreamChannelListItemTheme oldWidget) => data != oldWidget.data; +} + +/// Theme data for customizing [StreamChannelListItem] widgets. +/// +/// {@tool snippet} +/// +/// Customize channel list item appearance globally: +/// +/// ```dart +/// StreamTheme( +/// channelListItemTheme: StreamChannelListItemThemeData( +/// backgroundColor: Colors.white, +/// borderColor: Colors.grey.shade200, +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamChannelListItem], the widget that uses this theme data. +/// * [StreamChannelListItemTheme], for overriding theme in a widget subtree. +@themeGen +@immutable +class StreamChannelListItemThemeData with _$StreamChannelListItemThemeData { + /// Creates a channel list item theme with optional style overrides. + const StreamChannelListItemThemeData({ + this.titleStyle, + this.subtitleStyle, + this.timestampStyle, + this.backgroundColor, + this.hoverColor, + this.pressedColor, + this.borderColor, + }); + + /// The text style for the channel title. + /// + /// Falls back to [StreamTextTheme.headingSm] with [StreamColorScheme.textPrimary]. + final TextStyle? titleStyle; + + /// The text style for the message preview subtitle. + /// + /// Falls back to [StreamTextTheme.captionDefault] with [StreamColorScheme.textSecondary]. + final TextStyle? subtitleStyle; + + /// The text style for the timestamp. + /// + /// Falls back to [StreamTextTheme.captionDefault] with [StreamColorScheme.textTertiary]. + final TextStyle? timestampStyle; + + /// The background color of the list item in its default state. + /// + /// Falls back to [StreamColorScheme.backgroundApp]. + final Color? backgroundColor; + + /// The overlay color applied when the list item is hovered. + /// + /// Falls back to [StreamColorScheme.stateHover]. + final Color? hoverColor; + + /// The overlay color applied when the list item is pressed. + /// + /// Falls back to [StreamColorScheme.statePressed]. + final Color? pressedColor; + + /// The bottom border color of the list item. + /// + /// Falls back to [StreamColorScheme.borderSubtle]. + final Color? borderColor; + + /// Linearly interpolate between two [StreamChannelListItemThemeData] objects. + static StreamChannelListItemThemeData? lerp( + StreamChannelListItemThemeData? a, + StreamChannelListItemThemeData? b, + double t, + ) => _$StreamChannelListItemThemeData.lerp(a, b, t); +} diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.g.theme.dart new file mode 100644 index 0000000..388e489 --- /dev/null +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.g.theme.dart @@ -0,0 +1,128 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_element + +part of 'stream_channel_list_item_theme.dart'; + +// ************************************************************************** +// ThemeGenGenerator +// ************************************************************************** + +mixin _$StreamChannelListItemThemeData { + bool get canMerge => true; + + static StreamChannelListItemThemeData? lerp( + StreamChannelListItemThemeData? a, + StreamChannelListItemThemeData? 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 StreamChannelListItemThemeData( + titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), + subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t), + timestampStyle: TextStyle.lerp(a.timestampStyle, b.timestampStyle, t), + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + hoverColor: Color.lerp(a.hoverColor, b.hoverColor, t), + pressedColor: Color.lerp(a.pressedColor, b.pressedColor, t), + borderColor: Color.lerp(a.borderColor, b.borderColor, t), + ); + } + + StreamChannelListItemThemeData copyWith({ + TextStyle? titleStyle, + TextStyle? subtitleStyle, + TextStyle? timestampStyle, + Color? backgroundColor, + Color? hoverColor, + Color? pressedColor, + Color? borderColor, + }) { + final _this = (this as StreamChannelListItemThemeData); + + return StreamChannelListItemThemeData( + titleStyle: titleStyle ?? _this.titleStyle, + subtitleStyle: subtitleStyle ?? _this.subtitleStyle, + timestampStyle: timestampStyle ?? _this.timestampStyle, + backgroundColor: backgroundColor ?? _this.backgroundColor, + hoverColor: hoverColor ?? _this.hoverColor, + pressedColor: pressedColor ?? _this.pressedColor, + borderColor: borderColor ?? _this.borderColor, + ); + } + + StreamChannelListItemThemeData merge(StreamChannelListItemThemeData? other) { + final _this = (this as StreamChannelListItemThemeData); + + if (other == null || identical(_this, other)) { + return _this; + } + + if (!other.canMerge) { + return other; + } + + return copyWith( + titleStyle: _this.titleStyle?.merge(other.titleStyle) ?? other.titleStyle, + subtitleStyle: + _this.subtitleStyle?.merge(other.subtitleStyle) ?? + other.subtitleStyle, + timestampStyle: + _this.timestampStyle?.merge(other.timestampStyle) ?? + other.timestampStyle, + backgroundColor: other.backgroundColor, + hoverColor: other.hoverColor, + pressedColor: other.pressedColor, + borderColor: other.borderColor, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + final _this = (this as StreamChannelListItemThemeData); + final _other = (other as StreamChannelListItemThemeData); + + return _other.titleStyle == _this.titleStyle && + _other.subtitleStyle == _this.subtitleStyle && + _other.timestampStyle == _this.timestampStyle && + _other.backgroundColor == _this.backgroundColor && + _other.hoverColor == _this.hoverColor && + _other.pressedColor == _this.pressedColor && + _other.borderColor == _this.borderColor; + } + + @override + int get hashCode { + final _this = (this as StreamChannelListItemThemeData); + + return Object.hash( + runtimeType, + _this.titleStyle, + _this.subtitleStyle, + _this.timestampStyle, + _this.backgroundColor, + _this.hoverColor, + _this.pressedColor, + _this.borderColor, + ); + } +} 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 3870084..684e4f2 100644 --- a/packages/stream_core_flutter/lib/src/theme/stream_theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/stream_theme.dart @@ -7,6 +7,7 @@ import 'package:theme_extensions_builder_annotation/theme_extensions_builder_ann import 'components/stream_avatar_theme.dart'; import 'components/stream_badge_count_theme.dart'; import 'components/stream_button_theme.dart'; +import 'components/stream_channel_list_item_theme.dart'; import 'components/stream_checkbox_theme.dart'; import 'components/stream_context_menu_action_theme.dart'; import 'components/stream_context_menu_theme.dart'; @@ -92,6 +93,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { StreamAvatarThemeData? avatarTheme, StreamBadgeCountThemeData? badgeCountTheme, StreamButtonThemeData? buttonTheme, + StreamChannelListItemThemeData? channelListItemTheme, StreamCheckboxThemeData? checkboxTheme, StreamContextMenuThemeData? contextMenuTheme, StreamContextMenuActionThemeData? contextMenuActionTheme, @@ -119,6 +121,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { avatarTheme ??= const StreamAvatarThemeData(); badgeCountTheme ??= const StreamBadgeCountThemeData(); buttonTheme ??= const StreamButtonThemeData(); + channelListItemTheme ??= const StreamChannelListItemThemeData(); checkboxTheme ??= const StreamCheckboxThemeData(); contextMenuTheme ??= const StreamContextMenuThemeData(); contextMenuActionTheme ??= const StreamContextMenuActionThemeData(); @@ -140,6 +143,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { avatarTheme: avatarTheme, badgeCountTheme: badgeCountTheme, buttonTheme: buttonTheme, + channelListItemTheme: channelListItemTheme, checkboxTheme: checkboxTheme, contextMenuTheme: contextMenuTheme, contextMenuActionTheme: contextMenuActionTheme, @@ -175,6 +179,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { required this.avatarTheme, required this.badgeCountTheme, required this.buttonTheme, + required this.channelListItemTheme, required this.checkboxTheme, required this.contextMenuTheme, required this.contextMenuActionTheme, @@ -252,6 +257,9 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { /// The button theme for this theme. final StreamButtonThemeData buttonTheme; + /// The channel list item theme for this theme. + final StreamChannelListItemThemeData channelListItemTheme; + /// The checkbox theme for this theme. final StreamCheckboxThemeData checkboxTheme; @@ -308,6 +316,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { avatarTheme: avatarTheme, badgeCountTheme: badgeCountTheme, buttonTheme: buttonTheme, + channelListItemTheme: channelListItemTheme, checkboxTheme: checkboxTheme, contextMenuTheme: contextMenuTheme, contextMenuActionTheme: contextMenuActionTheme, 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 5af58e2..eda6231 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 @@ -23,6 +23,7 @@ mixin _$StreamTheme on ThemeExtension { StreamAvatarThemeData? avatarTheme, StreamBadgeCountThemeData? badgeCountTheme, StreamButtonThemeData? buttonTheme, + StreamChannelListItemThemeData? channelListItemTheme, StreamCheckboxThemeData? checkboxTheme, StreamContextMenuThemeData? contextMenuTheme, StreamContextMenuActionThemeData? contextMenuActionTheme, @@ -46,6 +47,7 @@ mixin _$StreamTheme on ThemeExtension { avatarTheme: avatarTheme ?? _this.avatarTheme, badgeCountTheme: badgeCountTheme ?? _this.badgeCountTheme, buttonTheme: buttonTheme ?? _this.buttonTheme, + channelListItemTheme: channelListItemTheme ?? _this.channelListItemTheme, checkboxTheme: checkboxTheme ?? _this.checkboxTheme, contextMenuTheme: contextMenuTheme ?? _this.contextMenuTheme, contextMenuActionTheme: @@ -94,6 +96,11 @@ mixin _$StreamTheme on ThemeExtension { other.buttonTheme, t, )!, + channelListItemTheme: StreamChannelListItemThemeData.lerp( + _this.channelListItemTheme, + other.channelListItemTheme, + t, + )!, checkboxTheme: StreamCheckboxThemeData.lerp( _this.checkboxTheme, other.checkboxTheme, @@ -153,6 +160,7 @@ mixin _$StreamTheme on ThemeExtension { _other.avatarTheme == _this.avatarTheme && _other.badgeCountTheme == _this.badgeCountTheme && _other.buttonTheme == _this.buttonTheme && + _other.channelListItemTheme == _this.channelListItemTheme && _other.checkboxTheme == _this.checkboxTheme && _other.contextMenuTheme == _this.contextMenuTheme && _other.contextMenuActionTheme == _this.contextMenuActionTheme && @@ -167,7 +175,7 @@ mixin _$StreamTheme on ThemeExtension { int get hashCode { final _this = (this as StreamTheme); - return Object.hash( + return Object.hashAll([ runtimeType, _this.brightness, _this.icons, @@ -180,6 +188,7 @@ mixin _$StreamTheme on ThemeExtension { _this.avatarTheme, _this.badgeCountTheme, _this.buttonTheme, + _this.channelListItemTheme, _this.checkboxTheme, _this.contextMenuTheme, _this.contextMenuActionTheme, @@ -188,6 +197,6 @@ mixin _$StreamTheme on ThemeExtension { _this.inputTheme, _this.onlineIndicatorTheme, _this.progressBarTheme, - ); + ]); } } 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 d6c414e..e536699 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 @@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart'; import 'components/stream_avatar_theme.dart'; import 'components/stream_badge_count_theme.dart'; import 'components/stream_button_theme.dart'; +import 'components/stream_channel_list_item_theme.dart'; import 'components/stream_checkbox_theme.dart'; import 'components/stream_context_menu_action_theme.dart'; import 'components/stream_context_menu_theme.dart'; @@ -74,6 +75,9 @@ extension StreamThemeExtension on BuildContext { /// Returns the [StreamButtonThemeData] from the nearest ancestor. StreamButtonThemeData get streamButtonTheme => StreamButtonTheme.of(this); + /// Returns the [StreamChannelListItemThemeData] from the nearest ancestor. + StreamChannelListItemThemeData get streamChannelListItemTheme => StreamChannelListItemTheme.of(this); + /// Returns the [StreamCheckboxThemeData] from the nearest ancestor. StreamCheckboxThemeData get streamCheckboxTheme => StreamCheckboxTheme.of(this); From 8d87d7d2504184687c32c41afcb91e1a02b17ec1 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 26 Feb 2026 10:02:53 +0100 Subject: [PATCH 02/15] update avatar sizes --- .../lib/components/avatar/stream_avatar.dart | 3 ++- .../lib/components/avatar/stream_avatar_group.dart | 3 ++- .../lib/src/components/avatar/stream_avatar.dart | 6 ++++-- .../src/components/avatar/stream_avatar_group.dart | 11 ++++++++--- .../lib/src/theme/components/stream_avatar_theme.dart | 7 +++++-- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/apps/design_system_gallery/lib/components/avatar/stream_avatar.dart b/apps/design_system_gallery/lib/components/avatar/stream_avatar.dart index 05c67dc..84aad6d 100644 --- a/apps/design_system_gallery/lib/components/avatar/stream_avatar.dart +++ b/apps/design_system_gallery/lib/components/avatar/stream_avatar.dart @@ -123,7 +123,8 @@ class _SizeCard extends StatelessWidget { StreamAvatarSize.sm => 'Chat list items, notifications', StreamAvatarSize.md => 'Message bubbles, comments', StreamAvatarSize.lg => 'Profile headers, user cards', - StreamAvatarSize.xl => 'Hero sections, large profile displays', + StreamAvatarSize.xl => 'Channel list items, conversation lists', + StreamAvatarSize.xxl => 'Hero sections, large profile displays', }; } diff --git a/apps/design_system_gallery/lib/components/avatar/stream_avatar_group.dart b/apps/design_system_gallery/lib/components/avatar/stream_avatar_group.dart index edfd1dc..5efe6b2 100644 --- a/apps/design_system_gallery/lib/components/avatar/stream_avatar_group.dart +++ b/apps/design_system_gallery/lib/components/avatar/stream_avatar_group.dart @@ -128,7 +128,8 @@ class _SizeCard extends StatelessWidget { String _getUsage(StreamAvatarGroupSize size) { return switch (size) { StreamAvatarGroupSize.lg => 'Channel list items, compact group displays', - StreamAvatarGroupSize.xl => 'Channel headers, prominent group displays', + StreamAvatarGroupSize.xl => 'Channel list items, standard group displays', + StreamAvatarGroupSize.xxl => 'Channel headers, prominent group displays', }; } diff --git a/packages/stream_core_flutter/lib/src/components/avatar/stream_avatar.dart b/packages/stream_core_flutter/lib/src/components/avatar/stream_avatar.dart index 8a3ab57..c123f2d 100644 --- a/packages/stream_core_flutter/lib/src/components/avatar/stream_avatar.dart +++ b/packages/stream_core_flutter/lib/src/components/avatar/stream_avatar.dart @@ -243,7 +243,8 @@ class DefaultStreamAvatar extends StatelessWidget { .xs => textTheme.metadataEmphasis, .sm || .md => textTheme.captionEmphasis, .lg => textTheme.bodyEmphasis, - .xl => textTheme.headingLg, + .xl => textTheme.headingMd, + .xxl => textTheme.headingLg, }; // Returns the appropriate icon size for the given avatar size. @@ -254,7 +255,8 @@ class DefaultStreamAvatar extends StatelessWidget { .sm => 12, .md => 16, .lg => 20, - .xl => 32, + .xl => 24, + .xxl => 32, }; } diff --git a/packages/stream_core_flutter/lib/src/components/avatar/stream_avatar_group.dart b/packages/stream_core_flutter/lib/src/components/avatar/stream_avatar_group.dart index 7984f5d..2580f78 100644 --- a/packages/stream_core_flutter/lib/src/components/avatar/stream_avatar_group.dart +++ b/packages/stream_core_flutter/lib/src/components/avatar/stream_avatar_group.dart @@ -18,8 +18,11 @@ enum StreamAvatarGroupSize { /// Large avatar group (40px diameter). lg(40), - /// Extra large avatar group (64px diameter). - xl(64) + /// Extra large avatar group (48px diameter). + xl(48), + + /// Extra-extra large avatar group (64px diameter). + xxl(64) ; /// Constructs a [StreamAvatarGroupSize] with the given diameter. @@ -359,7 +362,8 @@ class DefaultStreamAvatarGroup extends StatelessWidget { StreamAvatarGroupSize size, ) => switch (size) { .lg => StreamAvatarSize.sm, - .xl => StreamAvatarSize.lg, + .xl => StreamAvatarSize.md, + .xxl => StreamAvatarSize.lg, }; // Returns the appropriate badge count size for the given group size. @@ -368,5 +372,6 @@ class DefaultStreamAvatarGroup extends StatelessWidget { ) => switch (size) { .lg => StreamBadgeCountSize.sm, .xl => StreamBadgeCountSize.md, + .xxl => StreamBadgeCountSize.md, }; } diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_avatar_theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_avatar_theme.dart index eee2bb2..70cefea 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_avatar_theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_avatar_theme.dart @@ -27,8 +27,11 @@ enum StreamAvatarSize { /// Large avatar (40px diameter). lg(40), - /// Extra large avatar (64px diameter). - xl(64) + /// Extra large avatar (48px diameter). + xl(48), + + /// Extra-extra large avatar (64px diameter). + xxl(64) ; /// Constructs a [StreamAvatarSize] with the given diameter. From 249d4f35fab61b96cd4e0717c8ea0a9caa3f8e5c Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 26 Feb 2026 10:39:27 +0100 Subject: [PATCH 03/15] Add badge notification and snapshot tests --- .../lib/app/gallery_app.directories.g.dart | 19 + .../badge/stream_badge_notification.dart | 402 ++++++++++++++++++ .../stream_channel_list_item.dart | 18 +- .../lib/src/components.dart | 1 + .../badge/stream_badge_notification.dart | 233 ++++++++++ .../stream_channel_list_item.dart | 66 +-- .../src/factory/stream_component_factory.dart | 7 + .../stream_component_factory.g.theme.dart | 8 + .../stream_core_flutter/lib/src/theme.dart | 1 + .../stream_badge_notification_theme.dart | 131 ++++++ ...ream_badge_notification_theme.g.theme.dart | 135 ++++++ .../lib/src/theme/stream_theme.dart | 9 + .../lib/src/theme/stream_theme.g.theme.dart | 10 + .../src/theme/stream_theme_extensions.dart | 4 + ...stream_badge_notification_golden_test.dart | 92 ++++ .../stream_channel_list_item_golden_test.dart | 194 +++++++++ 16 files changed, 1290 insertions(+), 40 deletions(-) create mode 100644 apps/design_system_gallery/lib/components/badge/stream_badge_notification.dart create mode 100644 packages/stream_core_flutter/lib/src/components/badge/stream_badge_notification.dart create mode 100644 packages/stream_core_flutter/lib/src/theme/components/stream_badge_notification_theme.dart create mode 100644 packages/stream_core_flutter/lib/src/theme/components/stream_badge_notification_theme.g.theme.dart create mode 100644 packages/stream_core_flutter/test/components/badge/stream_badge_notification_golden_test.dart create mode 100644 packages/stream_core_flutter/test/components/channel_list/stream_channel_list_item_golden_test.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 123abfb..063603b 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 @@ -22,6 +22,8 @@ import 'package:design_system_gallery/components/avatar/stream_avatar_stack.dart as _design_system_gallery_components_avatar_stream_avatar_stack; import 'package:design_system_gallery/components/badge/stream_badge_count.dart' as _design_system_gallery_components_badge_stream_badge_count; +import 'package:design_system_gallery/components/badge/stream_badge_notification.dart' + as _design_system_gallery_components_badge_stream_badge_notification; import 'package:design_system_gallery/components/badge/stream_online_indicator.dart' as _design_system_gallery_components_badge_stream_online_indicator; import 'package:design_system_gallery/components/buttons/button.dart' @@ -282,6 +284,23 @@ final directories = <_widgetbook.WidgetbookNode>[ ), ], ), + _widgetbook.WidgetbookComponent( + name: 'StreamBadgeNotification', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'Playground', + builder: + _design_system_gallery_components_badge_stream_badge_notification + .buildStreamBadgeNotificationPlayground, + ), + _widgetbook.WidgetbookUseCase( + name: 'Showcase', + builder: + _design_system_gallery_components_badge_stream_badge_notification + .buildStreamBadgeNotificationShowcase, + ), + ], + ), _widgetbook.WidgetbookComponent( name: 'StreamOnlineIndicator', useCases: [ diff --git a/apps/design_system_gallery/lib/components/badge/stream_badge_notification.dart b/apps/design_system_gallery/lib/components/badge/stream_badge_notification.dart new file mode 100644 index 0000000..1b61c04 --- /dev/null +++ b/apps/design_system_gallery/lib/components/badge/stream_badge_notification.dart @@ -0,0 +1,402 @@ +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: StreamBadgeNotification, + path: '[Components]/Badge', +) +Widget buildStreamBadgeNotificationPlayground(BuildContext context) { + final label = context.knobs.string( + label: 'Label', + initialValue: '1', + description: 'The text to display in the badge.', + ); + + final type = context.knobs.object.dropdown( + label: 'Type', + options: StreamBadgeNotificationType.values, + initialOption: StreamBadgeNotificationType.primary, + labelBuilder: (option) => option.name[0].toUpperCase() + option.name.substring(1), + description: 'The visual type of the badge.', + ); + + final size = context.knobs.object.dropdown( + label: 'Size', + options: StreamBadgeNotificationSize.values, + initialOption: StreamBadgeNotificationSize.sm, + labelBuilder: (option) => '${option.name.toUpperCase()} (${option.value.toInt()}px)', + description: 'The size of the badge.', + ); + + return Center( + child: StreamBadgeNotification( + label: label, + type: type, + size: size, + ), + ); +} + +// ============================================================================= +// Showcase +// ============================================================================= + +@widgetbook.UseCase( + name: 'Showcase', + type: StreamBadgeNotification, + path: '[Components]/Badge', +) +Widget buildStreamBadgeNotificationShowcase(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 _TypeVariantsSection(), + SizedBox(height: spacing.xl), + const _SizeVariantsSection(), + SizedBox(height: spacing.xl), + const _CountVariantsSection(), + ], + ), + ), + ); +} + +// ============================================================================= +// Type Variants Section +// ============================================================================= + +class _TypeVariantsSection extends StatelessWidget { + const _TypeVariantsSection(); + + @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: 'TYPE 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( + 'Badge types determine background color', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + SizedBox(height: spacing.md), + Row( + children: [ + for (final type in StreamBadgeNotificationType.values) ...[ + _TypeDemo(type: type), + if (type != StreamBadgeNotificationType.values.last) + SizedBox(width: spacing.xl), + ], + ], + ), + ], + ), + ), + ], + ); + } +} + +class _TypeDemo extends StatelessWidget { + const _TypeDemo({required this.type}); + + final StreamBadgeNotificationType type; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return Column( + children: [ + SizedBox( + width: 48, + height: 32, + child: Center( + child: StreamBadgeNotification( + label: '1', + type: type, + ), + ), + ), + SizedBox(height: spacing.sm), + Text( + type.name[0].toUpperCase() + type.name.substring(1), + style: textTheme.metadataEmphasis.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + ], + ); + } +} + +// ============================================================================= +// 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( + 'Two sizes for different contexts', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + SizedBox(height: spacing.md), + Row( + children: [ + for (final size in StreamBadgeNotificationSize.values) ...[ + _SizeDemo(size: size), + if (size != StreamBadgeNotificationSize.values.last) + SizedBox(width: spacing.xl), + ], + ], + ), + ], + ), + ), + ], + ); + } +} + +class _SizeDemo extends StatelessWidget { + const _SizeDemo({required this.size}); + + final StreamBadgeNotificationSize size; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return Column( + children: [ + SizedBox( + width: 48, + height: 32, + child: Center( + child: StreamBadgeNotification( + label: '5', + size: size, + ), + ), + ), + SizedBox(height: spacing.sm), + Text( + size.name.toUpperCase(), + style: textTheme.metadataEmphasis.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + Text( + '${size.value.toInt()}px', + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, + fontFamily: 'monospace', + fontSize: 10, + ), + ), + ], + ); + } +} + +// ============================================================================= +// Count Variants Section +// ============================================================================= + +class _CountVariantsSection extends StatelessWidget { + const _CountVariantsSection(); + + @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: 'COUNT 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( + 'Badge adapts width based on count', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + SizedBox(height: spacing.md), + Wrap( + spacing: spacing.md, + runSpacing: spacing.md, + children: const [ + _CountDemo(count: 1), + _CountDemo(count: 9), + _CountDemo(count: 25), + _CountDemo(count: 99), + _CountDemo(count: 100), + ], + ), + ], + ), + ), + ], + ); + } +} + +class _CountDemo extends StatelessWidget { + const _CountDemo({required this.count}); + + final int count; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + final displayText = count > 99 ? '99+' : '$count'; + + return Column( + children: [ + StreamBadgeNotification(label: displayText), + SizedBox(height: spacing.xs), + Text( + displayText, + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textTertiary, + fontFamily: 'monospace', + ), + ), + ], + ); + } +} + +// ============================================================================= +// Shared Widgets +// ============================================================================= + +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/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart b/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart index 7deba98..f971ff5 100644 --- a/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart +++ b/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart @@ -62,7 +62,7 @@ Widget buildStreamChannelListItemPlayground(BuildContext context) { ), title: title, titleTrailing: showMuteIcon ? Icon(icons.mute, size: 16, color: colorScheme.textTertiary) : null, - subtitle: subtitle, + subtitle: subtitle != null ? Text(subtitle) : null, timestamp: timestamp, unreadCount: unreadCount, ), @@ -136,7 +136,7 @@ class _DirectMessageSection extends StatelessWidget { ), ), title: 'Jane Doe', - subtitle: 'Hey! Are you free for a call?', + subtitle: const Text('Hey! Are you free for a call?'), timestamp: '9:41', unreadCount: 3, ), @@ -149,7 +149,7 @@ class _DirectMessageSection extends StatelessWidget { ), ), title: 'Bob Smith', - subtitle: 'Thanks for the update!', + subtitle: const Text('Thanks for the update!'), timestamp: 'Yesterday', ), StreamChannelListItem( @@ -163,7 +163,7 @@ class _DirectMessageSection extends StatelessWidget { ), ), title: 'Carol White', - subtitle: 'See you tomorrow!', + subtitle: const Text('See you tomorrow!'), timestamp: 'Saturday', ), ], @@ -213,7 +213,7 @@ class _GroupChannelSection extends StatelessWidget { ], ), title: 'Design Team', - subtitle: 'Alice: New mockups ready for review', + subtitle: const Text('Alice: New mockups ready for review'), timestamp: '9:41', unreadCount: 5, ), @@ -226,7 +226,7 @@ class _GroupChannelSection extends StatelessWidget { ], ), title: 'Engineering', - subtitle: 'Bob: PR merged successfully', + subtitle: const Text('Bob: PR merged successfully'), timestamp: '10:15', unreadCount: 12, ), @@ -245,7 +245,7 @@ class _GroupChannelSection extends StatelessWidget { size: 16, color: colorScheme.textTertiary, ), - subtitle: 'Carol: Meeting notes attached', + subtitle: const Text('Carol: Meeting notes attached'), timestamp: 'Yesterday', ), ], @@ -284,7 +284,7 @@ class _EdgeCasesSection extends StatelessWidget { ), title: 'Very Long Channel Name That Should Be Truncated Properly', subtitle: - 'This is a very long message preview that should be truncated with an ellipsis when it overflows', + const Text('This is a very long message preview that should be truncated with an ellipsis when it overflows'), timestamp: '01/15/2026', unreadCount: 99, ), @@ -296,7 +296,7 @@ class _EdgeCasesSection extends StatelessWidget { placeholder: (context) => const Text('NR'), ), title: 'No Unread', - subtitle: 'All caught up!', + subtitle: const Text('All caught up!'), timestamp: '3:00', ), StreamChannelListItem( diff --git a/packages/stream_core_flutter/lib/src/components.dart b/packages/stream_core_flutter/lib/src/components.dart index 66f60d7..02d2bbc 100644 --- a/packages/stream_core_flutter/lib/src/components.dart +++ b/packages/stream_core_flutter/lib/src/components.dart @@ -4,6 +4,7 @@ export 'components/avatar/stream_avatar.dart' hide DefaultStreamAvatar; export 'components/avatar/stream_avatar_group.dart' hide DefaultStreamAvatarGroup; export 'components/avatar/stream_avatar_stack.dart' hide DefaultStreamAvatarStack; export 'components/badge/stream_badge_count.dart' hide DefaultStreamBadgeCount; +export 'components/badge/stream_badge_notification.dart' hide DefaultStreamBadgeNotification; export 'components/badge/stream_media_badge.dart'; export 'components/badge/stream_online_indicator.dart' hide DefaultStreamOnlineIndicator; export 'components/buttons/stream_button.dart' hide DefaultStreamButton; diff --git a/packages/stream_core_flutter/lib/src/components/badge/stream_badge_notification.dart b/packages/stream_core_flutter/lib/src/components/badge/stream_badge_notification.dart new file mode 100644 index 0000000..590dc39 --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/badge/stream_badge_notification.dart @@ -0,0 +1,233 @@ +import 'package:flutter/material.dart'; + +import '../../factory/stream_component_factory.dart'; +import '../../theme/components/stream_badge_notification_theme.dart'; +import '../../theme/primitives/stream_spacing.dart'; +import '../../theme/semantics/stream_color_scheme.dart'; +import '../../theme/semantics/stream_text_theme.dart'; +import '../../theme/stream_theme_extensions.dart'; + +/// A notification badge for displaying counts in colored pill shapes. +/// +/// [StreamBadgeNotification] displays a count label in a colored pill-shaped +/// badge with a border. It's used in channel list items and other places to +/// indicate unread messages or pending notifications. +/// +/// Unlike [StreamBadgeCount], which uses neutral colors, this badge uses +/// prominent colored backgrounds (primary, error, neutral) to draw attention. +/// +/// The badge has three visual types controlled by +/// [StreamBadgeNotificationType]: +/// +/// * [StreamBadgeNotificationType.primary] — Brand accent background. +/// * [StreamBadgeNotificationType.error] — Error/red background. +/// * [StreamBadgeNotificationType.neutral] — Muted gray background. +/// +/// {@tool snippet} +/// +/// Basic usage with unread count: +/// +/// ```dart +/// StreamBadgeNotification(label: '3') +/// ``` +/// {@end-tool} +/// +/// {@tool snippet} +/// +/// Error variant: +/// +/// ```dart +/// StreamBadgeNotification( +/// label: '!', +/// type: StreamBadgeNotificationType.error, +/// ) +/// ``` +/// {@end-tool} +/// +/// ## Theming +/// +/// [StreamBadgeNotification] uses [StreamBadgeNotificationThemeData] for +/// default styling. Colors are determined by the current [StreamColorScheme]. +/// +/// See also: +/// +/// * [StreamBadgeNotificationSize], the available size variants. +/// * [StreamBadgeNotificationType], the available style variants. +/// * [StreamBadgeNotificationThemeData], for customizing appearance. +/// * [StreamBadgeNotificationTheme], for overriding theme in a subtree. +/// * [StreamBadgeCount], a neutral count badge without colored backgrounds. +class StreamBadgeNotification extends StatelessWidget { + /// Creates a badge notification indicator. + StreamBadgeNotification({ + super.key, + StreamBadgeNotificationType? type, + StreamBadgeNotificationSize? size, + required String label, + }) : props = StreamBadgeNotificationProps( + type: type, + size: size, + label: label, + ); + + /// The properties that configure this badge notification. + final StreamBadgeNotificationProps props; + + @override + Widget build(BuildContext context) { + final builder = StreamComponentFactory.maybeOf(context)?.badgeNotification; + if (builder != null) return builder(context, props); + return DefaultStreamBadgeNotification(props: props); + } +} + +/// Properties for configuring a [StreamBadgeNotification]. +/// +/// This class holds all the configuration options for a badge notification, +/// allowing them to be passed through the [StreamComponentFactory]. +/// +/// See also: +/// +/// * [StreamBadgeNotification], which uses these properties. +/// * [DefaultStreamBadgeNotification], the default implementation. +class StreamBadgeNotificationProps { + /// Creates properties for a badge notification. + const StreamBadgeNotificationProps({ + this.type, + this.size, + required this.label, + }); + + /// The visual type determining the badge background color. + /// + /// If null, defaults to [StreamBadgeNotificationType.primary]. + final StreamBadgeNotificationType? type; + + /// The size of the badge. + /// + /// If null, uses [StreamBadgeNotificationThemeData.size], or falls back to + /// [StreamBadgeNotificationSize.sm]. + final StreamBadgeNotificationSize? size; + + /// The text label to display in the badge. + /// + /// Typically a numeric count (e.g., "5") or an overflow indicator + /// (e.g., "99+"). + final String label; +} + +/// The default implementation of [StreamBadgeNotification]. +/// +/// This widget renders the badge notification with theming support. +/// It's used as the default factory implementation in +/// [StreamComponentFactory]. +/// +/// See also: +/// +/// * [StreamBadgeNotification], the public API widget. +/// * [StreamBadgeNotificationProps], which configures this widget. +class DefaultStreamBadgeNotification extends StatelessWidget { + /// Creates a default badge notification with the given [props]. + const DefaultStreamBadgeNotification({super.key, required this.props}); + + /// The properties that configure this badge notification. + final StreamBadgeNotificationProps props; + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + final textTheme = context.streamTextTheme; + + final theme = context.streamBadgeNotificationTheme; + final defaults = _StreamBadgeNotificationThemeDefaults(context); + + final effectiveSize = props.size ?? theme.size ?? defaults.size; + final effectiveType = props.type ?? StreamBadgeNotificationType.primary; + final effectiveTextColor = theme.textColor ?? defaults.textColor; + final effectiveBorderColor = theme.borderColor ?? defaults.borderColor; + final effectiveBackgroundColor = _resolveBackgroundColor( + effectiveType, + theme, + defaults, + ); + + final padding = _paddingForSize(effectiveSize, spacing); + final textStyle = _textStyleForSize(effectiveSize, textTheme).copyWith(color: effectiveTextColor); + + return IntrinsicWidth( + child: Container( + constraints: BoxConstraints( + minWidth: effectiveSize.value, + minHeight: effectiveSize.value, + ), + padding: padding, + alignment: Alignment.center, + decoration: ShapeDecoration( + color: effectiveBackgroundColor, + shape: StadiumBorder( + side: BorderSide( + color: effectiveBorderColor, + width: 2, + strokeAlign: BorderSide.strokeAlignOutside, + ), + ), + ), + child: DefaultTextStyle( + style: textStyle, + child: Text(props.label), + ), + ), + ); + } + + Color _resolveBackgroundColor( + StreamBadgeNotificationType type, + StreamBadgeNotificationThemeData theme, + _StreamBadgeNotificationThemeDefaults defaults, + ) => switch (type) { + StreamBadgeNotificationType.primary => theme.primaryBackgroundColor ?? defaults.primaryBackgroundColor, + StreamBadgeNotificationType.error => theme.errorBackgroundColor ?? defaults.errorBackgroundColor, + StreamBadgeNotificationType.neutral => theme.neutralBackgroundColor ?? defaults.neutralBackgroundColor, + }; + + TextStyle _textStyleForSize( + StreamBadgeNotificationSize size, + StreamTextTheme textTheme, + ) => switch (size) { + .xs => textTheme.numericMd, + .sm => textTheme.numericXl, + }; + + EdgeInsetsGeometry _paddingForSize( + StreamBadgeNotificationSize size, + StreamSpacing spacing, + ) => switch (size) { + .xs => EdgeInsets.symmetric(horizontal: spacing.xxs), + .sm => EdgeInsets.symmetric(horizontal: spacing.xxs), + }; +} + +class _StreamBadgeNotificationThemeDefaults extends StreamBadgeNotificationThemeData { + _StreamBadgeNotificationThemeDefaults(this._context); + + final BuildContext _context; + + late final _colorScheme = _context.streamColorScheme; + + @override + StreamBadgeNotificationSize get size => StreamBadgeNotificationSize.sm; + + @override + Color get primaryBackgroundColor => _colorScheme.accentPrimary; + + @override + Color get errorBackgroundColor => _colorScheme.accentError; + + @override + Color get neutralBackgroundColor => _colorScheme.accentNeutral; + + @override + Color get textColor => _colorScheme.textOnAccent; + + @override + Color get borderColor => _colorScheme.borderOnDark; +} diff --git a/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart b/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart index fb7944d..312c4cc 100644 --- a/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart +++ b/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; +import '../../../stream_core_flutter.dart'; import '../../factory/stream_component_factory.dart'; import '../../theme/components/stream_channel_list_item_theme.dart'; import '../../theme/primitives/stream_spacing.dart'; import '../../theme/semantics/stream_color_scheme.dart'; import '../../theme/semantics/stream_text_theme.dart'; import '../../theme/stream_theme_extensions.dart'; -import '../badge/stream_badge_count.dart'; +import '../badge/stream_badge_notification.dart'; /// A list item for displaying a channel in a channel list. /// @@ -25,7 +26,7 @@ import '../badge/stream_badge_count.dart'; /// StreamChannelListItem( /// avatar: StreamAvatar(placeholder: (context) => Text('AB')), /// title: 'General', -/// subtitle: 'Hello, how are you?', +/// subtitle: Text('Hello, how are you?'), /// timestamp: '9:41', /// unreadCount: 3, /// ) @@ -41,7 +42,7 @@ import '../badge/stream_badge_count.dart'; /// avatar: StreamAvatar(placeholder: (context) => Text('AB')), /// title: 'Muted Channel', /// titleTrailing: Icon(Icons.volume_off, size: 16), -/// subtitle: 'Last message...', +/// subtitle: Text('Last message...'), /// timestamp: 'Yesterday', /// ) /// ``` @@ -57,7 +58,7 @@ import '../badge/stream_badge_count.dart'; /// /// * [StreamChannelListItemThemeData], for customizing appearance. /// * [StreamChannelListItemTheme], for overriding theme in a widget subtree. -/// * [StreamBadgeCount], which displays the unread count badge. +/// * [StreamBadgeNotification], which displays the unread count badge. class StreamChannelListItem extends StatelessWidget { /// Creates a channel list item. StreamChannelListItem({ @@ -65,7 +66,7 @@ class StreamChannelListItem extends StatelessWidget { required Widget avatar, required String title, Widget? titleTrailing, - String? subtitle, + Widget? subtitle, Widget? subtitleTrailing, String? timestamp, int unreadCount = 0, @@ -131,8 +132,11 @@ class StreamChannelListItemProps { /// Typically used for a mute icon or similar indicator. final Widget? titleTrailing; - /// The message preview text displayed below the title. - final String? subtitle; + /// The message preview widget displayed below the title. + /// + /// Typically a [Text] widget with the last message, but can be any widget + /// for richer content (e.g., icons, read receipts, sender prefix). + final Widget? subtitle; /// An optional trailing widget in the subtitle row. /// @@ -146,7 +150,7 @@ class StreamChannelListItemProps { /// The number of unread messages. /// - /// When greater than zero, a [StreamBadgeCount] is displayed. + /// When greater than zero, a [StreamBadgeNotification] is displayed. final int unreadCount; /// Called when the list item is tapped. @@ -191,7 +195,6 @@ class _DefaultStreamChannelListItemState extends State 0) StreamBadgeCount(label: '$unreadCount'), + if (unreadCount > 0) StreamBadgeNotification(label: '$unreadCount'), ], ), ], @@ -319,24 +325,22 @@ class _SubtitleRow extends StatelessWidget { required this.subtitleStyle, }); - final String subtitle; + final Widget subtitle; final Widget? subtitleTrailing; final TextStyle subtitleStyle; @override Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: Text( - subtitle, - style: subtitleStyle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ?subtitleTrailing, - ], + return DefaultTextStyle( + style: subtitleStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + child: Row( + children: [ + Expanded(child: subtitle), + ?subtitleTrailing, + ], + ), ); } } 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 589d206..acd3b7e 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 @@ -154,6 +154,7 @@ class StreamComponentBuilders with _$StreamComponentBuilders { this.avatarGroup, this.avatarStack, this.badgeCount, + this.badgeNotification, this.button, this.channelListItem, this.checkbox, @@ -185,6 +186,12 @@ class StreamComponentBuilders with _$StreamComponentBuilders { /// When null, [StreamBadgeCount] uses [DefaultStreamBadgeCount]. final StreamComponentBuilder? badgeCount; + /// Custom builder for badge notification widgets. + /// + /// When null, [StreamBadgeNotification] uses + /// [DefaultStreamBadgeNotification]. + final StreamComponentBuilder? badgeNotification; + /// Custom builder for button widgets. /// /// When null, [StreamButton] uses [DefaultStreamButton]. 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 9466048..5bd5b79 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 @@ -34,6 +34,8 @@ mixin _$StreamComponentBuilders { avatarGroup: t < 0.5 ? a.avatarGroup : b.avatarGroup, avatarStack: t < 0.5 ? a.avatarStack : b.avatarStack, badgeCount: t < 0.5 ? a.badgeCount : b.badgeCount, + badgeNotification: + t < 0.5 ? a.badgeNotification : b.badgeNotification, button: t < 0.5 ? a.button : b.button, channelListItem: t < 0.5 ? a.channelListItem : b.channelListItem, checkbox: t < 0.5 ? a.checkbox : b.checkbox, @@ -51,6 +53,8 @@ mixin _$StreamComponentBuilders { Widget Function(BuildContext, StreamAvatarGroupProps)? avatarGroup, Widget Function(BuildContext, StreamAvatarStackProps)? avatarStack, Widget Function(BuildContext, StreamBadgeCountProps)? badgeCount, + Widget Function(BuildContext, StreamBadgeNotificationProps)? + badgeNotification, Widget Function(BuildContext, StreamButtonProps)? button, Widget Function(BuildContext, StreamChannelListItemProps)? channelListItem, Widget Function(BuildContext, StreamCheckboxProps)? checkbox, @@ -69,6 +73,7 @@ mixin _$StreamComponentBuilders { avatarGroup: avatarGroup ?? _this.avatarGroup, avatarStack: avatarStack ?? _this.avatarStack, badgeCount: badgeCount ?? _this.badgeCount, + badgeNotification: badgeNotification ?? _this.badgeNotification, button: button ?? _this.button, channelListItem: channelListItem ?? _this.channelListItem, checkbox: checkbox ?? _this.checkbox, @@ -97,6 +102,7 @@ mixin _$StreamComponentBuilders { avatarGroup: other.avatarGroup, avatarStack: other.avatarStack, badgeCount: other.badgeCount, + badgeNotification: other.badgeNotification, button: other.button, channelListItem: other.channelListItem, checkbox: other.checkbox, @@ -126,6 +132,7 @@ mixin _$StreamComponentBuilders { _other.avatarGroup == _this.avatarGroup && _other.avatarStack == _this.avatarStack && _other.badgeCount == _this.badgeCount && + _other.badgeNotification == _this.badgeNotification && _other.button == _this.button && _other.channelListItem == _this.channelListItem && _other.checkbox == _this.checkbox && @@ -147,6 +154,7 @@ mixin _$StreamComponentBuilders { _this.avatarGroup, _this.avatarStack, _this.badgeCount, + _this.badgeNotification, _this.button, _this.channelListItem, _this.checkbox, diff --git a/packages/stream_core_flutter/lib/src/theme.dart b/packages/stream_core_flutter/lib/src/theme.dart index e08166d..69b251d 100644 --- a/packages/stream_core_flutter/lib/src/theme.dart +++ b/packages/stream_core_flutter/lib/src/theme.dart @@ -2,6 +2,7 @@ export 'factory/stream_component_factory.dart'; export 'theme/components/stream_avatar_theme.dart'; export 'theme/components/stream_badge_count_theme.dart'; +export 'theme/components/stream_badge_notification_theme.dart'; export 'theme/components/stream_button_theme.dart'; export 'theme/components/stream_channel_list_item_theme.dart'; export 'theme/components/stream_checkbox_theme.dart'; diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_badge_notification_theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_badge_notification_theme.dart new file mode 100644 index 0000000..72b27bf --- /dev/null +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_badge_notification_theme.dart @@ -0,0 +1,131 @@ +import 'package:flutter/widgets.dart'; +import 'package:theme_extensions_builder_annotation/theme_extensions_builder_annotation.dart'; + +import '../stream_theme.dart'; + +part 'stream_badge_notification_theme.g.theme.dart'; + +/// Predefined sizes for the badge notification indicator. +/// +/// Each size corresponds to a specific height in logical pixels. +/// +/// See also: +/// +/// * [StreamBadgeNotification], which uses these size variants. +/// * [StreamBadgeNotificationThemeData.size], for setting a global default. +enum StreamBadgeNotificationSize { + /// Extra small badge (16px height). + xs(16), + + /// Small badge (20px height). + sm(20); + + /// Constructs a [StreamBadgeNotificationSize] with the given height. + const StreamBadgeNotificationSize(this.value); + + /// The height of the badge in logical pixels. + final double value; +} + +/// The visual type of a [StreamBadgeNotification]. +/// +/// Determines which background color is applied to the badge. +enum StreamBadgeNotificationType { + /// Primary style — uses the brand accent color. + primary, + + /// Error style — uses the error accent color. + error, + + /// Neutral style — uses a muted neutral color. + neutral, +} + +/// Applies a badge notification theme to descendant widgets. +/// +/// Wrap a subtree with [StreamBadgeNotificationTheme] to override badge +/// notification styling. Access the merged theme using +/// [BuildContext.streamBadgeNotificationTheme]. +/// +/// See also: +/// +/// * [StreamBadgeNotificationThemeData], which describes the theme. +/// * [StreamBadgeNotification], the widget affected by this theme. +class StreamBadgeNotificationTheme extends InheritedTheme { + /// Creates a badge notification theme. + const StreamBadgeNotificationTheme({ + super.key, + required this.data, + required super.child, + }); + + /// The badge notification theme data for descendant widgets. + final StreamBadgeNotificationThemeData data; + + /// Returns the merged [StreamBadgeNotificationThemeData] from local and + /// global themes. + static StreamBadgeNotificationThemeData of(BuildContext context) { + final localTheme = context + .dependOnInheritedWidgetOfExactType(); + return StreamTheme.of(context) + .badgeNotificationTheme + .merge(localTheme?.data); + } + + @override + Widget wrap(BuildContext context, Widget child) { + return StreamBadgeNotificationTheme(data: data, child: child); + } + + @override + bool updateShouldNotify(StreamBadgeNotificationTheme oldWidget) => + data != oldWidget.data; +} + +/// Theme data for customizing [StreamBadgeNotification] widgets. +/// +/// See also: +/// +/// * [StreamBadgeNotification], the widget that uses this theme data. +/// * [StreamBadgeNotificationTheme], for overriding theme in a widget subtree. +@themeGen +@immutable +class StreamBadgeNotificationThemeData + with _$StreamBadgeNotificationThemeData { + /// Creates a badge notification theme with optional style overrides. + const StreamBadgeNotificationThemeData({ + this.size, + this.primaryBackgroundColor, + this.errorBackgroundColor, + this.neutralBackgroundColor, + this.textColor, + this.borderColor, + }); + + /// The default size for badge notifications. + /// + /// Falls back to [StreamBadgeNotificationSize.sm]. + final StreamBadgeNotificationSize? size; + + /// The background color for the [StreamBadgeNotificationType.primary] type. + final Color? primaryBackgroundColor; + + /// The background color for the [StreamBadgeNotificationType.error] type. + final Color? errorBackgroundColor; + + /// The background color for the [StreamBadgeNotificationType.neutral] type. + final Color? neutralBackgroundColor; + + /// The text color for the count label. + final Color? textColor; + + /// The border color of the badge. + final Color? borderColor; + + /// Linearly interpolate between two [StreamBadgeNotificationThemeData]. + static StreamBadgeNotificationThemeData? lerp( + StreamBadgeNotificationThemeData? a, + StreamBadgeNotificationThemeData? b, + double t, + ) => _$StreamBadgeNotificationThemeData.lerp(a, b, t); +} diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_badge_notification_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_badge_notification_theme.g.theme.dart new file mode 100644 index 0000000..bd0585a --- /dev/null +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_badge_notification_theme.g.theme.dart @@ -0,0 +1,135 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_element + +part of 'stream_badge_notification_theme.dart'; + +// ************************************************************************** +// ThemeGenGenerator +// ************************************************************************** + +mixin _$StreamBadgeNotificationThemeData { + bool get canMerge => true; + + static StreamBadgeNotificationThemeData? lerp( + StreamBadgeNotificationThemeData? a, + StreamBadgeNotificationThemeData? 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 StreamBadgeNotificationThemeData( + size: t < 0.5 ? a.size : b.size, + primaryBackgroundColor: Color.lerp( + a.primaryBackgroundColor, + b.primaryBackgroundColor, + t, + ), + errorBackgroundColor: Color.lerp( + a.errorBackgroundColor, + b.errorBackgroundColor, + t, + ), + neutralBackgroundColor: Color.lerp( + a.neutralBackgroundColor, + b.neutralBackgroundColor, + t, + ), + textColor: Color.lerp(a.textColor, b.textColor, t), + borderColor: Color.lerp(a.borderColor, b.borderColor, t), + ); + } + + StreamBadgeNotificationThemeData copyWith({ + StreamBadgeNotificationSize? size, + Color? primaryBackgroundColor, + Color? errorBackgroundColor, + Color? neutralBackgroundColor, + Color? textColor, + Color? borderColor, + }) { + final _this = (this as StreamBadgeNotificationThemeData); + + return StreamBadgeNotificationThemeData( + size: size ?? _this.size, + primaryBackgroundColor: + primaryBackgroundColor ?? _this.primaryBackgroundColor, + errorBackgroundColor: + errorBackgroundColor ?? _this.errorBackgroundColor, + neutralBackgroundColor: + neutralBackgroundColor ?? _this.neutralBackgroundColor, + textColor: textColor ?? _this.textColor, + borderColor: borderColor ?? _this.borderColor, + ); + } + + StreamBadgeNotificationThemeData merge( + StreamBadgeNotificationThemeData? other, + ) { + final _this = (this as StreamBadgeNotificationThemeData); + + if (other == null || identical(_this, other)) { + return _this; + } + + if (!other.canMerge) { + return other; + } + + return copyWith( + size: other.size, + primaryBackgroundColor: other.primaryBackgroundColor, + errorBackgroundColor: other.errorBackgroundColor, + neutralBackgroundColor: other.neutralBackgroundColor, + textColor: other.textColor, + borderColor: other.borderColor, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + final _this = (this as StreamBadgeNotificationThemeData); + final _other = (other as StreamBadgeNotificationThemeData); + + return _other.size == _this.size && + _other.primaryBackgroundColor == _this.primaryBackgroundColor && + _other.errorBackgroundColor == _this.errorBackgroundColor && + _other.neutralBackgroundColor == _this.neutralBackgroundColor && + _other.textColor == _this.textColor && + _other.borderColor == _this.borderColor; + } + + @override + int get hashCode { + final _this = (this as StreamBadgeNotificationThemeData); + + return Object.hash( + runtimeType, + _this.size, + _this.primaryBackgroundColor, + _this.errorBackgroundColor, + _this.neutralBackgroundColor, + _this.textColor, + _this.borderColor, + ); + } +} 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 684e4f2..289f61a 100644 --- a/packages/stream_core_flutter/lib/src/theme/stream_theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/stream_theme.dart @@ -6,6 +6,7 @@ import 'package:theme_extensions_builder_annotation/theme_extensions_builder_ann import 'components/stream_avatar_theme.dart'; import 'components/stream_badge_count_theme.dart'; +import 'components/stream_badge_notification_theme.dart'; import 'components/stream_button_theme.dart'; import 'components/stream_channel_list_item_theme.dart'; import 'components/stream_checkbox_theme.dart'; @@ -92,6 +93,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { // Components themes StreamAvatarThemeData? avatarTheme, StreamBadgeCountThemeData? badgeCountTheme, + StreamBadgeNotificationThemeData? badgeNotificationTheme, StreamButtonThemeData? buttonTheme, StreamChannelListItemThemeData? channelListItemTheme, StreamCheckboxThemeData? checkboxTheme, @@ -120,6 +122,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { // Components avatarTheme ??= const StreamAvatarThemeData(); badgeCountTheme ??= const StreamBadgeCountThemeData(); + badgeNotificationTheme ??= const StreamBadgeNotificationThemeData(); buttonTheme ??= const StreamButtonThemeData(); channelListItemTheme ??= const StreamChannelListItemThemeData(); checkboxTheme ??= const StreamCheckboxThemeData(); @@ -142,6 +145,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { boxShadow: boxShadow, avatarTheme: avatarTheme, badgeCountTheme: badgeCountTheme, + badgeNotificationTheme: badgeNotificationTheme, buttonTheme: buttonTheme, channelListItemTheme: channelListItemTheme, checkboxTheme: checkboxTheme, @@ -178,6 +182,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { required this.boxShadow, required this.avatarTheme, required this.badgeCountTheme, + required this.badgeNotificationTheme, required this.buttonTheme, required this.channelListItemTheme, required this.checkboxTheme, @@ -254,6 +259,9 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { /// The badge count theme for this theme. final StreamBadgeCountThemeData badgeCountTheme; + /// The badge notification theme for this theme. + final StreamBadgeNotificationThemeData badgeNotificationTheme; + /// The button theme for this theme. final StreamButtonThemeData buttonTheme; @@ -315,6 +323,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { boxShadow: boxShadow, avatarTheme: avatarTheme, badgeCountTheme: badgeCountTheme, + badgeNotificationTheme: badgeNotificationTheme, buttonTheme: buttonTheme, channelListItemTheme: channelListItemTheme, checkboxTheme: checkboxTheme, 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 eda6231..9a0289c 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 @@ -22,6 +22,7 @@ mixin _$StreamTheme on ThemeExtension { StreamBoxShadow? boxShadow, StreamAvatarThemeData? avatarTheme, StreamBadgeCountThemeData? badgeCountTheme, + StreamBadgeNotificationThemeData? badgeNotificationTheme, StreamButtonThemeData? buttonTheme, StreamChannelListItemThemeData? channelListItemTheme, StreamCheckboxThemeData? checkboxTheme, @@ -46,6 +47,8 @@ mixin _$StreamTheme on ThemeExtension { boxShadow: boxShadow ?? _this.boxShadow, avatarTheme: avatarTheme ?? _this.avatarTheme, badgeCountTheme: badgeCountTheme ?? _this.badgeCountTheme, + badgeNotificationTheme: + badgeNotificationTheme ?? _this.badgeNotificationTheme, buttonTheme: buttonTheme ?? _this.buttonTheme, channelListItemTheme: channelListItemTheme ?? _this.channelListItemTheme, checkboxTheme: checkboxTheme ?? _this.checkboxTheme, @@ -91,6 +94,11 @@ mixin _$StreamTheme on ThemeExtension { other.badgeCountTheme, t, )!, + badgeNotificationTheme: StreamBadgeNotificationThemeData.lerp( + _this.badgeNotificationTheme, + other.badgeNotificationTheme, + t, + )!, buttonTheme: StreamButtonThemeData.lerp( _this.buttonTheme, other.buttonTheme, @@ -159,6 +167,7 @@ mixin _$StreamTheme on ThemeExtension { _other.boxShadow == _this.boxShadow && _other.avatarTheme == _this.avatarTheme && _other.badgeCountTheme == _this.badgeCountTheme && + _other.badgeNotificationTheme == _this.badgeNotificationTheme && _other.buttonTheme == _this.buttonTheme && _other.channelListItemTheme == _this.channelListItemTheme && _other.checkboxTheme == _this.checkboxTheme && @@ -187,6 +196,7 @@ mixin _$StreamTheme on ThemeExtension { _this.boxShadow, _this.avatarTheme, _this.badgeCountTheme, + _this.badgeNotificationTheme, _this.buttonTheme, _this.channelListItemTheme, _this.checkboxTheme, 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 e536699..6068503 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 @@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart'; import 'components/stream_avatar_theme.dart'; import 'components/stream_badge_count_theme.dart'; +import 'components/stream_badge_notification_theme.dart'; import 'components/stream_button_theme.dart'; import 'components/stream_channel_list_item_theme.dart'; import 'components/stream_checkbox_theme.dart'; @@ -72,6 +73,9 @@ extension StreamThemeExtension on BuildContext { /// Returns the [StreamBadgeCountThemeData] from the nearest ancestor. StreamBadgeCountThemeData get streamBadgeCountTheme => StreamBadgeCountTheme.of(this); + /// Returns the [StreamBadgeNotificationThemeData] from the nearest ancestor. + StreamBadgeNotificationThemeData get streamBadgeNotificationTheme => StreamBadgeNotificationTheme.of(this); + /// Returns the [StreamButtonThemeData] from the nearest ancestor. StreamButtonThemeData get streamButtonTheme => StreamButtonTheme.of(this); diff --git a/packages/stream_core_flutter/test/components/badge/stream_badge_notification_golden_test.dart b/packages/stream_core_flutter/test/components/badge/stream_badge_notification_golden_test.dart new file mode 100644 index 0000000..4c91179 --- /dev/null +++ b/packages/stream_core_flutter/test/components/badge/stream_badge_notification_golden_test.dart @@ -0,0 +1,92 @@ +import 'package:alchemist/alchemist.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +void main() { + group('StreamBadgeNotification Golden Tests', () { + goldenTest( + 'renders light theme type and size matrix', + fileName: 'stream_badge_notification_light_matrix', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 100), + children: [ + for (final type in StreamBadgeNotificationType.values) + for (final size in StreamBadgeNotificationSize.values) + GoldenTestScenario( + name: '${type.name}_${size.name}', + child: _buildInTheme( + StreamBadgeNotification( + label: '1', + type: type, + size: size, + ), + ), + ), + ], + ), + ); + + goldenTest( + 'renders dark theme type and size matrix', + fileName: 'stream_badge_notification_dark_matrix', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 100), + children: [ + for (final type in StreamBadgeNotificationType.values) + for (final size in StreamBadgeNotificationSize.values) + GoldenTestScenario( + name: '${type.name}_${size.name}', + child: _buildInTheme( + StreamBadgeNotification( + label: '1', + type: type, + size: size, + ), + brightness: Brightness.dark, + ), + ), + ], + ), + ); + + goldenTest( + 'renders count variants correctly', + fileName: 'stream_badge_notification_counts', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 100), + children: [ + for (final count in ['1', '9', '25', '99', '99+']) + GoldenTestScenario( + name: 'count_$count', + child: _buildInTheme( + StreamBadgeNotification(label: count), + ), + ), + ], + ), + ); + }); +} + +Widget _buildInTheme( + Widget child, { + Brightness brightness = Brightness.light, +}) { + 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: child), + ), + ), + ), + ); +} diff --git a/packages/stream_core_flutter/test/components/channel_list/stream_channel_list_item_golden_test.dart b/packages/stream_core_flutter/test/components/channel_list/stream_channel_list_item_golden_test.dart new file mode 100644 index 0000000..d389c34 --- /dev/null +++ b/packages/stream_core_flutter/test/components/channel_list/stream_channel_list_item_golden_test.dart @@ -0,0 +1,194 @@ +import 'package:alchemist/alchemist.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +void main() { + group('StreamChannelListItem Golden Tests', () { + goldenTest( + 'renders light theme variants', + fileName: 'stream_channel_list_item_light', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 400), + children: [ + GoldenTestScenario( + name: 'full', + child: _buildInTheme( + StreamChannelListItem( + avatar: StreamAvatar( + size: StreamAvatarSize.xl, + placeholder: (context) => const Text('JD'), + ), + title: 'Jane Doe', + subtitle: const Text('Hey! Are you free for a call?'), + timestamp: '9:41', + unreadCount: 3, + ), + ), + ), + GoldenTestScenario( + name: 'no_unread', + child: _buildInTheme( + StreamChannelListItem( + avatar: StreamAvatar( + size: StreamAvatarSize.xl, + placeholder: (context) => const Text('BS'), + ), + title: 'Bob Smith', + subtitle: const Text('Thanks for the update!'), + timestamp: 'Yesterday', + ), + ), + ), + GoldenTestScenario( + name: 'with_mute_icon', + child: _buildInTheme( + Builder( + builder: (context) => StreamChannelListItem( + avatar: StreamAvatar( + size: StreamAvatarSize.xl, + placeholder: (context) => const Text('MT'), + ), + title: 'Muted Channel', + titleTrailing: Icon( + context.streamIcons.mute, + size: 16, + color: context.streamColorScheme.textTertiary, + ), + subtitle: const Text('Last message...'), + timestamp: '10:15', + unreadCount: 1, + ), + ), + ), + ), + GoldenTestScenario( + name: 'no_subtitle', + child: _buildInTheme( + StreamChannelListItem( + avatar: StreamAvatar( + size: StreamAvatarSize.xl, + placeholder: (context) => const Text('MN'), + ), + title: 'Minimal', + ), + ), + ), + GoldenTestScenario( + name: 'long_text', + child: _buildInTheme( + StreamChannelListItem( + avatar: StreamAvatar( + size: StreamAvatarSize.xl, + placeholder: (context) => const Text('LT'), + ), + title: 'Very Long Channel Name That Should Be Truncated', + subtitle: const Text( + 'This is a very long message preview that should ' + 'be truncated with an ellipsis', + ), + timestamp: '01/15/2026', + unreadCount: 99, + ), + ), + ), + ], + ), + ); + + goldenTest( + 'renders dark theme variants', + fileName: 'stream_channel_list_item_dark', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 400), + children: [ + GoldenTestScenario( + name: 'full', + child: _buildInTheme( + StreamChannelListItem( + avatar: StreamAvatar( + size: StreamAvatarSize.xl, + placeholder: (context) => const Text('JD'), + ), + title: 'Jane Doe', + subtitle: const Text('Hey! Are you free for a call?'), + timestamp: '9:41', + unreadCount: 3, + ), + brightness: Brightness.dark, + ), + ), + GoldenTestScenario( + name: 'no_unread', + child: _buildInTheme( + StreamChannelListItem( + avatar: StreamAvatar( + size: StreamAvatarSize.xl, + placeholder: (context) => const Text('BS'), + ), + title: 'Bob Smith', + subtitle: const Text('Thanks for the update!'), + timestamp: 'Yesterday', + ), + brightness: Brightness.dark, + ), + ), + GoldenTestScenario( + name: 'with_widget_subtitle', + child: _buildInTheme( + Builder( + builder: (context) => StreamChannelListItem( + avatar: StreamAvatar( + size: StreamAvatarSize.xl, + placeholder: (context) => const Text('GC'), + ), + title: 'Group Chat', + subtitle: Row( + children: [ + Text( + 'Alice: ', + style: TextStyle( + fontWeight: FontWeight.w600, + color: context.streamColorScheme.textTertiary, + ), + ), + const Expanded( + child: Text( + 'New mockups ready for review', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + timestamp: '9:41', + unreadCount: 5, + ), + ), + brightness: Brightness.dark, + ), + ), + ], + ), + ); + }); +} + +Widget _buildInTheme( + Widget child, { + Brightness brightness = Brightness.light, +}) { + 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: child, + ), + ), + ); +} From edc8c2b3f93d41d4a317a5c06190552d9b9c0c9d Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 26 Feb 2026 15:46:38 +0100 Subject: [PATCH 04/15] Improve channel list item for implementation --- .../stream_channel_list_item.dart | 45 ++++++++-------- .../stream_channel_list_item.dart | 52 ++++++++++++------- .../theme/components/stream_avatar_theme.dart | 2 +- .../stream_channel_list_item_golden_test.dart | 30 +++++------ 4 files changed, 71 insertions(+), 58 deletions(-) diff --git a/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart b/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart index f971ff5..86e9b32 100644 --- a/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart +++ b/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart @@ -60,10 +60,10 @@ Widget buildStreamChannelListItemPlayground(BuildContext context) { placeholder: (context) => const Text('DT'), ), ), - title: title, - titleTrailing: showMuteIcon ? Icon(icons.mute, size: 16, color: colorScheme.textTertiary) : null, + title: Text(title), + titleTrailing: showMuteIcon ? Icon(icons.mute, size: 20, color: colorScheme.textTertiary) : null, subtitle: subtitle != null ? Text(subtitle) : null, - timestamp: timestamp, + timestamp: timestamp != null ? Text(timestamp) : null, unreadCount: unreadCount, ), ), @@ -135,9 +135,9 @@ class _DirectMessageSection extends StatelessWidget { placeholder: (context) => const Text('JD'), ), ), - title: 'Jane Doe', + title: const Text('Jane Doe'), subtitle: const Text('Hey! Are you free for a call?'), - timestamp: '9:41', + timestamp: const Text('9:41'), unreadCount: 3, ), StreamChannelListItem( @@ -148,9 +148,9 @@ class _DirectMessageSection extends StatelessWidget { placeholder: (context) => const Text('BS'), ), ), - title: 'Bob Smith', + title: const Text('Bob Smith'), subtitle: const Text('Thanks for the update!'), - timestamp: 'Yesterday', + timestamp: const Text('Yesterday'), ), StreamChannelListItem( avatar: StreamOnlineIndicator( @@ -162,9 +162,9 @@ class _DirectMessageSection extends StatelessWidget { placeholder: (context) => const Text('CW'), ), ), - title: 'Carol White', + title: const Text('Carol White'), subtitle: const Text('See you tomorrow!'), - timestamp: 'Saturday', + timestamp: const Text('Saturday'), ), ], ), @@ -212,9 +212,9 @@ class _GroupChannelSection extends StatelessWidget { ), ], ), - title: 'Design Team', + title: const Text('Design Team'), subtitle: const Text('Alice: New mockups ready for review'), - timestamp: '9:41', + timestamp: const Text('9:41'), unreadCount: 5, ), StreamChannelListItem( @@ -225,9 +225,9 @@ class _GroupChannelSection extends StatelessWidget { StreamAvatar(placeholder: (context) => const Text('GH')), ], ), - title: 'Engineering', + title: const Text('Engineering'), subtitle: const Text('Bob: PR merged successfully'), - timestamp: '10:15', + timestamp: const Text('10:15'), unreadCount: 12, ), StreamChannelListItem( @@ -239,14 +239,14 @@ class _GroupChannelSection extends StatelessWidget { StreamAvatar(placeholder: (context) => const Text('MN')), ], ), - title: 'Muted Group', + title: const Text('Muted Group'), titleTrailing: Icon( icons.mute, size: 16, color: colorScheme.textTertiary, ), subtitle: const Text('Carol: Meeting notes attached'), - timestamp: 'Yesterday', + timestamp: const Text('Yesterday'), ), ], ), @@ -282,10 +282,11 @@ class _EdgeCasesSection extends StatelessWidget { size: StreamAvatarSize.xl, placeholder: (context) => const Text('LT'), ), - title: 'Very Long Channel Name That Should Be Truncated Properly', - subtitle: - const Text('This is a very long message preview that should be truncated with an ellipsis when it overflows'), - timestamp: '01/15/2026', + title: const Text('Very Long Channel Name That Should Be Truncated Properly'), + subtitle: const Text( + 'This is a very long message preview that should be truncated with an ellipsis when it overflows', + ), + timestamp: const Text('01/15/2026'), unreadCount: 99, ), StreamChannelListItem( @@ -295,16 +296,16 @@ class _EdgeCasesSection extends StatelessWidget { foregroundColor: colorScheme.avatarPalette[3].foregroundColor, placeholder: (context) => const Text('NR'), ), - title: 'No Unread', + title: const Text('No Unread'), subtitle: const Text('All caught up!'), - timestamp: '3:00', + timestamp: const Text('3:00'), ), StreamChannelListItem( avatar: StreamAvatar( size: StreamAvatarSize.xl, placeholder: (context) => const Text('MN'), ), - title: 'Minimal', + title: const Text('Minimal'), ), ], ), diff --git a/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart b/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart index 312c4cc..e2ed758 100644 --- a/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart +++ b/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart @@ -25,9 +25,9 @@ import '../badge/stream_badge_notification.dart'; /// ```dart /// StreamChannelListItem( /// avatar: StreamAvatar(placeholder: (context) => Text('AB')), -/// title: 'General', +/// title: Text('General'), /// subtitle: Text('Hello, how are you?'), -/// timestamp: '9:41', +/// timestamp: Text('9:41'), /// unreadCount: 3, /// ) /// ``` @@ -40,10 +40,10 @@ import '../badge/stream_badge_notification.dart'; /// ```dart /// StreamChannelListItem( /// avatar: StreamAvatar(placeholder: (context) => Text('AB')), -/// title: 'Muted Channel', +/// title: Text('Muted Channel'), /// titleTrailing: Icon(Icons.volume_off, size: 16), /// subtitle: Text('Last message...'), -/// timestamp: 'Yesterday', +/// timestamp: Text('Yesterday'), /// ) /// ``` /// {@end-tool} @@ -64,11 +64,11 @@ class StreamChannelListItem extends StatelessWidget { StreamChannelListItem({ super.key, required Widget avatar, - required String title, + required Widget title, Widget? titleTrailing, Widget? subtitle, Widget? subtitleTrailing, - String? timestamp, + Widget? timestamp, int unreadCount = 0, VoidCallback? onTap, VoidCallback? onLongPress, @@ -124,8 +124,11 @@ class StreamChannelListItemProps { /// in a [StreamOnlineIndicator]. final Widget avatar; - /// The channel title text. - final String title; + /// The channel title widget. + /// + /// Typically a [Text] widget with the channel name. The default text style + /// is provided by the theme's title style via [DefaultTextStyle]. + final Widget title; /// An optional widget displayed after the title text. /// @@ -143,10 +146,11 @@ class StreamChannelListItemProps { /// Typically used for a mute icon or similar indicator. final Widget? subtitleTrailing; - /// The formatted timestamp string. + /// The timestamp widget displayed in the trailing section of the title row. /// - /// Displayed in the trailing section of the title row. - final String? timestamp; + /// Typically a [Text] widget with a formatted date string. The default text + /// style is provided by the theme's timestamp style via [DefaultTextStyle]. + final Widget? timestamp; /// The number of unread messages. /// @@ -272,9 +276,9 @@ class _TitleRow extends StatelessWidget { required this.spacing, }); - final String title; + final Widget title; final Widget? titleTrailing; - final String? timestamp; + final Widget? timestamp; final int unreadCount; final TextStyle titleStyle; final TextStyle timestampStyle; @@ -289,14 +293,18 @@ class _TitleRow extends StatelessWidget { child: Row( spacing: spacing.xxs, children: [ - Expanded( + Flexible( child: ConstrainedBox( constraints: BoxConstraints(minHeight: StreamBadgeNotificationSize.sm.value), - child: Text( - title, - style: titleStyle, - maxLines: 1, - overflow: TextOverflow.ellipsis, + child: Align( + alignment: AlignmentDirectional.centerStart, + widthFactor: 1, + child: DefaultTextStyle.merge( + style: titleStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + child: title, + ), ), ), ), @@ -309,7 +317,11 @@ class _TitleRow extends StatelessWidget { mainAxisSize: MainAxisSize.min, spacing: spacing.xs, children: [ - if (timestamp != null) Text(timestamp!, style: timestampStyle), + if (timestamp case final timestamp?) + DefaultTextStyle.merge( + style: timestampStyle, + child: timestamp, + ), if (unreadCount > 0) StreamBadgeNotification(label: '$unreadCount'), ], ), diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_avatar_theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_avatar_theme.dart index 70cefea..c1dbccf 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_avatar_theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_avatar_theme.dart @@ -31,7 +31,7 @@ enum StreamAvatarSize { xl(48), /// Extra-extra large avatar (64px diameter). - xxl(64) + xxl(80) ; /// Constructs a [StreamAvatarSize] with the given diameter. diff --git a/packages/stream_core_flutter/test/components/channel_list/stream_channel_list_item_golden_test.dart b/packages/stream_core_flutter/test/components/channel_list/stream_channel_list_item_golden_test.dart index d389c34..d6da329 100644 --- a/packages/stream_core_flutter/test/components/channel_list/stream_channel_list_item_golden_test.dart +++ b/packages/stream_core_flutter/test/components/channel_list/stream_channel_list_item_golden_test.dart @@ -19,9 +19,9 @@ void main() { size: StreamAvatarSize.xl, placeholder: (context) => const Text('JD'), ), - title: 'Jane Doe', + title: const Text('Jane Doe'), subtitle: const Text('Hey! Are you free for a call?'), - timestamp: '9:41', + timestamp: const Text('9:41'), unreadCount: 3, ), ), @@ -34,9 +34,9 @@ void main() { size: StreamAvatarSize.xl, placeholder: (context) => const Text('BS'), ), - title: 'Bob Smith', + title: const Text('Bob Smith'), subtitle: const Text('Thanks for the update!'), - timestamp: 'Yesterday', + timestamp: const Text('Yesterday'), ), ), ), @@ -49,14 +49,14 @@ void main() { size: StreamAvatarSize.xl, placeholder: (context) => const Text('MT'), ), - title: 'Muted Channel', + title: const Text('Muted Channel'), titleTrailing: Icon( context.streamIcons.mute, size: 16, color: context.streamColorScheme.textTertiary, ), subtitle: const Text('Last message...'), - timestamp: '10:15', + timestamp: const Text('10:15'), unreadCount: 1, ), ), @@ -70,7 +70,7 @@ void main() { size: StreamAvatarSize.xl, placeholder: (context) => const Text('MN'), ), - title: 'Minimal', + title: const Text('Minimal'), ), ), ), @@ -82,12 +82,12 @@ void main() { size: StreamAvatarSize.xl, placeholder: (context) => const Text('LT'), ), - title: 'Very Long Channel Name That Should Be Truncated', + title: const Text('Very Long Channel Name That Should Be Truncated'), subtitle: const Text( 'This is a very long message preview that should ' 'be truncated with an ellipsis', ), - timestamp: '01/15/2026', + timestamp: const Text('01/15/2026'), unreadCount: 99, ), ), @@ -110,9 +110,9 @@ void main() { size: StreamAvatarSize.xl, placeholder: (context) => const Text('JD'), ), - title: 'Jane Doe', + title: const Text('Jane Doe'), subtitle: const Text('Hey! Are you free for a call?'), - timestamp: '9:41', + timestamp: const Text('9:41'), unreadCount: 3, ), brightness: Brightness.dark, @@ -126,9 +126,9 @@ void main() { size: StreamAvatarSize.xl, placeholder: (context) => const Text('BS'), ), - title: 'Bob Smith', + title: const Text('Bob Smith'), subtitle: const Text('Thanks for the update!'), - timestamp: 'Yesterday', + timestamp: const Text('Yesterday'), ), brightness: Brightness.dark, ), @@ -142,7 +142,7 @@ void main() { size: StreamAvatarSize.xl, placeholder: (context) => const Text('GC'), ), - title: 'Group Chat', + title: const Text('Group Chat'), subtitle: Row( children: [ Text( @@ -161,7 +161,7 @@ void main() { ), ], ), - timestamp: '9:41', + timestamp: const Text('9:41'), unreadCount: 5, ), ), From be33ab3088bd2df1a2a9504c6ace6d2dcac1f3aa Mon Sep 17 00:00:00 2001 From: renefloor <15101411+renefloor@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:07:52 +0000 Subject: [PATCH 05/15] chore: Update Goldens --- .../ci/stream_badge_notification_counts.png | Bin 0 -> 2688 bytes .../ci/stream_badge_notification_dark_matrix.png | Bin 0 -> 5183 bytes .../stream_badge_notification_light_matrix.png | Bin 0 -> 3648 bytes .../goldens/ci/stream_channel_list_item_dark.png | Bin 0 -> 6402 bytes .../ci/stream_channel_list_item_light.png | Bin 0 -> 6830 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/stream_core_flutter/test/components/badge/goldens/ci/stream_badge_notification_counts.png create mode 100644 packages/stream_core_flutter/test/components/badge/goldens/ci/stream_badge_notification_dark_matrix.png create mode 100644 packages/stream_core_flutter/test/components/badge/goldens/ci/stream_badge_notification_light_matrix.png create mode 100644 packages/stream_core_flutter/test/components/channel_list/goldens/ci/stream_channel_list_item_dark.png create mode 100644 packages/stream_core_flutter/test/components/channel_list/goldens/ci/stream_channel_list_item_light.png diff --git a/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_badge_notification_counts.png b/packages/stream_core_flutter/test/components/badge/goldens/ci/stream_badge_notification_counts.png new file mode 100644 index 0000000000000000000000000000000000000000..f77fb2d6e7e9326816b0c5236c65f9b54ed63bbc GIT binary patch literal 2688 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 GfBX++oybN2 literal 0 HcmV?d00001 From 698e4ac7b1c0962a9827d755b1ced18c0c4d9f0d Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 3 Mar 2026 15:16:49 +0100 Subject: [PATCH 06/15] remove unused imports --- .../components/channel_list/stream_channel_list_item.dart | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart b/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart index e2ed758..96fdf51 100644 --- a/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart +++ b/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart @@ -1,13 +1,6 @@ import 'package:flutter/material.dart'; import '../../../stream_core_flutter.dart'; -import '../../factory/stream_component_factory.dart'; -import '../../theme/components/stream_channel_list_item_theme.dart'; -import '../../theme/primitives/stream_spacing.dart'; -import '../../theme/semantics/stream_color_scheme.dart'; -import '../../theme/semantics/stream_text_theme.dart'; -import '../../theme/stream_theme_extensions.dart'; -import '../badge/stream_badge_notification.dart'; /// A list item for displaying a channel in a channel list. /// From dce330f754bec75cc2f06ad7cd6a7648fe01e605 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 4 Mar 2026 10:34:43 +0100 Subject: [PATCH 07/15] Merge channel list item with list tile --- .../badge/stream_badge_notification.dart | 6 +- .../stream_channel_list_item.dart | 1 + .../lib/src/components.dart | 2 +- .../stream_channel_list_item.dart | 150 +++++++++--------- .../src/components/list/stream_list_tile.dart | 145 ++++++++++------- .../stream_component_factory.g.theme.dart | 3 +- .../stream_badge_notification_theme.dart | 16 +- ...ream_badge_notification_theme.g.theme.dart | 3 +- .../stream_channel_list_item_theme.dart | 11 ++ .../stream_channel_list_item_golden_test.dart | 6 +- 10 files changed, 189 insertions(+), 154 deletions(-) diff --git a/apps/design_system_gallery/lib/components/badge/stream_badge_notification.dart b/apps/design_system_gallery/lib/components/badge/stream_badge_notification.dart index 1b61c04..8e64506 100644 --- a/apps/design_system_gallery/lib/components/badge/stream_badge_notification.dart +++ b/apps/design_system_gallery/lib/components/badge/stream_badge_notification.dart @@ -123,8 +123,7 @@ class _TypeVariantsSection extends StatelessWidget { children: [ for (final type in StreamBadgeNotificationType.values) ...[ _TypeDemo(type: type), - if (type != StreamBadgeNotificationType.values.last) - SizedBox(width: spacing.xl), + if (type != StreamBadgeNotificationType.values.last) SizedBox(width: spacing.xl), ], ], ), @@ -219,8 +218,7 @@ class _SizeVariantsSection extends StatelessWidget { children: [ for (final size in StreamBadgeNotificationSize.values) ...[ _SizeDemo(size: size), - if (size != StreamBadgeNotificationSize.values.last) - SizedBox(width: spacing.xl), + if (size != StreamBadgeNotificationSize.values.last) SizedBox(width: spacing.xl), ], ], ), diff --git a/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart b/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart index 86e9b32..a0d555a 100644 --- a/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart +++ b/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart @@ -65,6 +65,7 @@ Widget buildStreamChannelListItemPlayground(BuildContext context) { subtitle: subtitle != null ? Text(subtitle) : null, timestamp: timestamp != null ? Text(timestamp) : null, unreadCount: unreadCount, + onTap: () => print('onTap'), ), ), ); diff --git a/packages/stream_core_flutter/lib/src/components.dart b/packages/stream_core_flutter/lib/src/components.dart index f447c29..4eaaad1 100644 --- a/packages/stream_core_flutter/lib/src/components.dart +++ b/packages/stream_core_flutter/lib/src/components.dart @@ -21,7 +21,7 @@ export 'components/controls/stream_remove_control.dart'; export 'components/emoji/data/stream_emoji_data.dart'; export 'components/emoji/data/stream_supported_emojis.dart'; export 'components/emoji/stream_emoji_picker_sheet.dart'; -export 'components/list/stream_list_tile.dart' hide DefaultStreamListTile; +export 'components/list/stream_list_tile.dart' hide DefaultStreamListTile, ListTileContainer; export 'components/message_composer.dart'; export 'factory/stream_component_factory.dart'; diff --git a/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart b/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart index 96fdf51..53c4ac7 100644 --- a/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart +++ b/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../../../stream_core_flutter.dart'; +import '../list/stream_list_tile.dart' show ListTileContainer; /// A list item for displaying a channel in a channel list. /// @@ -58,23 +59,23 @@ class StreamChannelListItem extends StatelessWidget { super.key, required Widget avatar, required Widget title, - Widget? titleTrailing, Widget? subtitle, - Widget? subtitleTrailing, Widget? timestamp, int unreadCount = 0, + bool isMuted = false, VoidCallback? onTap, VoidCallback? onLongPress, + bool selected = false, }) : props = StreamChannelListItemProps( avatar: avatar, title: title, - titleTrailing: titleTrailing, subtitle: subtitle, - subtitleTrailing: subtitleTrailing, timestamp: timestamp, unreadCount: unreadCount, + isMuted: isMuted, onTap: onTap, onLongPress: onLongPress, + selected: selected, ); /// The properties that configure this channel list item. @@ -102,13 +103,13 @@ class StreamChannelListItemProps { const StreamChannelListItemProps({ required this.avatar, required this.title, - this.titleTrailing, this.subtitle, - this.subtitleTrailing, this.timestamp, this.unreadCount = 0, + this.isMuted = false, this.onTap, this.onLongPress, + this.selected = false, }); /// The avatar widget displayed at the leading edge. @@ -123,22 +124,12 @@ class StreamChannelListItemProps { /// is provided by the theme's title style via [DefaultTextStyle]. final Widget title; - /// An optional widget displayed after the title text. - /// - /// Typically used for a mute icon or similar indicator. - final Widget? titleTrailing; - /// The message preview widget displayed below the title. /// /// Typically a [Text] widget with the last message, but can be any widget /// for richer content (e.g., icons, read receipts, sender prefix). final Widget? subtitle; - /// An optional trailing widget in the subtitle row. - /// - /// Typically used for a mute icon or similar indicator. - final Widget? subtitleTrailing; - /// The timestamp widget displayed in the trailing section of the title row. /// /// Typically a [Text] widget with a formatted date string. The default text @@ -150,11 +141,19 @@ class StreamChannelListItemProps { /// When greater than zero, a [StreamBadgeNotification] is displayed. final int unreadCount; + /// Whether the channel is muted. + /// + /// When true, a mute icon is displayed in the title or subtitle. + final bool isMuted; + /// Called when the list item is tapped. final VoidCallback? onTap; /// Called when the list item is long-pressed. final VoidCallback? onLongPress; + + /// Whether the list item is in a selected state. + final bool selected; } /// The default implementation of [StreamChannelListItem]. @@ -178,9 +177,6 @@ class DefaultStreamChannelListItem extends StatefulWidget { } class _DefaultStreamChannelListItemState extends State { - var _isHovered = false; - var _isPressed = false; - StreamChannelListItemProps get props => widget.props; @override @@ -189,68 +185,65 @@ class _DefaultStreamChannelListItemState extends State setState(() => _isHovered = true), - onExit: (_) => setState(() => _isHovered = false), - child: GestureDetector( - onTap: props.onTap, - onLongPress: props.onLongPress, - onTapDown: (_) => setState(() => _isPressed = true), - onTapUp: (_) => setState(() => _isPressed = false), - onTapCancel: () => setState(() => _isPressed = false), - behavior: HitTestBehavior.opaque, - child: Container( - decoration: BoxDecoration( - color: background, - borderRadius: BorderRadius.circular(spacing.lg), - ), - padding: EdgeInsets.all(spacing.md - 4), - margin: const EdgeInsets.all(4), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: spacing.md, - children: [ - props.avatar, - Expanded( - child: Padding( - padding: EdgeInsets.symmetric(vertical: spacing.xxxs), - child: Column( - mainAxisSize: MainAxisSize.min, - spacing: spacing.xxs, - children: [ - _TitleRow( - title: props.title, - titleTrailing: props.titleTrailing, - timestamp: props.timestamp, - unreadCount: props.unreadCount, - titleStyle: effectiveTitleStyle, - timestampStyle: effectiveTimestampStyle, - spacing: spacing, - ), - if (props.subtitle case final subtitle?) - _SubtitleRow( - subtitle: subtitle, - subtitleTrailing: props.subtitleTrailing, - subtitleStyle: effectiveSubtitleStyle, + final effectiveMuteIconPosition = channelListItemTheme.muteIconPosition ?? defaults.muteIconPosition; + + final muteIcon = props.isMuted + ? Icon( + context.streamIcons.mute, + size: 20, + color: context.streamColorScheme.textTertiary, + ) + : null; + + final hasMuteIconInSubtitle = effectiveMuteIconPosition == MuteIconPosition.subtitle && props.isMuted; + + return StreamListTileTheme( + data: context.streamListTileTheme.copyWith(contentPadding: EdgeInsets.all(spacing.md - 4)), + child: Padding( + padding: const EdgeInsets.all(4), + child: Material( + type: MaterialType.transparency, + child: ListTileContainer( + enabled: true, + selected: props.selected, + onTap: props.onTap, + onLongPress: props.onLongPress, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: spacing.md, + children: [ + props.avatar, + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(vertical: spacing.xxxs), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: spacing.xxs, + children: [ + _TitleRow( + title: props.title, + titleTrailing: effectiveMuteIconPosition == MuteIconPosition.title ? muteIcon : null, + timestamp: props.timestamp, + unreadCount: props.unreadCount, + titleStyle: effectiveTitleStyle, + timestampStyle: effectiveTimestampStyle, + spacing: spacing, ), - ], + if (props.subtitle != null || hasMuteIconInSubtitle) + _SubtitleRow( + subtitle: props.subtitle, + subtitleTrailing: effectiveMuteIconPosition == MuteIconPosition.subtitle ? muteIcon : null, + subtitleStyle: effectiveSubtitleStyle, + ), + ], + ), ), ), - ), - ], + ], + ), ), ), ), @@ -330,7 +323,7 @@ class _SubtitleRow extends StatelessWidget { required this.subtitleStyle, }); - final Widget subtitle; + final Widget? subtitle; final Widget? subtitleTrailing; final TextStyle subtitleStyle; @@ -342,7 +335,7 @@ class _SubtitleRow extends StatelessWidget { overflow: TextOverflow.ellipsis, child: Row( children: [ - Expanded(child: subtitle), + Expanded(child: subtitle ?? const SizedBox.shrink()), ?subtitleTrailing, ], ), @@ -378,4 +371,7 @@ class _StreamChannelListItemThemeDefaults extends StreamChannelListItemThemeData @override Color get borderColor => _colorScheme.borderSubtle; + + @override + MuteIconPosition get muteIconPosition => MuteIconPosition.title; } diff --git a/packages/stream_core_flutter/lib/src/components/list/stream_list_tile.dart b/packages/stream_core_flutter/lib/src/components/list/stream_list_tile.dart index f287767..792be26 100644 --- a/packages/stream_core_flutter/lib/src/components/list/stream_list_tile.dart +++ b/packages/stream_core_flutter/lib/src/components/list/stream_list_tile.dart @@ -215,8 +215,6 @@ class DefaultStreamListTile extends StatelessWidget { if (props.selected) WidgetState.selected, }; - final textDirection = Directionality.of(context); - final effectiveTitleColor = (theme.titleColor ?? defaults.titleColor).resolve(states)!; final effectiveSubtitleColor = (theme.subtitleColor ?? defaults.subtitleColor).resolve(states)!; final effectiveDescriptionColor = (theme.descriptionColor ?? defaults.descriptionColor).resolve(states)!; @@ -225,19 +223,7 @@ class DefaultStreamListTile extends StatelessWidget { final effectiveTitleTextStyle = theme.titleTextStyle ?? defaults.titleTextStyle; final effectiveSubtitleTextStyle = theme.subtitleTextStyle ?? defaults.subtitleTextStyle; final effectiveDescriptionTextStyle = theme.descriptionTextStyle ?? defaults.descriptionTextStyle; - final effectiveBackgroundColor = (theme.backgroundColor ?? defaults.backgroundColor).resolve(states); - final effectiveShape = theme.shape ?? defaults.shape; - final effectiveContentPadding = (theme.contentPadding ?? defaults.contentPadding).resolve(textDirection); final effectiveMinTileHeight = theme.minTileHeight ?? defaults.minTileHeight; - final effectiveOverlayColor = theme.overlayColor ?? defaults.overlayColor; - - // Mouse cursor: show a non-interactive cursor when the tile is disabled - // OR when no gesture callbacks are wired. - final mouseStates = { - if (!props.enabled || (props.onTap == null && props.onLongPress == null)) WidgetState.disabled, - }; - - final effectiveMouseCursor = WidgetStateMouseCursor.clickable.resolve(mouseStates); Widget? leadingWidget; if (props.leading case final leading?) { @@ -284,47 +270,29 @@ class DefaultStreamListTile extends StatelessWidget { ); } - return InkWell( - customBorder: effectiveShape, - onTap: props.enabled ? props.onTap : null, - onLongPress: props.enabled ? props.onLongPress : null, - canRequestFocus: props.enabled, - mouseCursor: effectiveMouseCursor, - overlayColor: effectiveOverlayColor, - child: Semantics( - button: props.onTap != null || props.onLongPress != null, - selected: props.selected, - enabled: props.enabled, - child: Ink( - decoration: ShapeDecoration( - shape: effectiveShape, - color: effectiveBackgroundColor, - ), - child: SafeArea( - top: false, - bottom: false, - minimum: effectiveContentPadding, - child: IconTheme.merge( - data: IconThemeData(color: effectiveIconColor), - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: effectiveMinTileHeight), - child: Row( - spacing: spacing.xs, - children: [ - ?leadingWidget, - Expanded( - child: Column( - mainAxisSize: .min, - crossAxisAlignment: .start, - children: [?titleWidget, ?subtitleWidget], - ), - ), - ?descriptionWidget, - ?trailingWidget, - ], + return ListTileContainer( + enabled: props.enabled, + selected: props.selected, + onTap: props.onTap, + onLongPress: props.onLongPress, + child: IconTheme.merge( + data: IconThemeData(color: effectiveIconColor), + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: effectiveMinTileHeight), + child: Row( + spacing: spacing.xs, + children: [ + ?leadingWidget, + Expanded( + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [?titleWidget, ?subtitleWidget], ), ), - ), + ?descriptionWidget, + ?trailingWidget, + ], ), ), ), @@ -404,3 +372,74 @@ class _StreamListTileThemeDefaults extends StreamListTileThemeData { return StreamColors.transparent; }); } + +class ListTileContainer extends StatelessWidget { + const ListTileContainer({ + super.key, + required this.child, + required this.enabled, + required this.selected, + required this.onTap, + required this.onLongPress, + }); + + final Widget child; + + final bool enabled; + final bool selected; + final VoidCallback? onTap; + final VoidCallback? onLongPress; + + @override + Widget build(BuildContext context) { + final theme = context.streamListTileTheme; + final defaults = _StreamListTileThemeDefaults(context); + + // Build the WidgetState set once and share it across all color resolvers. + final states = { + if (!enabled) WidgetState.disabled, + if (selected) WidgetState.selected, + }; + + final textDirection = Directionality.of(context); + + final effectiveBackgroundColor = (theme.backgroundColor ?? defaults.backgroundColor).resolve(states); + final effectiveShape = theme.shape ?? defaults.shape; + final effectiveContentPadding = (theme.contentPadding ?? defaults.contentPadding).resolve(textDirection); + final effectiveOverlayColor = theme.overlayColor ?? defaults.overlayColor; + + // Mouse cursor: show a non-interactive cursor when the tile is disabled + // OR when no gesture callbacks are wired. + final mouseStates = { + if (!enabled || (onTap == null && onLongPress == null)) WidgetState.disabled, + }; + + final effectiveMouseCursor = WidgetStateMouseCursor.clickable.resolve(mouseStates); + + return InkWell( + customBorder: effectiveShape, + onTap: enabled ? onTap : null, + onLongPress: enabled ? onLongPress : null, + canRequestFocus: enabled, + mouseCursor: effectiveMouseCursor, + overlayColor: effectiveOverlayColor, + child: Semantics( + button: onTap != null || onLongPress != null, + selected: selected, + enabled: enabled, + child: Ink( + decoration: ShapeDecoration( + shape: effectiveShape, + color: effectiveBackgroundColor, + ), + child: SafeArea( + top: false, + bottom: false, + minimum: effectiveContentPadding, + child: child, + ), + ), + ), + ); + } +} 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 8ce1a73..7d4dd78 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 @@ -34,8 +34,7 @@ mixin _$StreamComponentBuilders { avatarGroup: t < 0.5 ? a.avatarGroup : b.avatarGroup, avatarStack: t < 0.5 ? a.avatarStack : b.avatarStack, badgeCount: t < 0.5 ? a.badgeCount : b.badgeCount, - badgeNotification: - t < 0.5 ? a.badgeNotification : b.badgeNotification, + badgeNotification: t < 0.5 ? a.badgeNotification : b.badgeNotification, button: t < 0.5 ? a.button : b.button, channelListItem: t < 0.5 ? a.channelListItem : b.channelListItem, checkbox: t < 0.5 ? a.checkbox : b.checkbox, diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_badge_notification_theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_badge_notification_theme.dart index 72b27bf..40fc0a0 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_badge_notification_theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_badge_notification_theme.dart @@ -18,7 +18,8 @@ enum StreamBadgeNotificationSize { xs(16), /// Small badge (20px height). - sm(20); + sm(20) + ; /// Constructs a [StreamBadgeNotificationSize] with the given height. const StreamBadgeNotificationSize(this.value); @@ -65,11 +66,8 @@ class StreamBadgeNotificationTheme extends InheritedTheme { /// Returns the merged [StreamBadgeNotificationThemeData] from local and /// global themes. static StreamBadgeNotificationThemeData of(BuildContext context) { - final localTheme = context - .dependOnInheritedWidgetOfExactType(); - return StreamTheme.of(context) - .badgeNotificationTheme - .merge(localTheme?.data); + final localTheme = context.dependOnInheritedWidgetOfExactType(); + return StreamTheme.of(context).badgeNotificationTheme.merge(localTheme?.data); } @override @@ -78,8 +76,7 @@ class StreamBadgeNotificationTheme extends InheritedTheme { } @override - bool updateShouldNotify(StreamBadgeNotificationTheme oldWidget) => - data != oldWidget.data; + bool updateShouldNotify(StreamBadgeNotificationTheme oldWidget) => data != oldWidget.data; } /// Theme data for customizing [StreamBadgeNotification] widgets. @@ -90,8 +87,7 @@ class StreamBadgeNotificationTheme extends InheritedTheme { /// * [StreamBadgeNotificationTheme], for overriding theme in a widget subtree. @themeGen @immutable -class StreamBadgeNotificationThemeData - with _$StreamBadgeNotificationThemeData { +class StreamBadgeNotificationThemeData with _$StreamBadgeNotificationThemeData { /// Creates a badge notification theme with optional style overrides. const StreamBadgeNotificationThemeData({ this.size, diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_badge_notification_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_badge_notification_theme.g.theme.dart index bd0585a..cbb6539 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_badge_notification_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_badge_notification_theme.g.theme.dart @@ -65,8 +65,7 @@ mixin _$StreamBadgeNotificationThemeData { size: size ?? _this.size, primaryBackgroundColor: primaryBackgroundColor ?? _this.primaryBackgroundColor, - errorBackgroundColor: - errorBackgroundColor ?? _this.errorBackgroundColor, + errorBackgroundColor: errorBackgroundColor ?? _this.errorBackgroundColor, neutralBackgroundColor: neutralBackgroundColor ?? _this.neutralBackgroundColor, textColor: textColor ?? _this.textColor, diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.dart index 234f651..f4b3aa7 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.dart @@ -94,6 +94,7 @@ class StreamChannelListItemThemeData with _$StreamChannelListItemThemeData { this.hoverColor, this.pressedColor, this.borderColor, + this.muteIconPosition, }); /// The text style for the channel title. @@ -131,6 +132,11 @@ class StreamChannelListItemThemeData with _$StreamChannelListItemThemeData { /// Falls back to [StreamColorScheme.borderSubtle]. final Color? borderColor; + /// The position of the mute icon. + /// + /// Falls back to [MuteIconPosition.title]. + final MuteIconPosition? muteIconPosition; + /// Linearly interpolate between two [StreamChannelListItemThemeData] objects. static StreamChannelListItemThemeData? lerp( StreamChannelListItemThemeData? a, @@ -138,3 +144,8 @@ class StreamChannelListItemThemeData with _$StreamChannelListItemThemeData { double t, ) => _$StreamChannelListItemThemeData.lerp(a, b, t); } + +enum MuteIconPosition { + title, + subtitle, +} diff --git a/packages/stream_core_flutter/test/components/channel_list/stream_channel_list_item_golden_test.dart b/packages/stream_core_flutter/test/components/channel_list/stream_channel_list_item_golden_test.dart index d6da329..800e481 100644 --- a/packages/stream_core_flutter/test/components/channel_list/stream_channel_list_item_golden_test.dart +++ b/packages/stream_core_flutter/test/components/channel_list/stream_channel_list_item_golden_test.dart @@ -50,11 +50,7 @@ void main() { placeholder: (context) => const Text('MT'), ), title: const Text('Muted Channel'), - titleTrailing: Icon( - context.streamIcons.mute, - size: 16, - color: context.streamColorScheme.textTertiary, - ), + isMuted: true, subtitle: const Text('Last message...'), timestamp: const Text('10:15'), unreadCount: 1, From 6eb0c43ca646fce23c6ca49df8ec26a2f6bdd132 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 4 Mar 2026 10:36:42 +0100 Subject: [PATCH 08/15] fix gallery --- .../components/channel_list/stream_channel_list_item.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart b/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart index a0d555a..2ef238c 100644 --- a/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart +++ b/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart @@ -61,7 +61,7 @@ Widget buildStreamChannelListItemPlayground(BuildContext context) { ), ), title: Text(title), - titleTrailing: showMuteIcon ? Icon(icons.mute, size: 20, color: colorScheme.textTertiary) : null, + isMuted: showMuteIcon, subtitle: subtitle != null ? Text(subtitle) : null, timestamp: timestamp != null ? Text(timestamp) : null, unreadCount: unreadCount, @@ -241,11 +241,7 @@ class _GroupChannelSection extends StatelessWidget { ], ), title: const Text('Muted Group'), - titleTrailing: Icon( - icons.mute, - size: 16, - color: colorScheme.textTertiary, - ), + isMuted: true, subtitle: const Text('Carol: Meeting notes attached'), timestamp: const Text('Yesterday'), ), From aaf1475bd34849a9f140a5331688e16d50cdb807 Mon Sep 17 00:00:00 2001 From: renefloor <15101411+renefloor@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:38:57 +0000 Subject: [PATCH 09/15] chore: Update Goldens --- .../ci/stream_channel_list_item_light.png | Bin 6830 -> 6843 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/stream_core_flutter/test/components/channel_list/goldens/ci/stream_channel_list_item_light.png b/packages/stream_core_flutter/test/components/channel_list/goldens/ci/stream_channel_list_item_light.png index 1432fb2b16bbb8c756585449548bc14bdd4f05f4..0545d5799f4886193dc434bfe960c45941b7b14f 100644 GIT binary patch literal 6843 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 GfBX++oybN2 From 9aa05af04b522c8431ae565f2b4547cbc147c6d1 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 4 Mar 2026 10:57:51 +0100 Subject: [PATCH 10/15] Fix background color theming --- .../stream_channel_list_item.dart | 2 -- .../stream_channel_list_item.dart | 14 +++------ .../stream_channel_list_item_theme.dart | 18 ++---------- ...tream_channel_list_item_theme.g.theme.dart | 29 +++++++++---------- 4 files changed, 21 insertions(+), 42 deletions(-) diff --git a/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart b/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart index 2ef238c..13cd85d 100644 --- a/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart +++ b/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart @@ -46,7 +46,6 @@ Widget buildStreamChannelListItemPlayground(BuildContext context) { ); final colorScheme = context.streamColorScheme; - final icons = context.streamIcons; return ColoredBox( color: colorScheme.backgroundApp, @@ -185,7 +184,6 @@ class _GroupChannelSection extends StatelessWidget { @override Widget build(BuildContext context) { final colorScheme = context.streamColorScheme; - final icons = context.streamIcons; final spacing = context.streamSpacing; return Column( diff --git a/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart b/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart index 53c4ac7..c319a1a 100644 --- a/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart +++ b/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart @@ -201,7 +201,10 @@ class _DefaultStreamChannelListItemState extends State _textTheme.captionDefault.copyWith(color: _colorScheme.textTertiary); - @override - Color get backgroundColor => _colorScheme.backgroundApp; - - @override - Color get hoverColor => _colorScheme.stateHover; - - @override - Color get pressedColor => _colorScheme.statePressed; - @override Color get borderColor => _colorScheme.borderSubtle; diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.dart index f4b3aa7..c019568 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.dart @@ -91,8 +91,6 @@ class StreamChannelListItemThemeData with _$StreamChannelListItemThemeData { this.subtitleStyle, this.timestampStyle, this.backgroundColor, - this.hoverColor, - this.pressedColor, this.borderColor, this.muteIconPosition, }); @@ -112,20 +110,10 @@ class StreamChannelListItemThemeData with _$StreamChannelListItemThemeData { /// Falls back to [StreamTextTheme.captionDefault] with [StreamColorScheme.textTertiary]. final TextStyle? timestampStyle; - /// The background color of the list item in its default state. + /// Defines the default background color of the tile. /// - /// Falls back to [StreamColorScheme.backgroundApp]. - final Color? backgroundColor; - - /// The overlay color applied when the list item is hovered. - /// - /// Falls back to [StreamColorScheme.stateHover]. - final Color? hoverColor; - - /// The overlay color applied when the list item is pressed. - /// - /// Falls back to [StreamColorScheme.statePressed]. - final Color? pressedColor; + /// This color is resolved from [WidgetState]s. + final WidgetStateProperty? backgroundColor; /// The bottom border color of the list item. /// diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.g.theme.dart index 388e489..bb1baaf 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.g.theme.dart @@ -33,10 +33,14 @@ mixin _$StreamChannelListItemThemeData { titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t), timestampStyle: TextStyle.lerp(a.timestampStyle, b.timestampStyle, t), - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - hoverColor: Color.lerp(a.hoverColor, b.hoverColor, t), - pressedColor: Color.lerp(a.pressedColor, b.pressedColor, t), + backgroundColor: WidgetStateProperty.lerp( + a.backgroundColor, + b.backgroundColor, + t, + Color.lerp, + ), borderColor: Color.lerp(a.borderColor, b.borderColor, t), + muteIconPosition: t < 0.5 ? a.muteIconPosition : b.muteIconPosition, ); } @@ -44,10 +48,9 @@ mixin _$StreamChannelListItemThemeData { TextStyle? titleStyle, TextStyle? subtitleStyle, TextStyle? timestampStyle, - Color? backgroundColor, - Color? hoverColor, - Color? pressedColor, + WidgetStateProperty? backgroundColor, Color? borderColor, + MuteIconPosition? muteIconPosition, }) { final _this = (this as StreamChannelListItemThemeData); @@ -56,9 +59,8 @@ mixin _$StreamChannelListItemThemeData { subtitleStyle: subtitleStyle ?? _this.subtitleStyle, timestampStyle: timestampStyle ?? _this.timestampStyle, backgroundColor: backgroundColor ?? _this.backgroundColor, - hoverColor: hoverColor ?? _this.hoverColor, - pressedColor: pressedColor ?? _this.pressedColor, borderColor: borderColor ?? _this.borderColor, + muteIconPosition: muteIconPosition ?? _this.muteIconPosition, ); } @@ -82,9 +84,8 @@ mixin _$StreamChannelListItemThemeData { _this.timestampStyle?.merge(other.timestampStyle) ?? other.timestampStyle, backgroundColor: other.backgroundColor, - hoverColor: other.hoverColor, - pressedColor: other.pressedColor, borderColor: other.borderColor, + muteIconPosition: other.muteIconPosition, ); } @@ -105,9 +106,8 @@ mixin _$StreamChannelListItemThemeData { _other.subtitleStyle == _this.subtitleStyle && _other.timestampStyle == _this.timestampStyle && _other.backgroundColor == _this.backgroundColor && - _other.hoverColor == _this.hoverColor && - _other.pressedColor == _this.pressedColor && - _other.borderColor == _this.borderColor; + _other.borderColor == _this.borderColor && + _other.muteIconPosition == _this.muteIconPosition; } @override @@ -120,9 +120,8 @@ mixin _$StreamChannelListItemThemeData { _this.subtitleStyle, _this.timestampStyle, _this.backgroundColor, - _this.hoverColor, - _this.pressedColor, _this.borderColor, + _this.muteIconPosition, ); } } From 6803dd2731921ef7856e784f945fa9992a87cbf8 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 4 Mar 2026 11:03:19 +0100 Subject: [PATCH 11/15] fix avatar size --- .../lib/src/components/avatar/stream_avatar_group.dart | 4 ++-- .../lib/src/theme/components/stream_avatar_theme.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/stream_core_flutter/lib/src/components/avatar/stream_avatar_group.dart b/packages/stream_core_flutter/lib/src/components/avatar/stream_avatar_group.dart index 2580f78..c80f728 100644 --- a/packages/stream_core_flutter/lib/src/components/avatar/stream_avatar_group.dart +++ b/packages/stream_core_flutter/lib/src/components/avatar/stream_avatar_group.dart @@ -21,8 +21,8 @@ enum StreamAvatarGroupSize { /// Extra large avatar group (48px diameter). xl(48), - /// Extra-extra large avatar group (64px diameter). - xxl(64) + /// Extra-extra large avatar group (80px diameter). + xxl(80) ; /// Constructs a [StreamAvatarGroupSize] with the given diameter. diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_avatar_theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_avatar_theme.dart index c1dbccf..ea24406 100644 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_avatar_theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_avatar_theme.dart @@ -30,7 +30,7 @@ enum StreamAvatarSize { /// Extra large avatar (48px diameter). xl(48), - /// Extra-extra large avatar (64px diameter). + /// Extra-extra large avatar (80px diameter). xxl(80) ; From da598bd48a6bdadd86048e360d4afc0eeb001e30 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 4 Mar 2026 14:41:07 +0100 Subject: [PATCH 12/15] Added claude code file --- CLAUDE.md | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..33001a9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,94 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +A Flutter monorepo managed with **Melos** containing: +- `packages/stream_core` — Pure Dart SDK (WebSocket, HTTP, models, utilities) +- `packages/stream_core_flutter` — Flutter UI component library with a full design system +- `apps/design_system_gallery` — Widgetbook-based interactive component showcase + +## Common Commands + +All commands use Melos and should be run from the repo root. + +```bash +# Setup +melos bootstrap + +# Linting & formatting +melos run lint:all # analyze + format check +melos run analyze +melos run format +melos run format:verify # check only, no changes + +# Testing +melos run test:all # all tests with coverage +melos run test:dart # stream_core only +melos run test:flutter # stream_core_flutter only + +# Golden tests +melos run update:goldens # regenerate golden images + +# Code generation (run after model/theme changes) +melos run generate:all +melos run generate:icons # regenerate icon font from SVGs +melos run gen-l10n # regenerate localizations +``` + +**Line width:** 120 characters (set in `analysis_options.yaml`). + +## Design + +UI components are designed in **Figma**. When implementing or modifying components, use the **Figma MCP** to inspect designs directly — check spacing, colors, typography, and component structure from the source rather than guessing. + +## Architecture + +### Theme System (`stream_core_flutter/lib/src/theme/`) + +Uses `theme_extensions_builder` to generate Material 3 theme extensions. The hierarchy is: + +1. **Primitives** — raw design tokens: colors, typography, spacing, radius, icons +2. **Semantics** — semantic mappings (e.g., `primaryColor`, `bodyText`) +3. **Component themes** — per-widget theme classes (50+ components), defined in `theme/components/` +4. **Tokens** — light/dark concrete values in `theme/tokens/` + +Generated files have `.g.theme.dart` extension. After modifying `.theme.dart` files, run `melos run generate:flutter`. + +### Component Structure (`stream_core_flutter/lib/src/components/`) + +Components are organized by category: `avatar/`, `buttons/`, `badge/`, `channel_list/`, `list/`, `message_composer/`, `emoji/`, `context_menu/`, `controls/`, `common/`, `accessories/`. + +Each component typically has: +- A widget file +- A theme file in `theme/components/` +- A golden test in `test/components//` +- A Widgetbook use-case in `apps/design_system_gallery/` + +### stream_core Package + +Pure Dart. Key modules: +- `src/ws/` — WebSocket client with reconnect/backoff logic (RxDart-based) +- `src/api/` — Dio HTTP client with interceptors +- `src/attachment/` — File upload and CDN client +- `src/query/` — Query builders and filter models +- `src/logger/` — Structured logging +- `src/user/` — User models and token management + +### Golden Testing + +Golden tests use **Alchemist** (`^0.13.0`). Goldens are stored under: +- `test/components//goldens/ci/` — for CI +- `test/components//goldens/macos/` — for local macOS development + +Golden tests are tagged with `golden` in `dart_test.yaml`. Run `melos run update:goldens` to regenerate after visual changes. + +### Code Generation + +- **json_serializable** — model serialization (`.g.dart` files) +- **build_runner** — orchestrates all generation +- **theme_extensions_builder** — generates theme extension classes (`.g.theme.dart`) +- **widgetbook_generator** — auto-generates Widgetbook entries + +After any model or theme annotation changes, run the appropriate generate command before running tests. From dafcb85c9a9221032d84d023756f46973a0cf82b Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 4 Mar 2026 16:01:49 +0100 Subject: [PATCH 13/15] remove channel list item from core --- .../lib/app/gallery_app.directories.g.dart | 24 -- .../stream_channel_list_item.dart | 405 ------------------ .../lib/src/components.dart | 3 +- .../lib/src/components/channel_list.dart | 1 - .../stream_channel_list_item.dart | 371 ---------------- .../src/components/list/stream_list_tile.dart | 6 +- .../src/factory/stream_component_factory.dart | 6 - .../ci/stream_channel_list_item_dark.png | Bin 6402 -> 0 bytes .../ci/stream_channel_list_item_light.png | Bin 6843 -> 0 bytes .../stream_channel_list_item_golden_test.dart | 190 -------- 10 files changed, 4 insertions(+), 1002 deletions(-) delete mode 100644 apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart delete mode 100644 packages/stream_core_flutter/lib/src/components/channel_list.dart delete mode 100644 packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart delete mode 100644 packages/stream_core_flutter/test/components/channel_list/goldens/ci/stream_channel_list_item_dark.png delete mode 100644 packages/stream_core_flutter/test/components/channel_list/goldens/ci/stream_channel_list_item_light.png delete mode 100644 packages/stream_core_flutter/test/components/channel_list/stream_channel_list_item_golden_test.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 eb878da..8d683e8 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 @@ -32,8 +32,6 @@ import 'package:design_system_gallery/components/buttons/button.dart' as _design_system_gallery_components_buttons_button; import 'package:design_system_gallery/components/buttons/stream_emoji_button.dart' as _design_system_gallery_components_buttons_stream_emoji_button; -import 'package:design_system_gallery/components/channel_list/stream_channel_list_item.dart' - as _design_system_gallery_components_channel_list_stream_channel_list_item; 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_progress_bar.dart' @@ -382,28 +380,6 @@ final directories = <_widgetbook.WidgetbookNode>[ ), ], ), - _widgetbook.WidgetbookFolder( - name: 'Channel List', - children: [ - _widgetbook.WidgetbookComponent( - name: 'StreamChannelListItem', - useCases: [ - _widgetbook.WidgetbookUseCase( - name: 'Playground', - builder: - _design_system_gallery_components_channel_list_stream_channel_list_item - .buildStreamChannelListItemPlayground, - ), - _widgetbook.WidgetbookUseCase( - name: 'Showcase', - builder: - _design_system_gallery_components_channel_list_stream_channel_list_item - .buildStreamChannelListItemShowcase, - ), - ], - ), - ], - ), _widgetbook.WidgetbookFolder( name: 'Common', children: [ diff --git a/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart b/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart deleted file mode 100644 index 13cd85d..0000000 --- a/apps/design_system_gallery/lib/components/channel_list/stream_channel_list_item.dart +++ /dev/null @@ -1,405 +0,0 @@ -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; - -const _sampleImageUrl = 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200'; - -// ============================================================================= -// Playground -// ============================================================================= - -@widgetbook.UseCase( - name: 'Playground', - type: StreamChannelListItem, - path: '[Components]/Channel List', -) -Widget buildStreamChannelListItemPlayground(BuildContext context) { - final title = context.knobs.string( - label: 'Title', - initialValue: 'Design Team', - description: 'The channel name.', - ); - - final subtitle = context.knobs.stringOrNull( - label: 'Subtitle', - initialValue: 'New mockups ready for review', - description: 'The message preview text.', - ); - - final timestamp = context.knobs.stringOrNull( - label: 'Timestamp', - initialValue: '9:41', - description: 'The formatted timestamp.', - ); - - final unreadCount = context.knobs.int.slider( - label: 'Unread Count', - initialValue: 3, - max: 99, - description: 'Number of unread messages. Shows a badge when > 0.', - ); - - final showMuteIcon = context.knobs.boolean( - label: 'Show Mute Icon', - description: 'Display a mute icon after the title.', - ); - - final colorScheme = context.streamColorScheme; - - return ColoredBox( - color: colorScheme.backgroundApp, - child: Center( - child: StreamChannelListItem( - avatar: StreamOnlineIndicator( - isOnline: true, - child: StreamAvatar( - imageUrl: _sampleImageUrl, - size: StreamAvatarSize.xl, - placeholder: (context) => const Text('DT'), - ), - ), - title: Text(title), - isMuted: showMuteIcon, - subtitle: subtitle != null ? Text(subtitle) : null, - timestamp: timestamp != null ? Text(timestamp) : null, - unreadCount: unreadCount, - onTap: () => print('onTap'), - ), - ), - ); -} - -// ============================================================================= -// Showcase -// ============================================================================= - -@widgetbook.UseCase( - name: 'Showcase', - type: StreamChannelListItem, - path: '[Components]/Channel List', -) -Widget buildStreamChannelListItemShowcase(BuildContext context) { - final colorScheme = context.streamColorScheme; - final textTheme = context.streamTextTheme; - final spacing = context.streamSpacing; - - return DefaultTextStyle( - style: textTheme.bodyDefault.copyWith(color: colorScheme.textPrimary), - child: ColoredBox( - color: colorScheme.backgroundApp, - child: SingleChildScrollView( - padding: EdgeInsets.all(spacing.lg), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const _DirectMessageSection(), - SizedBox(height: spacing.xl), - const _GroupChannelSection(), - SizedBox(height: spacing.xl), - const _EdgeCasesSection(), - ], - ), - ), - ), - ); -} - -// ============================================================================= -// Direct Message Section -// ============================================================================= - -class _DirectMessageSection extends StatelessWidget { - const _DirectMessageSection(); - - @override - Widget build(BuildContext context) { - final colorScheme = context.streamColorScheme; - final spacing = context.streamSpacing; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const _SectionLabel(label: 'DIRECT MESSAGES'), - SizedBox(height: spacing.md), - _ShowcaseCard( - description: 'One-on-one conversations with online indicator', - child: Column( - children: [ - StreamChannelListItem( - avatar: StreamOnlineIndicator( - isOnline: true, - child: StreamAvatar( - imageUrl: _sampleImageUrl, - size: StreamAvatarSize.xl, - placeholder: (context) => const Text('JD'), - ), - ), - title: const Text('Jane Doe'), - subtitle: const Text('Hey! Are you free for a call?'), - timestamp: const Text('9:41'), - unreadCount: 3, - ), - StreamChannelListItem( - avatar: StreamOnlineIndicator( - isOnline: false, - child: StreamAvatar( - size: StreamAvatarSize.xl, - placeholder: (context) => const Text('BS'), - ), - ), - title: const Text('Bob Smith'), - subtitle: const Text('Thanks for the update!'), - timestamp: const Text('Yesterday'), - ), - StreamChannelListItem( - avatar: StreamOnlineIndicator( - isOnline: true, - child: StreamAvatar( - size: StreamAvatarSize.xl, - backgroundColor: colorScheme.avatarPalette[2].backgroundColor, - foregroundColor: colorScheme.avatarPalette[2].foregroundColor, - placeholder: (context) => const Text('CW'), - ), - ), - title: const Text('Carol White'), - subtitle: const Text('See you tomorrow!'), - timestamp: const Text('Saturday'), - ), - ], - ), - ), - ], - ); - } -} - -// ============================================================================= -// Group Channel Section -// ============================================================================= - -class _GroupChannelSection extends StatelessWidget { - const _GroupChannelSection(); - - @override - Widget build(BuildContext context) { - final colorScheme = context.streamColorScheme; - final spacing = context.streamSpacing; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const _SectionLabel(label: 'GROUP CHANNELS'), - SizedBox(height: spacing.md), - _ShowcaseCard( - description: 'Group conversations with sender prefix and mute icon', - child: Column( - children: [ - StreamChannelListItem( - avatar: StreamAvatarGroup( - size: StreamAvatarGroupSize.xl, - children: [ - StreamAvatar(placeholder: (context) => const Text('JD')), - StreamAvatar( - imageUrl: _sampleImageUrl, - placeholder: (context) => const Text('AB'), - ), - StreamAvatar( - backgroundColor: colorScheme.avatarPalette[1].backgroundColor, - foregroundColor: colorScheme.avatarPalette[1].foregroundColor, - placeholder: (context) => const Text('CW'), - ), - ], - ), - title: const Text('Design Team'), - subtitle: const Text('Alice: New mockups ready for review'), - timestamp: const Text('9:41'), - unreadCount: 5, - ), - StreamChannelListItem( - avatar: StreamAvatarGroup( - size: StreamAvatarGroupSize.xl, - children: [ - StreamAvatar(placeholder: (context) => const Text('EF')), - StreamAvatar(placeholder: (context) => const Text('GH')), - ], - ), - title: const Text('Engineering'), - subtitle: const Text('Bob: PR merged successfully'), - timestamp: const Text('10:15'), - unreadCount: 12, - ), - StreamChannelListItem( - avatar: StreamAvatarGroup( - size: StreamAvatarGroupSize.xl, - children: [ - StreamAvatar(placeholder: (context) => const Text('IJ')), - StreamAvatar(placeholder: (context) => const Text('KL')), - StreamAvatar(placeholder: (context) => const Text('MN')), - ], - ), - title: const Text('Muted Group'), - isMuted: true, - subtitle: const Text('Carol: Meeting notes attached'), - timestamp: const Text('Yesterday'), - ), - ], - ), - ), - ], - ); - } -} - -// ============================================================================= -// Edge Cases Section -// ============================================================================= - -class _EdgeCasesSection extends StatelessWidget { - const _EdgeCasesSection(); - - @override - Widget build(BuildContext context) { - final colorScheme = context.streamColorScheme; - final spacing = context.streamSpacing; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const _SectionLabel(label: 'EDGE CASES'), - SizedBox(height: spacing.md), - _ShowcaseCard( - description: 'Long text, no unread, and minimal content', - child: Column( - children: [ - StreamChannelListItem( - avatar: StreamAvatar( - size: StreamAvatarSize.xl, - placeholder: (context) => const Text('LT'), - ), - title: const Text('Very Long Channel Name That Should Be Truncated Properly'), - subtitle: const Text( - 'This is a very long message preview that should be truncated with an ellipsis when it overflows', - ), - timestamp: const Text('01/15/2026'), - unreadCount: 99, - ), - StreamChannelListItem( - avatar: StreamAvatar( - size: StreamAvatarSize.xl, - backgroundColor: colorScheme.avatarPalette[3].backgroundColor, - foregroundColor: colorScheme.avatarPalette[3].foregroundColor, - placeholder: (context) => const Text('NR'), - ), - title: const Text('No Unread'), - subtitle: const Text('All caught up!'), - timestamp: const Text('3:00'), - ), - StreamChannelListItem( - avatar: StreamAvatar( - size: StreamAvatarSize.xl, - placeholder: (context) => const Text('MN'), - ), - title: const Text('Minimal'), - ), - ], - ), - ), - ], - ); - } -} - -// ============================================================================= -// Shared Widgets -// ============================================================================= - -class _ShowcaseCard extends StatelessWidget { - const _ShowcaseCard({ - required this.description, - required this.child, - }); - - final String description; - final Widget child; - - @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 Container( - width: double.infinity, - clipBehavior: Clip.antiAlias, - 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: [ - Padding( - padding: EdgeInsets.fromLTRB( - spacing.md, - spacing.sm, - spacing.md, - spacing.sm, - ), - child: Text( - description, - style: textTheme.captionDefault.copyWith( - color: colorScheme.textSecondary, - ), - ), - ), - Divider(height: 1, color: colorScheme.borderSubtle), - ColoredBox( - color: colorScheme.backgroundApp, - child: child, - ), - ], - ), - ); - } -} - -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 4eaaad1..c8515f7 100644 --- a/packages/stream_core_flutter/lib/src/components.dart +++ b/packages/stream_core_flutter/lib/src/components.dart @@ -10,7 +10,6 @@ export 'components/badge/stream_media_badge.dart'; export 'components/badge/stream_online_indicator.dart' hide DefaultStreamOnlineIndicator; export 'components/buttons/stream_button.dart' hide DefaultStreamButton; export 'components/buttons/stream_emoji_button.dart' hide DefaultStreamEmojiButton; -export 'components/channel_list.dart' hide DefaultStreamChannelListItem; export 'components/common/stream_checkbox.dart' hide DefaultStreamCheckbox; export 'components/common/stream_progress_bar.dart' hide DefaultStreamProgressBar; export 'components/context_menu/stream_context_menu.dart'; @@ -21,7 +20,7 @@ export 'components/controls/stream_remove_control.dart'; export 'components/emoji/data/stream_emoji_data.dart'; export 'components/emoji/data/stream_supported_emojis.dart'; export 'components/emoji/stream_emoji_picker_sheet.dart'; -export 'components/list/stream_list_tile.dart' hide DefaultStreamListTile, ListTileContainer; +export 'components/list/stream_list_tile.dart' hide DefaultStreamListTile; export 'components/message_composer.dart'; export 'factory/stream_component_factory.dart'; diff --git a/packages/stream_core_flutter/lib/src/components/channel_list.dart b/packages/stream_core_flutter/lib/src/components/channel_list.dart deleted file mode 100644 index 2913701..0000000 --- a/packages/stream_core_flutter/lib/src/components/channel_list.dart +++ /dev/null @@ -1 +0,0 @@ -export 'channel_list/stream_channel_list_item.dart'; diff --git a/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart b/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart deleted file mode 100644 index c319a1a..0000000 --- a/packages/stream_core_flutter/lib/src/components/channel_list/stream_channel_list_item.dart +++ /dev/null @@ -1,371 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../../stream_core_flutter.dart'; -import '../list/stream_list_tile.dart' show ListTileContainer; - -/// A list item for displaying a channel in a channel list. -/// -/// [StreamChannelListItem] displays a channel's avatar, title, message preview, -/// timestamp, and unread count in a standard list item layout. -/// -/// The [avatar] is passed as a widget, allowing full customization of -/// the avatar appearance (e.g., single user avatar, group avatar, or -/// avatar with online indicator). -/// -/// {@tool snippet} -/// -/// Basic usage: -/// -/// ```dart -/// StreamChannelListItem( -/// avatar: StreamAvatar(placeholder: (context) => Text('AB')), -/// title: Text('General'), -/// subtitle: Text('Hello, how are you?'), -/// timestamp: Text('9:41'), -/// unreadCount: 3, -/// ) -/// ``` -/// {@end-tool} -/// -/// {@tool snippet} -/// -/// With a mute icon after the title: -/// -/// ```dart -/// StreamChannelListItem( -/// avatar: StreamAvatar(placeholder: (context) => Text('AB')), -/// title: Text('Muted Channel'), -/// titleTrailing: Icon(Icons.volume_off, size: 16), -/// subtitle: Text('Last message...'), -/// timestamp: Text('Yesterday'), -/// ) -/// ``` -/// {@end-tool} -/// -/// ## Theming -/// -/// [StreamChannelListItem] uses [StreamChannelListItemThemeData] for default -/// styling. Colors and text styles are determined by the current -/// [StreamColorScheme] and [StreamTextTheme]. -/// -/// See also: -/// -/// * [StreamChannelListItemThemeData], for customizing appearance. -/// * [StreamChannelListItemTheme], for overriding theme in a widget subtree. -/// * [StreamBadgeNotification], which displays the unread count badge. -class StreamChannelListItem extends StatelessWidget { - /// Creates a channel list item. - StreamChannelListItem({ - super.key, - required Widget avatar, - required Widget title, - Widget? subtitle, - Widget? timestamp, - int unreadCount = 0, - bool isMuted = false, - VoidCallback? onTap, - VoidCallback? onLongPress, - bool selected = false, - }) : props = StreamChannelListItemProps( - avatar: avatar, - title: title, - subtitle: subtitle, - timestamp: timestamp, - unreadCount: unreadCount, - isMuted: isMuted, - onTap: onTap, - onLongPress: onLongPress, - selected: selected, - ); - - /// The properties that configure this channel list item. - final StreamChannelListItemProps props; - - @override - Widget build(BuildContext context) { - final builder = StreamComponentFactory.maybeOf(context)?.channelListItem; - if (builder != null) return builder(context, props); - return DefaultStreamChannelListItem(props: props); - } -} - -/// Properties for configuring a [StreamChannelListItem]. -/// -/// This class holds all the configuration options for a channel list item, -/// allowing them to be passed through the [StreamComponentFactory]. -/// -/// See also: -/// -/// * [StreamChannelListItem], which uses these properties. -/// * [DefaultStreamChannelListItem], the default implementation. -class StreamChannelListItemProps { - /// Creates properties for a channel list item. - const StreamChannelListItemProps({ - required this.avatar, - required this.title, - this.subtitle, - this.timestamp, - this.unreadCount = 0, - this.isMuted = false, - this.onTap, - this.onLongPress, - this.selected = false, - }); - - /// The avatar widget displayed at the leading edge. - /// - /// Typically a [StreamAvatar], [StreamAvatarGroup], or an avatar wrapped - /// in a [StreamOnlineIndicator]. - final Widget avatar; - - /// The channel title widget. - /// - /// Typically a [Text] widget with the channel name. The default text style - /// is provided by the theme's title style via [DefaultTextStyle]. - final Widget title; - - /// The message preview widget displayed below the title. - /// - /// Typically a [Text] widget with the last message, but can be any widget - /// for richer content (e.g., icons, read receipts, sender prefix). - final Widget? subtitle; - - /// The timestamp widget displayed in the trailing section of the title row. - /// - /// Typically a [Text] widget with a formatted date string. The default text - /// style is provided by the theme's timestamp style via [DefaultTextStyle]. - final Widget? timestamp; - - /// The number of unread messages. - /// - /// When greater than zero, a [StreamBadgeNotification] is displayed. - final int unreadCount; - - /// Whether the channel is muted. - /// - /// When true, a mute icon is displayed in the title or subtitle. - final bool isMuted; - - /// Called when the list item is tapped. - final VoidCallback? onTap; - - /// Called when the list item is long-pressed. - final VoidCallback? onLongPress; - - /// Whether the list item is in a selected state. - final bool selected; -} - -/// The default implementation of [StreamChannelListItem]. -/// -/// This widget renders the channel list item with theming support. -/// It's used as the default factory implementation in [StreamComponentFactory]. -/// -/// See also: -/// -/// * [StreamChannelListItem], the public API widget. -/// * [StreamChannelListItemProps], which configures this widget. -class DefaultStreamChannelListItem extends StatefulWidget { - /// Creates a default channel list item with the given [props]. - const DefaultStreamChannelListItem({super.key, required this.props}); - - /// The properties that configure this channel list item. - final StreamChannelListItemProps props; - - @override - State createState() => _DefaultStreamChannelListItemState(); -} - -class _DefaultStreamChannelListItemState extends State { - StreamChannelListItemProps get props => widget.props; - - @override - Widget build(BuildContext context) { - final spacing = context.streamSpacing; - final channelListItemTheme = context.streamChannelListItemTheme; - final defaults = _StreamChannelListItemThemeDefaults(context); - - final effectiveTitleStyle = channelListItemTheme.titleStyle ?? defaults.titleStyle; - final effectiveSubtitleStyle = channelListItemTheme.subtitleStyle ?? defaults.subtitleStyle; - final effectiveTimestampStyle = channelListItemTheme.timestampStyle ?? defaults.timestampStyle; - final effectiveMuteIconPosition = channelListItemTheme.muteIconPosition ?? defaults.muteIconPosition; - - final muteIcon = props.isMuted - ? Icon( - context.streamIcons.mute, - size: 20, - color: context.streamColorScheme.textTertiary, - ) - : null; - - final hasMuteIconInSubtitle = effectiveMuteIconPosition == MuteIconPosition.subtitle && props.isMuted; - - return StreamListTileTheme( - data: context.streamListTileTheme.copyWith( - contentPadding: EdgeInsets.all(spacing.md - 4), - backgroundColor: channelListItemTheme.backgroundColor, - ), - child: Padding( - padding: const EdgeInsets.all(4), - child: Material( - type: MaterialType.transparency, - child: ListTileContainer( - enabled: true, - selected: props.selected, - onTap: props.onTap, - onLongPress: props.onLongPress, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: spacing.md, - children: [ - props.avatar, - Expanded( - child: Padding( - padding: EdgeInsets.symmetric(vertical: spacing.xxxs), - child: Column( - mainAxisSize: MainAxisSize.min, - spacing: spacing.xxs, - children: [ - _TitleRow( - title: props.title, - titleTrailing: effectiveMuteIconPosition == MuteIconPosition.title ? muteIcon : null, - timestamp: props.timestamp, - unreadCount: props.unreadCount, - titleStyle: effectiveTitleStyle, - timestampStyle: effectiveTimestampStyle, - spacing: spacing, - ), - if (props.subtitle != null || hasMuteIconInSubtitle) - _SubtitleRow( - subtitle: props.subtitle, - subtitleTrailing: effectiveMuteIconPosition == MuteIconPosition.subtitle ? muteIcon : null, - subtitleStyle: effectiveSubtitleStyle, - ), - ], - ), - ), - ), - ], - ), - ), - ), - ), - ); - } -} - -class _TitleRow extends StatelessWidget { - const _TitleRow({ - required this.title, - this.titleTrailing, - this.timestamp, - required this.unreadCount, - required this.titleStyle, - required this.timestampStyle, - required this.spacing, - }); - - final Widget title; - final Widget? titleTrailing; - final Widget? timestamp; - final int unreadCount; - final TextStyle titleStyle; - final TextStyle timestampStyle; - final StreamSpacing spacing; - - @override - Widget build(BuildContext context) { - return Row( - spacing: spacing.md, - children: [ - Expanded( - child: Row( - spacing: spacing.xxs, - children: [ - Flexible( - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: StreamBadgeNotificationSize.sm.value), - child: Align( - alignment: AlignmentDirectional.centerStart, - widthFactor: 1, - child: DefaultTextStyle.merge( - style: titleStyle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - child: title, - ), - ), - ), - ), - ?titleTrailing, - ], - ), - ), - if (timestamp != null || unreadCount > 0) - Row( - mainAxisSize: MainAxisSize.min, - spacing: spacing.xs, - children: [ - if (timestamp case final timestamp?) - DefaultTextStyle.merge( - style: timestampStyle, - child: timestamp, - ), - if (unreadCount > 0) StreamBadgeNotification(label: '$unreadCount'), - ], - ), - ], - ); - } -} - -class _SubtitleRow extends StatelessWidget { - const _SubtitleRow({ - required this.subtitle, - this.subtitleTrailing, - required this.subtitleStyle, - }); - - final Widget? subtitle; - final Widget? subtitleTrailing; - final TextStyle subtitleStyle; - - @override - Widget build(BuildContext context) { - return DefaultTextStyle( - style: subtitleStyle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - child: Row( - children: [ - Expanded(child: subtitle ?? const SizedBox.shrink()), - ?subtitleTrailing, - ], - ), - ); - } -} - -class _StreamChannelListItemThemeDefaults extends StreamChannelListItemThemeData { - _StreamChannelListItemThemeDefaults(this._context); - - final BuildContext _context; - - late final _colorScheme = _context.streamColorScheme; - late final _textTheme = _context.streamTextTheme; - - @override - TextStyle get titleStyle => _textTheme.headingSm.copyWith(color: _colorScheme.textPrimary); - - @override - TextStyle get subtitleStyle => _textTheme.captionDefault.copyWith(color: _colorScheme.textSecondary); - - @override - TextStyle get timestampStyle => _textTheme.captionDefault.copyWith(color: _colorScheme.textTertiary); - - @override - Color get borderColor => _colorScheme.borderSubtle; - - @override - MuteIconPosition get muteIconPosition => MuteIconPosition.title; -} diff --git a/packages/stream_core_flutter/lib/src/components/list/stream_list_tile.dart b/packages/stream_core_flutter/lib/src/components/list/stream_list_tile.dart index 792be26..3a8b0d6 100644 --- a/packages/stream_core_flutter/lib/src/components/list/stream_list_tile.dart +++ b/packages/stream_core_flutter/lib/src/components/list/stream_list_tile.dart @@ -270,7 +270,7 @@ class DefaultStreamListTile extends StatelessWidget { ); } - return ListTileContainer( + return StreamListTileContainer( enabled: props.enabled, selected: props.selected, onTap: props.onTap, @@ -373,8 +373,8 @@ class _StreamListTileThemeDefaults extends StreamListTileThemeData { }); } -class ListTileContainer extends StatelessWidget { - const ListTileContainer({ +class StreamListTileContainer extends StatelessWidget { + const StreamListTileContainer({ super.key, required this.child, required this.enabled, 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 923f83e..c11e252 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 @@ -156,7 +156,6 @@ class StreamComponentBuilders with _$StreamComponentBuilders { this.badgeCount, this.badgeNotification, this.button, - this.channelListItem, this.checkbox, this.contextMenuAction, this.emoji, @@ -200,11 +199,6 @@ class StreamComponentBuilders with _$StreamComponentBuilders { /// When null, [StreamButton] uses [DefaultStreamButton]. final StreamComponentBuilder? button; - /// Custom builder for channel list item widgets. - /// - /// When null, [StreamChannelListItem] uses [DefaultStreamChannelListItem]. - final StreamComponentBuilder? channelListItem; - /// Custom builder for checkbox widgets. /// /// When null, [StreamCheckbox] uses [DefaultStreamCheckbox]. diff --git a/packages/stream_core_flutter/test/components/channel_list/goldens/ci/stream_channel_list_item_dark.png b/packages/stream_core_flutter/test/components/channel_list/goldens/ci/stream_channel_list_item_dark.png deleted file mode 100644 index a07266d7f1624990e2d5453e910e95bf0ad952a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6402 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 SSe5M#dBwu^QuPJzpZ*h GoldenTestGroup( - scenarioConstraints: const BoxConstraints(maxWidth: 400), - children: [ - GoldenTestScenario( - name: 'full', - child: _buildInTheme( - StreamChannelListItem( - avatar: StreamAvatar( - size: StreamAvatarSize.xl, - placeholder: (context) => const Text('JD'), - ), - title: const Text('Jane Doe'), - subtitle: const Text('Hey! Are you free for a call?'), - timestamp: const Text('9:41'), - unreadCount: 3, - ), - ), - ), - GoldenTestScenario( - name: 'no_unread', - child: _buildInTheme( - StreamChannelListItem( - avatar: StreamAvatar( - size: StreamAvatarSize.xl, - placeholder: (context) => const Text('BS'), - ), - title: const Text('Bob Smith'), - subtitle: const Text('Thanks for the update!'), - timestamp: const Text('Yesterday'), - ), - ), - ), - GoldenTestScenario( - name: 'with_mute_icon', - child: _buildInTheme( - Builder( - builder: (context) => StreamChannelListItem( - avatar: StreamAvatar( - size: StreamAvatarSize.xl, - placeholder: (context) => const Text('MT'), - ), - title: const Text('Muted Channel'), - isMuted: true, - subtitle: const Text('Last message...'), - timestamp: const Text('10:15'), - unreadCount: 1, - ), - ), - ), - ), - GoldenTestScenario( - name: 'no_subtitle', - child: _buildInTheme( - StreamChannelListItem( - avatar: StreamAvatar( - size: StreamAvatarSize.xl, - placeholder: (context) => const Text('MN'), - ), - title: const Text('Minimal'), - ), - ), - ), - GoldenTestScenario( - name: 'long_text', - child: _buildInTheme( - StreamChannelListItem( - avatar: StreamAvatar( - size: StreamAvatarSize.xl, - placeholder: (context) => const Text('LT'), - ), - title: const Text('Very Long Channel Name That Should Be Truncated'), - subtitle: const Text( - 'This is a very long message preview that should ' - 'be truncated with an ellipsis', - ), - timestamp: const Text('01/15/2026'), - unreadCount: 99, - ), - ), - ), - ], - ), - ); - - goldenTest( - 'renders dark theme variants', - fileName: 'stream_channel_list_item_dark', - builder: () => GoldenTestGroup( - scenarioConstraints: const BoxConstraints(maxWidth: 400), - children: [ - GoldenTestScenario( - name: 'full', - child: _buildInTheme( - StreamChannelListItem( - avatar: StreamAvatar( - size: StreamAvatarSize.xl, - placeholder: (context) => const Text('JD'), - ), - title: const Text('Jane Doe'), - subtitle: const Text('Hey! Are you free for a call?'), - timestamp: const Text('9:41'), - unreadCount: 3, - ), - brightness: Brightness.dark, - ), - ), - GoldenTestScenario( - name: 'no_unread', - child: _buildInTheme( - StreamChannelListItem( - avatar: StreamAvatar( - size: StreamAvatarSize.xl, - placeholder: (context) => const Text('BS'), - ), - title: const Text('Bob Smith'), - subtitle: const Text('Thanks for the update!'), - timestamp: const Text('Yesterday'), - ), - brightness: Brightness.dark, - ), - ), - GoldenTestScenario( - name: 'with_widget_subtitle', - child: _buildInTheme( - Builder( - builder: (context) => StreamChannelListItem( - avatar: StreamAvatar( - size: StreamAvatarSize.xl, - placeholder: (context) => const Text('GC'), - ), - title: const Text('Group Chat'), - subtitle: Row( - children: [ - Text( - 'Alice: ', - style: TextStyle( - fontWeight: FontWeight.w600, - color: context.streamColorScheme.textTertiary, - ), - ), - const Expanded( - child: Text( - 'New mockups ready for review', - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - timestamp: const Text('9:41'), - unreadCount: 5, - ), - ), - brightness: Brightness.dark, - ), - ), - ], - ), - ); - }); -} - -Widget _buildInTheme( - Widget child, { - Brightness brightness = Brightness.light, -}) { - 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: child, - ), - ), - ); -} From bd68b97c4ab9dbbffd50ce5239dcd838fb6422fb Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 4 Mar 2026 16:43:15 +0100 Subject: [PATCH 14/15] cleanup channel list item --- CLAUDE.md | 2 +- .../stream_core_flutter/lib/src/theme.dart | 1 - .../stream_channel_list_item_theme.dart | 139 ------------------ ...tream_channel_list_item_theme.g.theme.dart | 127 ---------------- .../lib/src/theme/stream_theme.dart | 9 -- .../src/theme/stream_theme_extensions.dart | 4 - 6 files changed, 1 insertion(+), 281 deletions(-) delete mode 100644 packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.dart delete mode 100644 packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.g.theme.dart diff --git a/CLAUDE.md b/CLAUDE.md index 33001a9..aabf9c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,7 +58,7 @@ Generated files have `.g.theme.dart` extension. After modifying `.theme.dart` fi ### Component Structure (`stream_core_flutter/lib/src/components/`) -Components are organized by category: `avatar/`, `buttons/`, `badge/`, `channel_list/`, `list/`, `message_composer/`, `emoji/`, `context_menu/`, `controls/`, `common/`, `accessories/`. +Components are organized by category: `avatar/`, `buttons/`, `badge/`, `list/`, `message_composer/`, `emoji/`, `context_menu/`, `controls/`, `common/`, `accessories/`. Each component typically has: - A widget file diff --git a/packages/stream_core_flutter/lib/src/theme.dart b/packages/stream_core_flutter/lib/src/theme.dart index 6a7ce8f..2371ac7 100644 --- a/packages/stream_core_flutter/lib/src/theme.dart +++ b/packages/stream_core_flutter/lib/src/theme.dart @@ -5,7 +5,6 @@ export 'theme/components/stream_avatar_theme.dart'; export 'theme/components/stream_badge_count_theme.dart'; export 'theme/components/stream_badge_notification_theme.dart'; export 'theme/components/stream_button_theme.dart'; -export 'theme/components/stream_channel_list_item_theme.dart'; export 'theme/components/stream_checkbox_theme.dart'; export 'theme/components/stream_context_menu_action_theme.dart'; export 'theme/components/stream_context_menu_theme.dart'; diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.dart deleted file mode 100644 index c019568..0000000 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:theme_extensions_builder_annotation/theme_extensions_builder_annotation.dart'; - -import '../stream_theme.dart'; - -part 'stream_channel_list_item_theme.g.theme.dart'; - -/// Applies a channel list item theme to descendant -/// [StreamChannelListItem] widgets. -/// -/// Wrap a subtree with [StreamChannelListItemTheme] to override styling. -/// Access the merged theme using [BuildContext.streamChannelListItemTheme]. -/// -/// {@tool snippet} -/// -/// Override channel list item colors for a specific section: -/// -/// ```dart -/// StreamChannelListItemTheme( -/// data: StreamChannelListItemThemeData( -/// backgroundColor: Colors.grey.shade50, -/// ), -/// child: StreamChannelListItem( -/// avatar: StreamAvatar(...), -/// title: 'General', -/// ), -/// ) -/// ``` -/// {@end-tool} -/// -/// See also: -/// -/// * [StreamChannelListItemThemeData], which describes the theme. -/// * [StreamChannelListItem], the widget affected by this theme. -class StreamChannelListItemTheme extends InheritedTheme { - /// Creates a channel list item theme that controls descendant widgets. - const StreamChannelListItemTheme({ - super.key, - required this.data, - required super.child, - }); - - /// The channel list item theme data for descendant widgets. - final StreamChannelListItemThemeData data; - - /// Returns the [StreamChannelListItemThemeData] merged from local and - /// global themes. - /// - /// Local values from the nearest [StreamChannelListItemTheme] ancestor - /// take precedence over global values from [StreamTheme.of]. - static StreamChannelListItemThemeData of(BuildContext context) { - final localTheme = context.dependOnInheritedWidgetOfExactType(); - return StreamTheme.of(context).channelListItemTheme.merge(localTheme?.data); - } - - @override - Widget wrap(BuildContext context, Widget child) { - return StreamChannelListItemTheme(data: data, child: child); - } - - @override - bool updateShouldNotify(StreamChannelListItemTheme oldWidget) => data != oldWidget.data; -} - -/// Theme data for customizing [StreamChannelListItem] widgets. -/// -/// {@tool snippet} -/// -/// Customize channel list item appearance globally: -/// -/// ```dart -/// StreamTheme( -/// channelListItemTheme: StreamChannelListItemThemeData( -/// backgroundColor: Colors.white, -/// borderColor: Colors.grey.shade200, -/// ), -/// ) -/// ``` -/// {@end-tool} -/// -/// See also: -/// -/// * [StreamChannelListItem], the widget that uses this theme data. -/// * [StreamChannelListItemTheme], for overriding theme in a widget subtree. -@themeGen -@immutable -class StreamChannelListItemThemeData with _$StreamChannelListItemThemeData { - /// Creates a channel list item theme with optional style overrides. - const StreamChannelListItemThemeData({ - this.titleStyle, - this.subtitleStyle, - this.timestampStyle, - this.backgroundColor, - this.borderColor, - this.muteIconPosition, - }); - - /// The text style for the channel title. - /// - /// Falls back to [StreamTextTheme.headingSm] with [StreamColorScheme.textPrimary]. - final TextStyle? titleStyle; - - /// The text style for the message preview subtitle. - /// - /// Falls back to [StreamTextTheme.captionDefault] with [StreamColorScheme.textSecondary]. - final TextStyle? subtitleStyle; - - /// The text style for the timestamp. - /// - /// Falls back to [StreamTextTheme.captionDefault] with [StreamColorScheme.textTertiary]. - final TextStyle? timestampStyle; - - /// Defines the default background color of the tile. - /// - /// This color is resolved from [WidgetState]s. - final WidgetStateProperty? backgroundColor; - - /// The bottom border color of the list item. - /// - /// Falls back to [StreamColorScheme.borderSubtle]. - final Color? borderColor; - - /// The position of the mute icon. - /// - /// Falls back to [MuteIconPosition.title]. - final MuteIconPosition? muteIconPosition; - - /// Linearly interpolate between two [StreamChannelListItemThemeData] objects. - static StreamChannelListItemThemeData? lerp( - StreamChannelListItemThemeData? a, - StreamChannelListItemThemeData? b, - double t, - ) => _$StreamChannelListItemThemeData.lerp(a, b, t); -} - -enum MuteIconPosition { - title, - subtitle, -} diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.g.theme.dart deleted file mode 100644 index bb1baaf..0000000 --- a/packages/stream_core_flutter/lib/src/theme/components/stream_channel_list_item_theme.g.theme.dart +++ /dev/null @@ -1,127 +0,0 @@ -// dart format width=80 -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint, unused_element - -part of 'stream_channel_list_item_theme.dart'; - -// ************************************************************************** -// ThemeGenGenerator -// ************************************************************************** - -mixin _$StreamChannelListItemThemeData { - bool get canMerge => true; - - static StreamChannelListItemThemeData? lerp( - StreamChannelListItemThemeData? a, - StreamChannelListItemThemeData? 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 StreamChannelListItemThemeData( - titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), - subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t), - timestampStyle: TextStyle.lerp(a.timestampStyle, b.timestampStyle, t), - backgroundColor: WidgetStateProperty.lerp( - a.backgroundColor, - b.backgroundColor, - t, - Color.lerp, - ), - borderColor: Color.lerp(a.borderColor, b.borderColor, t), - muteIconPosition: t < 0.5 ? a.muteIconPosition : b.muteIconPosition, - ); - } - - StreamChannelListItemThemeData copyWith({ - TextStyle? titleStyle, - TextStyle? subtitleStyle, - TextStyle? timestampStyle, - WidgetStateProperty? backgroundColor, - Color? borderColor, - MuteIconPosition? muteIconPosition, - }) { - final _this = (this as StreamChannelListItemThemeData); - - return StreamChannelListItemThemeData( - titleStyle: titleStyle ?? _this.titleStyle, - subtitleStyle: subtitleStyle ?? _this.subtitleStyle, - timestampStyle: timestampStyle ?? _this.timestampStyle, - backgroundColor: backgroundColor ?? _this.backgroundColor, - borderColor: borderColor ?? _this.borderColor, - muteIconPosition: muteIconPosition ?? _this.muteIconPosition, - ); - } - - StreamChannelListItemThemeData merge(StreamChannelListItemThemeData? other) { - final _this = (this as StreamChannelListItemThemeData); - - if (other == null || identical(_this, other)) { - return _this; - } - - if (!other.canMerge) { - return other; - } - - return copyWith( - titleStyle: _this.titleStyle?.merge(other.titleStyle) ?? other.titleStyle, - subtitleStyle: - _this.subtitleStyle?.merge(other.subtitleStyle) ?? - other.subtitleStyle, - timestampStyle: - _this.timestampStyle?.merge(other.timestampStyle) ?? - other.timestampStyle, - backgroundColor: other.backgroundColor, - borderColor: other.borderColor, - muteIconPosition: other.muteIconPosition, - ); - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - - if (other.runtimeType != runtimeType) { - return false; - } - - final _this = (this as StreamChannelListItemThemeData); - final _other = (other as StreamChannelListItemThemeData); - - return _other.titleStyle == _this.titleStyle && - _other.subtitleStyle == _this.subtitleStyle && - _other.timestampStyle == _this.timestampStyle && - _other.backgroundColor == _this.backgroundColor && - _other.borderColor == _this.borderColor && - _other.muteIconPosition == _this.muteIconPosition; - } - - @override - int get hashCode { - final _this = (this as StreamChannelListItemThemeData); - - return Object.hash( - runtimeType, - _this.titleStyle, - _this.subtitleStyle, - _this.timestampStyle, - _this.backgroundColor, - _this.borderColor, - _this.muteIconPosition, - ); - } -} 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 fa0a371..0189025 100644 --- a/packages/stream_core_flutter/lib/src/theme/stream_theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/stream_theme.dart @@ -9,7 +9,6 @@ import 'components/stream_avatar_theme.dart'; import 'components/stream_badge_count_theme.dart'; import 'components/stream_badge_notification_theme.dart'; import 'components/stream_button_theme.dart'; -import 'components/stream_channel_list_item_theme.dart'; import 'components/stream_checkbox_theme.dart'; import 'components/stream_context_menu_action_theme.dart'; import 'components/stream_context_menu_theme.dart'; @@ -99,7 +98,6 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { StreamBadgeCountThemeData? badgeCountTheme, StreamBadgeNotificationThemeData? badgeNotificationTheme, StreamButtonThemeData? buttonTheme, - StreamChannelListItemThemeData? channelListItemTheme, StreamCheckboxThemeData? checkboxTheme, StreamContextMenuThemeData? contextMenuTheme, StreamContextMenuActionThemeData? contextMenuActionTheme, @@ -131,7 +129,6 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { badgeCountTheme ??= const StreamBadgeCountThemeData(); badgeNotificationTheme ??= const StreamBadgeNotificationThemeData(); buttonTheme ??= const StreamButtonThemeData(); - channelListItemTheme ??= const StreamChannelListItemThemeData(); checkboxTheme ??= const StreamCheckboxThemeData(); contextMenuTheme ??= const StreamContextMenuThemeData(); contextMenuActionTheme ??= const StreamContextMenuActionThemeData(); @@ -157,7 +154,6 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { badgeCountTheme: badgeCountTheme, badgeNotificationTheme: badgeNotificationTheme, buttonTheme: buttonTheme, - channelListItemTheme: channelListItemTheme, checkboxTheme: checkboxTheme, contextMenuTheme: contextMenuTheme, contextMenuActionTheme: contextMenuActionTheme, @@ -197,7 +193,6 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { required this.badgeCountTheme, required this.badgeNotificationTheme, required this.buttonTheme, - required this.channelListItemTheme, required this.checkboxTheme, required this.contextMenuTheme, required this.contextMenuActionTheme, @@ -283,9 +278,6 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { /// The button theme for this theme. final StreamButtonThemeData buttonTheme; - /// The channel list item theme for this theme. - final StreamChannelListItemThemeData channelListItemTheme; - /// The checkbox theme for this theme. final StreamCheckboxThemeData checkboxTheme; @@ -350,7 +342,6 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { badgeCountTheme: badgeCountTheme, badgeNotificationTheme: badgeNotificationTheme, buttonTheme: buttonTheme, - channelListItemTheme: channelListItemTheme, checkboxTheme: checkboxTheme, contextMenuTheme: contextMenuTheme, contextMenuActionTheme: contextMenuActionTheme, 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 b787e5e..4917120 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 @@ -5,7 +5,6 @@ import 'components/stream_avatar_theme.dart'; import 'components/stream_badge_count_theme.dart'; import 'components/stream_badge_notification_theme.dart'; import 'components/stream_button_theme.dart'; -import 'components/stream_channel_list_item_theme.dart'; import 'components/stream_checkbox_theme.dart'; import 'components/stream_context_menu_action_theme.dart'; import 'components/stream_context_menu_theme.dart'; @@ -85,9 +84,6 @@ extension StreamThemeExtension on BuildContext { /// Returns the [StreamButtonThemeData] from the nearest ancestor. StreamButtonThemeData get streamButtonTheme => StreamButtonTheme.of(this); - /// Returns the [StreamChannelListItemThemeData] from the nearest ancestor. - StreamChannelListItemThemeData get streamChannelListItemTheme => StreamChannelListItemTheme.of(this); - /// Returns the [StreamCheckboxThemeData] from the nearest ancestor. StreamCheckboxThemeData get streamCheckboxTheme => StreamCheckboxTheme.of(this); From 77f66669da436947e7c146f90b64b825351cd6ef Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 4 Mar 2026 16:52:27 +0100 Subject: [PATCH 15/15] update generated files --- .../src/factory/stream_component_factory.g.theme.dart | 6 ------ .../lib/src/theme/stream_theme.g.theme.dart | 9 --------- 2 files changed, 15 deletions(-) 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 732155d..c8539d9 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 @@ -37,7 +37,6 @@ mixin _$StreamComponentBuilders { badgeCount: t < 0.5 ? a.badgeCount : b.badgeCount, badgeNotification: t < 0.5 ? a.badgeNotification : b.badgeNotification, button: t < 0.5 ? a.button : b.button, - channelListItem: t < 0.5 ? a.channelListItem : b.channelListItem, checkbox: t < 0.5 ? a.checkbox : b.checkbox, contextMenuAction: t < 0.5 ? a.contextMenuAction : b.contextMenuAction, emoji: t < 0.5 ? a.emoji : b.emoji, @@ -60,7 +59,6 @@ mixin _$StreamComponentBuilders { Widget Function(BuildContext, StreamBadgeNotificationProps)? badgeNotification, Widget Function(BuildContext, StreamButtonProps)? button, - Widget Function(BuildContext, StreamChannelListItemProps)? channelListItem, Widget Function(BuildContext, StreamCheckboxProps)? checkbox, Widget Function(BuildContext, StreamContextMenuActionProps)? contextMenuAction, @@ -84,7 +82,6 @@ mixin _$StreamComponentBuilders { badgeCount: badgeCount ?? _this.badgeCount, badgeNotification: badgeNotification ?? _this.badgeNotification, button: button ?? _this.button, - channelListItem: channelListItem ?? _this.channelListItem, checkbox: checkbox ?? _this.checkbox, contextMenuAction: contextMenuAction ?? _this.contextMenuAction, emoji: emoji ?? _this.emoji, @@ -117,7 +114,6 @@ mixin _$StreamComponentBuilders { badgeCount: other.badgeCount, badgeNotification: other.badgeNotification, button: other.button, - channelListItem: other.channelListItem, checkbox: other.checkbox, contextMenuAction: other.contextMenuAction, emoji: other.emoji, @@ -151,7 +147,6 @@ mixin _$StreamComponentBuilders { _other.badgeCount == _this.badgeCount && _other.badgeNotification == _this.badgeNotification && _other.button == _this.button && - _other.channelListItem == _this.channelListItem && _other.checkbox == _this.checkbox && _other.contextMenuAction == _this.contextMenuAction && _other.emoji == _this.emoji && @@ -177,7 +172,6 @@ mixin _$StreamComponentBuilders { _this.badgeCount, _this.badgeNotification, _this.button, - _this.channelListItem, _this.checkbox, _this.contextMenuAction, _this.emoji, 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 385fee3..8ef6173 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 @@ -25,7 +25,6 @@ mixin _$StreamTheme on ThemeExtension { StreamBadgeCountThemeData? badgeCountTheme, StreamBadgeNotificationThemeData? badgeNotificationTheme, StreamButtonThemeData? buttonTheme, - StreamChannelListItemThemeData? channelListItemTheme, StreamCheckboxThemeData? checkboxTheme, StreamContextMenuThemeData? contextMenuTheme, StreamContextMenuActionThemeData? contextMenuActionTheme, @@ -54,7 +53,6 @@ mixin _$StreamTheme on ThemeExtension { badgeNotificationTheme: badgeNotificationTheme ?? _this.badgeNotificationTheme, buttonTheme: buttonTheme ?? _this.buttonTheme, - channelListItemTheme: channelListItemTheme ?? _this.channelListItemTheme, checkboxTheme: checkboxTheme ?? _this.checkboxTheme, contextMenuTheme: contextMenuTheme ?? _this.contextMenuTheme, contextMenuActionTheme: @@ -115,11 +113,6 @@ mixin _$StreamTheme on ThemeExtension { other.buttonTheme, t, )!, - channelListItemTheme: StreamChannelListItemThemeData.lerp( - _this.channelListItemTheme, - other.channelListItemTheme, - t, - )!, checkboxTheme: StreamCheckboxThemeData.lerp( _this.checkboxTheme, other.checkboxTheme, @@ -191,7 +184,6 @@ mixin _$StreamTheme on ThemeExtension { _other.badgeCountTheme == _this.badgeCountTheme && _other.badgeNotificationTheme == _this.badgeNotificationTheme && _other.buttonTheme == _this.buttonTheme && - _other.channelListItemTheme == _this.channelListItemTheme && _other.checkboxTheme == _this.checkboxTheme && _other.contextMenuTheme == _this.contextMenuTheme && _other.contextMenuActionTheme == _this.contextMenuActionTheme && @@ -223,7 +215,6 @@ mixin _$StreamTheme on ThemeExtension { _this.badgeCountTheme, _this.badgeNotificationTheme, _this.buttonTheme, - _this.channelListItemTheme, _this.checkboxTheme, _this.contextMenuTheme, _this.contextMenuActionTheme,