Skip to content
Merged
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 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
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: [
const _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: () {},
),
),
],
),
const _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: () {}),
],
),
const _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,161 @@
import 'package:flutter/material.dart';

import '../../factory/stream_component_factory.dart';
import '../../theme/components/stream_command_chip_theme.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 effectiveForegroundColor = chipTheme.foregroundColor ?? defaults.foregroundColor;
final effectiveMinHeight = chipTheme.minHeight ?? defaults.minHeight;

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

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

final BuildContext _context;

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

@override
Color get backgroundColor => _colorScheme.backgroundInverse;

@override
Color get foregroundColor => _colorScheme.textInverse;

@override
TextStyle get labelStyle => _textTheme.metadataEmphasis;

@override
double get minHeight => 24;

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

@override
BorderRadius get borderRadius => BorderRadius.all(_radius.max);
}
Loading
Loading