From 226389ad533276d8daf2e03b90ed4cb3bfeecde3 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 19 Mar 2026 20:59:34 +0530 Subject: [PATCH] feat(ui): add StreamSkeletonLoading and StreamSkeletonBox components Add shimmer-based skeleton loading components using the `shimmer` package. StreamSkeletonLoading wraps children with an animated shimmer effect, while StreamSkeletonBox provides shaped placeholder boxes (rectangle or circular). - Add StreamSkeletonLoadingThemeData for theming base/highlight colors and period - Derive shimmer direction from ambient Directionality (LTR/RTL) - Use brightness-aware defaults (backgroundSurfaceStrong base, white20 highlight for light, chrome[0] for dark) instead of Figma overlay tokens - Remove unused skeletonLoadingBase/Highlight from StreamColorScheme and tokens - Add StreamSkeletonBox with .circular factory constructor - Add Widgetbook playground, showcase (message, list, image, enabled/disabled) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../lib/app/gallery_app.directories.g.dart | 19 + .../common/stream_skeleton_loading.dart | 498 ++++++++++++++++++ melos.yaml | 4 + .../lib/src/components.dart | 1 + .../common/stream_skeleton_loading.dart | 279 ++++++++++ .../src/factory/stream_component_factory.dart | 8 + .../stream_component_factory.g.theme.dart | 8 +- .../stream_skeleton_loading_theme.dart | 120 +++++ ...stream_skeleton_loading_theme.g.theme.dart | 100 ++++ .../primitives/tokens/dark/stream_tokens.dart | 2 - .../tokens/light/stream_tokens.dart | 2 - .../theme/semantics/stream_color_scheme.dart | 29 - .../stream_color_scheme.g.theme.dart | 21 - .../lib/src/theme/stream_theme.dart | 9 + .../lib/src/theme/stream_theme.g.theme.dart | 11 +- .../src/theme/stream_theme_extensions.dart | 4 + packages/stream_core_flutter/pubspec.yaml | 1 + 17 files changed, 1060 insertions(+), 56 deletions(-) create mode 100644 apps/design_system_gallery/lib/components/common/stream_skeleton_loading.dart create mode 100644 packages/stream_core_flutter/lib/src/components/common/stream_skeleton_loading.dart create mode 100644 packages/stream_core_flutter/lib/src/theme/components/stream_skeleton_loading_theme.dart create mode 100644 packages/stream_core_flutter/lib/src/theme/components/stream_skeleton_loading_theme.g.theme.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 6cda8a02..681c566f 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 @@ -40,6 +40,8 @@ import 'package:design_system_gallery/components/common/stream_loading_spinner.d as _design_system_gallery_components_common_stream_loading_spinner; 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/common/stream_skeleton_loading.dart' + as _design_system_gallery_components_common_stream_skeleton_loading; 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' @@ -471,6 +473,23 @@ final directories = <_widgetbook.WidgetbookNode>[ ), ], ), + _widgetbook.WidgetbookComponent( + name: 'StreamSkeletonLoading', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'Playground', + builder: + _design_system_gallery_components_common_stream_skeleton_loading + .buildStreamSkeletonLoadingPlayground, + ), + _widgetbook.WidgetbookUseCase( + name: 'Showcase', + builder: + _design_system_gallery_components_common_stream_skeleton_loading + .buildStreamSkeletonLoadingShowcase, + ), + ], + ), ], ), _widgetbook.WidgetbookFolder( diff --git a/apps/design_system_gallery/lib/components/common/stream_skeleton_loading.dart b/apps/design_system_gallery/lib/components/common/stream_skeleton_loading.dart new file mode 100644 index 00000000..4e842ca5 --- /dev/null +++ b/apps/design_system_gallery/lib/components/common/stream_skeleton_loading.dart @@ -0,0 +1,498 @@ +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: StreamSkeletonLoading, + path: '[Components]/Common', +) +Widget buildStreamSkeletonLoadingPlayground(BuildContext context) { + final enabled = context.knobs.boolean( + label: 'Enabled', + initialValue: true, + description: 'Whether the shimmer animation is running.', + ); + + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: StreamSkeletonLoading( + enabled: enabled, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Avatar + text row + Row( + children: [ + const StreamSkeletonBox.circular(radius: 20), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StreamSkeletonBox(width: 120, height: 12, borderRadius: BorderRadius.circular(4)), + const SizedBox(height: 8), + StreamSkeletonBox(width: 80, height: 10, borderRadius: BorderRadius.circular(4)), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + // Content lines + StreamSkeletonBox( + width: double.infinity, + height: 12, + borderRadius: BorderRadius.circular(4), + ), + const SizedBox(height: 8), + StreamSkeletonBox( + width: double.infinity, + height: 12, + borderRadius: BorderRadius.circular(4), + ), + const SizedBox(height: 8), + StreamSkeletonBox(width: 200, height: 12, borderRadius: BorderRadius.circular(4)), + ], + ), + ), + ), + ); +} + +// ============================================================================= +// Showcase +// ============================================================================= + +@widgetbook.UseCase( + name: 'Showcase', + type: StreamSkeletonLoading, + path: '[Components]/Common', +) +Widget buildStreamSkeletonLoadingShowcase(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, + children: [ + const _MessagePlaceholderSection(), + SizedBox(height: spacing.xl), + const _ListPlaceholderSection(), + SizedBox(height: spacing.xl), + const _ImagePlaceholderSection(), + SizedBox(height: spacing.xl), + const _EnabledStateSection(), + ], + ), + ), + ); +} + +// ============================================================================= +// Message Placeholder Section +// ============================================================================= + +class _MessagePlaceholderSection extends StatelessWidget { + const _MessagePlaceholderSection(); + + @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 Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'MESSAGE PLACEHOLDER'), + SizedBox(height: spacing.md), + Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + padding: EdgeInsets.all(spacing.md), + decoration: BoxDecoration( + color: colorScheme.backgroundSurface, + 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: [ + Text( + 'Simulates a loading message bubble', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + SizedBox(height: spacing.md), + StreamSkeletonLoading( + child: Row( + children: [ + const StreamSkeletonBox.circular(radius: 18), + SizedBox(width: spacing.sm), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StreamSkeletonBox(width: 100, height: 10, borderRadius: BorderRadius.circular(4)), + const SizedBox(height: 8), + StreamSkeletonBox( + width: double.infinity, + height: 10, + borderRadius: BorderRadius.circular(4), + ), + const SizedBox(height: 8), + StreamSkeletonBox(width: 160, height: 10, borderRadius: BorderRadius.circular(4)), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ], + ); + } +} + +// ============================================================================= +// List Placeholder Section +// ============================================================================= + +class _ListPlaceholderSection extends StatelessWidget { + const _ListPlaceholderSection(); + + @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 Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'LIST PLACEHOLDER'), + SizedBox(height: spacing.md), + Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + padding: EdgeInsets.all(spacing.md), + decoration: BoxDecoration( + color: colorScheme.backgroundSurface, + 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: [ + Text( + 'Simulates a loading list of items', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + SizedBox(height: spacing.md), + StreamSkeletonLoading( + child: Column( + children: [ + for (var i = 0; i < 3; i++) ...[ + if (i > 0) SizedBox(height: spacing.md), + Row( + children: [ + const StreamSkeletonBox.circular(radius: 22), + SizedBox(width: spacing.sm), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StreamSkeletonBox( + width: [140.0, 100.0, 120.0][i], + height: 12, + borderRadius: BorderRadius.circular(4), + ), + const SizedBox(height: 6), + StreamSkeletonBox( + width: double.infinity, + height: 10, + borderRadius: BorderRadius.circular(4), + ), + ], + ), + ), + ], + ), + ], + ], + ), + ), + ], + ), + ), + ], + ); + } +} + +// ============================================================================= +// Image Placeholder Section +// ============================================================================= + +class _ImagePlaceholderSection extends StatelessWidget { + const _ImagePlaceholderSection(); + + @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 Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'IMAGE PLACEHOLDER'), + SizedBox(height: spacing.md), + Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + padding: EdgeInsets.all(spacing.md), + decoration: BoxDecoration( + color: colorScheme.backgroundSurface, + 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: [ + Text( + 'Simulates an image gallery loading state', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + SizedBox(height: spacing.md), + StreamSkeletonLoading( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Large hero image placeholder + StreamSkeletonBox( + width: double.infinity, + height: 180, + borderRadius: BorderRadius.all(radius.md), + ), + SizedBox(height: spacing.sm), + // Thumbnail row + Row( + children: [ + Expanded( + child: StreamSkeletonBox( + height: 80, + borderRadius: BorderRadius.all(radius.sm), + ), + ), + SizedBox(width: spacing.sm), + Expanded( + child: StreamSkeletonBox( + height: 80, + borderRadius: BorderRadius.all(radius.sm), + ), + ), + SizedBox(width: spacing.sm), + Expanded( + child: StreamSkeletonBox( + height: 80, + borderRadius: BorderRadius.all(radius.sm), + ), + ), + ], + ), + SizedBox(height: spacing.md), + // Caption lines below images + StreamSkeletonBox( + width: 160, + height: 12, + borderRadius: BorderRadius.circular(4), + ), + const SizedBox(height: 6), + StreamSkeletonBox( + width: 100, + height: 10, + borderRadius: BorderRadius.circular(4), + ), + ], + ), + ), + ], + ), + ), + ], + ); + } +} + +// ============================================================================= +// Enabled State Section +// ============================================================================= + +class _EnabledStateSection extends StatelessWidget { + const _EnabledStateSection(); + + @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 Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _SectionLabel(label: 'ENABLED VS DISABLED'), + SizedBox(height: spacing.md), + Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + padding: EdgeInsets.all(spacing.md), + decoration: BoxDecoration( + color: colorScheme.backgroundSurface, + 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: [ + Text( + 'Animation can be paused with enabled: false', + style: textTheme.captionDefault.copyWith( + color: colorScheme.textSecondary, + ), + ), + SizedBox(height: spacing.md), + Row( + children: [ + const Expanded( + child: _EnabledDemo(label: 'Enabled', enabled: true), + ), + SizedBox(width: spacing.md), + const Expanded( + child: _EnabledDemo(label: 'Disabled', enabled: false), + ), + ], + ), + ], + ), + ), + ], + ); + } +} + +// ============================================================================= +// Shared Widgets +// ============================================================================= + +class _EnabledDemo extends StatelessWidget { + const _EnabledDemo({required this.label, required this.enabled}); + + final String label; + final bool enabled; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return Column( + children: [ + StreamSkeletonLoading( + enabled: enabled, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StreamSkeletonBox(width: double.infinity, height: 12, borderRadius: BorderRadius.circular(4)), + const SizedBox(height: 8), + StreamSkeletonBox(width: 100, height: 12, borderRadius: BorderRadius.circular(4)), + ], + ), + ), + SizedBox(height: spacing.sm), + Text( + label, + style: textTheme.metadataEmphasis.copyWith( + color: colorScheme.accentPrimary, + fontFamily: 'monospace', + ), + ), + ], + ); + } +} + +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, + ), + ), + ); + } +} diff --git a/melos.yaml b/melos.yaml index 1ba84374..5152c275 100644 --- a/melos.yaml +++ b/melos.yaml @@ -23,14 +23,17 @@ command: cross_file: ^0.3.4+2 dio: ^5.8.0+1 equatable: ^2.0.7 + flutter_markdown_plus: ^1.0.7 flutter_svg: ^2.2.3 http_parser: ^4.1.2 intl: ">=0.19.0 <=0.21.0" jose: ^0.3.4 json_annotation: ^4.9.0 + markdown: ^7.3.0 meta: ^1.15.0 mime: ^2.0.0 rxdart: ^0.28.0 + shimmer: ^3.0.0 stream_core: ^0.4.0 svg_icon_widget: ^0.0.1+1 synchronized: ^3.3.0 @@ -42,6 +45,7 @@ command: # List of all the dev_dependencies used in the project. dev_dependencies: + alchemist: ^0.13.0 build_runner: ^2.10.5 json_serializable: ^6.9.5 melos: ^6.2.0 diff --git a/packages/stream_core_flutter/lib/src/components.dart b/packages/stream_core_flutter/lib/src/components.dart index fc71b9c9..ee89ddb0 100644 --- a/packages/stream_core_flutter/lib/src/components.dart +++ b/packages/stream_core_flutter/lib/src/components.dart @@ -14,6 +14,7 @@ export 'components/common/stream_checkbox.dart' hide DefaultStreamCheckbox; export 'components/common/stream_flex.dart'; export 'components/common/stream_loading_spinner.dart' hide DefaultStreamLoadingSpinner; export 'components/common/stream_progress_bar.dart' hide DefaultStreamProgressBar; +export 'components/common/stream_skeleton_loading.dart' hide DefaultStreamSkeletonLoading; export 'components/common/stream_visibility.dart'; export 'components/context_menu/stream_context_menu.dart'; export 'components/context_menu/stream_context_menu_action.dart' hide DefaultStreamContextMenuAction; diff --git a/packages/stream_core_flutter/lib/src/components/common/stream_skeleton_loading.dart b/packages/stream_core_flutter/lib/src/components/common/stream_skeleton_loading.dart new file mode 100644 index 00000000..beafc905 --- /dev/null +++ b/packages/stream_core_flutter/lib/src/components/common/stream_skeleton_loading.dart @@ -0,0 +1,279 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +import '../../factory/stream_component_factory.dart'; +import '../../theme/components/stream_skeleton_loading_theme.dart'; +import '../../theme/primitives/stream_colors.dart'; +import '../../theme/semantics/stream_color_scheme.dart'; +import '../../theme/stream_theme_extensions.dart'; + +/// A skeleton shimmer loading component. +/// +/// [StreamSkeletonLoading] displays a shimmer animation effect over its +/// [child], typically used as a placeholder while content is loading. +/// +/// The shimmer colors, animation period, and direction can be customized +/// via direct properties or through [StreamSkeletonLoadingTheme]. +/// +/// {@tool snippet} +/// +/// Basic usage with a placeholder container: +/// +/// ```dart +/// StreamSkeletonLoading( +/// child: Container( +/// width: double.infinity, +/// height: 20, +/// decoration: BoxDecoration( +/// color: Colors.white, +/// borderRadius: BorderRadius.circular(4), +/// ), +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamSkeletonLoadingTheme], for theming skeleton widgets in a subtree. +/// * [StreamSkeletonLoadingThemeData], which describes the skeleton theme. +class StreamSkeletonLoading extends StatelessWidget { + /// Creates a [StreamSkeletonLoading]. + StreamSkeletonLoading({ + super.key, + Color? baseColor, + Color? highlightColor, + Duration? period, + ShimmerDirection? direction, + bool enabled = true, + required Widget child, + }) : props = .new( + baseColor: baseColor, + highlightColor: highlightColor, + period: period, + direction: direction, + enabled: enabled, + child: child, + ); + + /// The props controlling the appearance of this skeleton loading. + final StreamSkeletonLoadingProps props; + + @override + Widget build(BuildContext context) { + final builder = StreamComponentFactory.of(context).skeletonLoading; + if (builder != null) return builder(context, props); + return DefaultStreamSkeletonLoading(props: props); + } +} + +/// Properties for configuring a [StreamSkeletonLoading]. +/// +/// This class holds all the configuration options for a skeleton shimmer, +/// allowing them to be passed through the [StreamComponentFactory]. +/// +/// See also: +/// +/// * [StreamSkeletonLoading], which uses these properties. +/// * [DefaultStreamSkeletonLoading], the default implementation. +class StreamSkeletonLoadingProps { + /// Creates properties for a skeleton shimmer. + const StreamSkeletonLoadingProps({ + this.enabled = true, + this.baseColor, + this.highlightColor, + this.period, + this.direction, + required this.child, + }); + + /// Whether the shimmer animation is running. + /// + /// Defaults to true. + final bool enabled; + + /// The base color of the shimmer effect. + /// + /// If null, uses the theme's base color. + final Color? baseColor; + + /// The highlight color of the shimmer effect. + /// + /// If null, uses the theme's highlight color. + final Color? highlightColor; + + /// The duration of one shimmer animation cycle. + /// + /// If null, uses the theme's period. + final Duration? period; + + /// The direction of the shimmer animation. + /// + /// If null, follows the ambient [Directionality]. + final ShimmerDirection? direction; + + /// The widget to display the shimmer effect over. + final Widget child; +} + +/// Default implementation of [StreamSkeletonLoading]. +/// +/// Renders a shimmer effect using [Shimmer.fromColors] from the `shimmer` +/// package. Styling is resolved from widget props, the current +/// [StreamSkeletonLoadingTheme], and falls back to [StreamColorScheme] tokens. +/// +/// See also: +/// +/// * [StreamSkeletonLoading], the public API widget. +class DefaultStreamSkeletonLoading extends StatelessWidget { + /// Creates a default Stream skeleton loading shimmer. + const DefaultStreamSkeletonLoading({super.key, required this.props}); + + /// The props controlling the appearance of this skeleton loading. + final StreamSkeletonLoadingProps props; + + @override + Widget build(BuildContext context) { + final theme = context.streamSkeletonLoadingTheme; + final defaults = _StreamSkeletonLoadingThemeDefaults(context); + + final effectiveBaseColor = props.baseColor ?? theme.baseColor ?? defaults.baseColor; + final effectiveHighlightColor = props.highlightColor ?? theme.highlightColor ?? defaults.highlightColor; + final effectivePeriod = props.period ?? theme.period ?? defaults.period; + + final textDirection = Directionality.of(context); + final defaultDirection = textDirection == .ltr ? ShimmerDirection.ltr : ShimmerDirection.rtl; + final effectiveDirection = props.direction ?? defaultDirection; + + return Shimmer.fromColors( + enabled: props.enabled, + baseColor: effectiveBaseColor, + highlightColor: effectiveHighlightColor, + period: effectivePeriod, + direction: effectiveDirection, + child: props.child, + ); + } +} + +/// A simple shaped placeholder box intended for use inside a +/// [StreamSkeletonLoading]. +/// +/// Renders a filled rectangle (or circle) using +/// [StreamColorScheme.backgroundSurfaceStrong] as its default color. +/// +/// {@tool snippet} +/// +/// Compose with [StreamSkeletonLoading] to create loading placeholders: +/// +/// ```dart +/// StreamSkeletonLoading( +/// child: Column( +/// children: [ +/// StreamSkeletonBox(width: double.infinity, height: 16), +/// SizedBox(height: 8), +/// StreamSkeletonBox(width: 120, height: 16), +/// ], +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamSkeletonLoading], which wraps children with a shimmer effect. +class StreamSkeletonBox extends StatelessWidget { + /// Creates a skeleton placeholder box. + const StreamSkeletonBox({ + super.key, + required this.height, + this.width, + this.borderRadius, + this.shape = .rectangle, + this.color, + }); + + /// Creates a circular skeleton placeholder. + /// + /// The resulting box has a diameter of `radius * 2`. + const StreamSkeletonBox.circular({ + super.key, + required double radius, + this.color, + }) : shape = .circle, + width = radius * 2, + height = radius * 2, + borderRadius = null; + + /// The height of the placeholder. + final double height; + + /// The width of the placeholder. + /// + /// If null, the box sizes itself based on incoming constraints. + final double? width; + + /// The border radius of the placeholder. + /// + /// Ignored when [shape] is [BoxShape.circle]. + final BorderRadiusGeometry? borderRadius; + + /// The shape of the placeholder. + /// + /// Defaults to [BoxShape.rectangle]. + final BoxShape shape; + + /// The fill color of the placeholder. + /// + /// If null, uses [StreamColorScheme.backgroundSurfaceStrong]. + final Color? color; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final effectiveColor = color ?? colorScheme.backgroundSurfaceStrong; + + return Container( + width: width, + height: height, + decoration: BoxDecoration( + shape: shape, + color: effectiveColor, + borderRadius: borderRadius, + ), + ); + } +} + +// Provides default values for [StreamSkeletonLoadingThemeData] based on the +// current [StreamColorScheme]. +class _StreamSkeletonLoadingThemeDefaults extends StreamSkeletonLoadingThemeData { + _StreamSkeletonLoadingThemeDefaults(this.context); + + final BuildContext context; + + late final StreamColorScheme _colorScheme = context.streamColorScheme; + + // The shimmer package uses BlendMode.srcATop which replaces child pixels + // with the gradient. The highlight color is brightness-aware: light mode + // uses a semi-transparent white overlay, dark mode uses the deepest chrome + // shade to create a subtle darkening sweep. + // + // NOTE: Component theme defaults should generally avoid depending on + // [Theme.brightnessOf] and instead rely solely on [StreamColorScheme] + // semantics. This is an exception because the chrome surface hierarchy + // inverts between light and dark modes, so no single semantic color pair + // produces the correct base→highlight contrast in both brightnesses. + + @override + Color get baseColor => _colorScheme.backgroundSurfaceStrong; + + @override + Color get highlightColor { + final brightness = Theme.brightnessOf(context); + return brightness == .light ? StreamColors.white20 : _colorScheme.chrome[0]!; + } + + @override + Duration get period => const Duration(milliseconds: 1500); +} 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 42381e62..421c3982 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 @@ -157,6 +157,7 @@ class StreamComponentBuilders with _$StreamComponentBuilders { StreamComponentBuilder? progressBar, StreamComponentBuilder? reactionPicker, StreamComponentBuilder? reactions, + StreamComponentBuilder? skeletonLoading, Iterable>? extensions, }) { extensions ??= >[]; @@ -188,6 +189,7 @@ class StreamComponentBuilders with _$StreamComponentBuilders { progressBar: progressBar, reactionPicker: reactionPicker, reactions: reactions, + skeletonLoading: skeletonLoading, extensions: _extensionIterableToMap(extensions), ); } @@ -220,6 +222,7 @@ class StreamComponentBuilders with _$StreamComponentBuilders { required this.progressBar, required this.reactionPicker, required this.reactions, + required this.skeletonLoading, required this.extensions, }); @@ -373,6 +376,11 @@ class StreamComponentBuilders with _$StreamComponentBuilders { /// When null, [StreamReactions] uses [DefaultStreamReactions]. final StreamComponentBuilder? reactions; + /// Custom builder for skeleton loading shimmer widgets. + /// + /// When null, [StreamSkeletonLoading] uses [DefaultStreamSkeletonLoading]. + final StreamComponentBuilder? skeletonLoading; + // Convert the [extensionsIterable] passed to [StreamComponentBuilders.new] // to the stored [extensions] map, where each entry's key consists of the extension's type. static Map> _extensionIterableToMap( diff --git a/packages/stream_core_flutter/lib/src/factory/stream_component_factory.g.theme.dart b/packages/stream_core_flutter/lib/src/factory/stream_component_factory.g.theme.dart index 4dd9b244..9bd03e9a 100644 --- a/packages/stream_core_flutter/lib/src/factory/stream_component_factory.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/factory/stream_component_factory.g.theme.dart @@ -57,6 +57,7 @@ mixin _$StreamComponentBuilders { progressBar: t < 0.5 ? a.progressBar : b.progressBar, reactionPicker: t < 0.5 ? a.reactionPicker : b.reactionPicker, reactions: t < 0.5 ? a.reactions : b.reactions, + skeletonLoading: t < 0.5 ? a.skeletonLoading : b.skeletonLoading, ); } @@ -92,6 +93,7 @@ mixin _$StreamComponentBuilders { Widget Function(BuildContext, StreamProgressBarProps)? progressBar, Widget Function(BuildContext, StreamReactionPickerProps)? reactionPicker, Widget Function(BuildContext, StreamReactionsProps)? reactions, + Widget Function(BuildContext, StreamSkeletonLoadingProps)? skeletonLoading, }) { final _this = (this as StreamComponentBuilders); @@ -123,6 +125,7 @@ mixin _$StreamComponentBuilders { progressBar: progressBar ?? _this.progressBar, reactionPicker: reactionPicker ?? _this.reactionPicker, reactions: reactions ?? _this.reactions, + skeletonLoading: skeletonLoading ?? _this.skeletonLoading, ); } @@ -165,6 +168,7 @@ mixin _$StreamComponentBuilders { progressBar: other.progressBar, reactionPicker: other.reactionPicker, reactions: other.reactions, + skeletonLoading: other.skeletonLoading, ); } @@ -207,7 +211,8 @@ mixin _$StreamComponentBuilders { _other.onlineIndicator == _this.onlineIndicator && _other.progressBar == _this.progressBar && _other.reactionPicker == _this.reactionPicker && - _other.reactions == _this.reactions; + _other.reactions == _this.reactions && + _other.skeletonLoading == _this.skeletonLoading; } @override @@ -243,6 +248,7 @@ mixin _$StreamComponentBuilders { _this.progressBar, _this.reactionPicker, _this.reactions, + _this.skeletonLoading, ]); } } diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_skeleton_loading_theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_skeleton_loading_theme.dart new file mode 100644 index 00000000..76e5a44f --- /dev/null +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_skeleton_loading_theme.dart @@ -0,0 +1,120 @@ +import 'package:flutter/widgets.dart'; +import 'package:theme_extensions_builder_annotation/theme_extensions_builder_annotation.dart'; + +import '../stream_theme.dart'; + +part 'stream_skeleton_loading_theme.g.theme.dart'; + +/// Applies a skeleton loading theme to descendant [StreamSkeletonLoading] +/// widgets. +/// +/// Wrap a subtree with [StreamSkeletonLoadingTheme] to override skeleton +/// shimmer styling. Access the merged theme using +/// [BuildContext.streamSkeletonLoadingTheme]. +/// +/// {@tool snippet} +/// +/// Override skeleton colors for a specific section: +/// +/// ```dart +/// StreamSkeletonLoadingTheme( +/// data: StreamSkeletonLoadingThemeData( +/// baseColor: Colors.grey.shade300, +/// highlightColor: Colors.grey.shade100, +/// ), +/// child: Column( +/// children: [ +/// StreamSkeletonLoading(child: Container(height: 20)), +/// StreamSkeletonLoading(child: Container(height: 40)), +/// ], +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamSkeletonLoadingThemeData], which describes the skeleton theme. +/// * [StreamSkeletonLoading], the widget affected by this theme. +class StreamSkeletonLoadingTheme extends InheritedTheme { + /// Creates a skeleton loading theme that controls descendant skeleton + /// widgets. + const StreamSkeletonLoadingTheme({ + super.key, + required this.data, + required super.child, + }); + + /// The skeleton loading theme data for descendant widgets. + final StreamSkeletonLoadingThemeData data; + + /// Returns the [StreamSkeletonLoadingThemeData] merged from local and + /// global themes. + /// + /// Local values from the nearest [StreamSkeletonLoadingTheme] ancestor take + /// precedence over global values from [StreamTheme.of]. + static StreamSkeletonLoadingThemeData of(BuildContext context) { + final localTheme = context.dependOnInheritedWidgetOfExactType(); + return StreamTheme.of(context).skeletonLoadingTheme.merge(localTheme?.data); + } + + @override + Widget wrap(BuildContext context, Widget child) { + return StreamSkeletonLoadingTheme(data: data, child: child); + } + + @override + bool updateShouldNotify(StreamSkeletonLoadingTheme oldWidget) => data != oldWidget.data; +} + +/// Theme data for customizing [StreamSkeletonLoading] widgets. +/// +/// {@tool snippet} +/// +/// Customize skeleton loading appearance globally: +/// +/// ```dart +/// StreamTheme( +/// skeletonLoadingTheme: StreamSkeletonLoadingThemeData( +/// baseColor: Colors.grey.shade300, +/// highlightColor: Colors.grey.shade100, +/// period: Duration(milliseconds: 1200), +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamSkeletonLoading], the widget that uses this theme data. +/// * [StreamSkeletonLoadingTheme], for overriding theme in a widget subtree. +@themeGen +@immutable +class StreamSkeletonLoadingThemeData with _$StreamSkeletonLoadingThemeData { + /// Creates skeleton loading theme data with optional style overrides. + const StreamSkeletonLoadingThemeData({ + this.baseColor, + this.highlightColor, + this.period, + }); + + /// The base color of the shimmer effect. + /// + /// Falls back to [StreamColorScheme.backgroundSurfaceStrong]. + final Color? baseColor; + + /// The highlight color of the shimmer effect. + final Color? highlightColor; + + /// The duration of one shimmer animation cycle. + /// + /// Falls back to 1500 milliseconds. + final Duration? period; + + /// Linearly interpolate between two [StreamSkeletonLoadingThemeData] objects. + static StreamSkeletonLoadingThemeData? lerp( + StreamSkeletonLoadingThemeData? a, + StreamSkeletonLoadingThemeData? b, + double t, + ) => _$StreamSkeletonLoadingThemeData.lerp(a, b, t); +} diff --git a/packages/stream_core_flutter/lib/src/theme/components/stream_skeleton_loading_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/components/stream_skeleton_loading_theme.g.theme.dart new file mode 100644 index 00000000..8f86d599 --- /dev/null +++ b/packages/stream_core_flutter/lib/src/theme/components/stream_skeleton_loading_theme.g.theme.dart @@ -0,0 +1,100 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_element + +part of 'stream_skeleton_loading_theme.dart'; + +// ************************************************************************** +// ThemeGenGenerator +// ************************************************************************** + +mixin _$StreamSkeletonLoadingThemeData { + bool get canMerge => true; + + static StreamSkeletonLoadingThemeData? lerp( + StreamSkeletonLoadingThemeData? a, + StreamSkeletonLoadingThemeData? b, + double t, + ) { + if (identical(a, b)) { + return a; + } + + if (a == null) { + return t == 1.0 ? b : null; + } + + if (b == null) { + return t == 0.0 ? a : null; + } + + return StreamSkeletonLoadingThemeData( + baseColor: Color.lerp(a.baseColor, b.baseColor, t), + highlightColor: Color.lerp(a.highlightColor, b.highlightColor, t), + period: lerpDuration$(a.period, b.period, t), + ); + } + + StreamSkeletonLoadingThemeData copyWith({ + Color? baseColor, + Color? highlightColor, + Duration? period, + }) { + final _this = (this as StreamSkeletonLoadingThemeData); + + return StreamSkeletonLoadingThemeData( + baseColor: baseColor ?? _this.baseColor, + highlightColor: highlightColor ?? _this.highlightColor, + period: period ?? _this.period, + ); + } + + StreamSkeletonLoadingThemeData merge(StreamSkeletonLoadingThemeData? other) { + final _this = (this as StreamSkeletonLoadingThemeData); + + if (other == null || identical(_this, other)) { + return _this; + } + + if (!other.canMerge) { + return other; + } + + return copyWith( + baseColor: other.baseColor, + highlightColor: other.highlightColor, + period: other.period, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + final _this = (this as StreamSkeletonLoadingThemeData); + final _other = (other as StreamSkeletonLoadingThemeData); + + return _other.baseColor == _this.baseColor && + _other.highlightColor == _this.highlightColor && + _other.period == _this.period; + } + + @override + int get hashCode { + final _this = (this as StreamSkeletonLoadingThemeData); + + return Object.hash( + runtimeType, + _this.baseColor, + _this.highlightColor, + _this.period, + ); + } +} diff --git a/packages/stream_core_flutter/lib/src/theme/primitives/tokens/dark/stream_tokens.dart b/packages/stream_core_flutter/lib/src/theme/primitives/tokens/dark/stream_tokens.dart index 08652b9f..538bd62f 100644 --- a/packages/stream_core_flutter/lib/src/theme/primitives/tokens/dark/stream_tokens.dart +++ b/packages/stream_core_flutter/lib/src/theme/primitives/tokens/dark/stream_tokens.dart @@ -603,8 +603,6 @@ class StreamTokens { static const brand700 = Color(0xFFC3D9FF); static const brand800 = Color(0xFFE3EDFF); static const brand900 = Color(0xFFF3F7FF); - static const skeletonLoadingBase = Color(0x00FFFFFF); - static const skeletonLoadingHighlight = Color(0x33FFFFFF); static const chrome0 = Color(0xFF000000); static const chrome50 = Color(0xFF1C1C1C); static const chrome100 = Color(0xFF323232); diff --git a/packages/stream_core_flutter/lib/src/theme/primitives/tokens/light/stream_tokens.dart b/packages/stream_core_flutter/lib/src/theme/primitives/tokens/light/stream_tokens.dart index ee07e22b..2b705d1c 100644 --- a/packages/stream_core_flutter/lib/src/theme/primitives/tokens/light/stream_tokens.dart +++ b/packages/stream_core_flutter/lib/src/theme/primitives/tokens/light/stream_tokens.dart @@ -603,8 +603,6 @@ class StreamTokens { static const brand700 = Color(0xFF19418D); static const brand800 = Color(0xFF142F63); static const brand900 = Color(0xFF091A3B); - static const skeletonLoadingBase = Color(0x00FFFFFF); - static const skeletonLoadingHighlight = Color(0xFFFFFFFF); static const chrome0 = Color(0xFFFFFFFF); static const chrome50 = Color(0xFFF6F8FA); static const chrome100 = Color(0xFFEBEEF1); 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 3a36477f..8a626927 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 @@ -110,9 +110,6 @@ class StreamColorScheme with _$StreamColorScheme { // System Color? systemText, Color? systemScrollbar, - // Skeleton Loading - Color? skeletonLoadingBase, - Color? skeletonLoadingHighlight, // Avatar List? avatarPalette, }) { @@ -189,10 +186,6 @@ class StreamColorScheme with _$StreamColorScheme { systemText ??= light_tokens.StreamTokens.systemText; systemScrollbar ??= light_tokens.StreamTokens.systemScrollbar; - // Skeleton Loading - skeletonLoadingBase ??= light_tokens.StreamTokens.skeletonLoadingBase; - skeletonLoadingHighlight ??= light_tokens.StreamTokens.skeletonLoadingHighlight; - // Avatar avatarPalette ??= [ StreamAvatarColorPair( @@ -273,8 +266,6 @@ class StreamColorScheme with _$StreamColorScheme { stateDisabled: stateDisabled, systemText: systemText, systemScrollbar: systemScrollbar, - skeletonLoadingBase: skeletonLoadingBase, - skeletonLoadingHighlight: skeletonLoadingHighlight, avatarPalette: avatarPalette, ); } @@ -345,9 +336,6 @@ class StreamColorScheme with _$StreamColorScheme { // System Color? systemText, Color? systemScrollbar, - // Skeleton Loading - Color? skeletonLoadingBase, - Color? skeletonLoadingHighlight, // Avatar List? avatarPalette, }) { @@ -424,10 +412,6 @@ class StreamColorScheme with _$StreamColorScheme { systemText ??= dark_tokens.StreamTokens.systemText; systemScrollbar ??= dark_tokens.StreamTokens.systemScrollbar; - // Skeleton Loading - skeletonLoadingBase ??= dark_tokens.StreamTokens.skeletonLoadingBase; - skeletonLoadingHighlight ??= dark_tokens.StreamTokens.skeletonLoadingHighlight; - // Avatar avatarPalette ??= [ StreamAvatarColorPair( @@ -508,8 +492,6 @@ class StreamColorScheme with _$StreamColorScheme { stateDisabled: stateDisabled, systemText: systemText, systemScrollbar: systemScrollbar, - skeletonLoadingBase: skeletonLoadingBase, - skeletonLoadingHighlight: skeletonLoadingHighlight, avatarPalette: avatarPalette, ); } @@ -578,9 +560,6 @@ class StreamColorScheme with _$StreamColorScheme { // System required this.systemText, required this.systemScrollbar, - // Skeleton Loading - required this.skeletonLoadingBase, - required this.skeletonLoadingHighlight, // Avatar required this.avatarPalette, }); @@ -768,14 +747,6 @@ class StreamColorScheme with _$StreamColorScheme { /// The system scrollbar color. final Color systemScrollbar; - // ---- Skeleton Loading colors ---- - - /// The base color for skeleton loading shimmer. - final Color skeletonLoadingBase; - - /// The highlight color for skeleton loading shimmer. - final Color skeletonLoadingHighlight; - // ---- Avatar colors ---- /// The color palette for generating avatar colors based on user identity. 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 3b62f652..c464c59d 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 @@ -153,16 +153,6 @@ mixin _$StreamColorScheme { stateDisabled: Color.lerp(a.stateDisabled, b.stateDisabled, t)!, systemText: Color.lerp(a.systemText, b.systemText, t)!, systemScrollbar: Color.lerp(a.systemScrollbar, b.systemScrollbar, t)!, - skeletonLoadingBase: Color.lerp( - a.skeletonLoadingBase, - b.skeletonLoadingBase, - t, - )!, - skeletonLoadingHighlight: Color.lerp( - a.skeletonLoadingHighlight, - b.skeletonLoadingHighlight, - t, - )!, avatarPalette: t < 0.5 ? a.avatarPalette : b.avatarPalette, ); } @@ -223,8 +213,6 @@ mixin _$StreamColorScheme { Color? stateDisabled, Color? systemText, Color? systemScrollbar, - Color? skeletonLoadingBase, - Color? skeletonLoadingHighlight, List? avatarPalette, }) { final _this = (this as StreamColorScheme); @@ -290,9 +278,6 @@ mixin _$StreamColorScheme { stateDisabled: stateDisabled ?? _this.stateDisabled, systemText: systemText ?? _this.systemText, systemScrollbar: systemScrollbar ?? _this.systemScrollbar, - skeletonLoadingBase: skeletonLoadingBase ?? _this.skeletonLoadingBase, - skeletonLoadingHighlight: - skeletonLoadingHighlight ?? _this.skeletonLoadingHighlight, avatarPalette: avatarPalette ?? _this.avatarPalette, ); } @@ -364,8 +349,6 @@ mixin _$StreamColorScheme { stateDisabled: other.stateDisabled, systemText: other.systemText, systemScrollbar: other.systemScrollbar, - skeletonLoadingBase: other.skeletonLoadingBase, - skeletonLoadingHighlight: other.skeletonLoadingHighlight, avatarPalette: other.avatarPalette, ); } @@ -438,8 +421,6 @@ mixin _$StreamColorScheme { _other.stateDisabled == _this.stateDisabled && _other.systemText == _this.systemText && _other.systemScrollbar == _this.systemScrollbar && - _other.skeletonLoadingBase == _this.skeletonLoadingBase && - _other.skeletonLoadingHighlight == _this.skeletonLoadingHighlight && _other.avatarPalette == _this.avatarPalette; } @@ -504,8 +485,6 @@ mixin _$StreamColorScheme { _this.stateDisabled, _this.systemText, _this.systemScrollbar, - _this.skeletonLoadingBase, - _this.skeletonLoadingHighlight, _this.avatarPalette, ]); } diff --git a/packages/stream_core_flutter/lib/src/theme/stream_theme.dart b/packages/stream_core_flutter/lib/src/theme/stream_theme.dart index 04cffc6a..b8f2a820 100644 --- a/packages/stream_core_flutter/lib/src/theme/stream_theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/stream_theme.dart @@ -23,6 +23,7 @@ import 'components/stream_online_indicator_theme.dart'; import 'components/stream_progress_bar_theme.dart'; import 'components/stream_reaction_picker_theme.dart'; import 'components/stream_reactions_theme.dart'; +import 'components/stream_skeleton_loading_theme.dart'; import 'primitives/stream_icons.dart'; import 'primitives/stream_radius.dart'; import 'primitives/stream_spacing.dart'; @@ -116,6 +117,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { StreamProgressBarThemeData? progressBarTheme, StreamReactionPickerThemeData? reactionPickerTheme, StreamReactionsThemeData? reactionsTheme, + StreamSkeletonLoadingThemeData? skeletonLoadingTheme, }) { platform ??= defaultTargetPlatform; final isDark = brightness == Brightness.dark; @@ -151,6 +153,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { progressBarTheme ??= const StreamProgressBarThemeData(); reactionPickerTheme ??= const StreamReactionPickerThemeData(); reactionsTheme ??= const StreamReactionsThemeData(); + skeletonLoadingTheme ??= const StreamSkeletonLoadingThemeData(); return .raw( brightness: brightness, @@ -180,6 +183,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { progressBarTheme: progressBarTheme, reactionPickerTheme: reactionPickerTheme, reactionsTheme: reactionsTheme, + skeletonLoadingTheme: skeletonLoadingTheme, ); } @@ -223,6 +227,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { required this.progressBarTheme, required this.reactionPickerTheme, required this.reactionsTheme, + required this.skeletonLoadingTheme, }); /// Returns the [StreamTheme] from the closest [Theme] ancestor. @@ -342,6 +347,9 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { /// The reaction theme for this theme. final StreamReactionsThemeData reactionsTheme; + /// The skeleton theme for this theme. + final StreamSkeletonLoadingThemeData skeletonLoadingTheme; + /// Creates a copy of this theme but with platform-dependent primitives /// recomputed for the given [platform]. /// @@ -390,6 +398,7 @@ class StreamTheme extends ThemeExtension with _$StreamTheme { progressBarTheme: progressBarTheme, reactionPickerTheme: reactionPickerTheme, reactionsTheme: reactionsTheme, + skeletonLoadingTheme: skeletonLoadingTheme, ); } } diff --git a/packages/stream_core_flutter/lib/src/theme/stream_theme.g.theme.dart b/packages/stream_core_flutter/lib/src/theme/stream_theme.g.theme.dart index 18c78cd7..cab1b2bd 100644 --- a/packages/stream_core_flutter/lib/src/theme/stream_theme.g.theme.dart +++ b/packages/stream_core_flutter/lib/src/theme/stream_theme.g.theme.dart @@ -39,6 +39,7 @@ mixin _$StreamTheme on ThemeExtension { StreamProgressBarThemeData? progressBarTheme, StreamReactionPickerThemeData? reactionPickerTheme, StreamReactionsThemeData? reactionsTheme, + StreamSkeletonLoadingThemeData? skeletonLoadingTheme, }) { final _this = (this as StreamTheme); @@ -72,6 +73,7 @@ mixin _$StreamTheme on ThemeExtension { progressBarTheme: progressBarTheme ?? _this.progressBarTheme, reactionPickerTheme: reactionPickerTheme ?? _this.reactionPickerTheme, reactionsTheme: reactionsTheme ?? _this.reactionsTheme, + skeletonLoadingTheme: skeletonLoadingTheme ?? _this.skeletonLoadingTheme, ); } @@ -183,6 +185,11 @@ mixin _$StreamTheme on ThemeExtension { other.reactionsTheme, t, )!, + skeletonLoadingTheme: StreamSkeletonLoadingThemeData.lerp( + _this.skeletonLoadingTheme, + other.skeletonLoadingTheme, + t, + )!, ); } @@ -225,7 +232,8 @@ mixin _$StreamTheme on ThemeExtension { _other.onlineIndicatorTheme == _this.onlineIndicatorTheme && _other.progressBarTheme == _this.progressBarTheme && _other.reactionPickerTheme == _this.reactionPickerTheme && - _other.reactionsTheme == _this.reactionsTheme; + _other.reactionsTheme == _this.reactionsTheme && + _other.skeletonLoadingTheme == _this.skeletonLoadingTheme; } @override @@ -261,6 +269,7 @@ mixin _$StreamTheme on ThemeExtension { _this.progressBarTheme, _this.reactionPickerTheme, _this.reactionsTheme, + _this.skeletonLoadingTheme, ]); } } diff --git a/packages/stream_core_flutter/lib/src/theme/stream_theme_extensions.dart b/packages/stream_core_flutter/lib/src/theme/stream_theme_extensions.dart index b3050e07..67b86b7a 100644 --- a/packages/stream_core_flutter/lib/src/theme/stream_theme_extensions.dart +++ b/packages/stream_core_flutter/lib/src/theme/stream_theme_extensions.dart @@ -19,6 +19,7 @@ import 'components/stream_online_indicator_theme.dart'; import 'components/stream_progress_bar_theme.dart'; import 'components/stream_reaction_picker_theme.dart'; import 'components/stream_reactions_theme.dart'; +import 'components/stream_skeleton_loading_theme.dart'; import 'primitives/stream_icons.dart'; import 'primitives/stream_radius.dart'; import 'primitives/stream_spacing.dart'; @@ -129,4 +130,7 @@ extension StreamThemeExtension on BuildContext { /// Returns the [StreamReactionsThemeData] from the nearest ancestor. StreamReactionsThemeData get streamReactionsTheme => StreamReactionsTheme.of(this); + + /// Returns the [StreamSkeletonLoadingThemeData] from the nearest ancestor. + StreamSkeletonLoadingThemeData get streamSkeletonLoadingTheme => StreamSkeletonLoadingTheme.of(this); } diff --git a/packages/stream_core_flutter/pubspec.yaml b/packages/stream_core_flutter/pubspec.yaml index 9f1f8171..55410bca 100644 --- a/packages/stream_core_flutter/pubspec.yaml +++ b/packages/stream_core_flutter/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: flutter_markdown_plus: ^1.0.7 flutter_svg: ^2.2.3 markdown: ^7.3.0 + shimmer: ^3.0.0 stream_core: ^0.4.0 theme_extensions_builder_annotation: ^7.1.0