Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/design_system_gallery/devtools_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import 'package:design_system_gallery/components/common/stream_progress_bar.dart
as _design_system_gallery_components_common_stream_progress_bar;
import 'package:design_system_gallery/components/context_menu/stream_context_menu.dart'
as _design_system_gallery_components_context_menu_stream_context_menu;
import 'package:design_system_gallery/components/controls/stream_command_chip.dart'
as _design_system_gallery_components_controls_stream_command_chip;
import 'package:design_system_gallery/components/controls/stream_emoji_chip.dart'
as _design_system_gallery_components_controls_stream_emoji_chip;
import 'package:design_system_gallery/components/controls/stream_emoji_chip_bar.dart'
Expand All @@ -55,7 +57,7 @@ import 'package:design_system_gallery/components/message_composer/message_compos
import 'package:design_system_gallery/components/message_composer/message_composer_attachment_reply.dart'
as _design_system_gallery_components_message_composer_message_composer_attachment_reply;
import 'package:design_system_gallery/components/reaction/stream_reactions.dart'
as _design_system_gallery_components_reaction_stream_reaction;
as _design_system_gallery_components_reaction_stream_reactions;
import 'package:design_system_gallery/components/tiles/stream_list_tile.dart'
as _design_system_gallery_components_tiles_stream_list_tile;
import 'package:design_system_gallery/primitives/colors.dart'
Expand Down Expand Up @@ -463,6 +465,23 @@ final directories = <_widgetbook.WidgetbookNode>[
_widgetbook.WidgetbookFolder(
name: 'Controls',
children: [
_widgetbook.WidgetbookComponent(
name: 'StreamCommandChip',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'Playground',
builder:
_design_system_gallery_components_controls_stream_command_chip
.buildStreamCommandChipPlayground,
),
_widgetbook.WidgetbookUseCase(
name: 'Showcase',
builder:
_design_system_gallery_components_controls_stream_command_chip
.buildStreamCommandChipShowcase,
),
],
),
_widgetbook.WidgetbookComponent(
name: 'StreamEmojiChip',
useCases: [
Expand Down Expand Up @@ -579,13 +598,13 @@ final directories = <_widgetbook.WidgetbookNode>[
_widgetbook.WidgetbookUseCase(
name: 'Playground',
builder:
_design_system_gallery_components_reaction_stream_reaction
_design_system_gallery_components_reaction_stream_reactions
.buildStreamReactionsPlayground,
),
_widgetbook.WidgetbookUseCase(
name: 'Showcase',
builder:
_design_system_gallery_components_reaction_stream_reaction
_design_system_gallery_components_reaction_stream_reactions
.buildStreamReactionsShowcase,
),
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import 'package:flutter/material.dart';
import 'package:stream_core_flutter/stream_core_flutter.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;

// =============================================================================
// Playground
// =============================================================================

@widgetbook.UseCase(
name: 'Playground',
type: StreamCommandChip,
path: '[Components]/Controls',
)
Widget buildStreamCommandChipPlayground(BuildContext context) {
final label = context.knobs.string(
label: 'Label',
initialValue: '/giphy',
description: 'The command label displayed inside the chip.',
);

final enableDismiss = context.knobs.boolean(
label: 'On Dismiss',
initialValue: true,
description: 'Whether the dismiss callback is active.',
);

return Center(
child: StreamCommandChip(
label: label,
onDismiss: enableDismiss
? () {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
const SnackBar(
content: Text('Command dismissed'),
duration: Duration(seconds: 1),
),
);
}
: null,
),
);
}

// =============================================================================
// Showcase
// =============================================================================

@widgetbook.UseCase(
name: 'Showcase',
type: StreamCommandChip,
path: '[Components]/Controls',
)
Widget buildStreamCommandChipShowcase(BuildContext context) {
final colorScheme = context.streamColorScheme;
final spacing = context.streamSpacing;
final radius = context.streamRadius;

return SingleChildScrollView(
padding: EdgeInsets.all(spacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: spacing.xl,
children: [
_SectionLabel(label: 'COMMAND CHIP — IMAGE OVERLAY'),
// Simulated attachment overlay
Stack(
alignment: Alignment.topLeft,
children: [
Container(
width: 200,
height: 120,
decoration: BoxDecoration(
color: colorScheme.backgroundSurfaceSubtle,
borderRadius: BorderRadius.all(radius.md),
border: Border.all(color: colorScheme.borderSubtle),
),
child: Center(
child: Icon(Icons.image, size: 48, color: colorScheme.textDisabled),
),
),
Padding(
padding: EdgeInsets.all(spacing.xs),
child: StreamCommandChip(
label: '/giphy',
onDismiss: () {},
),
),
],
),
_SectionLabel(label: 'LABEL VARIANTS'),
Wrap(
spacing: spacing.xs,
runSpacing: spacing.xs,
children: [
StreamCommandChip(label: '/giphy', onDismiss: () {}),
StreamCommandChip(label: '/img', onDismiss: () {}),
StreamCommandChip(label: '/ban', onDismiss: () {}),
StreamCommandChip(label: '/very-long-command-name', onDismiss: () {}),
],
),
_SectionLabel(label: 'WITHOUT DISMISS'),
StreamCommandChip(label: '/giphy'),
],
),
);
}

class _SectionLabel extends StatelessWidget {
const _SectionLabel({required this.label});

final String label;

@override
Widget build(BuildContext context) {
final colorScheme = context.streamColorScheme;
final textTheme = context.streamTextTheme;
final radius = context.streamRadius;
final spacing = context.streamSpacing;

return Container(
padding: EdgeInsets.symmetric(horizontal: spacing.sm, vertical: spacing.xs),
decoration: BoxDecoration(
color: colorScheme.accentPrimary,
borderRadius: BorderRadius.all(radius.xs),
),
child: Text(
label,
style: textTheme.metadataEmphasis.copyWith(
color: colorScheme.textOnAccent,
letterSpacing: 1,
fontSize: 9,
),
),
);
}
}
1 change: 1 addition & 0 deletions packages/stream_core_flutter/lib/src/components.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export 'components/common/stream_flex.dart';
export 'components/common/stream_progress_bar.dart' hide DefaultStreamProgressBar;
export 'components/context_menu/stream_context_menu.dart';
export 'components/context_menu/stream_context_menu_action.dart' hide DefaultStreamContextMenuAction;
export 'components/controls/stream_command_chip.dart' hide DefaultStreamCommandChip;
export 'components/controls/stream_emoji_chip.dart' hide DefaultStreamEmojiChip;
export 'components/controls/stream_emoji_chip_bar.dart' hide DefaultStreamEmojiChipBar;
export 'components/controls/stream_remove_control.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import 'package:flutter/material.dart';

import '../../factory/stream_component_factory.dart';
import '../../theme/stream_theme_extensions.dart';

/// A pill-shaped chip for displaying a slash command selection.
///
/// [StreamCommandChip] renders a thunder icon, a command label, and a dismiss
/// button. It is typically used as an overlay on a message attachment when a
/// slash command is active in the message composer.
///
/// {@tool snippet}
///
/// Display a command chip with a dismiss callback:
///
/// ```dart
/// StreamCommandChip(
/// label: '/giphy',
/// onDismiss: () => clearCommand(),
/// )
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [StreamCommandChipTheme], for customizing chip appearance.
class StreamCommandChip extends StatelessWidget {
/// Creates a command chip with a [label] and optional [onDismiss] callback.
StreamCommandChip({
super.key,
required String label,
VoidCallback? onDismiss,
}) : props = StreamCommandChipProps(label: label, onDismiss: onDismiss);

/// The props controlling the appearance and behavior of this chip.
final StreamCommandChipProps props;

@override
Widget build(BuildContext context) {
final builder = StreamComponentFactory.of(context).commandChip;
if (builder != null) return builder(context, props);
return DefaultStreamCommandChip(props: props);
}
}

/// Properties for configuring a [StreamCommandChip].
///
/// See also:
///
/// * [StreamCommandChip], which uses these properties.
/// * [DefaultStreamCommandChip], the default implementation.
class StreamCommandChipProps {
/// Creates properties for a command chip.
const StreamCommandChipProps({
required this.label,
this.onDismiss,
});

/// The command label to display inside the chip.
final String label;

/// Called when the dismiss (×) button is tapped.
///
/// When null the dismiss button is still shown but does nothing.
final VoidCallback? onDismiss;
}

/// Default implementation of [StreamCommandChip].
class DefaultStreamCommandChip extends StatelessWidget {
/// Creates a default command chip.
const DefaultStreamCommandChip({super.key, required this.props});

/// The props controlling the appearance and behavior of this chip.
final StreamCommandChipProps props;

@override
Widget build(BuildContext context) {
final defaults = _StreamCommandChipDefaults(context);
final chipTheme = context.streamCommandChipTheme;

final effectiveBackgroundColor = chipTheme.backgroundColor ?? defaults.backgroundColor;
final effectiveLabelColor = chipTheme.labelColor ?? defaults.labelColor;
final effectiveIconColor = chipTheme.iconColor ?? defaults.iconColor;

return Container(
padding: defaults.padding,
decoration: BoxDecoration(
color: effectiveBackgroundColor,
borderRadius: defaults.borderRadius,
),
constraints: const BoxConstraints(minHeight: 24),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
context.streamIcons.thunder,
size: 12,
color: effectiveIconColor,
),
SizedBox(width: defaults.spacing.xxxs),
MediaQuery.withNoTextScaling(
child: Text(
props.label,
style: defaults.labelStyle.copyWith(color: effectiveLabelColor),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
SizedBox(width: defaults.spacing.xxxs),
GestureDetector(
onTap: props.onDismiss,
behavior: HitTestBehavior.opaque,
child: SizedBox(
width: 16,
height: 16,
child: Icon(
context.streamIcons.crossMedium,
size: 12,
color: effectiveIconColor,
),
),
),
],
),
);
}
}

// Provides default values for [StreamCommandChip] based on the current theme.
class _StreamCommandChipDefaults {
_StreamCommandChipDefaults(this._context);

final BuildContext _context;

late final _colorScheme = _context.streamColorScheme;
late final _textTheme = _context.streamTextTheme;
late final spacing = _context.streamSpacing;

Color get backgroundColor => _colorScheme.backgroundInverse;

Color get labelColor => _colorScheme.textInverse;

Color get iconColor => _colorScheme.textInverse;

TextStyle get labelStyle => _textTheme.metadataEmphasis;

EdgeInsetsGeometry get padding => EdgeInsets.symmetric(
horizontal: spacing.xs,
vertical: spacing.xxxs,
);

BorderRadius get borderRadius => const BorderRadius.all(Radius.circular(9999));
}
Loading
Loading