From e74cdc4f260f6e93cd97eacd532a7cfb7c9fc6d7 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 30 Jan 2026 13:26:40 +0100 Subject: [PATCH 01/11] Add basic message composer structure --- .../lib/app/gallery_app.directories.g.dart | 28 ++ .../lib/components/message_composer.dart | 303 ++++++++++++++++++ .../lib/src/components.dart | 1 + .../lib/src/components/message_composer.dart | 7 + .../message_composer/message_composer.dart | 55 ++++ .../message_composer_input.dart | 89 +++++ .../message_composer_input_header.dart | 67 ++++ .../message_composer_input_leading.dart | 32 ++ .../message_composer_input_trailing.dart | 38 +++ .../message_composer_leading.dart | 37 +++ .../message_composer_trailing.dart | 32 ++ .../src/factory/stream_component_factory.dart | 32 +- .../theme/semantics/stream_color_scheme.dart | 57 ++++ .../stream_color_scheme.g.theme.dart | 50 +++ 14 files changed, 827 insertions(+), 1 deletion(-) create mode 100644 apps/design_system_gallery/lib/components/message_composer.dart create mode 100644 packages/stream_core_flutter/lib/src/components/message_composer.dart create mode 100644 packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart create mode 100644 packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input.dart create mode 100644 packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_header.dart create mode 100644 packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_leading.dart create mode 100644 packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart create mode 100644 packages/stream_core_flutter/lib/src/components/message_composer/message_composer_leading.dart create mode 100644 packages/stream_core_flutter/lib/src/components/message_composer/message_composer_trailing.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 5192641..59e53a4 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 @@ -12,6 +12,8 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:design_system_gallery/components/button.dart' as _design_system_gallery_components_button; +import 'package:design_system_gallery/components/message_composer.dart' + as _design_system_gallery_components_message_composer; import 'package:design_system_gallery/components/stream_avatar.dart' as _design_system_gallery_components_stream_avatar; import 'package:design_system_gallery/components/stream_avatar_stack.dart' @@ -237,6 +239,32 @@ final directories = <_widgetbook.WidgetbookNode>[ ), ], ), + _widgetbook.WidgetbookFolder( + name: 'Message Composer', + isInitiallyExpanded: false, + children: [ + _widgetbook.WidgetbookComponent( + name: 'StreamMessageComposer', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'Component Structure', + builder: _design_system_gallery_components_message_composer + .buildStreamMessageComposerStructure, + ), + _widgetbook.WidgetbookUseCase( + name: 'Playground', + builder: _design_system_gallery_components_message_composer + .buildStreamMessageComposerPlayground, + ), + _widgetbook.WidgetbookUseCase( + name: 'Real-world Example', + builder: _design_system_gallery_components_message_composer + .buildStreamMessageComposerExample, + ), + ], + ), + ], + ), ], ), ]; diff --git a/apps/design_system_gallery/lib/components/message_composer.dart b/apps/design_system_gallery/lib/components/message_composer.dart new file mode 100644 index 0000000..9904a89 --- /dev/null +++ b/apps/design_system_gallery/lib/components/message_composer.dart @@ -0,0 +1,303 @@ +import 'package:flutter/material.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +// ============================================================================= +// Playground +// ============================================================================= + +@widgetbook.UseCase( + name: 'Playground', + type: StreamMessageComposer, + path: '[Components]/Message Composer', +) +Widget buildStreamMessageComposerPlayground(BuildContext context) { + return const Center( + child: StreamMessageComposer(), + ); +} + +// ============================================================================= +// Component Structure +// ============================================================================= + +@widgetbook.UseCase( + name: 'Component Structure', + type: StreamMessageComposer, + path: '[Components]/Message Composer', +) +Widget buildStreamMessageComposerStructure(BuildContext context) { + final theme = StreamTheme.of(context); + final colorScheme = theme.colorScheme; + final textTheme = theme.textTheme; + + return SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 500), + decoration: BoxDecoration( + color: colorScheme.backgroundSurface, + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Message Composer Structure', + style: textTheme.headingSm.copyWith( + color: colorScheme.textPrimary, + ), + ), + const SizedBox(height: 8), + Text( + 'The composer is built from customizable sub-components:', + style: textTheme.bodyDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + const SizedBox(height: 24), + + // Full Composer + const _ComponentCard( + label: 'StreamMessageComposer', + description: 'Main composer widget', + child: StreamMessageComposer(), + ), + const SizedBox(height: 16), + + // Leading + const _ComponentCard( + label: 'StreamMessageComposerLeading', + description: 'Action button(s) before the input', + child: StreamMessageComposerLeading(props: MessageComposerComponentProps()), + ), + const SizedBox(height: 16), + + // Input + const _ComponentCard( + label: 'StreamMessageComposerInput', + description: 'Input area with header, text field, and actions', + child: StreamMessageComposerInput(props: MessageComposerComponentProps()), + ), + const SizedBox(height: 16), + + // Input Header + const _ComponentCard( + label: 'StreamMessageComposerInputHeader', + description: 'Header slots for replies, attachments, etc.', + child: StreamMessageComposerInputHeader(props: MessageComposerComponentProps()), + ), + const SizedBox(height: 16), + + // Input Trailing + const _ComponentCard( + label: 'StreamMessageComposerInputTrailing', + description: 'Send button or other trailing actions', + child: StreamMessageComposerInputTrailing(props: MessageComposerComponentProps()), + ), + ], + ), + ), + ), + ); +} + +class _ComponentCard extends StatelessWidget { + const _ComponentCard({ + required this.label, + required this.description, + required this.child, + }); + + final String label; + final String description; + final Widget child; + + @override + Widget build(BuildContext context) { + final theme = StreamTheme.of(context); + final colorScheme = theme.colorScheme; + final textTheme = theme.textTheme; + + return Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: colorScheme.backgroundApp, + borderRadius: BorderRadius.circular(8), + ), + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: colorScheme.borderSurfaceSubtle), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + const SizedBox(height: 4), + Text( + description, + style: textTheme.captionDefault.copyWith( + color: colorScheme.textTertiary, + ), + ), + ], + ), + ), + Divider( + height: 1, + color: colorScheme.borderSurfaceSubtle, + ), + Padding( + padding: const EdgeInsets.all(12), + child: child, + ), + ], + ), + ); + } +} + +// ============================================================================= +// Real-world Example +// ============================================================================= + +@widgetbook.UseCase( + name: 'Real-world Example', + type: StreamMessageComposer, + path: '[Components]/Message Composer', +) +Widget buildStreamMessageComposerExample(BuildContext context) { + final theme = StreamTheme.of(context); + final colorScheme = theme.colorScheme; + final textTheme = theme.textTheme; + + return Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 400), + decoration: BoxDecoration( + color: colorScheme.backgroundSurface, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Chat header + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: colorScheme.borderSurfaceSubtle), + ), + ), + child: Row( + children: [ + StreamAvatar( + size: StreamAvatarSize.sm, + placeholder: (context) => const Text('JD'), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'John Doe', + style: textTheme.bodyEmphasis.copyWith( + color: colorScheme.textPrimary, + ), + ), + Text( + 'Online', + style: textTheme.captionDefault.copyWith( + color: colorScheme.accentSuccess, + ), + ), + ], + ), + ], + ), + ), + // Messages area (mock) + Container( + padding: const EdgeInsets.all(16), + height: 200, + child: const Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _MessageBubble( + message: 'Hey! How are you?', + isMe: false, + ), + SizedBox(height: 8), + _MessageBubble( + message: "I'm doing great, thanks!", + isMe: true, + ), + ], + ), + ), + // Composer + Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide(color: colorScheme.borderSurfaceSubtle), + ), + ), + padding: const EdgeInsets.symmetric(vertical: 8), + child: const StreamMessageComposer(), + ), + ], + ), + ), + ); +} + +class _MessageBubble extends StatelessWidget { + const _MessageBubble({ + required this.message, + required this.isMe, + }); + + final String message; + final bool isMe; + + @override + Widget build(BuildContext context) { + final theme = StreamTheme.of(context); + final colorScheme = theme.colorScheme; + final textTheme = theme.textTheme; + + return Align( + alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: isMe ? colorScheme.accentPrimary : colorScheme.backgroundApp, + borderRadius: BorderRadius.circular(16), + border: isMe ? null : Border.all(color: colorScheme.borderSurfaceSubtle), + ), + child: Text( + message, + style: textTheme.bodyDefault.copyWith( + color: isMe ? colorScheme.textOnAccent : colorScheme.textPrimary, + ), + ), + ), + ); + } +} diff --git a/packages/stream_core_flutter/lib/src/components.dart b/packages/stream_core_flutter/lib/src/components.dart index c99f49a..3bceb12 100644 --- a/packages/stream_core_flutter/lib/src/components.dart +++ b/packages/stream_core_flutter/lib/src/components.dart @@ -2,3 +2,4 @@ export 'components/avatar/stream_avatar.dart'; export 'components/avatar/stream_avatar_stack.dart'; export 'components/buttons/stream_button.dart' hide DefaultStreamButton; export 'components/indicator/stream_online_indicator.dart'; +export 'components/message_composer.dart'; diff --git a/packages/stream_core_flutter/lib/src/components/message_composer.dart b/packages/stream_core_flutter/lib/src/components/message_composer.dart new file mode 100644 index 0000000..fe2649f --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/message_composer.dart @@ -0,0 +1,7 @@ +export 'message_composer/message_composer.dart'; +export 'message_composer/message_composer_input.dart'; +export 'message_composer/message_composer_input_header.dart'; +export 'message_composer/message_composer_input_leading.dart'; +export 'message_composer/message_composer_input_trailing.dart'; +export 'message_composer/message_composer_leading.dart'; +export 'message_composer/message_composer_trailing.dart'; diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart new file mode 100644 index 0000000..b99ebc2 --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart @@ -0,0 +1,55 @@ +import 'package:flutter/widgets.dart'; + +import '../../../stream_core_flutter.dart'; +import '../../factory/stream_component_factory.dart'; + +class StreamMessageComposer extends StatelessWidget { + const StreamMessageComposer({super.key}); + + @override + Widget build(BuildContext context) { + return StreamTheme.of( + context, + ).componentFactory.messageComposer.messageComposer(context, const MessageComposerProps()); + } +} + +/// Properties to build the main message composer component +class MessageComposerProps { + const MessageComposerProps(); +} + +/// Properties to build any of the sub-components. +/// These properties are all the same, so features such as 'add attachment', +/// can be added to any of the sub-components. +class MessageComposerComponentProps { + const MessageComposerComponentProps(); +} + +class DefaultMessageComposer extends StatelessWidget { + const DefaultMessageComposer({super.key, required this.props}); + + static StreamComponentBuilder get factory => + (context, props) => DefaultMessageComposer(props: props); + + final MessageComposerProps props; + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + + final componentProps = MessageComposerComponentProps(); + + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SizedBox(width: spacing.md), + StreamMessageComposerLeading(props: componentProps), + SizedBox(width: spacing.xs), + Expanded(child: StreamMessageComposerInput(props: componentProps)), + StreamMessageComposerTrailing(props: componentProps), + SizedBox(width: spacing.md), + ], + ); + } +} 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 new file mode 100644 index 0000000..c42bc8f --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import '../../../stream_core_flutter.dart'; +import '../../factory/stream_component_factory.dart'; + +class StreamMessageComposerInput extends StatelessWidget { + const StreamMessageComposerInput({super.key, required this.props}); + + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + return StreamTheme.of( + context, + ).componentFactory.messageComposer.input(context, props); + } +} + +class DefaultStreamMessageComposerInput extends StatelessWidget { + const DefaultStreamMessageComposerInput({super.key, required this.props}); + + static StreamComponentBuilder get factory => + (context, props) => DefaultStreamMessageComposerInput(props: props); + + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + // TODO: Add message composer theme + + return Container( + decoration: BoxDecoration( + color: context.streamColorScheme.backgroundElevation1, + borderRadius: BorderRadius.all(context.streamRadius.xxxl), + border: Border.all( + color: context.streamColorScheme.borderDefault, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + StreamMessageComposerInputHeader(props: props), + Row( + children: [ + StreamMessageComposerInputLeading(props: props), + Expanded(child: _MessageComposerInputField(props: props)), + StreamMessageComposerInputTrailing(props: props), + ], + ), + ], + ), + ); + } +} + +class _MessageComposerInputField extends StatelessWidget { + const _MessageComposerInputField({required this.props}); + + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + final defaultBorderRadius = context.streamRadius.lg; + final composerBorderRadius = context.streamRadius.xxxl; + + final border = OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.only( + bottomLeft: composerBorderRadius, + bottomRight: defaultBorderRadius, + topLeft: defaultBorderRadius, + topRight: defaultBorderRadius, + ), + ); + + return TextField( + decoration: InputDecoration( + border: border, + focusedBorder: border, + enabledBorder: border, + errorBorder: border, + disabledBorder: border, + fillColor: Colors.transparent, + hintText: 'Placeholder', + ), + ); + } +} diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_header.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_header.dart new file mode 100644 index 0000000..a024432 --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_header.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import '../../../stream_core_flutter.dart'; +import '../../factory/stream_component_factory.dart'; + +class StreamMessageComposerInputHeader extends StatelessWidget { + const StreamMessageComposerInputHeader({super.key, required this.props}); + + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + return StreamTheme.of( + context, + ).componentFactory.messageComposer.inputHeader(context, props); + } +} + +class DefaultStreamMessageComposerInputHeader extends StatelessWidget { + const DefaultStreamMessageComposerInputHeader({super.key, required this.props}); + + static StreamComponentBuilder get factory => + (context, props) => DefaultStreamMessageComposerInputHeader(props: props); + + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only( + left: context.streamSpacing.xs, + right: context.streamSpacing.xs, + top: context.streamSpacing.xs, + ), + child: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + HeaderSlotPlaceholder(), + HeaderSlotPlaceholder(), + HeaderSlotPlaceholder(), + ], + ), + ); + } +} + +class HeaderSlotPlaceholder extends StatelessWidget { + const HeaderSlotPlaceholder({super.key}); + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + return Padding( + padding: EdgeInsets.all(spacing.xxs), + child: Container( + height: 40, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.pinkAccent, + border: Border.all(color: Colors.pink), + borderRadius: BorderRadius.all(context.streamRadius.md), + ), + ), + ); + } +} diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_leading.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_leading.dart new file mode 100644 index 0000000..f7a0e0e --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_leading.dart @@ -0,0 +1,32 @@ +import 'package:flutter/widgets.dart'; + +import '../../../stream_core_flutter.dart'; +import '../../factory/stream_component_factory.dart'; + +class StreamMessageComposerInputLeading extends StatelessWidget { + const StreamMessageComposerInputLeading({super.key, required this.props}); + + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + return StreamTheme.of( + context, + ).componentFactory.messageComposer.inputLeading(context, props); + } +} + +class DefaultStreamMessageComposerInputLeading extends StatelessWidget { + const DefaultStreamMessageComposerInputLeading({super.key, required this.props}); + + static StreamComponentBuilder get factory => + (context, props) => DefaultStreamMessageComposerInputLeading(props: props); + + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + // Doesn't show anything by default + return const SizedBox.shrink(); + } +} diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart new file mode 100644 index 0000000..b03d95f --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import '../../../stream_core_flutter.dart'; +import '../../factory/stream_component_factory.dart'; + +class StreamMessageComposerInputTrailing extends StatelessWidget { + const StreamMessageComposerInputTrailing({super.key, required this.props}); + + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + return StreamTheme.of( + context, + ).componentFactory.messageComposer.inputTrailing(context, props); + } +} + +class DefaultStreamMessageComposerInputTrailing extends StatelessWidget { + const DefaultStreamMessageComposerInputTrailing({super.key, required this.props}); + + static StreamComponentBuilder get factory => + (context, props) => DefaultStreamMessageComposerInputTrailing(props: props); + + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + // TODO: Implement the trailing component + return StreamButton.icon( + icon: Icons.send, + type: StreamButtonType.ghost, + style: StreamButtonStyle.secondary, + onTap: () {}, + ); + } +} diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_leading.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_leading.dart new file mode 100644 index 0000000..8c60635 --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_leading.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +import '../../../stream_core_flutter.dart'; +import '../../factory/stream_component_factory.dart'; + +class StreamMessageComposerLeading extends StatelessWidget { + const StreamMessageComposerLeading({super.key, required this.props}); + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + return StreamTheme.of( + context, + ).componentFactory.messageComposer.leading(context, props); + } +} + +class DefaultStreamMessageComposerLeading extends StatelessWidget { + const DefaultStreamMessageComposerLeading({super.key, required this.props}); + + static StreamComponentBuilder get factory => + (context, props) => DefaultStreamMessageComposerLeading(props: props); + + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + // TODO: Implement the leading component + return StreamButton.icon( + icon: Icons.add, + type: StreamButtonType.outline, + style: StreamButtonStyle.secondary, + size: StreamButtonSize.large, + onTap: () {}, + ); + } +} diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_trailing.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_trailing.dart new file mode 100644 index 0000000..f52e507 --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_trailing.dart @@ -0,0 +1,32 @@ +import 'package:flutter/widgets.dart'; + +import '../../../stream_core_flutter.dart'; +import '../../factory/stream_component_factory.dart'; + +class StreamMessageComposerTrailing extends StatelessWidget { + const StreamMessageComposerTrailing({super.key, required this.props}); + + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + return StreamTheme.of( + context, + ).componentFactory.messageComposer.trailing(context, props); + } +} + +class DefaultStreamMessageComposerTrailing extends StatelessWidget { + const DefaultStreamMessageComposerTrailing({super.key, required this.props}); + + static StreamComponentBuilder get factory => + (context, props) => DefaultStreamMessageComposerTrailing(props: props); + + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + // Doesn't show anything by default + return const SizedBox.shrink(); + } +} 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 17a08de..df7940b 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 @@ -2,13 +2,43 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import '../components/buttons/stream_button.dart' show DefaultStreamButton, StreamButtonProps; +import '../components/message_composer.dart'; typedef StreamComponentBuilder = Widget Function(BuildContext context, T props); class StreamComponentFactory { StreamComponentFactory({ StreamComponentBuilder? buttonFactory, - }) : buttonFactory = buttonFactory ?? DefaultStreamButton.factory; + StreamMessageComposerFactory? messageComposer, + }) : buttonFactory = buttonFactory ?? DefaultStreamButton.factory, + messageComposer = messageComposer ?? StreamMessageComposerFactory(); StreamComponentBuilder buttonFactory; + StreamMessageComposerFactory messageComposer; +} + +class StreamMessageComposerFactory { + StreamMessageComposerFactory({ + StreamComponentBuilder? messageComposer, + StreamComponentBuilder? leading, + StreamComponentBuilder? trailing, + StreamComponentBuilder? input, + StreamComponentBuilder? inputLeading, + StreamComponentBuilder? inputHeader, + StreamComponentBuilder? inputTrailing, + }) : messageComposer = messageComposer ?? DefaultMessageComposer.factory, + leading = leading ?? DefaultStreamMessageComposerLeading.factory, + trailing = trailing ?? DefaultStreamMessageComposerTrailing.factory, + input = input ?? DefaultStreamMessageComposerInput.factory, + inputLeading = inputLeading ?? DefaultStreamMessageComposerInputLeading.factory, + inputHeader = inputHeader ?? DefaultStreamMessageComposerInputHeader.factory, + inputTrailing = inputTrailing ?? DefaultStreamMessageComposerInputTrailing.factory; + + StreamComponentBuilder messageComposer; + StreamComponentBuilder leading; + StreamComponentBuilder trailing; + StreamComponentBuilder input; + StreamComponentBuilder inputLeading; + StreamComponentBuilder inputHeader; + StreamComponentBuilder inputTrailing; } diff --git a/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart b/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart index 1ff1ea0..988e948 100644 --- a/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart +++ b/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart @@ -67,6 +67,12 @@ class StreamColorScheme with _$StreamColorScheme { Color? backgroundSurfaceStrong, Color? backgroundOverlay, Color? backgroundDisabled, + // Background - Elevation + Color? backgroundElevation0, + Color? backgroundElevation1, + Color? backgroundElevation2, + Color? backgroundElevation3, + Color? backgroundElevation4, // Border - Core Color? borderDefault, Color? borderSurface, @@ -122,6 +128,12 @@ class StreamColorScheme with _$StreamColorScheme { backgroundOverlay ??= StreamColors.black10; backgroundDisabled ??= StreamColors.slate.shade100; + backgroundElevation0 ??= light_tokens.StreamTokens.backgroundElevationElevation0; + backgroundElevation1 ??= light_tokens.StreamTokens.backgroundElevationElevation1; + backgroundElevation2 ??= light_tokens.StreamTokens.backgroundElevationElevation2; + backgroundElevation3 ??= light_tokens.StreamTokens.backgroundElevationElevation3; + backgroundElevation4 ??= light_tokens.StreamTokens.backgroundElevationElevation4; + // Border - Core borderDefault ??= light_tokens.StreamTokens.borderCoreDefault; borderSurface ??= StreamColors.slate.shade400; @@ -195,6 +207,11 @@ class StreamColorScheme with _$StreamColorScheme { backgroundSurfaceStrong: backgroundSurfaceStrong, backgroundOverlay: backgroundOverlay, backgroundDisabled: backgroundDisabled, + backgroundElevation0: backgroundElevation0, + backgroundElevation1: backgroundElevation1, + backgroundElevation2: backgroundElevation2, + backgroundElevation3: backgroundElevation3, + backgroundElevation4: backgroundElevation4, borderDefault: borderDefault, borderSurface: borderSurface, borderSurfaceSubtle: borderSurfaceSubtle, @@ -245,6 +262,12 @@ class StreamColorScheme with _$StreamColorScheme { Color? backgroundSurfaceStrong, Color? backgroundOverlay, Color? backgroundDisabled, + // Background - Elevation + Color? backgroundElevation0, + Color? backgroundElevation1, + Color? backgroundElevation2, + Color? backgroundElevation3, + Color? backgroundElevation4, // Border - Core Color? borderDefault, Color? borderSurface, @@ -300,6 +323,12 @@ class StreamColorScheme with _$StreamColorScheme { backgroundOverlay ??= StreamColors.black50; backgroundDisabled ??= StreamColors.neutral.shade900; + backgroundElevation0 ??= dark_tokens.StreamTokens.backgroundElevationElevation0; + backgroundElevation1 ??= dark_tokens.StreamTokens.backgroundElevationElevation1; + backgroundElevation2 ??= dark_tokens.StreamTokens.backgroundElevationElevation2; + backgroundElevation3 ??= dark_tokens.StreamTokens.backgroundElevationElevation3; + backgroundElevation4 ??= dark_tokens.StreamTokens.backgroundElevationElevation4; + // Border - Core borderDefault ??= dark_tokens.StreamTokens.borderCoreDefault; borderSurface ??= StreamColors.neutral.shade500; @@ -373,6 +402,11 @@ class StreamColorScheme with _$StreamColorScheme { backgroundSurfaceStrong: backgroundSurfaceStrong, backgroundOverlay: backgroundOverlay, backgroundDisabled: backgroundDisabled, + backgroundElevation0: backgroundElevation0, + backgroundElevation1: backgroundElevation1, + backgroundElevation2: backgroundElevation2, + backgroundElevation3: backgroundElevation3, + backgroundElevation4: backgroundElevation4, borderDefault: borderDefault, borderSurface: borderSurface, borderSurfaceSubtle: borderSurfaceSubtle, @@ -421,6 +455,12 @@ class StreamColorScheme with _$StreamColorScheme { required this.backgroundSurfaceStrong, required this.backgroundOverlay, required this.backgroundDisabled, + // Background - Elevation + required this.backgroundElevation0, + required this.backgroundElevation1, + required this.backgroundElevation2, + required this.backgroundElevation3, + required this.backgroundElevation4, // Border - Core required this.borderDefault, required this.borderSurface, @@ -515,6 +555,23 @@ class StreamColorScheme with _$StreamColorScheme { /// Disabled background for inputs, buttons, or chips. final Color backgroundDisabled; + // ---- Background - Elevation ---- + + /// The elevation 0 background color. + final Color backgroundElevation0; + + /// The elevation 1 background color. + final Color backgroundElevation1; + + /// The elevation 2 background color. + final Color backgroundElevation2; + + /// The elevation 3 background color. + final Color backgroundElevation3; + + /// The elevation 4 background color. + final Color backgroundElevation4; + // ---- Border colors - Core ---- /// Standard surface border diff --git a/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.g.theme.dart index 926c312..f3deb2d 100644 --- a/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.g.theme.dart @@ -69,6 +69,31 @@ mixin _$StreamColorScheme { b.backgroundDisabled, t, )!, + backgroundElevation0: Color.lerp( + a.backgroundElevation0, + b.backgroundElevation0, + t, + )!, + backgroundElevation1: Color.lerp( + a.backgroundElevation1, + b.backgroundElevation1, + t, + )!, + backgroundElevation2: Color.lerp( + a.backgroundElevation2, + b.backgroundElevation2, + t, + )!, + backgroundElevation3: Color.lerp( + a.backgroundElevation3, + b.backgroundElevation3, + t, + )!, + backgroundElevation4: Color.lerp( + a.backgroundElevation4, + b.backgroundElevation4, + t, + )!, borderDefault: Color.lerp(a.borderDefault, b.borderDefault, t)!, borderSurface: Color.lerp(a.borderSurface, b.borderSurface, t)!, borderSurfaceSubtle: Color.lerp( @@ -122,6 +147,11 @@ mixin _$StreamColorScheme { Color? backgroundSurfaceStrong, Color? backgroundOverlay, Color? backgroundDisabled, + Color? backgroundElevation0, + Color? backgroundElevation1, + Color? backgroundElevation2, + Color? backgroundElevation3, + Color? backgroundElevation4, Color? borderDefault, Color? borderSurface, Color? borderSurfaceSubtle, @@ -169,6 +199,11 @@ mixin _$StreamColorScheme { backgroundSurfaceStrong ?? _this.backgroundSurfaceStrong, backgroundOverlay: backgroundOverlay ?? _this.backgroundOverlay, backgroundDisabled: backgroundDisabled ?? _this.backgroundDisabled, + backgroundElevation0: backgroundElevation0 ?? _this.backgroundElevation0, + backgroundElevation1: backgroundElevation1 ?? _this.backgroundElevation1, + backgroundElevation2: backgroundElevation2 ?? _this.backgroundElevation2, + backgroundElevation3: backgroundElevation3 ?? _this.backgroundElevation3, + backgroundElevation4: backgroundElevation4 ?? _this.backgroundElevation4, borderDefault: borderDefault ?? _this.borderDefault, borderSurface: borderSurface ?? _this.borderSurface, borderSurfaceSubtle: borderSurfaceSubtle ?? _this.borderSurfaceSubtle, @@ -225,6 +260,11 @@ mixin _$StreamColorScheme { backgroundSurfaceStrong: other.backgroundSurfaceStrong, backgroundOverlay: other.backgroundOverlay, backgroundDisabled: other.backgroundDisabled, + backgroundElevation0: other.backgroundElevation0, + backgroundElevation1: other.backgroundElevation1, + backgroundElevation2: other.backgroundElevation2, + backgroundElevation3: other.backgroundElevation3, + backgroundElevation4: other.backgroundElevation4, borderDefault: other.borderDefault, borderSurface: other.borderSurface, borderSurfaceSubtle: other.borderSurfaceSubtle, @@ -282,6 +322,11 @@ mixin _$StreamColorScheme { _other.backgroundSurfaceStrong == _this.backgroundSurfaceStrong && _other.backgroundOverlay == _this.backgroundOverlay && _other.backgroundDisabled == _this.backgroundDisabled && + _other.backgroundElevation0 == _this.backgroundElevation0 && + _other.backgroundElevation1 == _this.backgroundElevation1 && + _other.backgroundElevation2 == _this.backgroundElevation2 && + _other.backgroundElevation3 == _this.backgroundElevation3 && + _other.backgroundElevation4 == _this.backgroundElevation4 && _other.borderDefault == _this.borderDefault && _other.borderSurface == _this.borderSurface && _other.borderSurfaceSubtle == _this.borderSurfaceSubtle && @@ -331,6 +376,11 @@ mixin _$StreamColorScheme { _this.backgroundSurfaceStrong, _this.backgroundOverlay, _this.backgroundDisabled, + _this.backgroundElevation0, + _this.backgroundElevation1, + _this.backgroundElevation2, + _this.backgroundElevation3, + _this.backgroundElevation4, _this.borderDefault, _this.borderSurface, _this.borderSurfaceSubtle, From bd435d746250a7ec59bf18c1573647e71c3f7b60 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 30 Jan 2026 14:08:13 +0100 Subject: [PATCH 02/11] Add textinputcontroller --- .../lib/components/message_composer.dart | 18 +++---- .../src/components/buttons/stream_button.dart | 4 +- .../message_composer/message_composer.dart | 29 +++++++++-- .../message_composer_input.dart | 3 ++ .../message_composer_input_header.dart | 1 + .../message_composer_input_trailing.dart | 48 ++++++++++++++++++- .../message_composer_leading.dart | 2 +- 7 files changed, 90 insertions(+), 15 deletions(-) diff --git a/apps/design_system_gallery/lib/components/message_composer.dart b/apps/design_system_gallery/lib/components/message_composer.dart index 9904a89..3408228 100644 --- a/apps/design_system_gallery/lib/components/message_composer.dart +++ b/apps/design_system_gallery/lib/components/message_composer.dart @@ -31,6 +31,8 @@ Widget buildStreamMessageComposerStructure(BuildContext context) { final colorScheme = theme.colorScheme; final textTheme = theme.textTheme; + final componentProps = MessageComposerComponentProps(controller: TextEditingController()); + return SingleChildScrollView( padding: const EdgeInsets.all(24), child: Center( @@ -69,34 +71,34 @@ Widget buildStreamMessageComposerStructure(BuildContext context) { const SizedBox(height: 16), // Leading - const _ComponentCard( + _ComponentCard( label: 'StreamMessageComposerLeading', description: 'Action button(s) before the input', - child: StreamMessageComposerLeading(props: MessageComposerComponentProps()), + child: StreamMessageComposerLeading(props: componentProps), ), const SizedBox(height: 16), // Input - const _ComponentCard( + _ComponentCard( label: 'StreamMessageComposerInput', description: 'Input area with header, text field, and actions', - child: StreamMessageComposerInput(props: MessageComposerComponentProps()), + child: StreamMessageComposerInput(props: componentProps), ), const SizedBox(height: 16), // Input Header - const _ComponentCard( + _ComponentCard( label: 'StreamMessageComposerInputHeader', description: 'Header slots for replies, attachments, etc.', - child: StreamMessageComposerInputHeader(props: MessageComposerComponentProps()), + child: StreamMessageComposerInputHeader(props: componentProps), ), const SizedBox(height: 16), // Input Trailing - const _ComponentCard( + _ComponentCard( label: 'StreamMessageComposerInputTrailing', description: 'Send button or other trailing actions', - child: StreamMessageComposerInputTrailing(props: MessageComposerComponentProps()), + child: StreamMessageComposerInputTrailing(props: componentProps), ), ], ), diff --git a/packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart b/packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart index 3e2cd46..f09a341 100644 --- a/packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart +++ b/packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart @@ -126,13 +126,15 @@ class DefaultStreamButton extends StatelessWidget { }; const iconSize = 20.0; + final isIconButton = props.label == null; return ElevatedButton( onPressed: props.onTap, style: ButtonStyle( backgroundColor: backgroundColor, foregroundColor: foregroundColor, - minimumSize: WidgetStateProperty.all(Size(minimumSize, minimumSize)), + minimumSize: isIconButton ? null : WidgetStateProperty.all(Size(minimumSize, minimumSize)), + fixedSize: isIconButton ? WidgetStateProperty.all(Size(minimumSize, minimumSize)) : null, padding: WidgetStateProperty.all(EdgeInsets.symmetric(horizontal: spacing.md)), side: borderColor == null ? null diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart index b99ebc2..4c30cf8 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart @@ -23,10 +23,14 @@ class MessageComposerProps { /// These properties are all the same, so features such as 'add attachment', /// can be added to any of the sub-components. class MessageComposerComponentProps { - const MessageComposerComponentProps(); + const MessageComposerComponentProps({ + required this.controller, + }); + + final TextEditingController controller; } -class DefaultMessageComposer extends StatelessWidget { +class DefaultMessageComposer extends StatefulWidget { const DefaultMessageComposer({super.key, required this.props}); static StreamComponentBuilder get factory => @@ -34,11 +38,30 @@ class DefaultMessageComposer extends StatelessWidget { final MessageComposerProps props; + @override + State createState() => _DefaultMessageComposerState(); +} + +class _DefaultMessageComposerState extends State { + late TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final spacing = context.streamSpacing; - final componentProps = MessageComposerComponentProps(); + final componentProps = MessageComposerComponentProps(controller: _controller); return Row( crossAxisAlignment: CrossAxisAlignment.end, 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 c42bc8f..f5b161c 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 @@ -61,6 +61,8 @@ class _MessageComposerInputField extends StatelessWidget { @override Widget build(BuildContext context) { + // TODO: fully implement the input field + final defaultBorderRadius = context.streamRadius.lg; final composerBorderRadius = context.streamRadius.xxxl; @@ -75,6 +77,7 @@ class _MessageComposerInputField extends StatelessWidget { ); return TextField( + controller: props.controller, decoration: InputDecoration( border: border, focusedBorder: border, diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_header.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_header.dart index a024432..a10415e 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_header.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_header.dart @@ -27,6 +27,7 @@ class DefaultStreamMessageComposerInputHeader extends StatelessWidget { @override Widget build(BuildContext context) { + // TODO: Implement the header component return Padding( padding: EdgeInsets.only( left: context.streamSpacing.xs, diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart index b03d95f..0266ae0 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart @@ -17,7 +17,7 @@ class StreamMessageComposerInputTrailing extends StatelessWidget { } } -class DefaultStreamMessageComposerInputTrailing extends StatelessWidget { +class DefaultStreamMessageComposerInputTrailing extends StatefulWidget { const DefaultStreamMessageComposerInputTrailing({super.key, required this.props}); static StreamComponentBuilder get factory => @@ -25,14 +25,58 @@ class DefaultStreamMessageComposerInputTrailing extends StatelessWidget { final MessageComposerComponentProps props; + @override + State createState() => _DefaultStreamMessageComposerInputTrailingState(); +} + +class _DefaultStreamMessageComposerInputTrailingState extends State { + var _hasText = false; + + @override + void initState() { + super.initState(); + widget.props.controller.addListener(_onInputTextChanged); + _hasText = widget.props.controller.text.isNotEmpty; + } + + @override + void didUpdateWidget(DefaultStreamMessageComposerInputTrailing oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.props.controller != oldWidget.props.controller) { + oldWidget.props.controller.removeListener(_onInputTextChanged); + widget.props.controller.addListener(_onInputTextChanged); + } + } + + void _onInputTextChanged() { + final hasText = widget.props.controller.text.isNotEmpty; + if (_hasText != hasText) { + setState(() => _hasText = hasText); + } + } + @override Widget build(BuildContext context) { // TODO: Implement the trailing component + + if (_hasText) { + return StreamButton.icon( + key: _messageComposerInputTrailingSendKey, + icon: context.streamIcons.paperPlane, + size: StreamButtonSize.small, + onTap: () {}, + ); + } return StreamButton.icon( - icon: Icons.send, + key: _messageComposerInputTrailingMicrophoneKey, + icon: context.streamIcons.microphone, type: StreamButtonType.ghost, style: StreamButtonStyle.secondary, + size: StreamButtonSize.small, onTap: () {}, ); } } + +final _messageComposerInputTrailingSendKey = UniqueKey(); +final _messageComposerInputTrailingMicrophoneKey = UniqueKey(); diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_leading.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_leading.dart index 8c60635..45711eb 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_leading.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_leading.dart @@ -27,7 +27,7 @@ class DefaultStreamMessageComposerLeading extends StatelessWidget { Widget build(BuildContext context) { // TODO: Implement the leading component return StreamButton.icon( - icon: Icons.add, + icon: context.streamIcons.plusLarge, type: StreamButtonType.outline, style: StreamButtonStyle.secondary, size: StreamButtonSize.large, From e5d4e18aa60c4940592d58909624a4284261dfee Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 30 Jan 2026 15:15:33 +0100 Subject: [PATCH 03/11] Add option for floating --- .../lib/components/message_composer.dart | 12 +--- .../src/components/buttons/stream_button.dart | 13 +++- .../message_composer/message_composer.dart | 59 ++++++++++++++----- .../message_composer_input.dart | 5 +- .../message_composer_leading.dart | 24 ++++++-- 5 files changed, 78 insertions(+), 35 deletions(-) diff --git a/apps/design_system_gallery/lib/components/message_composer.dart b/apps/design_system_gallery/lib/components/message_composer.dart index 3408228..2240f40 100644 --- a/apps/design_system_gallery/lib/components/message_composer.dart +++ b/apps/design_system_gallery/lib/components/message_composer.dart @@ -190,13 +190,11 @@ Widget buildStreamMessageComposerExample(BuildContext context) { return Center( child: Container( - constraints: const BoxConstraints(maxWidth: 400), decoration: BoxDecoration( color: colorScheme.backgroundSurface, borderRadius: BorderRadius.circular(12), ), child: Column( - mainAxisSize: MainAxisSize.min, children: [ // Chat header Container( @@ -254,14 +252,8 @@ Widget buildStreamMessageComposerExample(BuildContext context) { ), ), // Composer - Container( - decoration: BoxDecoration( - border: Border( - top: BorderSide(color: colorScheme.borderSurfaceSubtle), - ), - ), - padding: const EdgeInsets.symmetric(vertical: 8), - child: const StreamMessageComposer(), + const StreamMessageComposer( + isFloating: true, ), ], ), diff --git a/packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart b/packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart index f09a341..8632259 100644 --- a/packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart +++ b/packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart @@ -31,6 +31,7 @@ class StreamButton extends StatelessWidget { StreamButtonType type = StreamButtonType.solid, StreamButtonSize size = StreamButtonSize.medium, IconData? icon, + bool isFloating = false, }) : props = StreamButtonProps( label: null, onTap: onTap, @@ -39,6 +40,7 @@ class StreamButton extends StatelessWidget { size: size, iconLeft: icon, iconRight: null, + isFloating: isFloating, ); final StreamButtonProps props; @@ -60,6 +62,7 @@ class StreamButtonProps { required this.size, required this.iconLeft, required this.iconRight, + this.isFloating = false, }); final String? label; @@ -69,6 +72,7 @@ class StreamButtonProps { final StreamButtonSize size; final IconData? iconLeft; final IconData? iconRight; + final bool isFloating; } enum StreamButtonStyle { primary, secondary, destructive } @@ -89,6 +93,7 @@ class DefaultStreamButton extends StatelessWidget { Widget build(BuildContext context) { final spacing = context.streamSpacing; final buttonTheme = context.streamButtonTheme; + final colorScheme = context.streamColorScheme; final defaults = _StreamButtonDefaults(context: context); final themeButtonTypeStyle = switch (props.style) { @@ -114,8 +119,12 @@ class DefaultStreamButton extends StatelessWidget { StreamButtonType.ghost => defaultButtonTypeStyle.ghost, }; + final fallbackBackgroundColor = props.isFloating ? colorScheme.backgroundElevation1 : Colors.transparent; + final backgroundColor = - themeStyle?.backgroundColor ?? defaultStyle?.backgroundColor ?? WidgetStateProperty.all(Colors.transparent); + themeStyle?.backgroundColor ?? + defaultStyle?.backgroundColor ?? + WidgetStateProperty.all(fallbackBackgroundColor); final foregroundColor = themeStyle?.foregroundColor ?? defaultStyle?.foregroundColor; final borderColor = themeStyle?.borderColor ?? defaultStyle?.borderColor; @@ -135,13 +144,13 @@ class DefaultStreamButton extends StatelessWidget { foregroundColor: foregroundColor, minimumSize: isIconButton ? null : WidgetStateProperty.all(Size(minimumSize, minimumSize)), fixedSize: isIconButton ? WidgetStateProperty.all(Size(minimumSize, minimumSize)) : null, + elevation: WidgetStateProperty.all(props.isFloating ? 4 : 0), padding: WidgetStateProperty.all(EdgeInsets.symmetric(horizontal: spacing.md)), side: borderColor == null ? null : WidgetStateProperty.resolveWith( (states) => BorderSide(color: borderColor.resolve(states)), ), - elevation: WidgetStateProperty.all(0), shape: props.label == null ? WidgetStateProperty.all(const CircleBorder()) : WidgetStateProperty.all( diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart index 4c30cf8..514f00d 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart @@ -4,19 +4,28 @@ import '../../../stream_core_flutter.dart'; import '../../factory/stream_component_factory.dart'; class StreamMessageComposer extends StatelessWidget { - const StreamMessageComposer({super.key}); + const StreamMessageComposer({ + super.key, + this.isFloating = false, + }); + + final bool isFloating; @override Widget build(BuildContext context) { return StreamTheme.of( context, - ).componentFactory.messageComposer.messageComposer(context, const MessageComposerProps()); + ).componentFactory.messageComposer.messageComposer(context, MessageComposerProps(isFloating: isFloating)); } } /// Properties to build the main message composer component class MessageComposerProps { - const MessageComposerProps(); + const MessageComposerProps({ + this.isFloating = false, + }); + + final bool isFloating; } /// Properties to build any of the sub-components. @@ -25,9 +34,13 @@ class MessageComposerProps { class MessageComposerComponentProps { const MessageComposerComponentProps({ required this.controller, + this.onAddAttachment, + this.isFloating = false, }); final TextEditingController controller; + final VoidCallback? onAddAttachment; + final bool isFloating; } class DefaultMessageComposer extends StatefulWidget { @@ -61,18 +74,34 @@ class _DefaultMessageComposerState extends State { Widget build(BuildContext context) { final spacing = context.streamSpacing; - final componentProps = MessageComposerComponentProps(controller: _controller); - - return Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - SizedBox(width: spacing.md), - StreamMessageComposerLeading(props: componentProps), - SizedBox(width: spacing.xs), - Expanded(child: StreamMessageComposerInput(props: componentProps)), - StreamMessageComposerTrailing(props: componentProps), - SizedBox(width: spacing.md), - ], + final componentProps = MessageComposerComponentProps( + controller: _controller, + onAddAttachment: () {}, + isFloating: widget.props.isFloating, + ); + + return SafeArea( + child: Container( + padding: EdgeInsets.only(top: spacing.md), + decoration: widget.props.isFloating + ? null + : BoxDecoration( + color: context.streamColorScheme.backgroundElevation1, + border: Border( + top: BorderSide(color: context.streamColorScheme.borderDefault), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SizedBox(width: spacing.md), + StreamMessageComposerLeading(props: componentProps), + Expanded(child: StreamMessageComposerInput(props: componentProps)), + StreamMessageComposerTrailing(props: componentProps), + SizedBox(width: spacing.md), + ], + ), + ), ); } } 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 f5b161c..8cd334d 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 @@ -29,13 +29,14 @@ class DefaultStreamMessageComposerInput extends StatelessWidget { Widget build(BuildContext context) { // TODO: Add message composer theme - return Container( + return DecoratedBox( decoration: BoxDecoration( color: context.streamColorScheme.backgroundElevation1, borderRadius: BorderRadius.all(context.streamRadius.xxxl), border: Border.all( color: context.streamColorScheme.borderDefault, ), + boxShadow: props.isFloating ? context.streamBoxShadow.elevation3 : null, ), child: Column( mainAxisSize: MainAxisSize.min, @@ -62,7 +63,7 @@ class _MessageComposerInputField extends StatelessWidget { @override Widget build(BuildContext context) { // TODO: fully implement the input field - + final defaultBorderRadius = context.streamRadius.lg; final composerBorderRadius = context.streamRadius.xxxl; diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_leading.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_leading.dart index 45711eb..0c54e45 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_leading.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_leading.dart @@ -26,12 +26,24 @@ class DefaultStreamMessageComposerLeading extends StatelessWidget { @override Widget build(BuildContext context) { // TODO: Implement the leading component - return StreamButton.icon( - icon: context.streamIcons.plusLarge, - type: StreamButtonType.outline, - style: StreamButtonStyle.secondary, - size: StreamButtonSize.large, - onTap: () {}, + final spacing = context.streamSpacing; + if (props.onAddAttachment == null) { + return const SizedBox.shrink(); + } + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + StreamButton.icon( + isFloating: props.isFloating, + icon: context.streamIcons.plusLarge, + type: StreamButtonType.outline, + style: StreamButtonStyle.secondary, + size: StreamButtonSize.large, + onTap: () {}, + ), + SizedBox(width: spacing.xs), + ], ); } } From 68fa3ca0a28c9ac699a5f71586297c7a188b7ea1 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 30 Jan 2026 16:35:37 +0100 Subject: [PATCH 04/11] improve bottom padding in message composer --- .../lib/app/gallery_app.directories.g.dart | 5 - .../lib/components/message_composer.dart | 167 +++++++++++------- .../message_composer/message_composer.dart | 45 ++--- 3 files changed, 129 insertions(+), 88 deletions(-) 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 59e53a4..6cd364a 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 @@ -37,7 +37,6 @@ import 'package:widgetbook/widgetbook.dart' as _widgetbook; final directories = <_widgetbook.WidgetbookNode>[ _widgetbook.WidgetbookCategory( name: 'App Foundation', - isInitiallyExpanded: false, children: [ _widgetbook.WidgetbookFolder( name: 'Primitives', @@ -146,7 +145,6 @@ final directories = <_widgetbook.WidgetbookNode>[ children: [ _widgetbook.WidgetbookFolder( name: 'Avatar', - isInitiallyExpanded: false, children: [ _widgetbook.WidgetbookComponent( name: 'StreamAvatar', @@ -182,7 +180,6 @@ final directories = <_widgetbook.WidgetbookNode>[ ), _widgetbook.WidgetbookFolder( name: 'Button', - isInitiallyExpanded: false, children: [ _widgetbook.WidgetbookComponent( name: 'StreamButton', @@ -218,7 +215,6 @@ final directories = <_widgetbook.WidgetbookNode>[ ), _widgetbook.WidgetbookFolder( name: 'Indicator', - isInitiallyExpanded: false, children: [ _widgetbook.WidgetbookComponent( name: 'StreamOnlineIndicator', @@ -241,7 +237,6 @@ final directories = <_widgetbook.WidgetbookNode>[ ), _widgetbook.WidgetbookFolder( name: 'Message Composer', - isInitiallyExpanded: false, children: [ _widgetbook.WidgetbookComponent( name: 'StreamMessageComposer', diff --git a/apps/design_system_gallery/lib/components/message_composer.dart b/apps/design_system_gallery/lib/components/message_composer.dart index 2240f40..fa71a19 100644 --- a/apps/design_system_gallery/lib/components/message_composer.dart +++ b/apps/design_system_gallery/lib/components/message_composer.dart @@ -1,5 +1,6 @@ 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; // ============================================================================= @@ -188,76 +189,118 @@ Widget buildStreamMessageComposerExample(BuildContext context) { final colorScheme = theme.colorScheme; final textTheme = theme.textTheme; - return Center( - child: Container( - decoration: BoxDecoration( - color: colorScheme.backgroundSurface, - borderRadius: BorderRadius.circular(12), - ), - child: Column( + final isFloating = context.knobs.boolean( + label: 'Floating', + initialValue: false, + description: 'When true, the composer has no background or border.', + ); + + // Sample messages for scrollable list + const messages = [ + (message: 'Hey! How are you doing today?', isMe: false), + (message: "I'm doing great, thanks for asking!", isMe: true), + (message: 'Did you see the new design updates?', isMe: false), + (message: 'Yes! They look amazing. Great work on the color scheme.', isMe: true), + (message: 'Thanks! We spent a lot of time on the details.', isMe: false), + (message: 'It really shows. The typography is much cleaner now.', isMe: true), + (message: 'Glad you like it! Any feedback?', isMe: false), + (message: 'Maybe we could add more spacing in some areas?', isMe: true), + (message: "Good point, I'll look into that.", isMe: false), + (message: 'Perfect! Let me know if you need any help.', isMe: true), + (message: 'Should be finished by tomorrow.', isMe: false), + (message: 'Great! Thanks for the update.', isMe: true), + (message: "No problem! You're welcome.", isMe: false), + (message: 'I need to go now. See you later!', isMe: false), + (message: 'Bye! Take care.', isMe: true), + (message: 'Thanks! You too!', isMe: false), + (message: 'See you soon!', isMe: true), + (message: 'Bye!', isMe: false), + (message: 'See you soon!', isMe: true), + ]; + + return Scaffold( + appBar: AppBar( + title: Row( children: [ - // Chat header - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: colorScheme.borderSurfaceSubtle), - ), - ), - child: Row( - children: [ - StreamAvatar( - size: StreamAvatarSize.sm, - placeholder: (context) => const Text('JD'), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'John Doe', - style: textTheme.bodyEmphasis.copyWith( - color: colorScheme.textPrimary, - ), - ), - Text( - 'Online', - style: textTheme.captionDefault.copyWith( - color: colorScheme.accentSuccess, - ), - ), - ], - ), - ], - ), + StreamAvatar( + size: StreamAvatarSize.sm, + placeholder: (context) => const Text('JD'), ), - // Messages area (mock) - Container( - padding: const EdgeInsets.all(16), - height: 200, - child: const Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _MessageBubble( - message: 'Hey! How are you?', - isMe: false, + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'John Doe', + style: textTheme.bodyEmphasis.copyWith( + color: colorScheme.textPrimary, ), - SizedBox(height: 8), - _MessageBubble( - message: "I'm doing great, thanks!", - isMe: true, + ), + Text( + 'Online', + style: textTheme.captionDefault.copyWith( + color: colorScheme.accentSuccess, ), - ], - ), - ), - // Composer - const StreamMessageComposer( - isFloating: true, + ), + ], ), ], ), ), + body: isFloating + ? Stack( + children: [ + // Scrollable messages area (with bottom padding for composer) + ListView.builder( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 250), + itemCount: messages.length, + itemBuilder: (context, index) { + final msg = messages[index]; + return Padding( + padding: EdgeInsets.only( + bottom: index < messages.length - 1 ? 8 : 0, + ), + child: _MessageBubble( + message: msg.message, + isMe: msg.isMe, + ), + ); + }, + ), + // Floating composer at bottom + const Positioned( + left: 0, + right: 0, + bottom: 0, + child: StreamMessageComposer(isFloating: true), + ), + ], + ) + : Column( + children: [ + // Scrollable messages area + Expanded( + child: ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: messages.length, + itemBuilder: (context, index) { + final msg = messages[index]; + return Padding( + padding: EdgeInsets.only( + bottom: index < messages.length - 1 ? 8 : 0, + ), + child: _MessageBubble( + message: msg.message, + isMe: msg.isMe, + ), + ); + }, + ), + ), + // Non-floating composer + const StreamMessageComposer(isFloating: false), + ], + ), ); } diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart index 514f00d..f3de3bf 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'package:flutter/widgets.dart'; import '../../../stream_core_flutter.dart'; @@ -79,28 +81,29 @@ class _DefaultMessageComposerState extends State { onAddAttachment: () {}, isFloating: widget.props.isFloating, ); - - return SafeArea( - child: Container( - padding: EdgeInsets.only(top: spacing.md), - decoration: widget.props.isFloating - ? null - : BoxDecoration( - color: context.streamColorScheme.backgroundElevation1, - border: Border( - top: BorderSide(color: context.streamColorScheme.borderDefault), - ), + final bottomPaddingSafeArea = MediaQuery.of(context).padding.bottom; + final minimumBottomPadding = spacing.md; + final bottomPadding = math.max(bottomPaddingSafeArea, minimumBottomPadding); + + return Container( + padding: EdgeInsets.only(top: spacing.md, bottom: bottomPadding), + decoration: widget.props.isFloating + ? null + : BoxDecoration( + color: context.streamColorScheme.backgroundElevation1, + border: Border( + top: BorderSide(color: context.streamColorScheme.borderDefault), ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - SizedBox(width: spacing.md), - StreamMessageComposerLeading(props: componentProps), - Expanded(child: StreamMessageComposerInput(props: componentProps)), - StreamMessageComposerTrailing(props: componentProps), - SizedBox(width: spacing.md), - ], - ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SizedBox(width: spacing.md), + StreamMessageComposerLeading(props: componentProps), + Expanded(child: StreamMessageComposerInput(props: componentProps)), + StreamMessageComposerTrailing(props: componentProps), + SizedBox(width: spacing.md), + ], ), ); } From e1e4b2cc37cd9475b85d531f373dc339ecc4d1b6 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 3 Feb 2026 10:37:45 +0100 Subject: [PATCH 05/11] Started with attachments --- .../assets/attachment_image.png | Bin 0 -> 14616 bytes .../lib/app/gallery_app.directories.g.dart | 32 +++++++--- .../message_composer.dart | 0 ...essage_composer_attachment_media_file.dart | 22 +++++++ .../lib/src/components.dart | 2 +- .../src/components/badges/media_badge.dart | 59 ++++++++++++++++++ .../stream_online_indicator.dart | 0 .../components/controls/remove_control.dart | 33 ++++++++++ .../lib/src/components/message_composer.dart | 1 + ...essage_composer_attachment_media_file.dart | 39 ++++++++++++ .../message_composer/message_composer.dart | 35 +++++++---- .../message_composer_input.dart | 8 +-- .../message_composer_input_header.dart | 41 +----------- .../message_composer_leading.dart | 29 ++------- .../src/factory/stream_component_factory.dart | 6 +- .../theme/semantics/stream_color_scheme.dart | 21 +++++++ .../stream_color_scheme.g.theme.dart | 16 +++++ 17 files changed, 250 insertions(+), 94 deletions(-) create mode 100644 apps/design_system_gallery/assets/attachment_image.png rename apps/design_system_gallery/lib/components/{ => message_composer}/message_composer.dart (100%) create mode 100644 apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_media_file.dart create mode 100644 packages/stream_core_flutter/lib/src/components/badges/media_badge.dart rename packages/stream_core_flutter/lib/src/components/{indicator => badges}/stream_online_indicator.dart (100%) create mode 100644 packages/stream_core_flutter/lib/src/components/controls/remove_control.dart create mode 100644 packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_attachment_media_file.dart diff --git a/apps/design_system_gallery/assets/attachment_image.png b/apps/design_system_gallery/assets/attachment_image.png new file mode 100644 index 0000000000000000000000000000000000000000..7c129f1d2b21b0e2f4ce58911b0776a6225857a3 GIT binary patch literal 14616 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 literal 0 HcmV?d00001 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 6cd364a..5128b3a 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 @@ -12,8 +12,10 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:design_system_gallery/components/button.dart' as _design_system_gallery_components_button; -import 'package:design_system_gallery/components/message_composer.dart' - as _design_system_gallery_components_message_composer; +import 'package:design_system_gallery/components/message_composer/message_composer.dart' + as _design_system_gallery_components_message_composer_message_composer; +import 'package:design_system_gallery/components/message_composer/message_composer_attachment_media_file.dart' + as _design_system_gallery_components_message_composer_message_composer_attachment_media_file; import 'package:design_system_gallery/components/stream_avatar.dart' as _design_system_gallery_components_stream_avatar; import 'package:design_system_gallery/components/stream_avatar_stack.dart' @@ -238,23 +240,37 @@ final directories = <_widgetbook.WidgetbookNode>[ _widgetbook.WidgetbookFolder( name: 'Message Composer', children: [ + _widgetbook.WidgetbookComponent( + name: 'MessageComposerAttachmentMediaFile', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'Playground', + builder: + _design_system_gallery_components_message_composer_message_composer_attachment_media_file + .buildMessageComposerAttachmentMediaFilePlayground, + ), + ], + ), _widgetbook.WidgetbookComponent( name: 'StreamMessageComposer', useCases: [ _widgetbook.WidgetbookUseCase( name: 'Component Structure', - builder: _design_system_gallery_components_message_composer - .buildStreamMessageComposerStructure, + builder: + _design_system_gallery_components_message_composer_message_composer + .buildStreamMessageComposerStructure, ), _widgetbook.WidgetbookUseCase( name: 'Playground', - builder: _design_system_gallery_components_message_composer - .buildStreamMessageComposerPlayground, + builder: + _design_system_gallery_components_message_composer_message_composer + .buildStreamMessageComposerPlayground, ), _widgetbook.WidgetbookUseCase( name: 'Real-world Example', - builder: _design_system_gallery_components_message_composer - .buildStreamMessageComposerExample, + builder: + _design_system_gallery_components_message_composer_message_composer + .buildStreamMessageComposerExample, ), ], ), diff --git a/apps/design_system_gallery/lib/components/message_composer.dart b/apps/design_system_gallery/lib/components/message_composer/message_composer.dart similarity index 100% rename from apps/design_system_gallery/lib/components/message_composer.dart rename to apps/design_system_gallery/lib/components/message_composer/message_composer.dart diff --git a/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_media_file.dart b/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_media_file.dart new file mode 100644 index 0000000..42a7873 --- /dev/null +++ b/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_media_file.dart @@ -0,0 +1,22 @@ +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: MessageComposerAttachmentMediaFile, + path: '[Components]/Message Composer', +) +Widget buildMessageComposerAttachmentMediaFilePlayground(BuildContext context) { + return Center( + child: MessageComposerAttachmentMediaFile( + image: const AssetImage('assets/attachment_image.png'), + onRemovePressed: () {}, + ), + ); +} diff --git a/packages/stream_core_flutter/lib/src/components.dart b/packages/stream_core_flutter/lib/src/components.dart index 3bceb12..fc737d4 100644 --- a/packages/stream_core_flutter/lib/src/components.dart +++ b/packages/stream_core_flutter/lib/src/components.dart @@ -1,5 +1,5 @@ export 'components/avatar/stream_avatar.dart'; export 'components/avatar/stream_avatar_stack.dart'; export 'components/buttons/stream_button.dart' hide DefaultStreamButton; -export 'components/indicator/stream_online_indicator.dart'; +export 'components/badges/stream_online_indicator.dart'; export 'components/message_composer.dart'; diff --git a/packages/stream_core_flutter/lib/src/components/badges/media_badge.dart b/packages/stream_core_flutter/lib/src/components/badges/media_badge.dart new file mode 100644 index 0000000..3fd52cc --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/badges/media_badge.dart @@ -0,0 +1,59 @@ +import 'package:flutter/widgets.dart'; + +import '../../../stream_core_flutter.dart'; + +class MediaBadge extends StatelessWidget { + const MediaBadge({ + super.key, + required this.type, + required this.duration, + }); + + final MediaBadgeType type; + final Duration duration; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: context.streamColorScheme.backgroundInverse, + shape: BoxShape.circle, + ), + padding: EdgeInsets.symmetric( + horizontal: context.streamSpacing.xs, + vertical: context.streamSpacing.xxs, + ), + child: Column( + children: [ + Icon( + switch (type) { + MediaBadgeType.video => context.streamIcons.videoSolid, + MediaBadgeType.audio => context.streamIcons.microphoneSolid, + }, + size: 12, + color: context.streamColorScheme.textPrimary, + ), + + Text(duration.toReadableString()), + ], + ), + ); + } +} + +extension on Duration { + String toReadableString() { + if (inSeconds < 60) { + return '${inSeconds}s'; + } + if (inSeconds < 3600) { + return '${inMinutes}m'; + } + return '${inHours}h'; + } +} + +enum MediaBadgeType { + video, + audio, +} diff --git a/packages/stream_core_flutter/lib/src/components/indicator/stream_online_indicator.dart b/packages/stream_core_flutter/lib/src/components/badges/stream_online_indicator.dart similarity index 100% rename from packages/stream_core_flutter/lib/src/components/indicator/stream_online_indicator.dart rename to packages/stream_core_flutter/lib/src/components/badges/stream_online_indicator.dart diff --git a/packages/stream_core_flutter/lib/src/components/controls/remove_control.dart b/packages/stream_core_flutter/lib/src/components/controls/remove_control.dart new file mode 100644 index 0000000..4f55338 --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/controls/remove_control.dart @@ -0,0 +1,33 @@ +import 'package:flutter/widgets.dart'; + +import '../../../stream_core_flutter.dart'; + +class RemoveControl extends StatelessWidget { + const RemoveControl({ + super.key, + required this.onPressed, + }); + + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + + return Container( + decoration: BoxDecoration( + color: colorScheme.accentBlack, + shape: BoxShape.circle, + border: Border.all(color: colorScheme.borderOnDark, width: 2), + ), + alignment: Alignment.center, + height: 20, + width: 20, + child: Icon( + context.streamIcons.crossSmall, + color: colorScheme.textInverse, + size: 10, + ), + ); + } +} diff --git a/packages/stream_core_flutter/lib/src/components/message_composer.dart b/packages/stream_core_flutter/lib/src/components/message_composer.dart index fe2649f..52ccdd4 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer.dart @@ -1,3 +1,4 @@ +export 'message_composer/attachment/message_composer_attachment_media_file.dart'; export 'message_composer/message_composer.dart'; export 'message_composer/message_composer_input.dart'; export 'message_composer/message_composer_input_header.dart'; diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_attachment_media_file.dart b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_attachment_media_file.dart new file mode 100644 index 0000000..9c4a577 --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/message_composer/attachment/message_composer_attachment_media_file.dart @@ -0,0 +1,39 @@ +import 'package:flutter/widgets.dart'; + +import '../../../../stream_core_flutter.dart'; +import '../../controls/remove_control.dart'; + +class MessageComposerAttachmentMediaFile extends StatelessWidget { + const MessageComposerAttachmentMediaFile({super.key, required this.image, required this.onRemovePressed}); + + final ImageProvider image; + final VoidCallback onRemovePressed; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 80, + width: 80, + child: Stack( + children: [ + Padding( + padding: EdgeInsets.all(context.streamSpacing.xxs), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(context.streamRadius.lg), + border: Border.all( + color: context.streamColorScheme.borderDefault.withAlpha(25), + ), + image: DecorationImage(image: image, fit: BoxFit.cover), + ), + ), + ), + Align( + alignment: Alignment.topRight, + child: RemoveControl(onPressed: onRemovePressed), + ), + ], + ), + ); + } +} diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart index f3de3bf..7bdb0e3 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart @@ -5,59 +5,66 @@ import 'package:flutter/widgets.dart'; import '../../../stream_core_flutter.dart'; import '../../factory/stream_component_factory.dart'; -class StreamMessageComposer extends StatelessWidget { +class StreamMessageComposer extends StatelessWidget { const StreamMessageComposer({ super.key, this.isFloating = false, + this.messageData, }); final bool isFloating; + final T? messageData; @override Widget build(BuildContext context) { return StreamTheme.of( context, - ).componentFactory.messageComposer.messageComposer(context, MessageComposerProps(isFloating: isFloating)); + ).componentFactory.messageComposer.messageComposer( + context, + MessageComposerProps(isFloating: isFloating, messageData: messageData), + ); } } /// Properties to build the main message composer component -class MessageComposerProps { +class MessageComposerProps { const MessageComposerProps({ this.isFloating = false, + this.messageData, }); final bool isFloating; + final T? messageData; } /// Properties to build any of the sub-components. /// These properties are all the same, so features such as 'add attachment', /// can be added to any of the sub-components. -class MessageComposerComponentProps { +class MessageComposerComponentProps { const MessageComposerComponentProps({ required this.controller, - this.onAddAttachment, this.isFloating = false, + this.messageData, }); final TextEditingController controller; - final VoidCallback? onAddAttachment; final bool isFloating; + final T? messageData; } -class DefaultMessageComposer extends StatefulWidget { +class DefaultMessageComposer extends StatefulWidget { const DefaultMessageComposer({super.key, required this.props}); - static StreamComponentBuilder get factory => - (context, props) => DefaultMessageComposer(props: props); + static StreamComponentBuilder> get factory => + (context, props) => DefaultMessageComposer(props: props); - final MessageComposerProps props; + final MessageComposerProps props; @override - State createState() => _DefaultMessageComposerState(); + State> createState() => _DefaultMessageComposerState(); } -class _DefaultMessageComposerState extends State { +class _DefaultMessageComposerState extends State> { late TextEditingController _controller; @override @@ -78,8 +85,8 @@ class _DefaultMessageComposerState extends State { final componentProps = MessageComposerComponentProps( controller: _controller, - onAddAttachment: () {}, isFloating: widget.props.isFloating, + messageData: widget.props.messageData, ); final bottomPaddingSafeArea = MediaQuery.of(context).padding.bottom; final minimumBottomPadding = spacing.md; @@ -108,3 +115,5 @@ class _DefaultMessageComposerState extends State { ); } } + +class MessageData {} 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 8cd334d..9dc6eb0 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 @@ -64,17 +64,11 @@ class _MessageComposerInputField extends StatelessWidget { Widget build(BuildContext context) { // TODO: fully implement the input field - final defaultBorderRadius = context.streamRadius.lg; final composerBorderRadius = context.streamRadius.xxxl; final border = OutlineInputBorder( borderSide: BorderSide.none, - borderRadius: BorderRadius.only( - bottomLeft: composerBorderRadius, - bottomRight: defaultBorderRadius, - topLeft: defaultBorderRadius, - topRight: defaultBorderRadius, - ), + borderRadius: BorderRadius.all(composerBorderRadius), ); return TextField( diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_header.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_header.dart index a10415e..ea4e776 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_header.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_header.dart @@ -20,49 +20,14 @@ class StreamMessageComposerInputHeader extends StatelessWidget { class DefaultStreamMessageComposerInputHeader extends StatelessWidget { const DefaultStreamMessageComposerInputHeader({super.key, required this.props}); - static StreamComponentBuilder get factory => + static StreamComponentBuilder> get factory => (context, props) => DefaultStreamMessageComposerInputHeader(props: props); final MessageComposerComponentProps props; @override Widget build(BuildContext context) { - // TODO: Implement the header component - return Padding( - padding: EdgeInsets.only( - left: context.streamSpacing.xs, - right: context.streamSpacing.xs, - top: context.streamSpacing.xs, - ), - child: const Column( - mainAxisSize: MainAxisSize.min, - children: [ - HeaderSlotPlaceholder(), - HeaderSlotPlaceholder(), - HeaderSlotPlaceholder(), - ], - ), - ); - } -} - -class HeaderSlotPlaceholder extends StatelessWidget { - const HeaderSlotPlaceholder({super.key}); - - @override - Widget build(BuildContext context) { - final spacing = context.streamSpacing; - return Padding( - padding: EdgeInsets.all(spacing.xxs), - child: Container( - height: 40, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.pinkAccent, - border: Border.all(color: Colors.pink), - borderRadius: BorderRadius.all(context.streamRadius.md), - ), - ), - ); + // Empty by default + return const SizedBox.shrink(); } } diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_leading.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_leading.dart index 0c54e45..cf721d9 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_leading.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_leading.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; import '../../../stream_core_flutter.dart'; import '../../factory/stream_component_factory.dart'; -class StreamMessageComposerLeading extends StatelessWidget { +class StreamMessageComposerLeading extends StatelessWidget { const StreamMessageComposerLeading({super.key, required this.props}); - final MessageComposerComponentProps props; + final MessageComposerComponentProps props; @override Widget build(BuildContext context) { @@ -15,35 +15,16 @@ class StreamMessageComposerLeading extends StatelessWidget { } } -class DefaultStreamMessageComposerLeading extends StatelessWidget { +class DefaultStreamMessageComposerLeading extends StatelessWidget { const DefaultStreamMessageComposerLeading({super.key, required this.props}); static StreamComponentBuilder get factory => (context, props) => DefaultStreamMessageComposerLeading(props: props); - final MessageComposerComponentProps props; + final MessageComposerComponentProps props; @override Widget build(BuildContext context) { - // TODO: Implement the leading component - final spacing = context.streamSpacing; - if (props.onAddAttachment == null) { - return const SizedBox.shrink(); - } - - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - StreamButton.icon( - isFloating: props.isFloating, - icon: context.streamIcons.plusLarge, - type: StreamButtonType.outline, - style: StreamButtonStyle.secondary, - size: StreamButtonSize.large, - onTap: () {}, - ), - SizedBox(width: spacing.xs), - ], - ); + return const SizedBox.shrink(); } } 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 df7940b..1caa927 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 @@ -9,15 +9,15 @@ typedef StreamComponentBuilder = Widget Function(BuildContext context, T prop class StreamComponentFactory { StreamComponentFactory({ StreamComponentBuilder? buttonFactory, - StreamMessageComposerFactory? messageComposer, + StreamMessageComposerFactory? messageComposer, }) : buttonFactory = buttonFactory ?? DefaultStreamButton.factory, messageComposer = messageComposer ?? StreamMessageComposerFactory(); StreamComponentBuilder buttonFactory; - StreamMessageComposerFactory messageComposer; + StreamMessageComposerFactory messageComposer; } -class StreamMessageComposerFactory { +class StreamMessageComposerFactory { StreamMessageComposerFactory({ StreamComponentBuilder? messageComposer, StreamComponentBuilder? leading, diff --git a/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart b/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart index 988e948..f4e5c55 100644 --- a/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart +++ b/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart @@ -52,6 +52,7 @@ class StreamColorScheme with _$StreamColorScheme { Color? accentWarning, Color? accentError, Color? accentNeutral, + Color? accentBlack, // Text Color? textPrimary, Color? textSecondary, @@ -67,6 +68,8 @@ class StreamColorScheme with _$StreamColorScheme { Color? backgroundSurfaceStrong, Color? backgroundOverlay, Color? backgroundDisabled, + Color? backgroundInverse, + // Background - Elevation Color? backgroundElevation0, Color? backgroundElevation1, @@ -110,6 +113,7 @@ class StreamColorScheme with _$StreamColorScheme { accentWarning ??= StreamColors.yellow.shade500; accentError ??= StreamColors.red.shade500; accentNeutral ??= StreamColors.slate.shade500; + accentBlack ??= light_tokens.StreamTokens.accentBlack; // Text textPrimary ??= StreamColors.slate.shade900; @@ -127,6 +131,7 @@ class StreamColorScheme with _$StreamColorScheme { backgroundSurfaceStrong ??= StreamColors.slate.shade200; backgroundOverlay ??= StreamColors.black10; backgroundDisabled ??= StreamColors.slate.shade100; + backgroundInverse ??= light_tokens.StreamTokens.badgeBgInverse; // TODO move to backgroundCoreInverse backgroundElevation0 ??= light_tokens.StreamTokens.backgroundElevationElevation0; backgroundElevation1 ??= light_tokens.StreamTokens.backgroundElevationElevation1; @@ -194,6 +199,7 @@ class StreamColorScheme with _$StreamColorScheme { accentWarning: accentWarning, accentError: accentError, accentNeutral: accentNeutral, + accentBlack: accentBlack, textPrimary: textPrimary, textSecondary: textSecondary, textTertiary: textTertiary, @@ -207,6 +213,7 @@ class StreamColorScheme with _$StreamColorScheme { backgroundSurfaceStrong: backgroundSurfaceStrong, backgroundOverlay: backgroundOverlay, backgroundDisabled: backgroundDisabled, + backgroundInverse: backgroundInverse, backgroundElevation0: backgroundElevation0, backgroundElevation1: backgroundElevation1, backgroundElevation2: backgroundElevation2, @@ -247,6 +254,7 @@ class StreamColorScheme with _$StreamColorScheme { Color? accentWarning, Color? accentError, Color? accentNeutral, + Color? accentBlack, // Text Color? textPrimary, Color? textSecondary, @@ -262,6 +270,7 @@ class StreamColorScheme with _$StreamColorScheme { Color? backgroundSurfaceStrong, Color? backgroundOverlay, Color? backgroundDisabled, + Color? backgroundInverse, // Background - Elevation Color? backgroundElevation0, Color? backgroundElevation1, @@ -305,6 +314,7 @@ class StreamColorScheme with _$StreamColorScheme { accentWarning ??= StreamColors.yellow.shade400; accentError ??= StreamColors.red.shade400; accentNeutral ??= StreamColors.neutral.shade500; + accentBlack ??= dark_tokens.StreamTokens.accentBlack; // Text textPrimary ??= StreamColors.neutral.shade50; @@ -322,6 +332,7 @@ class StreamColorScheme with _$StreamColorScheme { backgroundSurfaceStrong ??= StreamColors.neutral.shade700; backgroundOverlay ??= StreamColors.black50; backgroundDisabled ??= StreamColors.neutral.shade900; + backgroundInverse ??= dark_tokens.StreamTokens.badgeBgInverse; // TODO move to backgroundCoreInverse backgroundElevation0 ??= dark_tokens.StreamTokens.backgroundElevationElevation0; backgroundElevation1 ??= dark_tokens.StreamTokens.backgroundElevationElevation1; @@ -389,6 +400,7 @@ class StreamColorScheme with _$StreamColorScheme { accentWarning: accentWarning, accentError: accentError, accentNeutral: accentNeutral, + accentBlack: accentBlack, textPrimary: textPrimary, textSecondary: textSecondary, textTertiary: textTertiary, @@ -402,6 +414,7 @@ class StreamColorScheme with _$StreamColorScheme { backgroundSurfaceStrong: backgroundSurfaceStrong, backgroundOverlay: backgroundOverlay, backgroundDisabled: backgroundDisabled, + backgroundInverse: backgroundInverse, backgroundElevation0: backgroundElevation0, backgroundElevation1: backgroundElevation1, backgroundElevation2: backgroundElevation2, @@ -440,6 +453,7 @@ class StreamColorScheme with _$StreamColorScheme { required this.accentWarning, required this.accentError, required this.accentNeutral, + required this.accentBlack, // Text required this.textPrimary, required this.textSecondary, @@ -455,6 +469,7 @@ class StreamColorScheme with _$StreamColorScheme { required this.backgroundSurfaceStrong, required this.backgroundOverlay, required this.backgroundDisabled, + required this.backgroundInverse, // Background - Elevation required this.backgroundElevation0, required this.backgroundElevation1, @@ -512,6 +527,9 @@ class StreamColorScheme with _$StreamColorScheme { /// The neutral accent color. final Color accentNeutral; + /// The black accent color. + final Color accentBlack; + // ---- Text colors ---- /// The primary text color. @@ -555,6 +573,9 @@ class StreamColorScheme with _$StreamColorScheme { /// Disabled background for inputs, buttons, or chips. final Color backgroundDisabled; + /// The inverse background color. + final Color backgroundInverse; + // ---- Background - Elevation ---- /// The elevation 0 background color. diff --git a/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.g.theme.dart index f3deb2d..b123474 100644 --- a/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.g.theme.dart @@ -36,6 +36,7 @@ mixin _$StreamColorScheme { accentWarning: Color.lerp(a.accentWarning, b.accentWarning, t)!, accentError: Color.lerp(a.accentError, b.accentError, t)!, accentNeutral: Color.lerp(a.accentNeutral, b.accentNeutral, t)!, + accentBlack: Color.lerp(a.accentBlack, b.accentBlack, t)!, textPrimary: Color.lerp(a.textPrimary, b.textPrimary, t)!, textSecondary: Color.lerp(a.textSecondary, b.textSecondary, t)!, textTertiary: Color.lerp(a.textTertiary, b.textTertiary, t)!, @@ -69,6 +70,11 @@ mixin _$StreamColorScheme { b.backgroundDisabled, t, )!, + backgroundInverse: Color.lerp( + a.backgroundInverse, + b.backgroundInverse, + t, + )!, backgroundElevation0: Color.lerp( a.backgroundElevation0, b.backgroundElevation0, @@ -134,6 +140,7 @@ mixin _$StreamColorScheme { Color? accentWarning, Color? accentError, Color? accentNeutral, + Color? accentBlack, Color? textPrimary, Color? textSecondary, Color? textTertiary, @@ -147,6 +154,7 @@ mixin _$StreamColorScheme { Color? backgroundSurfaceStrong, Color? backgroundOverlay, Color? backgroundDisabled, + Color? backgroundInverse, Color? backgroundElevation0, Color? backgroundElevation1, Color? backgroundElevation2, @@ -184,6 +192,7 @@ mixin _$StreamColorScheme { accentWarning: accentWarning ?? _this.accentWarning, accentError: accentError ?? _this.accentError, accentNeutral: accentNeutral ?? _this.accentNeutral, + accentBlack: accentBlack ?? _this.accentBlack, textPrimary: textPrimary ?? _this.textPrimary, textSecondary: textSecondary ?? _this.textSecondary, textTertiary: textTertiary ?? _this.textTertiary, @@ -199,6 +208,7 @@ mixin _$StreamColorScheme { backgroundSurfaceStrong ?? _this.backgroundSurfaceStrong, backgroundOverlay: backgroundOverlay ?? _this.backgroundOverlay, backgroundDisabled: backgroundDisabled ?? _this.backgroundDisabled, + backgroundInverse: backgroundInverse ?? _this.backgroundInverse, backgroundElevation0: backgroundElevation0 ?? _this.backgroundElevation0, backgroundElevation1: backgroundElevation1 ?? _this.backgroundElevation1, backgroundElevation2: backgroundElevation2 ?? _this.backgroundElevation2, @@ -247,6 +257,7 @@ mixin _$StreamColorScheme { accentWarning: other.accentWarning, accentError: other.accentError, accentNeutral: other.accentNeutral, + accentBlack: other.accentBlack, textPrimary: other.textPrimary, textSecondary: other.textSecondary, textTertiary: other.textTertiary, @@ -260,6 +271,7 @@ mixin _$StreamColorScheme { backgroundSurfaceStrong: other.backgroundSurfaceStrong, backgroundOverlay: other.backgroundOverlay, backgroundDisabled: other.backgroundDisabled, + backgroundInverse: other.backgroundInverse, backgroundElevation0: other.backgroundElevation0, backgroundElevation1: other.backgroundElevation1, backgroundElevation2: other.backgroundElevation2, @@ -309,6 +321,7 @@ mixin _$StreamColorScheme { _other.accentWarning == _this.accentWarning && _other.accentError == _this.accentError && _other.accentNeutral == _this.accentNeutral && + _other.accentBlack == _this.accentBlack && _other.textPrimary == _this.textPrimary && _other.textSecondary == _this.textSecondary && _other.textTertiary == _this.textTertiary && @@ -322,6 +335,7 @@ mixin _$StreamColorScheme { _other.backgroundSurfaceStrong == _this.backgroundSurfaceStrong && _other.backgroundOverlay == _this.backgroundOverlay && _other.backgroundDisabled == _this.backgroundDisabled && + _other.backgroundInverse == _this.backgroundInverse && _other.backgroundElevation0 == _this.backgroundElevation0 && _other.backgroundElevation1 == _this.backgroundElevation1 && _other.backgroundElevation2 == _this.backgroundElevation2 && @@ -363,6 +377,7 @@ mixin _$StreamColorScheme { _this.accentWarning, _this.accentError, _this.accentNeutral, + _this.accentBlack, _this.textPrimary, _this.textSecondary, _this.textTertiary, @@ -376,6 +391,7 @@ mixin _$StreamColorScheme { _this.backgroundSurfaceStrong, _this.backgroundOverlay, _this.backgroundDisabled, + _this.backgroundInverse, _this.backgroundElevation0, _this.backgroundElevation1, _this.backgroundElevation2, From bdcf61b5541f31e955a2a76e7f6f739706e64335 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 4 Feb 2026 15:39:05 +0100 Subject: [PATCH 06/11] fix some factory properties --- .../message_composer_input.dart | 1 + .../src/factory/stream_component_factory.dart | 36 +++++++++---------- .../stream_core_flutter/lib/src/theme.dart | 2 ++ 3 files changed, 21 insertions(+), 18 deletions(-) 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 9dc6eb0..df23f23 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 @@ -80,6 +80,7 @@ class _MessageComposerInputField extends StatelessWidget { errorBorder: border, disabledBorder: border, fillColor: Colors.transparent, + contentPadding: const EdgeInsets.fromLTRB(12, 8, 12, 8), hintText: 'Placeholder', ), ); 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 e7c005d..0262546 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 @@ -7,29 +7,29 @@ import '../components/message_composer.dart'; typedef StreamComponentBuilder = Widget Function(BuildContext context, T props); -class StreamComponentFactory { +class StreamComponentFactory { StreamComponentFactory({ StreamComponentBuilder? buttonFactory, StreamComponentBuilder? fileTypeIconFactory, - StreamMessageComposerFactory? messageComposer, + StreamMessageComposerFactory? messageComposer, }) : buttonFactory = buttonFactory ?? DefaultStreamButton.factory, fileTypeIconFactory = fileTypeIconFactory ?? DefaultStreamFileTypeIcon.factory, messageComposer = messageComposer ?? StreamMessageComposerFactory(); StreamComponentBuilder buttonFactory; StreamComponentBuilder fileTypeIconFactory; - StreamMessageComposerFactory messageComposer; + StreamMessageComposerFactory messageComposer; } -class StreamMessageComposerFactory { +class StreamMessageComposerFactory { StreamMessageComposerFactory({ - StreamComponentBuilder? messageComposer, - StreamComponentBuilder? leading, - StreamComponentBuilder? trailing, - StreamComponentBuilder? input, - StreamComponentBuilder? inputLeading, - StreamComponentBuilder? inputHeader, - StreamComponentBuilder? inputTrailing, + StreamComponentBuilder>? messageComposer, + StreamComponentBuilder>? leading, + StreamComponentBuilder>? trailing, + StreamComponentBuilder>? input, + StreamComponentBuilder>? inputLeading, + StreamComponentBuilder>? inputHeader, + StreamComponentBuilder>? inputTrailing, }) : messageComposer = messageComposer ?? DefaultMessageComposer.factory, leading = leading ?? DefaultStreamMessageComposerLeading.factory, trailing = trailing ?? DefaultStreamMessageComposerTrailing.factory, @@ -38,11 +38,11 @@ class StreamMessageComposerFactory { inputHeader = inputHeader ?? DefaultStreamMessageComposerInputHeader.factory, inputTrailing = inputTrailing ?? DefaultStreamMessageComposerInputTrailing.factory; - StreamComponentBuilder messageComposer; - StreamComponentBuilder leading; - StreamComponentBuilder trailing; - StreamComponentBuilder input; - StreamComponentBuilder inputLeading; - StreamComponentBuilder inputHeader; - StreamComponentBuilder inputTrailing; + StreamComponentBuilder> messageComposer; + StreamComponentBuilder> leading; + StreamComponentBuilder> trailing; + StreamComponentBuilder> input; + StreamComponentBuilder> inputLeading; + StreamComponentBuilder> inputHeader; + StreamComponentBuilder> inputTrailing; } diff --git a/packages/stream_core_flutter/lib/src/theme.dart b/packages/stream_core_flutter/lib/src/theme.dart index f9832cb..18bf5f7 100644 --- a/packages/stream_core_flutter/lib/src/theme.dart +++ b/packages/stream_core_flutter/lib/src/theme.dart @@ -1,3 +1,5 @@ +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_online_indicator_theme.dart'; From 38333c1318ec3bb76ff4ca48dbe90792f69d7051 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Wed, 4 Feb 2026 16:18:16 +0100 Subject: [PATCH 07/11] simplified design system composer --- .../lib/src/components/message_composer.dart | 4 - .../message_composer/message_composer.dart | 114 ++++++++---------- .../message_composer_input.dart | 57 ++++----- .../message_composer_input_header.dart | 33 ----- .../message_composer_input_leading.dart | 32 ----- .../message_composer_input_trailing.dart | 50 +++----- .../message_composer_leading.dart | 30 ----- .../message_composer_trailing.dart | 32 ----- .../src/factory/stream_component_factory.dart | 31 +---- 9 files changed, 103 insertions(+), 280 deletions(-) delete mode 100644 packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_header.dart delete mode 100644 packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_leading.dart delete mode 100644 packages/stream_core_flutter/lib/src/components/message_composer/message_composer_leading.dart delete mode 100644 packages/stream_core_flutter/lib/src/components/message_composer/message_composer_trailing.dart diff --git a/packages/stream_core_flutter/lib/src/components/message_composer.dart b/packages/stream_core_flutter/lib/src/components/message_composer.dart index 52ccdd4..072a96d 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer.dart @@ -1,8 +1,4 @@ export 'message_composer/attachment/message_composer_attachment_media_file.dart'; export 'message_composer/message_composer.dart'; export 'message_composer/message_composer_input.dart'; -export 'message_composer/message_composer_input_header.dart'; -export 'message_composer/message_composer_input_leading.dart'; export 'message_composer/message_composer_input_trailing.dart'; -export 'message_composer/message_composer_leading.dart'; -export 'message_composer/message_composer_trailing.dart'; diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart index 7bdb0e3..bdc0d02 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart @@ -3,98 +3,79 @@ import 'dart:math' as math; import 'package:flutter/widgets.dart'; import '../../../stream_core_flutter.dart'; -import '../../factory/stream_component_factory.dart'; -class StreamMessageComposer extends StatelessWidget { - const StreamMessageComposer({ +class StreamBaseMessageComposer extends StatefulWidget { + const StreamBaseMessageComposer({ super.key, - this.isFloating = false, - this.messageData, - }); - - final bool isFloating; - final T? messageData; - - @override - Widget build(BuildContext context) { - return StreamTheme.of( - context, - ).componentFactory.messageComposer.messageComposer( - context, - MessageComposerProps(isFloating: isFloating, messageData: messageData), - ); - } -} - -/// Properties to build the main message composer component -class MessageComposerProps { - const MessageComposerProps({ - this.isFloating = false, - this.messageData, - }); - - final bool isFloating; - final T? messageData; -} - -/// Properties to build any of the sub-components. -/// These properties are all the same, so features such as 'add attachment', -/// can be added to any of the sub-components. -class MessageComposerComponentProps { - const MessageComposerComponentProps({ required this.controller, - this.isFloating = false, - this.messageData, + required this.isFloating, + this.placeholder = '', + this.composerLeading, + this.composerTrailing, + this.inputLeading, + this.inputTrailing, + this.inputHeader, }); - final TextEditingController controller; + final TextEditingController? controller; final bool isFloating; - final T? messageData; -} - -class DefaultMessageComposer extends StatefulWidget { - const DefaultMessageComposer({super.key, required this.props}); - - static StreamComponentBuilder> get factory => - (context, props) => DefaultMessageComposer(props: props); + final String placeholder; - final MessageComposerProps props; + final Widget? composerLeading; + final Widget? composerTrailing; + final Widget? inputLeading; + final Widget? inputTrailing; + final Widget? inputHeader; @override - State> createState() => _DefaultMessageComposerState(); + State createState() => _StreamBaseMessageComposerState(); } -class _DefaultMessageComposerState extends State> { +class _StreamBaseMessageComposerState extends State { late TextEditingController _controller; @override void initState() { super.initState(); - _controller = TextEditingController(); + _initController(); + } + + @override + void didUpdateWidget(StreamBaseMessageComposer oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.controller != oldWidget.controller) { + _disposeController(oldWidget); + _initController(); + } } @override void dispose() { - _controller.dispose(); + _disposeController(widget); super.dispose(); } + void _initController() { + _controller = widget.controller ?? TextEditingController(); + } + + void _disposeController(StreamBaseMessageComposer widget) { + if (widget.controller == null) { + _controller.dispose(); + } + } + @override Widget build(BuildContext context) { final spacing = context.streamSpacing; - final componentProps = MessageComposerComponentProps( - controller: _controller, - isFloating: widget.props.isFloating, - messageData: widget.props.messageData, - ); final bottomPaddingSafeArea = MediaQuery.of(context).padding.bottom; final minimumBottomPadding = spacing.md; final bottomPadding = math.max(bottomPaddingSafeArea, minimumBottomPadding); return Container( padding: EdgeInsets.only(top: spacing.md, bottom: bottomPadding), - decoration: widget.props.isFloating + decoration: widget.isFloating ? null : BoxDecoration( color: context.streamColorScheme.backgroundElevation1, @@ -106,9 +87,18 @@ class _DefaultMessageComposerState extends State get factory => - (context, props) => DefaultStreamMessageComposerInput(props: props); - - final MessageComposerComponentProps props; + final TextEditingController controller; + final String placeholder; + final bool isFloating; + final Widget? inputLeading; + final Widget? inputTrailing; + final Widget? inputHeader; @override Widget build(BuildContext context) { @@ -36,17 +33,22 @@ class DefaultStreamMessageComposerInput extends StatelessWidget { border: Border.all( color: context.streamColorScheme.borderDefault, ), - boxShadow: props.isFloating ? context.streamBoxShadow.elevation3 : null, + boxShadow: isFloating ? context.streamBoxShadow.elevation3 : null, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ - StreamMessageComposerInputHeader(props: props), + ?inputHeader, Row( children: [ - StreamMessageComposerInputLeading(props: props), - Expanded(child: _MessageComposerInputField(props: props)), - StreamMessageComposerInputTrailing(props: props), + ?inputLeading, + Expanded( + child: _MessageComposerInputField( + controller: controller, + placeholder: placeholder, + ), + ), + ?inputTrailing, ], ), ], @@ -56,9 +58,10 @@ class DefaultStreamMessageComposerInput extends StatelessWidget { } class _MessageComposerInputField extends StatelessWidget { - const _MessageComposerInputField({required this.props}); + _MessageComposerInputField({required this.controller, required this.placeholder}); - final MessageComposerComponentProps props; + TextEditingController controller; + String placeholder; @override Widget build(BuildContext context) { @@ -72,7 +75,7 @@ class _MessageComposerInputField extends StatelessWidget { ); return TextField( - controller: props.controller, + controller: controller, decoration: InputDecoration( border: border, focusedBorder: border, @@ -81,7 +84,7 @@ class _MessageComposerInputField extends StatelessWidget { disabledBorder: border, fillColor: Colors.transparent, contentPadding: const EdgeInsets.fromLTRB(12, 8, 12, 8), - hintText: 'Placeholder', + hintText: placeholder, ), ); } diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_header.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_header.dart deleted file mode 100644 index ea4e776..0000000 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_header.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -import '../../../stream_core_flutter.dart'; -import '../../factory/stream_component_factory.dart'; - -class StreamMessageComposerInputHeader extends StatelessWidget { - const StreamMessageComposerInputHeader({super.key, required this.props}); - - final MessageComposerComponentProps props; - - @override - Widget build(BuildContext context) { - return StreamTheme.of( - context, - ).componentFactory.messageComposer.inputHeader(context, props); - } -} - -class DefaultStreamMessageComposerInputHeader extends StatelessWidget { - const DefaultStreamMessageComposerInputHeader({super.key, required this.props}); - - static StreamComponentBuilder> get factory => - (context, props) => DefaultStreamMessageComposerInputHeader(props: props); - - final MessageComposerComponentProps props; - - @override - Widget build(BuildContext context) { - // Empty by default - return const SizedBox.shrink(); - } -} diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_leading.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_leading.dart deleted file mode 100644 index f7a0e0e..0000000 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_leading.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/widgets.dart'; - -import '../../../stream_core_flutter.dart'; -import '../../factory/stream_component_factory.dart'; - -class StreamMessageComposerInputLeading extends StatelessWidget { - const StreamMessageComposerInputLeading({super.key, required this.props}); - - final MessageComposerComponentProps props; - - @override - Widget build(BuildContext context) { - return StreamTheme.of( - context, - ).componentFactory.messageComposer.inputLeading(context, props); - } -} - -class DefaultStreamMessageComposerInputLeading extends StatelessWidget { - const DefaultStreamMessageComposerInputLeading({super.key, required this.props}); - - static StreamComponentBuilder get factory => - (context, props) => DefaultStreamMessageComposerInputLeading(props: props); - - final MessageComposerComponentProps props; - - @override - Widget build(BuildContext context) { - // Doesn't show anything by default - return const SizedBox.shrink(); - } -} diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart index 0266ae0..215bcb2 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart @@ -2,54 +2,44 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import '../../../stream_core_flutter.dart'; -import '../../factory/stream_component_factory.dart'; -class StreamMessageComposerInputTrailing extends StatelessWidget { - const StreamMessageComposerInputTrailing({super.key, required this.props}); +class StreamMessageComposerInputTrailing extends StatefulWidget { + const StreamMessageComposerInputTrailing({ + super.key, + required this.controller, + required this.onSendPressed, + required this.onMicrophonePressed, + }); - final MessageComposerComponentProps props; + final TextEditingController controller; + final VoidCallback onSendPressed; + final VoidCallback? onMicrophonePressed; @override - Widget build(BuildContext context) { - return StreamTheme.of( - context, - ).componentFactory.messageComposer.inputTrailing(context, props); - } -} - -class DefaultStreamMessageComposerInputTrailing extends StatefulWidget { - const DefaultStreamMessageComposerInputTrailing({super.key, required this.props}); - - static StreamComponentBuilder get factory => - (context, props) => DefaultStreamMessageComposerInputTrailing(props: props); - - final MessageComposerComponentProps props; - - @override - State createState() => _DefaultStreamMessageComposerInputTrailingState(); + State createState() => _StreamMessageComposerInputTrailingState(); } -class _DefaultStreamMessageComposerInputTrailingState extends State { +class _StreamMessageComposerInputTrailingState extends State { var _hasText = false; @override void initState() { super.initState(); - widget.props.controller.addListener(_onInputTextChanged); - _hasText = widget.props.controller.text.isNotEmpty; + widget.controller.addListener(_onInputTextChanged); + _hasText = widget.controller.text.isNotEmpty; } @override - void didUpdateWidget(DefaultStreamMessageComposerInputTrailing oldWidget) { + void didUpdateWidget(StreamMessageComposerInputTrailing oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.props.controller != oldWidget.props.controller) { - oldWidget.props.controller.removeListener(_onInputTextChanged); - widget.props.controller.addListener(_onInputTextChanged); + if (widget.controller != oldWidget.controller) { + oldWidget.controller.removeListener(_onInputTextChanged); + widget.controller.addListener(_onInputTextChanged); } } void _onInputTextChanged() { - final hasText = widget.props.controller.text.isNotEmpty; + final hasText = widget.controller.text.isNotEmpty; if (_hasText != hasText) { setState(() => _hasText = hasText); } @@ -59,7 +49,7 @@ class _DefaultStreamMessageComposerInputTrailingState extends State extends StatelessWidget { - const StreamMessageComposerLeading({super.key, required this.props}); - final MessageComposerComponentProps props; - - @override - Widget build(BuildContext context) { - return StreamTheme.of( - context, - ).componentFactory.messageComposer.leading(context, props); - } -} - -class DefaultStreamMessageComposerLeading extends StatelessWidget { - const DefaultStreamMessageComposerLeading({super.key, required this.props}); - - static StreamComponentBuilder get factory => - (context, props) => DefaultStreamMessageComposerLeading(props: props); - - final MessageComposerComponentProps props; - - @override - Widget build(BuildContext context) { - return const SizedBox.shrink(); - } -} diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_trailing.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_trailing.dart deleted file mode 100644 index f52e507..0000000 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_trailing.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/widgets.dart'; - -import '../../../stream_core_flutter.dart'; -import '../../factory/stream_component_factory.dart'; - -class StreamMessageComposerTrailing extends StatelessWidget { - const StreamMessageComposerTrailing({super.key, required this.props}); - - final MessageComposerComponentProps props; - - @override - Widget build(BuildContext context) { - return StreamTheme.of( - context, - ).componentFactory.messageComposer.trailing(context, props); - } -} - -class DefaultStreamMessageComposerTrailing extends StatelessWidget { - const DefaultStreamMessageComposerTrailing({super.key, required this.props}); - - static StreamComponentBuilder get factory => - (context, props) => DefaultStreamMessageComposerTrailing(props: props); - - final MessageComposerComponentProps props; - - @override - Widget build(BuildContext context) { - // Doesn't show anything by default - return const SizedBox.shrink(); - } -} 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 0262546..f63915c 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 @@ -11,38 +11,9 @@ class StreamComponentFactory { StreamComponentFactory({ StreamComponentBuilder? buttonFactory, StreamComponentBuilder? fileTypeIconFactory, - StreamMessageComposerFactory? messageComposer, }) : buttonFactory = buttonFactory ?? DefaultStreamButton.factory, - fileTypeIconFactory = fileTypeIconFactory ?? DefaultStreamFileTypeIcon.factory, - messageComposer = messageComposer ?? StreamMessageComposerFactory(); + fileTypeIconFactory = fileTypeIconFactory ?? DefaultStreamFileTypeIcon.factory; StreamComponentBuilder buttonFactory; StreamComponentBuilder fileTypeIconFactory; - StreamMessageComposerFactory messageComposer; -} - -class StreamMessageComposerFactory { - StreamMessageComposerFactory({ - StreamComponentBuilder>? messageComposer, - StreamComponentBuilder>? leading, - StreamComponentBuilder>? trailing, - StreamComponentBuilder>? input, - StreamComponentBuilder>? inputLeading, - StreamComponentBuilder>? inputHeader, - StreamComponentBuilder>? inputTrailing, - }) : messageComposer = messageComposer ?? DefaultMessageComposer.factory, - leading = leading ?? DefaultStreamMessageComposerLeading.factory, - trailing = trailing ?? DefaultStreamMessageComposerTrailing.factory, - input = input ?? DefaultStreamMessageComposerInput.factory, - inputLeading = inputLeading ?? DefaultStreamMessageComposerInputLeading.factory, - inputHeader = inputHeader ?? DefaultStreamMessageComposerInputHeader.factory, - inputTrailing = inputTrailing ?? DefaultStreamMessageComposerInputTrailing.factory; - - StreamComponentBuilder> messageComposer; - StreamComponentBuilder> leading; - StreamComponentBuilder> trailing; - StreamComponentBuilder> input; - StreamComponentBuilder> inputLeading; - StreamComponentBuilder> inputHeader; - StreamComponentBuilder> inputTrailing; } From 58c25a7e7c6c17912b3d2ef8e9d7ec86ebcfc489 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 6 Feb 2026 09:46:56 +0100 Subject: [PATCH 08/11] fix icon button --- .../src/components/buttons/stream_button.dart | 39 +++++++++++-------- .../message_composer_input_trailing.dart | 4 +- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart b/packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart index 8632259..9587453 100644 --- a/packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart +++ b/packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart @@ -142,10 +142,13 @@ class DefaultStreamButton extends StatelessWidget { style: ButtonStyle( backgroundColor: backgroundColor, foregroundColor: foregroundColor, - minimumSize: isIconButton ? null : WidgetStateProperty.all(Size(minimumSize, minimumSize)), - fixedSize: isIconButton ? WidgetStateProperty.all(Size(minimumSize, minimumSize)) : null, + minimumSize: WidgetStateProperty.all(Size(minimumSize, minimumSize)), + maximumSize: isIconButton ? WidgetStateProperty.all(Size(minimumSize, minimumSize)) : null, elevation: WidgetStateProperty.all(props.isFloating ? 4 : 0), - padding: WidgetStateProperty.all(EdgeInsets.symmetric(horizontal: spacing.md)), + padding: WidgetStateProperty.all( + isIconButton ? EdgeInsets.zero : EdgeInsets.symmetric(horizontal: spacing.md), + ), + side: borderColor == null ? null : WidgetStateProperty.resolveWith( @@ -154,27 +157,29 @@ class DefaultStreamButton extends StatelessWidget { shape: props.label == null ? WidgetStateProperty.all(const CircleBorder()) : WidgetStateProperty.all( - RoundedRectangleBorder(borderRadius: BorderRadius.all(context.streamRadius.max)), + RoundedRectangleBorder( + borderRadius: BorderRadius.all(context.streamRadius.max), + ), ), ), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - spacing: spacing.xs, - children: [ - if (props.iconLeft case final iconLeft?) Icon(iconLeft, size: iconSize), - if (props.label case final label?) Text(label), - if (props.iconRight case final iconRight?) Icon(iconRight, size: iconSize), - ], - ), + child: isIconButton + ? Icon(props.iconLeft, size: iconSize) + : Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + spacing: spacing.xs, + children: [ + if (props.iconLeft case final iconLeft?) Icon(iconLeft, size: iconSize), + if (props.label case final label?) Text(label), + if (props.iconRight case final iconRight?) Icon(iconRight, size: iconSize), + ], + ), ); } } class _StreamButtonDefaults { - _StreamButtonDefaults({ - required this.context, - }) : _colorScheme = context.streamColorScheme; + _StreamButtonDefaults({required this.context}) : _colorScheme = context.streamColorScheme; final BuildContext context; final StreamColorScheme _colorScheme; diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart index 215bcb2..c8caed3 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart @@ -54,7 +54,7 @@ class _StreamMessageComposerInputTrailingState extends State Date: Fri, 6 Feb 2026 10:57:27 +0100 Subject: [PATCH 09/11] Added focusNode for input field --- .../components/message_composer/message_composer.dart | 3 +++ .../message_composer/message_composer_input.dart | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart index bdc0d02..f60d578 100644 --- a/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart +++ b/packages/stream_core_flutter/lib/src/components/message_composer/message_composer.dart @@ -10,6 +10,7 @@ class StreamBaseMessageComposer extends StatefulWidget { required this.controller, required this.isFloating, this.placeholder = '', + this.focusNode, this.composerLeading, this.composerTrailing, this.inputLeading, @@ -20,6 +21,7 @@ class StreamBaseMessageComposer extends StatefulWidget { final TextEditingController? controller; final bool isFloating; final String placeholder; + final FocusNode? focusNode; final Widget? composerLeading; final Widget? composerTrailing; @@ -96,6 +98,7 @@ class _StreamBaseMessageComposerState extends State { inputLeading: widget.inputLeading, inputTrailing: widget.inputTrailing, inputHeader: widget.inputHeader, + focusNode: widget.focusNode, ), ), ?widget.composerTrailing, 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 776a047..57570cd 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 @@ -13,6 +13,7 @@ class StreamMessageComposerInput extends StatelessWidget { this.inputLeading, this.inputTrailing, this.inputHeader, + this.focusNode, }); final TextEditingController controller; @@ -21,6 +22,7 @@ class StreamMessageComposerInput extends StatelessWidget { final Widget? inputLeading; final Widget? inputTrailing; final Widget? inputHeader; + final FocusNode? focusNode; @override Widget build(BuildContext context) { @@ -46,6 +48,7 @@ class StreamMessageComposerInput extends StatelessWidget { child: _MessageComposerInputField( controller: controller, placeholder: placeholder, + focusNode: focusNode, ), ), ?inputTrailing, @@ -58,10 +61,15 @@ class StreamMessageComposerInput extends StatelessWidget { } class _MessageComposerInputField extends StatelessWidget { - _MessageComposerInputField({required this.controller, required this.placeholder}); + _MessageComposerInputField({ + required this.controller, + required this.placeholder, + this.focusNode, + }); TextEditingController controller; String placeholder; + FocusNode? focusNode; @override Widget build(BuildContext context) { @@ -76,6 +84,7 @@ class _MessageComposerInputField extends StatelessWidget { return TextField( controller: controller, + focusNode: focusNode, decoration: InputDecoration( border: border, focusedBorder: border, From f0af49b3cb1426efed625d0a24e58874363e9538 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 6 Feb 2026 14:06:32 +0100 Subject: [PATCH 10/11] update gallery --- .../lib/app/gallery_app.directories.g.dart | 8 +- .../message_composer/message_composer.dart | 198 +++--------------- ...essage_composer_attachment_media_file.dart | 1 - .../src/components/buttons/stream_button.dart | 1 + .../message_composer_input.dart | 10 +- 5 files changed, 40 insertions(+), 178 deletions(-) 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 56a08af..e826c76 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 @@ -315,14 +315,8 @@ final directories = <_widgetbook.WidgetbookNode>[ ], ), _widgetbook.WidgetbookComponent( - name: 'StreamMessageComposer', + name: 'StreamBaseMessageComposer', useCases: [ - _widgetbook.WidgetbookUseCase( - name: 'Component Structure', - builder: - _design_system_gallery_components_message_composer_message_composer - .buildStreamMessageComposerStructure, - ), _widgetbook.WidgetbookUseCase( name: 'Playground', builder: diff --git a/apps/design_system_gallery/lib/components/message_composer/message_composer.dart b/apps/design_system_gallery/lib/components/message_composer/message_composer.dart index fa71a19..e74dccc 100644 --- a/apps/design_system_gallery/lib/components/message_composer/message_composer.dart +++ b/apps/design_system_gallery/lib/components/message_composer/message_composer.dart @@ -9,179 +9,32 @@ import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; @widgetbook.UseCase( name: 'Playground', - type: StreamMessageComposer, + type: StreamBaseMessageComposer, path: '[Components]/Message Composer', ) Widget buildStreamMessageComposerPlayground(BuildContext context) { - return const Center( - child: StreamMessageComposer(), - ); -} - -// ============================================================================= -// Component Structure -// ============================================================================= - -@widgetbook.UseCase( - name: 'Component Structure', - type: StreamMessageComposer, - path: '[Components]/Message Composer', -) -Widget buildStreamMessageComposerStructure(BuildContext context) { - final theme = StreamTheme.of(context); - final colorScheme = theme.colorScheme; - final textTheme = theme.textTheme; - - final componentProps = MessageComposerComponentProps(controller: TextEditingController()); - - return SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 500), - decoration: BoxDecoration( - color: colorScheme.backgroundSurface, - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Message Composer Structure', - style: textTheme.headingSm.copyWith( - color: colorScheme.textPrimary, - ), - ), - const SizedBox(height: 8), - Text( - 'The composer is built from customizable sub-components:', - style: textTheme.bodyDefault.copyWith( - color: colorScheme.textSecondary, - ), - ), - const SizedBox(height: 24), - - // Full Composer - const _ComponentCard( - label: 'StreamMessageComposer', - description: 'Main composer widget', - child: StreamMessageComposer(), - ), - const SizedBox(height: 16), - - // Leading - _ComponentCard( - label: 'StreamMessageComposerLeading', - description: 'Action button(s) before the input', - child: StreamMessageComposerLeading(props: componentProps), - ), - const SizedBox(height: 16), - - // Input - _ComponentCard( - label: 'StreamMessageComposerInput', - description: 'Input area with header, text field, and actions', - child: StreamMessageComposerInput(props: componentProps), - ), - const SizedBox(height: 16), - - // Input Header - _ComponentCard( - label: 'StreamMessageComposerInputHeader', - description: 'Header slots for replies, attachments, etc.', - child: StreamMessageComposerInputHeader(props: componentProps), - ), - const SizedBox(height: 16), - - // Input Trailing - _ComponentCard( - label: 'StreamMessageComposerInputTrailing', - description: 'Send button or other trailing actions', - child: StreamMessageComposerInputTrailing(props: componentProps), - ), - ], - ), + final textEditingController = TextEditingController(); + + return Center( + child: StreamBaseMessageComposer( + controller: textEditingController, + isFloating: false, + inputTrailing: StreamMessageComposerInputTrailing( + controller: textEditingController, + onSendPressed: () {}, + onMicrophonePressed: () {}, ), ), ); } -class _ComponentCard extends StatelessWidget { - const _ComponentCard({ - required this.label, - required this.description, - required this.child, - }); - - final String label; - final String description; - final Widget child; - - @override - Widget build(BuildContext context) { - final theme = StreamTheme.of(context); - final colorScheme = theme.colorScheme; - final textTheme = theme.textTheme; - - return Container( - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: colorScheme.backgroundApp, - borderRadius: BorderRadius.circular(8), - ), - foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: colorScheme.borderSurfaceSubtle), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: textTheme.captionEmphasis.copyWith( - color: colorScheme.accentPrimary, - fontFamily: 'monospace', - ), - ), - const SizedBox(height: 4), - Text( - description, - style: textTheme.captionDefault.copyWith( - color: colorScheme.textTertiary, - ), - ), - ], - ), - ), - Divider( - height: 1, - color: colorScheme.borderSurfaceSubtle, - ), - Padding( - padding: const EdgeInsets.all(12), - child: child, - ), - ], - ), - ); - } -} - // ============================================================================= // Real-world Example // ============================================================================= @widgetbook.UseCase( name: 'Real-world Example', - type: StreamMessageComposer, + type: StreamBaseMessageComposer, path: '[Components]/Message Composer', ) Widget buildStreamMessageComposerExample(BuildContext context) { @@ -191,7 +44,6 @@ Widget buildStreamMessageComposerExample(BuildContext context) { final isFloating = context.knobs.boolean( label: 'Floating', - initialValue: false, description: 'When true, the composer has no background or border.', ); @@ -218,6 +70,8 @@ Widget buildStreamMessageComposerExample(BuildContext context) { (message: 'See you soon!', isMe: true), ]; + final textEditingController = TextEditingController(); + return Scaffold( appBar: AppBar( title: Row( @@ -268,11 +122,19 @@ Widget buildStreamMessageComposerExample(BuildContext context) { }, ), // Floating composer at bottom - const Positioned( + Positioned( left: 0, right: 0, bottom: 0, - child: StreamMessageComposer(isFloating: true), + child: StreamBaseMessageComposer( + controller: textEditingController, + isFloating: true, + inputTrailing: StreamMessageComposerInputTrailing( + controller: textEditingController, + onSendPressed: () {}, + onMicrophonePressed: () {}, + ), + ), ), ], ) @@ -298,7 +160,15 @@ Widget buildStreamMessageComposerExample(BuildContext context) { ), ), // Non-floating composer - const StreamMessageComposer(isFloating: false), + StreamBaseMessageComposer( + controller: textEditingController, + isFloating: false, + inputTrailing: StreamMessageComposerInputTrailing( + controller: textEditingController, + onSendPressed: () {}, + onMicrophonePressed: () {}, + ), + ), ], ), ); @@ -326,7 +196,7 @@ class _MessageBubble extends StatelessWidget { decoration: BoxDecoration( color: isMe ? colorScheme.accentPrimary : colorScheme.backgroundApp, borderRadius: BorderRadius.circular(16), - border: isMe ? null : Border.all(color: colorScheme.borderSurfaceSubtle), + border: isMe ? null : Border.all(color: colorScheme.borderSubtle), ), child: Text( message, diff --git a/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_media_file.dart b/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_media_file.dart index 42a7873..5698d92 100644 --- a/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_media_file.dart +++ b/apps/design_system_gallery/lib/components/message_composer/message_composer_attachment_media_file.dart @@ -1,6 +1,5 @@ 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; // ============================================================================= diff --git a/packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart b/packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart index 5881e34..f86393f 100644 --- a/packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart +++ b/packages/stream_core_flutter/lib/src/components/buttons/stream_button.dart @@ -142,6 +142,7 @@ class DefaultStreamButton extends StatelessWidget { foregroundColor: foregroundColor, minimumSize: WidgetStateProperty.all(Size(minimumSize, minimumSize)), maximumSize: isIconButton ? WidgetStateProperty.all(Size(minimumSize, minimumSize)) : null, + tapTargetSize: MaterialTapTargetSize.padded, elevation: WidgetStateProperty.all(props.isFloating ? 4 : 0), padding: WidgetStateProperty.all( isIconButton ? EdgeInsets.zero : EdgeInsets.symmetric(horizontal: spacing.md), 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 57570cd..dcb56ba 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 @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import '../../../stream_core_flutter.dart'; -import '../../factory/stream_component_factory.dart'; class StreamMessageComposerInput extends StatelessWidget { const StreamMessageComposerInput({ @@ -61,15 +59,15 @@ class StreamMessageComposerInput extends StatelessWidget { } class _MessageComposerInputField extends StatelessWidget { - _MessageComposerInputField({ + const _MessageComposerInputField({ required this.controller, required this.placeholder, this.focusNode, }); - TextEditingController controller; - String placeholder; - FocusNode? focusNode; + final TextEditingController controller; + final String placeholder; + final FocusNode? focusNode; @override Widget build(BuildContext context) { From 26d94f31df94618ef3bea063f82f607e02fab1b2 Mon Sep 17 00:00:00 2001 From: renefloor <15101411+renefloor@users.noreply.github.com> Date: Fri, 6 Feb 2026 13:12:05 +0000 Subject: [PATCH 11/11] chore: Update Goldens --- .../goldens/ci/stream_button_icon_only.png | Bin 8561 -> 8564 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/stream_core_flutter/test/components/buttons/goldens/ci/stream_button_icon_only.png b/packages/stream_core_flutter/test/components/buttons/goldens/ci/stream_button_icon_only.png index 4db5f2c48769e71b946c4caf5376dfdaafc20b2e..6b6879122c3b6d66e6bba9d54d58a80cfa0d07d2 100644 GIT binary patch literal 8564 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