From 176186e3b128be500ff49ced9e8d09b1016434a6 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 11 Sep 2026 14:16:35 +0200 Subject: [PATCH 1/2] fix(samples): match the share call accordion to design The card now sits 8 inside the participant tile on every side, which is where the participant label sits, so their edges line up. Its inset is read from the grid's own padding instead of being a fixed 24. Contents follow the design too: full-width "Add Others" and "Copy Call ID" buttons 8 apart, the QR panel on a surface fill with its caption underneath, core icons throughout, and an elevation-3 shadow under the card. A phone gives the card the width between the insets; a wider window centres it at 360. The copy confirmation also loses its hardcoded text colour, which had gone black on the dark SnackBar. Co-Authored-By: Claude Opus 5 --- dogfooding/lib/widgets/share_call_card.dart | 186 ++++++++++++-------- 1 file changed, 112 insertions(+), 74 deletions(-) diff --git a/dogfooding/lib/widgets/share_call_card.dart b/dogfooding/lib/widgets/share_call_card.dart index 7220288a2..7ec10c838 100644 --- a/dogfooding/lib/widgets/share_call_card.dart +++ b/dogfooding/lib/widgets/share_call_card.dart @@ -29,48 +29,84 @@ class _ShareCallWelcomeCardState extends State { @override Widget build(BuildContext context) { - final theme = StreamVideoTheme.of(context); - final colorScheme = StreamTheme.of(context).colorScheme; + final colorScheme = context.streamColorScheme; + final spacing = context.streamSpacing; + final borderRadius = BorderRadius.all(context.streamRadius.lg); + + // The design puts the card's edges on the participant label's. The label + // sits spacing.xs inside the tile, and the tile sits the grid's own + // padding inside the call, so the card clears both. + final gridPadding = + StreamCallParticipantsGridTheme.of(context).padding?.resolve( + Directionality.maybeOf(context), + ) ?? + EdgeInsets.all(spacing.xs); return Align( alignment: Alignment.bottomCenter, child: Padding( - padding: const EdgeInsets.all(24), - child: Container( - decoration: BoxDecoration( - color: colorScheme.backgroundElevation1, - borderRadius: BorderRadius.circular(16), + padding: gridPadding + EdgeInsets.all(spacing.xs), + child: ConstrainedBox( + constraints: BoxConstraints( + // A phone gives the card the full width between those insets; a + // wider window keeps it at the width the web design draws. + maxWidth: context.streamScreenSize.isSmall ? double.infinity : 360, ), - foregroundDecoration: BoxDecoration( - border: Border.all(color: colorScheme.borderDefault), - borderRadius: BorderRadius.circular(16), - ), - clipBehavior: .antiAlias, - constraints: const BoxConstraints(maxWidth: 600), - child: ExpansionTile( - title: Padding( - padding: const EdgeInsets.all(8), - child: Text( - 'Your meeting is live!', - style: theme.textTheme.title3, + child: Material( + // Elevation rather than a painted shadow, so the card lifts off the + // call the same way every other raised Stream surface does. + elevation: context.streamElevation.level3, + color: colorScheme.backgroundElevation1, + shape: RoundedRectangleBorder(borderRadius: borderRadius), + clipBehavior: Clip.antiAlias, + child: DecoratedBox( + // Drawn over the children, which the Material clips to its own + // shape — an outward-aligned border would be eaten otherwise. + position: DecorationPosition.foreground, + decoration: BoxDecoration( + borderRadius: borderRadius, + border: Border.all(color: colorScheme.borderDefault), ), - ), - shape: const Border( - top: BorderSide(color: Colors.transparent), - bottom: BorderSide(color: Colors.transparent), - ), - trailing: Icon( - _isExpanded ? Icons.expand_more : Icons.expand_less, - color: colorScheme.textPrimary, - ), - childrenPadding: const EdgeInsets.all(16), - onExpansionChanged: (value) => setState(() => _isExpanded = value), - children: [ - _ShareCardContent( - call: widget.call, - encryptionKey: widget.encryptionKey, + child: ExpansionTile( + title: Text( + 'Your Meeting is Live!', + style: context.streamTextTheme.headingSm.copyWith( + color: colorScheme.textPrimary, + ), + ), + shape: const Border( + top: BorderSide(color: Colors.transparent), + bottom: BorderSide(color: Colors.transparent), + ), + trailing: Icon( + _isExpanded + ? context.streamIcons.chevronUp + : context.streamIcons.chevronDown, + size: spacing.lg, + color: colorScheme.textPrimary, + ), + // minTileHeight sizes the row inside tilePadding, so the + // 16 above and below the title comes from centring it in 52. + tilePadding: EdgeInsets.symmetric(horizontal: spacing.md), + minTileHeight: 52, + childrenPadding: EdgeInsets.fromLTRB( + spacing.md, + 0, + spacing.md, + spacing.md, + ), + // The content sizes itself to the card, not to its widest row. + expandedCrossAxisAlignment: CrossAxisAlignment.stretch, + onExpansionChanged: (value) => + setState(() => _isExpanded = value), + children: [ + _ShareCardContent( + call: widget.call, + encryptionKey: widget.encryptionKey, + ), + ], ), - ], + ), ), ), ), @@ -96,7 +132,7 @@ class ShareCallParticipantsCard extends StatelessWidget { return Padding( padding: const EdgeInsets.all(24), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text('Share the link', style: theme.textTheme.title1), const SizedBox(height: 16), @@ -115,8 +151,8 @@ class _ShareCardContent extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = StreamVideoTheme.of(context); - final colorScheme = StreamTheme.of(context).colorScheme; + final colorScheme = context.streamColorScheme; + final spacing = context.streamSpacing; final callId = call.id; // An encrypted call cannot be joined without the key, so an invite to one @@ -129,29 +165,33 @@ class _ShareCardContent extends StatelessWidget { ); return Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (callUrl != null) ...[ StreamButton( - iconLeft: const Icon(Icons.person_add_alt_1), + iconLeft: Icon(context.streamIcons.userAddFill), + // The default padded tap target grows each 40px pill to 48 and so + // doubles the gap the design puts between the two; a full-width + // button keeps a large tap area without it. + themeStyle: const StreamButtonThemeStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), onPressed: () async { await SharePlus.instance.share( ShareParams(uri: Uri.parse(callUrl)), ); }, - child: const Text('Share link with others'), + child: const Text('Add Others'), ), - const SizedBox(height: 16), + SizedBox(height: spacing.xs), ], - Text( - 'Share this call ID with others you want in the meeting.', - style: theme.textTheme.body, - ), - const SizedBox(height: 8), StreamButton( style: StreamButtonStyle.secondary, type: StreamButtonType.outline, - iconLeft: const Icon(Icons.copy_all), + iconLeft: Icon(context.streamIcons.copyFill), + themeStyle: const StreamButtonThemeStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), onPressed: () async { await Clipboard.setData(ClipboardData(text: callId)); @@ -159,54 +199,52 @@ class _ShareCardContent extends StatelessWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( + spacing: spacing.xs, children: [ - Icon(Icons.check, color: colorScheme.accentSuccess), - const SizedBox(width: 8), - Text( - 'Call ID copied to clipboard', - style: theme.textTheme.body.copyWith( - color: theme.colorTheme.textHighEmphasis, - ), + Icon( + context.streamIcons.checkmark, + color: colorScheme.accentSuccess, ), + // Unstyled, so the text keeps the light-on-dark colour + // the SnackBar theme gives its content. + const Text('Call ID copied to clipboard'), ], ), ), ); } }, - child: Text('Call id: $callId'), + child: const Text('Copy Call ID'), ), if (callUrl != null) ...[ - Padding( - padding: const EdgeInsets.only(top: 16, bottom: 8), - child: Text( - 'Or scan the QR code to join from another device', - style: theme.textTheme.body, - ), - ), + SizedBox(height: spacing.md), Container( - width: double.infinity, + height: 160, alignment: Alignment.center, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: BorderRadius.all(context.streamRadius.lg), + color: colorScheme.backgroundSurface, ), - padding: const EdgeInsets.all(16), - child: Container( + padding: EdgeInsets.all(spacing.md), + child: DecoratedBox( decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.all(context.streamRadius.xl), ), - constraints: const BoxConstraints(maxHeight: 150), child: AspectRatio( aspectRatio: 1, - child: QrImageView( - data: callUrl, - size: 200, - ), + child: QrImageView(data: callUrl, size: 200), ), ), ), + SizedBox(height: spacing.sm), + Text( + 'Scan the QR code to join from another device.', + textAlign: TextAlign.center, + style: context.streamTextTheme.metadataDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), ], ], ); From 529c5695d1cb2797442661823db62169cfba4ee8 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Fri, 11 Sep 2026 14:44:21 +0200 Subject: [PATCH 2/2] refactor(samples): show the copy confirmation in a StreamSnackbar The design system ships a snackbar, and its success variant already carries the checkmark this was hand-assembling. A StreamSnackbarScope sits above the router so any route can reach a messenger. Co-Authored-By: Claude Opus 5 --- dogfooding/lib/app/app_content.dart | 4 +++- dogfooding/lib/widgets/share_call_card.dart | 18 ++++-------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/dogfooding/lib/app/app_content.dart b/dogfooding/lib/app/app_content.dart index a5bb675a8..c3384d1d5 100644 --- a/dogfooding/lib/app/app_content.dart +++ b/dogfooding/lib/app/app_content.dart @@ -208,7 +208,9 @@ class _StreamDogFoodingAppContentState builder: (context, child) { return StreamComponentFactory( builders: _componentBuilders, - child: child!, + // Above the router, so a snackbar shown from any route has a + // messenger to reach and outlives the screen that showed it. + child: StreamSnackbarScope(child: child!), ); }, ); diff --git a/dogfooding/lib/widgets/share_call_card.dart b/dogfooding/lib/widgets/share_call_card.dart index 7ec10c838..bd00a4ef8 100644 --- a/dogfooding/lib/widgets/share_call_card.dart +++ b/dogfooding/lib/widgets/share_call_card.dart @@ -196,20 +196,10 @@ class _ShareCardContent extends StatelessWidget { await Clipboard.setData(ClipboardData(text: callId)); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - spacing: spacing.xs, - children: [ - Icon( - context.streamIcons.checkmark, - color: colorScheme.accentSuccess, - ), - // Unstyled, so the text keeps the light-on-dark colour - // the SnackBar theme gives its content. - const Text('Call ID copied to clipboard'), - ], - ), + StreamSnackbarMessenger.of(context).show( + StreamSnackbar( + message: const Text('Call ID copied to clipboard'), + variant: StreamSnackbarVariant.success, ), ); }