From d2b2a362054ed629372c7b0efa777a7d78420fc2 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 13 Mar 2026 17:31:50 +0100 Subject: [PATCH 1/2] make command chip --- .../devtools_options.yaml | 3 + .../lib/app/gallery_app.directories.g.dart | 25 ++- .../controls/stream_command_chip.dart | 139 ++++++++++++++++ .../lib/src/components.dart | 1 + .../controls/stream_command_chip.dart | 153 ++++++++++++++++++ .../message_composer_input.dart | 56 ++++--- .../src/factory/stream_component_factory.dart | 8 + .../stream_component_factory.g.theme.dart | 6 + .../stream_core_flutter/lib/src/theme.dart | 1 + .../components/stream_command_chip_theme.dart | 75 +++++++++ .../stream_command_chip_theme.g.theme.dart | 100 ++++++++++++ .../lib/src/theme/stream_theme.dart | 9 ++ .../lib/src/theme/stream_theme.g.theme.dart | 9 ++ .../src/theme/stream_theme_extensions.dart | 4 + .../stream_command_chip_golden_test.dart | 91 +++++++++++ 15 files changed, 658 insertions(+), 22 deletions(-) create mode 100644 apps/design_system_gallery/devtools_options.yaml create mode 100644 apps/design_system_gallery/lib/components/controls/stream_command_chip.dart create mode 100644 packages/stream_core_flutter/lib/src/components/controls/stream_command_chip.dart create mode 100644 packages/stream_core_flutter/lib/src/theme/components/stream_command_chip_theme.dart create mode 100644 packages/stream_core_flutter/lib/src/theme/components/stream_command_chip_theme.g.theme.dart create mode 100644 packages/stream_core_flutter/test/components/controls/stream_command_chip_golden_test.dart diff --git a/apps/design_system_gallery/devtools_options.yaml b/apps/design_system_gallery/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/apps/design_system_gallery/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: 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 d6494aa..b4a6e01 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 @@ -40,6 +40,8 @@ import 'package:design_system_gallery/components/common/stream_progress_bar.dart as _design_system_gallery_components_common_stream_progress_bar; import 'package:design_system_gallery/components/context_menu/stream_context_menu.dart' as _design_system_gallery_components_context_menu_stream_context_menu; +import 'package:design_system_gallery/components/controls/stream_command_chip.dart' + as _design_system_gallery_components_controls_stream_command_chip; import 'package:design_system_gallery/components/controls/stream_emoji_chip.dart' as _design_system_gallery_components_controls_stream_emoji_chip; import 'package:design_system_gallery/components/controls/stream_emoji_chip_bar.dart' @@ -55,7 +57,7 @@ import 'package:design_system_gallery/components/message_composer/message_compos import 'package:design_system_gallery/components/message_composer/message_composer_attachment_reply.dart' as _design_system_gallery_components_message_composer_message_composer_attachment_reply; import 'package:design_system_gallery/components/reaction/stream_reactions.dart' - as _design_system_gallery_components_reaction_stream_reaction; + as _design_system_gallery_components_reaction_stream_reactions; import 'package:design_system_gallery/components/tiles/stream_list_tile.dart' as _design_system_gallery_components_tiles_stream_list_tile; import 'package:design_system_gallery/primitives/colors.dart' @@ -463,6 +465,23 @@ final directories = <_widgetbook.WidgetbookNode>[ _widgetbook.WidgetbookFolder( name: 'Controls', children: [ + _widgetbook.WidgetbookComponent( + name: 'StreamCommandChip', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'Playground', + builder: + _design_system_gallery_components_controls_stream_command_chip + .buildStreamCommandChipPlayground, + ), + _widgetbook.WidgetbookUseCase( + name: 'Showcase', + builder: + _design_system_gallery_components_controls_stream_command_chip + .buildStreamCommandChipShowcase, + ), + ], + ), _widgetbook.WidgetbookComponent( name: 'StreamEmojiChip', useCases: [ @@ -579,13 +598,13 @@ final directories = <_widgetbook.WidgetbookNode>[ _widgetbook.WidgetbookUseCase( name: 'Playground', builder: - _design_system_gallery_components_reaction_stream_reaction + _design_system_gallery_components_reaction_stream_reactions .buildStreamReactionsPlayground, ), _widgetbook.WidgetbookUseCase( name: 'Showcase', builder: - _design_system_gallery_components_reaction_stream_reaction + _design_system_gallery_components_reaction_stream_reactions .buildStreamReactionsShowcase, ), ], diff --git a/apps/design_system_gallery/lib/components/controls/stream_command_chip.dart b/apps/design_system_gallery/lib/components/controls/stream_command_chip.dart new file mode 100644 index 0000000..ba1ac11 --- /dev/null +++ b/apps/design_system_gallery/lib/components/controls/stream_command_chip.dart @@ -0,0 +1,139 @@ +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: StreamCommandChip, + path: '[Components]/Controls', +) +Widget buildStreamCommandChipPlayground(BuildContext context) { + final label = context.knobs.string( + label: 'Label', + initialValue: '/giphy', + description: 'The command label displayed inside the chip.', + ); + + final enableDismiss = context.knobs.boolean( + label: 'On Dismiss', + initialValue: true, + description: 'Whether the dismiss callback is active.', + ); + + return Center( + child: StreamCommandChip( + label: label, + onDismiss: enableDismiss + ? () { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + const SnackBar( + content: Text('Command dismissed'), + duration: Duration(seconds: 1), + ), + ); + } + : null, + ), + ); +} + +// ============================================================================= +// Showcase +// ============================================================================= + +@widgetbook.UseCase( + name: 'Showcase', + type: StreamCommandChip, + path: '[Components]/Controls', +) +Widget buildStreamCommandChipShowcase(BuildContext context) { + final colorScheme = context.streamColorScheme; + final spacing = context.streamSpacing; + final radius = context.streamRadius; + + return SingleChildScrollView( + padding: EdgeInsets.all(spacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: spacing.xl, + children: [ + _SectionLabel(label: 'COMMAND CHIP — IMAGE OVERLAY'), + // Simulated attachment overlay + Stack( + alignment: Alignment.topLeft, + children: [ + Container( + width: 200, + height: 120, + decoration: BoxDecoration( + color: colorScheme.backgroundSurfaceSubtle, + borderRadius: BorderRadius.all(radius.md), + border: Border.all(color: colorScheme.borderSubtle), + ), + child: Center( + child: Icon(Icons.image, size: 48, color: colorScheme.textDisabled), + ), + ), + Padding( + padding: EdgeInsets.all(spacing.xs), + child: StreamCommandChip( + label: '/giphy', + onDismiss: () {}, + ), + ), + ], + ), + _SectionLabel(label: 'LABEL VARIANTS'), + Wrap( + spacing: spacing.xs, + runSpacing: spacing.xs, + children: [ + StreamCommandChip(label: '/giphy', onDismiss: () {}), + StreamCommandChip(label: '/img', onDismiss: () {}), + StreamCommandChip(label: '/ban', onDismiss: () {}), + StreamCommandChip(label: '/very-long-command-name', onDismiss: () {}), + ], + ), + _SectionLabel(label: 'WITHOUT DISMISS'), + StreamCommandChip(label: '/giphy'), + ], + ), + ); +} + +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 07ad15c..6a669de 100644 --- a/packages/stream_core_flutter/lib/src/components.dart +++ b/packages/stream_core_flutter/lib/src/components.dart @@ -15,6 +15,7 @@ export 'components/common/stream_flex.dart'; export 'components/common/stream_progress_bar.dart' hide DefaultStreamProgressBar; export 'components/context_menu/stream_context_menu.dart'; export 'components/context_menu/stream_context_menu_action.dart' hide DefaultStreamContextMenuAction; +export 'components/controls/stream_command_chip.dart' hide DefaultStreamCommandChip; export 'components/controls/stream_emoji_chip.dart' hide DefaultStreamEmojiChip; export 'components/controls/stream_emoji_chip_bar.dart' hide DefaultStreamEmojiChipBar; export 'components/controls/stream_remove_control.dart'; diff --git a/packages/stream_core_flutter/lib/src/components/controls/stream_command_chip.dart b/packages/stream_core_flutter/lib/src/components/controls/stream_command_chip.dart new file mode 100644 index 0000000..e5acb5d --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/controls/stream_command_chip.dart @@ -0,0 +1,153 @@ +import 'package:flutter/material.dart'; + +import '../../factory/stream_component_factory.dart'; +import '../../theme/stream_theme_extensions.dart'; + +/// A pill-shaped chip for displaying a slash command selection. +/// +/// [StreamCommandChip] renders a thunder icon, a command label, and a dismiss +/// button. It is typically used as an overlay on a message attachment when a +/// slash command is active in the message composer. +/// +/// {@tool snippet} +/// +/// Display a command chip with a dismiss callback: +/// +/// ```dart +/// StreamCommandChip( +/// label: '/giphy', +/// onDismiss: () => clearCommand(), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamCommandChipTheme], for customizing chip appearance. +class StreamCommandChip extends StatelessWidget { + /// Creates a command chip with a [label] and optional [onDismiss] callback. + StreamCommandChip({ + super.key, + required String label, + VoidCallback? onDismiss, + }) : props = StreamCommandChipProps(label: label, onDismiss: onDismiss); + + /// The props controlling the appearance and behavior of this chip. + final StreamCommandChipProps props; + + @override + Widget build(BuildContext context) { + final builder = StreamComponentFactory.of(context).commandChip; + if (builder != null) return builder(context, props); + return DefaultStreamCommandChip(props: props); + } +} + +/// Properties for configuring a [StreamCommandChip]. +/// +/// See also: +/// +/// * [StreamCommandChip], which uses these properties. +/// * [DefaultStreamCommandChip], the default implementation. +class StreamCommandChipProps { + /// Creates properties for a command chip. + const StreamCommandChipProps({ + required this.label, + this.onDismiss, + }); + + /// The command label to display inside the chip. + final String label; + + /// Called when the dismiss (×) button is tapped. + /// + /// When null the dismiss button is still shown but does nothing. + final VoidCallback? onDismiss; +} + +/// Default implementation of [StreamCommandChip]. +class DefaultStreamCommandChip extends StatelessWidget { + /// Creates a default command chip. + const DefaultStreamCommandChip({super.key, required this.props}); + + /// The props controlling the appearance and behavior of this chip. + final StreamCommandChipProps props; + + @override + Widget build(BuildContext context) { + final defaults = _StreamCommandChipDefaults(context); + final chipTheme = context.streamCommandChipTheme; + + final effectiveBackgroundColor = chipTheme.backgroundColor ?? defaults.backgroundColor; + final effectiveLabelColor = chipTheme.labelColor ?? defaults.labelColor; + final effectiveIconColor = chipTheme.iconColor ?? defaults.iconColor; + + return Container( + padding: defaults.padding, + decoration: BoxDecoration( + color: effectiveBackgroundColor, + borderRadius: defaults.borderRadius, + ), + constraints: const BoxConstraints(minHeight: 24), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + context.streamIcons.thunder, + size: 12, + color: effectiveIconColor, + ), + SizedBox(width: defaults.spacing.xxxs), + MediaQuery.withNoTextScaling( + child: Text( + props.label, + style: defaults.labelStyle.copyWith(color: effectiveLabelColor), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + SizedBox(width: defaults.spacing.xxxs), + GestureDetector( + onTap: props.onDismiss, + behavior: HitTestBehavior.opaque, + child: SizedBox( + width: 16, + height: 16, + child: Icon( + context.streamIcons.crossMedium, + size: 12, + color: effectiveIconColor, + ), + ), + ), + ], + ), + ); + } +} + +// Provides default values for [StreamCommandChip] based on the current theme. +class _StreamCommandChipDefaults { + _StreamCommandChipDefaults(this._context); + + final BuildContext _context; + + late final _colorScheme = _context.streamColorScheme; + late final _textTheme = _context.streamTextTheme; + late final spacing = _context.streamSpacing; + + Color get backgroundColor => _colorScheme.backgroundInverse; + + Color get labelColor => _colorScheme.textInverse; + + Color get iconColor => _colorScheme.textInverse; + + TextStyle get labelStyle => _textTheme.metadataEmphasis; + + EdgeInsetsGeometry get padding => EdgeInsets.symmetric( + horizontal: spacing.xs, + vertical: spacing.xxxs, + ); + + BorderRadius get borderRadius => const BorderRadius.all(Radius.circular(9999)); +} diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input.dart index faf8f7e..4908481 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input.dart @@ -74,11 +74,15 @@ class StreamMessageComposerInputField extends StatelessWidget { required this.controller, required this.placeholder, this.focusNode, + this.command, + this.onDismissCommand, }); final TextEditingController controller; final String placeholder; final FocusNode? focusNode; + final String? command; + final VoidCallback? onDismissCommand; @override Widget build(BuildContext context) { @@ -93,26 +97,40 @@ class StreamMessageComposerInputField extends StatelessWidget { return ConstrainedBox( constraints: const BoxConstraints(maxHeight: 124), - child: TextField( - controller: controller, - focusNode: focusNode, - style: TextStyle( - color: inputTheme.textColor ?? inputDefaults.textColor, - ), - maxLines: null, - decoration: InputDecoration( - border: border, - focusedBorder: border, - enabledBorder: border, - errorBorder: border, - disabledBorder: border, - fillColor: Colors.transparent, - contentPadding: const EdgeInsets.fromLTRB(12, 8, 12, 8), - hintText: placeholder, - hintStyle: TextStyle( - color: inputTheme.placeholderColor ?? inputDefaults.placeholderColor, + child: Row( + children: [ + if (command case final command?) + Padding( + padding: EdgeInsets.only(left: context.streamSpacing.sm), + child: StreamCommandChip( + label: command, + onDismiss: onDismissCommand, + ), + ), + Expanded( + child: TextField( + controller: controller, + focusNode: focusNode, + style: TextStyle( + color: inputTheme.textColor ?? inputDefaults.textColor, + ), + maxLines: null, + decoration: InputDecoration( + border: border, + focusedBorder: border, + enabledBorder: border, + errorBorder: border, + disabledBorder: border, + fillColor: Colors.transparent, + contentPadding: EdgeInsets.fromLTRB(command == null ? 12 : 0, 8, 12, 8), + hintText: placeholder, + hintStyle: TextStyle( + color: inputTheme.placeholderColor ?? inputDefaults.placeholderColor, + ), + ), + ), ), - ), + ], ), ); } 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 a0c3a68..5caa76d 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 @@ -138,6 +138,7 @@ class StreamComponentBuilders with _$StreamComponentBuilders { StreamComponentBuilder? badgeNotification, StreamComponentBuilder? button, StreamComponentBuilder? checkbox, + StreamComponentBuilder? commandChip, StreamComponentBuilder? contextMenuAction, StreamComponentBuilder? emoji, StreamComponentBuilder? emojiButton, @@ -160,6 +161,7 @@ class StreamComponentBuilders with _$StreamComponentBuilders { badgeNotification: badgeNotification, button: button, checkbox: checkbox, + commandChip: commandChip, contextMenuAction: contextMenuAction, emoji: emoji, emojiButton: emojiButton, @@ -183,6 +185,7 @@ class StreamComponentBuilders with _$StreamComponentBuilders { required this.badgeNotification, required this.button, required this.checkbox, + required this.commandChip, required this.contextMenuAction, required this.emoji, required this.emojiButton, @@ -251,6 +254,11 @@ class StreamComponentBuilders with _$StreamComponentBuilders { /// When null, [StreamCheckbox] uses [DefaultStreamCheckbox]. final StreamComponentBuilder? checkbox; + /// Custom builder for command chip widgets. + /// + /// When null, [StreamCommandChip] uses [DefaultStreamCommandChip]. + final StreamComponentBuilder? commandChip; + /// Custom builder for context menu action widgets. /// /// When null, [StreamContextMenuAction] uses [DefaultStreamContextMenuAction]. 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 2e76e62..8e8e973 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 @@ -38,6 +38,7 @@ mixin _$StreamComponentBuilders { badgeNotification: t < 0.5 ? a.badgeNotification : b.badgeNotification, button: t < 0.5 ? a.button : b.button, checkbox: t < 0.5 ? a.checkbox : b.checkbox, + commandChip: t < 0.5 ? a.commandChip : b.commandChip, contextMenuAction: t < 0.5 ? a.contextMenuAction : b.contextMenuAction, emoji: t < 0.5 ? a.emoji : b.emoji, emojiButton: t < 0.5 ? a.emojiButton : b.emojiButton, @@ -61,6 +62,7 @@ mixin _$StreamComponentBuilders { badgeNotification, Widget Function(BuildContext, StreamButtonProps)? button, Widget Function(BuildContext, StreamCheckboxProps)? checkbox, + Widget Function(BuildContext, StreamCommandChipProps)? commandChip, Widget Function(BuildContext, StreamContextMenuActionProps)? contextMenuAction, Widget Function(BuildContext, StreamEmojiProps)? emoji, @@ -85,6 +87,7 @@ mixin _$StreamComponentBuilders { badgeNotification: badgeNotification ?? _this.badgeNotification, button: button ?? _this.button, checkbox: checkbox ?? _this.checkbox, + commandChip: commandChip ?? _this.commandChip, contextMenuAction: contextMenuAction ?? _this.contextMenuAction, emoji: emoji ?? _this.emoji, emojiButton: emojiButton ?? _this.emojiButton, @@ -118,6 +121,7 @@ mixin _$StreamComponentBuilders { badgeNotification: other.badgeNotification, button: other.button, checkbox: other.checkbox, + commandChip: other.commandChip, contextMenuAction: other.contextMenuAction, emoji: other.emoji, emojiButton: other.emojiButton, @@ -152,6 +156,7 @@ mixin _$StreamComponentBuilders { _other.badgeNotification == _this.badgeNotification && _other.button == _this.button && _other.checkbox == _this.checkbox && + _other.commandChip == _this.commandChip && _other.contextMenuAction == _this.contextMenuAction && _other.emoji == _this.emoji && _other.emojiButton == _this.emojiButton && @@ -178,6 +183,7 @@ mixin _$StreamComponentBuilders { _this.badgeNotification, _this.button, _this.checkbox, + _this.commandChip, _this.contextMenuAction, _this.emoji, _this.emojiButton, diff --git a/packages/stream_core_flutter/lib/src/theme.dart b/packages/stream_core_flutter/lib/src/theme.dart index c4e8284..d73c589 100644 --- a/packages/stream_core_flutter/lib/src/theme.dart +++ b/packages/stream_core_flutter/lib/src/theme.dart @@ -6,6 +6,7 @@ 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_checkbox_theme.dart'; +export 'theme/components/stream_command_chip_theme.dart'; export 'theme/components/stream_context_menu_action_theme.dart'; export 'theme/components/stream_context_menu_theme.dart'; export 'theme/components/stream_emoji_button_theme.dart'; diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_command_chip_theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_command_chip_theme.dart new file mode 100644 index 0000000..87b5206 --- /dev/null +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_command_chip_theme.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:theme_extensions_builder_annotation/theme_extensions_builder_annotation.dart'; + +import '../stream_theme.dart'; + +part 'stream_command_chip_theme.g.theme.dart'; + +/// Applies a command chip theme to descendant command chip widgets. +/// +/// Wrap a subtree with [StreamCommandChipTheme] to override command chip +/// styling. Access the merged theme using [BuildContext.streamCommandChipTheme]. +/// +/// See also: +/// +/// * [StreamCommandChipThemeData], which describes the command chip theme. +class StreamCommandChipTheme extends InheritedTheme { + /// Creates a command chip theme that controls descendant command chips. + const StreamCommandChipTheme({ + super.key, + required this.data, + required super.child, + }); + + /// The command chip theme data for descendant widgets. + final StreamCommandChipThemeData data; + + /// Returns the [StreamCommandChipThemeData] from the current theme context. + /// + /// This merges the local theme (if any) with the global theme from + /// [StreamTheme]. + static StreamCommandChipThemeData of(BuildContext context) { + final localTheme = context.dependOnInheritedWidgetOfExactType(); + return StreamTheme.of(context).commandChipTheme.merge(localTheme?.data); + } + + @override + Widget wrap(BuildContext context, Widget child) { + return StreamCommandChipTheme(data: data, child: child); + } + + @override + bool updateShouldNotify(StreamCommandChipTheme oldWidget) => data != oldWidget.data; +} + +/// Theme data for customizing command chip widgets. +/// +/// See also: +/// +/// * [StreamCommandChipTheme], for overriding theme in a widget subtree. +@themeGen +@immutable +class StreamCommandChipThemeData with _$StreamCommandChipThemeData { + /// Creates a command chip theme with optional color overrides. + const StreamCommandChipThemeData({ + this.backgroundColor, + this.labelColor, + this.iconColor, + }); + + /// The background color of the chip. + final Color? backgroundColor; + + /// The color of the label text. + final Color? labelColor; + + /// The color of the leading and trailing icons. + final Color? iconColor; + + /// Linearly interpolate between two [StreamCommandChipThemeData] objects. + static StreamCommandChipThemeData? lerp( + StreamCommandChipThemeData? a, + StreamCommandChipThemeData? b, + double t, + ) => _$StreamCommandChipThemeData.lerp(a, b, t); +} diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_command_chip_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_command_chip_theme.g.theme.dart new file mode 100644 index 0000000..fb5d2a3 --- /dev/null +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_command_chip_theme.g.theme.dart @@ -0,0 +1,100 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_element + +part of 'stream_command_chip_theme.dart'; + +// ************************************************************************** +// ThemeGenGenerator +// ************************************************************************** + +mixin _$StreamCommandChipThemeData { + bool get canMerge => true; + + static StreamCommandChipThemeData? lerp( + StreamCommandChipThemeData? a, + StreamCommandChipThemeData? 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 StreamCommandChipThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + labelColor: Color.lerp(a.labelColor, b.labelColor, t), + iconColor: Color.lerp(a.iconColor, b.iconColor, t), + ); + } + + StreamCommandChipThemeData copyWith({ + Color? backgroundColor, + Color? labelColor, + Color? iconColor, + }) { + final _this = (this as StreamCommandChipThemeData); + + return StreamCommandChipThemeData( + backgroundColor: backgroundColor ?? _this.backgroundColor, + labelColor: labelColor ?? _this.labelColor, + iconColor: iconColor ?? _this.iconColor, + ); + } + + StreamCommandChipThemeData merge(StreamCommandChipThemeData? other) { + final _this = (this as StreamCommandChipThemeData); + + if (other == null || identical(_this, other)) { + return _this; + } + + if (!other.canMerge) { + return other; + } + + return copyWith( + backgroundColor: other.backgroundColor, + labelColor: other.labelColor, + iconColor: other.iconColor, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + final _this = (this as StreamCommandChipThemeData); + final _other = (other as StreamCommandChipThemeData); + + return _other.backgroundColor == _this.backgroundColor && + _other.labelColor == _this.labelColor && + _other.iconColor == _this.iconColor; + } + + @override + int get hashCode { + final _this = (this as StreamCommandChipThemeData); + + return Object.hash( + runtimeType, + _this.backgroundColor, + _this.labelColor, + _this.iconColor, + ); + } +} 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 f0d35e2..0f938e2 100644 --- a/packages/stream_core_flutter/lib/src/theme/stream_theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/stream_theme.dart @@ -10,6 +10,7 @@ import 'components/stream_badge_count_theme.dart'; import 'components/stream_badge_notification_theme.dart'; import 'components/stream_button_theme.dart'; import 'components/stream_checkbox_theme.dart'; +import 'components/stream_command_chip_theme.dart'; import 'components/stream_context_menu_action_theme.dart'; import 'components/stream_context_menu_theme.dart'; import 'components/stream_emoji_button_theme.dart'; @@ -100,6 +101,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { StreamBadgeNotificationThemeData? badgeNotificationTheme, StreamButtonThemeData? buttonTheme, StreamCheckboxThemeData? checkboxTheme, + StreamCommandChipThemeData? commandChipTheme, StreamContextMenuThemeData? contextMenuTheme, StreamContextMenuActionThemeData? contextMenuActionTheme, StreamEmojiButtonThemeData? emojiButtonTheme, @@ -132,6 +134,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { badgeNotificationTheme ??= const StreamBadgeNotificationThemeData(); buttonTheme ??= const StreamButtonThemeData(); checkboxTheme ??= const StreamCheckboxThemeData(); + commandChipTheme ??= const StreamCommandChipThemeData(); contextMenuTheme ??= const StreamContextMenuThemeData(); contextMenuActionTheme ??= const StreamContextMenuActionThemeData(); emojiButtonTheme ??= const StreamEmojiButtonThemeData(); @@ -158,6 +161,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { badgeNotificationTheme: badgeNotificationTheme, buttonTheme: buttonTheme, checkboxTheme: checkboxTheme, + commandChipTheme: commandChipTheme, contextMenuTheme: contextMenuTheme, contextMenuActionTheme: contextMenuActionTheme, emojiButtonTheme: emojiButtonTheme, @@ -198,6 +202,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { required this.badgeNotificationTheme, required this.buttonTheme, required this.checkboxTheme, + required this.commandChipTheme, required this.contextMenuTheme, required this.contextMenuActionTheme, required this.emojiButtonTheme, @@ -286,6 +291,9 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { /// The checkbox theme for this theme. final StreamCheckboxThemeData checkboxTheme; + /// The command chip theme for this theme. + final StreamCommandChipThemeData commandChipTheme; + /// The context menu theme for this theme. final StreamContextMenuThemeData contextMenuTheme; @@ -351,6 +359,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { badgeNotificationTheme: badgeNotificationTheme, buttonTheme: buttonTheme, checkboxTheme: checkboxTheme, + commandChipTheme: commandChipTheme, contextMenuTheme: contextMenuTheme, contextMenuActionTheme: contextMenuActionTheme, emojiButtonTheme: emojiButtonTheme, 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 46d74ea..e38c0a6 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 @@ -26,6 +26,7 @@ mixin _$StreamTheme on ThemeExtension { StreamBadgeNotificationThemeData? badgeNotificationTheme, StreamButtonThemeData? buttonTheme, StreamCheckboxThemeData? checkboxTheme, + StreamCommandChipThemeData? commandChipTheme, StreamContextMenuThemeData? contextMenuTheme, StreamContextMenuActionThemeData? contextMenuActionTheme, StreamEmojiButtonThemeData? emojiButtonTheme, @@ -55,6 +56,7 @@ mixin _$StreamTheme on ThemeExtension { badgeNotificationTheme ?? _this.badgeNotificationTheme, buttonTheme: buttonTheme ?? _this.buttonTheme, checkboxTheme: checkboxTheme ?? _this.checkboxTheme, + commandChipTheme: commandChipTheme ?? _this.commandChipTheme, contextMenuTheme: contextMenuTheme ?? _this.contextMenuTheme, contextMenuActionTheme: contextMenuActionTheme ?? _this.contextMenuActionTheme, @@ -120,6 +122,11 @@ mixin _$StreamTheme on ThemeExtension { other.checkboxTheme, t, )!, + commandChipTheme: StreamCommandChipThemeData.lerp( + _this.commandChipTheme, + other.commandChipTheme, + t, + )!, contextMenuTheme: StreamContextMenuThemeData.lerp( _this.contextMenuTheme, other.contextMenuTheme, @@ -192,6 +199,7 @@ mixin _$StreamTheme on ThemeExtension { _other.badgeNotificationTheme == _this.badgeNotificationTheme && _other.buttonTheme == _this.buttonTheme && _other.checkboxTheme == _this.checkboxTheme && + _other.commandChipTheme == _this.commandChipTheme && _other.contextMenuTheme == _this.contextMenuTheme && _other.contextMenuActionTheme == _this.contextMenuActionTheme && _other.emojiButtonTheme == _this.emojiButtonTheme && @@ -224,6 +232,7 @@ mixin _$StreamTheme on ThemeExtension { _this.badgeNotificationTheme, _this.buttonTheme, _this.checkboxTheme, + _this.commandChipTheme, _this.contextMenuTheme, _this.contextMenuActionTheme, _this.emojiButtonTheme, 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 3321b72..110e64f 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 @@ -6,6 +6,7 @@ import 'components/stream_badge_count_theme.dart'; import 'components/stream_badge_notification_theme.dart'; import 'components/stream_button_theme.dart'; import 'components/stream_checkbox_theme.dart'; +import 'components/stream_command_chip_theme.dart'; import 'components/stream_context_menu_action_theme.dart'; import 'components/stream_context_menu_theme.dart'; import 'components/stream_emoji_button_theme.dart'; @@ -88,6 +89,9 @@ extension StreamThemeExtension on BuildContext { /// Returns the [StreamCheckboxThemeData] from the nearest ancestor. StreamCheckboxThemeData get streamCheckboxTheme => StreamCheckboxTheme.of(this); + /// Returns the [StreamCommandChipThemeData] from the nearest ancestor. + StreamCommandChipThemeData get streamCommandChipTheme => StreamCommandChipTheme.of(this); + /// Returns the [StreamContextMenuThemeData] from the nearest ancestor. StreamContextMenuThemeData get streamContextMenuTheme => StreamContextMenuTheme.of(this); diff --git a/packages/stream_core_flutter/test/components/controls/stream_command_chip_golden_test.dart b/packages/stream_core_flutter/test/components/controls/stream_command_chip_golden_test.dart new file mode 100644 index 0000000..8b23d5a --- /dev/null +++ b/packages/stream_core_flutter/test/components/controls/stream_command_chip_golden_test.dart @@ -0,0 +1,91 @@ +// ignore_for_file: avoid_redundant_argument_values + +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('StreamCommandChip Golden Tests', () { + goldenTest( + 'renders light theme', + fileName: 'stream_command_chip_light', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 300), + children: [ + GoldenTestScenario( + name: 'short label', + child: _buildInTheme( + StreamCommandChip(label: '/giphy', onDismiss: () {}), + ), + ), + GoldenTestScenario( + name: 'long label', + child: _buildInTheme( + StreamCommandChip(label: '/very-long-command-name', onDismiss: () {}), + ), + ), + GoldenTestScenario( + name: 'no dismiss', + child: _buildInTheme( + StreamCommandChip(label: '/giphy'), + ), + ), + ], + ), + ); + + goldenTest( + 'renders dark theme', + fileName: 'stream_command_chip_dark', + builder: () => GoldenTestGroup( + scenarioConstraints: const BoxConstraints(maxWidth: 300), + children: [ + GoldenTestScenario( + name: 'short label', + child: _buildInTheme( + StreamCommandChip(label: '/giphy', onDismiss: () {}), + brightness: Brightness.dark, + ), + ), + GoldenTestScenario( + name: 'long label', + child: _buildInTheme( + StreamCommandChip(label: '/very-long-command-name', onDismiss: () {}), + brightness: Brightness.dark, + ), + ), + GoldenTestScenario( + name: 'no dismiss', + child: _buildInTheme( + StreamCommandChip(label: '/giphy'), + brightness: Brightness.dark, + ), + ), + ], + ), + ); + }); +} + +Widget _buildInTheme( + Widget chip, { + 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: chip, + ), + ), + ), + ); +} From 188939dcb7408e9a94e1dd628a92637af598f430 Mon Sep 17 00:00:00 2001 From: renefloor <15101411+renefloor@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:45:37 +0000 Subject: [PATCH 2/2] chore: Update Goldens --- .../goldens/ci/stream_command_chip_dark.png | Bin 0 -> 2781 bytes .../goldens/ci/stream_command_chip_light.png | Bin 0 -> 2815 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/stream_core_flutter/test/components/controls/goldens/ci/stream_command_chip_dark.png create mode 100644 packages/stream_core_flutter/test/components/controls/goldens/ci/stream_command_chip_light.png diff --git a/packages/stream_core_flutter/test/components/controls/goldens/ci/stream_command_chip_dark.png b/packages/stream_core_flutter/test/components/controls/goldens/ci/stream_command_chip_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..154259292acc8c7d89ac394357fbb5e2271d6842 GIT binary patch literal 2781 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 literal 0 HcmV?d00001 diff --git a/packages/stream_core_flutter/test/components/controls/goldens/ci/stream_command_chip_light.png b/packages/stream_core_flutter/test/components/controls/goldens/ci/stream_command_chip_light.png new file mode 100644 index 0000000000000000000000000000000000000000..fa502a37e4c0922e99ce43f73e6228d6a9f5ad9d GIT binary patch literal 2815 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 literal 0 HcmV?d00001