Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import 'package:design_system_gallery/components/badge/stream_retry_badge.dart'
as _design_system_gallery_components_badge_stream_retry_badge;
import 'package:design_system_gallery/components/buttons/button.dart'
as _design_system_gallery_components_buttons_button;
import 'package:design_system_gallery/components/buttons/split_button.dart'
as _design_system_gallery_components_buttons_split_button;
import 'package:design_system_gallery/components/buttons/stream_emoji_button.dart'
as _design_system_gallery_components_buttons_stream_emoji_button;
import 'package:design_system_gallery/components/buttons/stream_jump_to_unread_button.dart'
Expand Down Expand Up @@ -520,6 +522,21 @@ final directories = <_widgetbook.WidgetbookNode>[
),
],
),
_widgetbook.WidgetbookComponent(
name: 'StreamSplitButton',
useCases: [
_widgetbook.WidgetbookUseCase(
name: 'Playground',
builder: _design_system_gallery_components_buttons_split_button
.buildStreamSplitButtonPlayground,
),
_widgetbook.WidgetbookUseCase(
name: 'Showcase',
builder: _design_system_gallery_components_buttons_split_button
.buildStreamSplitButtonShowcase,
),
],
),
],
),
_widgetbook.WidgetbookFolder(
Expand Down
298 changes: 298 additions & 0 deletions apps/design_system_gallery/lib/components/buttons/split_button.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
// ignore_for_file: experimental_member_use

import 'package:flutter/material.dart';
import 'package:stream_core_flutter/video.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;

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

@widgetbook.UseCase(
name: 'Playground',
type: StreamSplitButton,
path: '[Components]/Buttons',
)
Widget buildStreamSplitButtonPlayground(BuildContext context) {
final icons = context.streamIcons;

final style = context.knobs.object.dropdown(
label: 'Style',
options: StreamButtonStyle.values,
initialOption: StreamButtonStyle.secondary,
labelBuilder: (option) => option.name,
description: 'Split button visual style variant.',
);

final type = context.knobs.object.dropdown(
label: 'Type',
options: StreamButtonType.values,
initialOption: StreamButtonType.solid,
labelBuilder: (option) => option.name,
description: 'Split button type variant. Outline draws one border around both halves.',
);

final caretUp = context.knobs.boolean(
label: 'Caret Up',
description: 'Point the trailing caret up, as when the menu it opens is already showing.',
);

final leadingEnabled = context.knobs.boolean(
label: 'Leading Enabled',
initialValue: true,
description: 'Whether the primary half accepts taps.',
);

final trailingEnabled = context.knobs.boolean(
label: 'Trailing Enabled',
initialValue: true,
description: 'Whether the trailing half accepts taps.',
);

final showErrorBadge = context.knobs.boolean(
label: 'Error Badge',
description: 'Overlay a StreamErrorBadge, as a call control does when the mic fails.',
);

return Center(
child: _MaybeBadged(
showErrorBadge: showErrorBadge,
child: StreamSplitButton.icon(
icon: Icon(icons.voiceFill),
trailingIcon: Icon(caretUp ? icons.caretUp : icons.caretDown),
style: style,
type: type,
tooltip: 'Mute',
trailingTooltip: 'Audio settings',
onPressed: leadingEnabled ? () {} : null,
onTrailingPressed: trailingEnabled ? () {} : null,
),
),
);
}

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

@widgetbook.UseCase(
name: 'Showcase',
type: StreamSplitButton,
path: '[Components]/Buttons',
)
Widget buildStreamSplitButtonShowcase(BuildContext context) {
final colorScheme = context.streamColorScheme;
final textTheme = context.streamTextTheme;
final spacing = context.streamSpacing;

return DefaultTextStyle(
style: textTheme.bodyDefault.copyWith(color: colorScheme.textPrimary),
child: SingleChildScrollView(
padding: EdgeInsets.all(spacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: spacing.xl,
children: const [
_StyleTypeMatrixSection(),
_DisabledSection(),
_CallControlSection(),
],
),
),
);
}

class _StyleTypeMatrixSection extends StatelessWidget {
const _StyleTypeMatrixSection();

@override
Widget build(BuildContext context) {
final icons = context.streamIcons;
final spacing = context.streamSpacing;

return _ExampleCard(
title: 'Style × type',
description: 'The surface resolves from the same button style the halves use, so the two never drift apart.',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: spacing.md,
children: [
for (final style in StreamButtonStyle.values)
Row(
spacing: spacing.md,
children: [
SizedBox(width: 88, child: Text(style.name)),
for (final type in StreamButtonType.values)
StreamSplitButton.icon(
icon: Icon(icons.voiceFill),
trailingIcon: Icon(icons.caretDown),
style: style,
type: type,
tooltip: 'Mute',
trailingTooltip: 'Audio settings',
onPressed: () {},
onTrailingPressed: () {},
),
],
),
],
),
);
}
}

class _DisabledSection extends StatelessWidget {
const _DisabledSection();

@override
Widget build(BuildContext context) {
final icons = context.streamIcons;
final spacing = context.streamSpacing;

return _ExampleCard(
title: 'Disabled halves',
description: 'Each half disables on its own. The surface only goes disabled once both halves are.',
child: Row(
spacing: spacing.md,
children: [
for (final (label, leading, trailing) in const [
('leading', false, true),
('trailing', true, false),
('both', false, false),
])
Column(
spacing: spacing.xs,
children: [
StreamSplitButton.icon(
icon: Icon(icons.voiceFill),
trailingIcon: Icon(icons.caretDown),
style: StreamButtonStyle.secondary,
onPressed: leading ? () {} : null,
onTrailingPressed: trailing ? () {} : null,
),
Text(label),
],
),
],
),
);
}
}

class _CallControlSection extends StatefulWidget {
const _CallControlSection();

@override
State<_CallControlSection> createState() => _CallControlSectionState();
}

class _CallControlSectionState extends State<_CallControlSection> {
var _isMuted = false;
var _isSettingsOpen = false;

@override
Widget build(BuildContext context) {
final icons = context.streamIcons;

return _ExampleCard(
title: 'Call control',
description:
'The compact form from the design: a microphone toggle, a caret that opens the audio '
'settings, and an error badge for when the device fails.',
child: Center(
child: _MaybeBadged(
showErrorBadge: true,
child: StreamSplitButton.icon(
icon: Icon(_isMuted ? icons.voiceOffFill : icons.voiceFill),
trailingIcon: Icon(_isSettingsOpen ? icons.caretUp : icons.caretDown),
style: _isMuted ? StreamButtonStyle.destructive : StreamButtonStyle.secondary,
tooltip: _isMuted ? 'Unmute' : 'Mute',
trailingTooltip: 'Audio settings',
onPressed: () => setState(() => _isMuted = !_isMuted),
onTrailingPressed: () => setState(() => _isSettingsOpen = !_isSettingsOpen),
),
),
),
);
}
}

// =============================================================================
// Shared Widgets
// =============================================================================

/// Overlays a [StreamErrorBadge] on the trailing top corner of [child].
///
/// The badge is not part of [StreamSplitButton] — call controls compose the
/// two, and this shows what that looks like.
class _MaybeBadged extends StatelessWidget {
const _MaybeBadged({required this.showErrorBadge, required this.child});

final bool showErrorBadge;
final Widget child;

@override
Widget build(BuildContext context) {
if (!showErrorBadge) return child;

return Stack(
clipBehavior: Clip.none,
children: [
child,
PositionedDirectional(top: -4, end: -4, child: StreamErrorBadge(size: StreamErrorBadgeSize.sm)),
],
);
}
}

class _ExampleCard extends StatelessWidget {
const _ExampleCard({
required this.title,
required this.description,
required this.child,
});

final String title;
final String description;
final Widget child;

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

return Container(
width: double.infinity,
decoration: BoxDecoration(
color: colorScheme.backgroundSurfaceSubtle,
borderRadius: BorderRadius.all(radius.lg),
boxShadow: boxShadow.elevation1,
),
foregroundDecoration: BoxDecoration(
borderRadius: BorderRadius.all(radius.lg),
border: Border.all(color: colorScheme.borderSubtle),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.fromLTRB(spacing.md, spacing.sm, spacing.md, spacing.sm),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: textTheme.captionEmphasis.copyWith(color: colorScheme.textPrimary)),
Text(description, style: textTheme.metadataDefault.copyWith(color: colorScheme.textTertiary)),
],
),
),
Divider(height: 1, color: colorScheme.borderSubtle),
Padding(padding: EdgeInsets.all(spacing.md), child: child),
],
),
);
}
}
1 change: 1 addition & 0 deletions packages/stream_core_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### ✨ Features

- Added `StreamReactions.onReactionLongPressed`, reporting the long-pressed `StreamReactionsItem` — or `null` for the cluster/overflow chip. When null, the chips register no long-press gesture, leaving it to an ancestor.
- Added `StreamSplitButton`.
- Refreshed the icon set from the design tokens and added 44 icons, including a
filled variant for many existing icons: `blurFill`, `boltFill`,
`cameraFlipFill`, `captionFill`, `caretDown`, `caretUp`, `copyFill`,
Expand Down
3 changes: 3 additions & 0 deletions packages/stream_core_flutter/check_barrels.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,19 @@ package_name: stream_core_flutter
barrels:
- lib/chat.dart
- lib/core.dart
- lib/video.dart

# Public-facing libraries that files under `source_root` must never import.
# Typically: the barrels themselves, plus any deprecated entry points.
forbidden_src_imports:
- lib/chat.dart
- lib/core.dart
- lib/stream_core_flutter.dart
- lib/video.dart

# Full paths (relative to the package root) of directories whose files are
# internal to the package. Anything under these is excluded from coverage.
internal_dirs:
- lib/src/theme/primitives/internal
- lib/src/cache/internal
- lib/src/components/buttons/internal
Loading
Loading