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/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 0000000..1542592 Binary files /dev/null and b/packages/stream_core_flutter/test/components/controls/goldens/ci/stream_command_chip_dark.png differ 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 0000000..fa502a3 Binary files /dev/null and b/packages/stream_core_flutter/test/components/controls/goldens/ci/stream_command_chip_light.png differ 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, + ), + ), + ), + ); +}